diff --git a/src/apis/approveProposal.ts b/src/apis/approveProposal.ts new file mode 100644 index 0000000..f14194b --- /dev/null +++ b/src/apis/approveProposal.ts @@ -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; + } +} diff --git a/src/apis/resolveGooglePlace.ts b/src/apis/resolveGooglePlace.ts new file mode 100644 index 0000000..9be8ce1 --- /dev/null +++ b/src/apis/resolveGooglePlace.ts @@ -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 }; +} \ No newline at end of file diff --git a/src/pages/admin/AdminConfirmSpacePage.tsx b/src/pages/admin/AdminConfirmSpacePage.tsx index f545fa4..e500c7c 100644 --- a/src/pages/admin/AdminConfirmSpacePage.tsx +++ b/src/pages/admin/AdminConfirmSpacePage.tsx @@ -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 & { 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(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({ + 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 (
-

- 이 공간 정보를 등록하시겠습니까? - {"\n"}확인 후 진행해주세요. + 이 공간 정보를 등록하시겠습니까?{"\n"}확인 후 진행해주세요.

-

{space.name}

+

{fullSpaceForView.name}

- +
- -
diff --git a/src/pages/admin/AdminInitSpaceInfoPage.tsx b/src/pages/admin/AdminInitSpaceInfoPage.tsx index 654db41..ae46617 100644 --- a/src/pages/admin/AdminInitSpaceInfoPage.tsx +++ b/src/pages/admin/AdminInitSpaceInfoPage.tsx @@ -1,15 +1,20 @@ -// src/pages/AdminInitSpaceInfoPage.tsx import TopHeader from "../../components/TopHeader"; import FilterSection from "../../components/mapsearch/FilterSection"; import type { TabLabel } from "../../hooks/useSearchFilters"; import { useState } from "react"; import { useLocation, useNavigate } from "react-router-dom"; +import { approveProposal } from "../../apis/approveProposal"; +import { toServerLabel } from "../../utils/label"; const AdminInitSpaceInfoPage = () => { - const location = useLocation() as { state?: { placeName?: string; spaceFromKakao?: any } }; - const spaceFromKakao = location.state?.spaceFromKakao; + const location = useLocation() as { + state?: { placeName?: string; spaceFromKakao?: any; googleMapsLink?: string; proposalId?: number } + }; const navigate = useNavigate(); - const { placeName } = location.state || { placeName: "공간명 없음" }; + + const placeName = location.state?.placeName ?? "공간명 없음"; + const spaceFromKakao = location.state?.spaceFromKakao; + const proposalId = location.state?.proposalId; const [selectedFilters, setSelectedFilters] = useState>({ "이용 목적": [], @@ -18,13 +23,12 @@ const AdminInitSpaceInfoPage = () => { 부가시설: [], 지역: [], }); + const [isFree, setIsFree] = useState(false); const toggleFilter = (category: TabLabel, label: string) => { setSelectedFilters((prev) => { - const hasLabel = prev[category]?.includes(label); - const updated = hasLabel - ? prev[category].filter((item) => item !== label) - : [...(prev[category] || []), label]; + const has = prev[category]?.includes(label); + const updated = has ? prev[category].filter((x) => x !== label) : [...(prev[category] || []), label]; return { ...prev, [category]: updated }; }); }; @@ -37,37 +41,73 @@ const AdminInitSpaceInfoPage = () => { { title: "지역" as TabLabel, labels: ["강남권", "강북권", "도심권", "서남권", "서북권", "동남권", "성동·광진권"] }, ]; - // 등록완료 핸들러에서 localStorage에 저장 - const handleComplete = () => { - if (spaceFromKakao) { - localStorage.setItem( - "admin:newSpaceDraft", - JSON.stringify({ - space: spaceFromKakao, // 카카오에서 받은 공간 - filters: selectedFilters, // 이 페이지에서 고른 필터들 - }) - ); + // TabLabel 키가 "이용목적"/"이용 목적" 혼재할 수 있어 안전하게 꺼내기 + const pick = (a: TabLabel, b: TabLabel) => + (selectedFilters[a] && selectedFilters[a].length ? selectedFilters[a] : selectedFilters[b] || []); + + const handleComplete = async () => { + if (!proposalId) { + alert("proposalId가 없습니다. 요청 목록 → 생성 흐름에서 proposalId를 state로 넘겨주세요."); + return; + } + if (!spaceFromKakao) { + alert("카카오 장소 데이터가 없습니다. 이전 단계에서 다시 시도해주세요."); + return; + } + + const lat = spaceFromKakao?.lat; + const lng = spaceFromKakao?.lng; + + const body = { + googlePlace: { + placeId: "", // 구글 placeId가 없으므로 빈 값(백엔드 허용 가정) + name: placeName, + formattedAddress: spaceFromKakao?.address ?? "", + internationalPhoneNumber: spaceFromKakao?.phone ?? "", + geometry: lat && lng ? `${lat},${lng}` : "", + openingHours: "", // 카카오 검색으로는 영업시간 수집 불가 → 빈 값 + secondaryOpeningHours: "", + photoUrl: spaceFromKakao?.image ?? "", + }, + purpose: pick("이용 목적" as TabLabel, "이용목적" as TabLabel).map(toServerLabel), + type: + pick("공간 종류" as TabLabel, "공간종류" as TabLabel)[0] + ? toServerLabel(pick("공간 종류" as TabLabel, "공간종류" as TabLabel)[0]) + : "", + mood: pick("분위기" as TabLabel, "분위기" as TabLabel).map(toServerLabel), + facilities: pick("부가시설" as TabLabel, "부가시설" as TabLabel).map(toServerLabel), + location: pick("지역" as TabLabel, "지역" as TabLabel).map(toServerLabel), + isFree, + }; + + const ok = await approveProposal(proposalId, body); + if (!ok) { + alert("승인 API 호출에 실패했습니다."); + return; } navigate("/admin/all-requests", { state: { defaultTab: "reviewed" } }); }; return (
- {/* 상단 헤더 */} - {/* 안내 문구 */}

공간 초기 정보를 설정해주세요.

- {/* 장소명 */}

{placeName}

+ {/* 무료 여부 */} +
+ setIsFree(e.target.checked)} /> + +
+ {/* 필터 섹션 */} -
+
{sections.map((section) => ( {
); - }; export default AdminInitSpaceInfoPage; + diff --git a/src/utils/label.ts b/src/utils/label.ts new file mode 100644 index 0000000..5257ce3 --- /dev/null +++ b/src/utils/label.ts @@ -0,0 +1,5 @@ +export const toServerLabel = (s: string) => { + if (s === "Wi-Fi") return "WiFi"; + if (s === "노트북 작업") return "노트북작업"; + return s; +};