Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/apis/approveProposal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// approveProposal.ts
import { axiosInstance } from "./axios";

export async function approveProposal(proposalId: number, body: any) {
try {
const res = await axiosInstance.post("/places/approve", body, {
params: { proposalId },
withCredentials: true,
});
return !!res.data?.isSuccess;
} catch (e: any) {
console.log("[approveProposal] status:", e?.response?.status);
console.log("[approveProposal] payload:", e?.response?.data);
return false;
}
}
25 changes: 25 additions & 0 deletions src/apis/resolveGooglePlace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// resolveGooglePlace.ts
import { axiosInstance } from "./axios";

export async function resolveGooglePlace(params: { shortUrl?: string; name?: string }) {
try {
const res = await axiosInstance.get("/places/google/resolve", {
params,
withCredentials: true, // axiosInstance에 이미 있다면 생략 가능
});
return res.data?.result ?? null;
} catch (e: any) {
console.log("[resolveGooglePlace] error payload:", e?.response?.data);
console.log("[resolveGooglePlace] status:", e?.response?.status);
return null;
}
}

export function parseGeometry(geometry?: string): { lat?: number; lng?: number } {
if (!geometry) return {};
const [latStr, lngStr] = geometry.split(",");
const lat = Number(latStr?.trim());
const lng = Number(lngStr?.trim());
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return {};
return { lat, lng };
}
165 changes: 109 additions & 56 deletions src/pages/admin/AdminConfirmSpacePage.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,163 @@
// src/pages/admin/AdminConfirmSpacePage.tsx
import TopHeader from "../../components/TopHeader";
import SpaceInfoSimple from "../../components/detail/SpaceInfoSimple";
import dummySpaces from "../../constants/dummySpaces";
import type { Space } from "../../constants/dummySpaces";
import { useNavigate, useLocation } from "react-router-dom";
import { useEffect, useState } from "react";
import { loadKakaoScript } from "../../utils/kakaoMapLoader"; // 카카오 스크립트 로더만 사용
import { loadKakaoScript } from "../../utils/kakaoMapLoader";
import { resolveGooglePlace, parseGeometry } from "../../apis/resolveGooglePlace";

declare global {
interface Window { kakao: any; }
}
declare global { interface Window { kakao: any; } }

// 부분만 갖고 있어도 되게
type UISpace = Partial<Space> & { lat?: number; lng?: number };

const AdminConfirmSpacePage = () => {
const navigate = useNavigate();
const { state } = useLocation() as { state?: { placeName?: string } };
const initialName = state?.placeName?.trim() ?? "";

// UI는 유지하고, 데이터만 더미→실데이터로 치환
const [space, setSpace] = useState<Space>(dummySpaces[0]);
const { state } = useLocation() as {
state?: { placeName?: string; googleMapsLink?: string; proposalId?: number }
};

// 카카오 장소검색 → UI Space로 매핑
const toUISpaceFromKakao = (k: any): Space => {
const base = dummySpaces[0]; // 누락 필드는 더미가 채움 (UI 타입 보장)
const initialName = state?.placeName?.trim() ?? "";
const googleMapsLink = state?.googleMapsLink ?? "";
const proposalId = state?.proposalId;

const [space, setSpace] = useState<UISpace>({
id: 0,
name: initialName,
image: "",
rating: 0,
distance: 0,
tags: [],
isLiked: false,
address: "",
opening: "", // 운영시간 없으면 빈칸
holiday: "",
phone: "",
});

const fromGoogle = (g: any): UISpace => {
const { lat, lng } = parseGeometry(g?.geometry);
return {
...base,
id: Number(k.id ?? base.id),
name: k.place_name ?? base.name,
// Kakao 검색결과에는 대표사진이 없음 → 더미 유지(또는 플레이스 썸네일 로직 추가 가능)
image: base.image,
rating: base.rating,
distance: base.distance,
tags: base.tags,
isLiked: base.isLiked,

// 상세정보 필드 매핑
address: k.road_address_name || k.address_name || base.address,
phone: k.phone || base.phone,
opening: base.opening, // 카카오는 영업시간 미제공 → 더미 유지
isFree: base.isFree,
id: 0,
name: g?.name ?? initialName,
image: g?.photoUrl ?? "",
rating: 0,
distance: 0,
tags: [],
isLiked: false,

address: g?.formattedAddress ?? "",
phone: g?.internationalPhoneNumber ?? "",
opening: g?.openingHours || g?.secondaryOpeningHours || "",

lat, lng,
};
};

const fromKakao = (k: any): UISpace => ({
id: Number(k.id ?? 0),
name: k.place_name ?? initialName,
image: "",
rating: 0,
distance: 0,
tags: [],
isLiked: false,

address: k.road_address_name || k.address_name || "",
phone: k.phone || "",
opening: "", // 카카오는 운영시간 없음 → 빈칸

lat: k?.y ? Number(k.y) : undefined,
lng: k?.x ? Number(k.x) : undefined,
});

useEffect(() => {
if (!initialName) return;
let cancelled = false;

(async () => {
// 1) 구글 Resolve 먼저 시도 (링크가 있거나, 이름만으로도 시도)
const g = await resolveGooglePlace({ shortUrl: googleMapsLink, name: initialName });
if (!cancelled && g) {
setSpace(fromGoogle(g));
return; // 성공했으면 여기서 끝
}

// 2) 실패 시 카카오 키워드 검색 폴백
if (!initialName) return;
try {
await loadKakaoScript(); // services 로드 필수
await loadKakaoScript();
const ps = new window.kakao.maps.services.Places();

ps.keywordSearch(initialName, (data: any[], status: any) => {
if (cancelled) return;
if (status !== window.kakao.maps.services.Status.OK || !data?.length) return;

// 공백 무시 정확매칭 우선, 없으면 첫 번째
const norm = (s: string) => s.replace(/\s/g, "");
const hit = data.find(d => norm(d.place_name) === norm(initialName)) ?? data[0];

setSpace(toUISpaceFromKakao(hit));
setSpace(fromKakao(hit));
});
} catch (e) {
console.warn("카카오 장소검색 실패:", e);
// 실패 시 더미 유지 (UI 변경 없음)
} catch {
/* 폴백도 실패 → 빈값 유지 */
}
})();
}, [initialName]);

// handleConfirm 수정
return () => { cancelled = true; };
}, [googleMapsLink, initialName]);

// SpaceInfoSimple 이 기대하는 전체 타입으로 표시 직전에 치환
const fullSpaceForView: Space = {
id: space.id ?? 0,
name: space.name ?? "",
image: space.image ?? "",
rating: space.rating ?? 0,
distance: space.distance ?? 0,
tags: space.tags ?? [],
isLiked: space.isLiked ?? false,

address: space.address ?? "",
opening: space.opening ?? "", // 비어 있으면 그대로 빈칸 표시
holiday: space.holiday ?? "",
phone: space.phone ?? "",

// 화면 안 쓰는 필드들 기본값
isFree: (space as any).isFree ?? false,
congestionGraph: (space as any).congestionGraph ?? [],
realTimeCongestion: (space as any).realTimeCongestion ?? 0,
reviews: (space as any).reviews ?? [],
reviewStats: (space as any).reviewStats ?? { total: 0, avg: 0, breakdown: {} },
};

const handleConfirm = () => {
navigate("/admin/init-space-info", {
state: {
placeName: space.name,
spaceFromKakao: space, // 카카오에서 매핑한 공간 데이터 함께 전달
placeName: fullSpaceForView.name,
spaceFromKakao: space, // 좌표/주소/전화 포함(부분 객체)
googleMapsLink,
proposalId,
},
});
};


return (
<div className="min-h-screen bg-white">
<TopHeader title="새 공간 등록" />

<p className="w-[224px] h-[28px] absolute top-[66px] left-1/2 -translate-x-1/2 text-md text-black text-center opacity-100">
이 공간 정보를 등록하시겠습니까?
{"\n"}확인 후 진행해주세요.
이 공간 정보를 등록하시겠습니까?{"\n"}확인 후 진행해주세요.
</p>

<div className="w-full px-[15px] pt-[100px] flex flex-col items-center">
<div className="w-[344px] flex flex-col gap-[10px]">
<h2 className="text-xl font-bold">{space.name}</h2>
<h2 className="text-xl font-bold">{fullSpaceForView.name}</h2>
<div className="border border-gray-300 bg-white rounded-lg p-4">
<SpaceInfoSimple space={space} />
<SpaceInfoSimple space={fullSpaceForView} />
</div>
</div>
</div>

<div className="fixed bottom-[20px] left-1/2 -translate-x-1/2 w-[334px] h-[45px] bg-white flex justify-between gap-[6px]">
<button
onClick={() => navigate(-1)}
className="w-1/2 h-full border border-gray-300 rounded-md text-sm font-semibold text-gray-600 bg-white"
>
<button onClick={() => navigate(-1)} className="w-1/2 h-full border border-gray-300 rounded-md text-sm font-semibold text-gray-600 bg-white">
취소
</button>
<button
onClick={handleConfirm}
className="w-1/2 h-full rounded-md text-sm font-semibold text-white bg-[#4cb1f1]"
>
<button onClick={handleConfirm} className="w-1/2 h-full rounded-md text-sm font-semibold text-white bg-[#4cb1f1]">
확인
</button>
</div>
Expand Down
Loading