diff --git a/src/api/Detail/detail.api.ts b/src/api/Detail/detail.api.ts new file mode 100644 index 0000000..9f5c2c5 --- /dev/null +++ b/src/api/Detail/detail.api.ts @@ -0,0 +1,28 @@ +import api from '../api'; + +export interface KorDetailItem { + title?: string; + firstimage?: string; + firstimage2?: string; + mapx?: string; + mapy?: string; + addr1?: string; + overview?: string; +} + +export interface KorDetailResponse { + header: { resultCode: string; resultMsg: string }; + body: { + items?: { item?: KorDetailItem[] }; + totalCount: number; + pageNo: number; + numOfRows: number; + }; +} + +export async function getKorDetail(contentId: string, pageNo = 1, numOfRows = 1) { + const { data } = await api.get('/kor/detail', { + params: { contentId, pageNo, numOfRows }, + }); + return data?.body?.items?.item?.[0] ?? null; +} diff --git a/src/api/user/auth.api.ts b/src/api/user/auth.api.ts index 8375706..62b8eac 100644 --- a/src/api/user/auth.api.ts +++ b/src/api/user/auth.api.ts @@ -9,9 +9,9 @@ export type LoginResult = { }; export async function loginWithKakaoAccessToken(kakaoAccessToken: string) { - const { data } = await api.post>('/auth/login/kakao', { - accessToken: kakaoAccessToken, - }); + const body = { KakaoTokenRequestDto: { accessToken: kakaoAccessToken } }; + + const { data } = await api.post>('/auth/login/kakao', body); localStorage.setItem('accessToken', data.data.accessToken); localStorage.setItem('refreshToken', data.data.refreshToken); return data; diff --git a/src/pages/ai/TravelSpotDetail.tsx b/src/pages/ai/TravelSpotDetail.tsx index b2ff65d..630ea92 100644 --- a/src/pages/ai/TravelSpotDetail.tsx +++ b/src/pages/ai/TravelSpotDetail.tsx @@ -9,20 +9,18 @@ import { HeartOutline, } from '@/assets'; import type { PlaceDetail } from '@/types/Detail'; -import { mockPlaceDetails } from '@/__mocks/placeDetail.mock'; -import { useEffect, useMemo, useState } from 'react'; -import { Badge, Image, ParkingTable } from '@/component'; +import { getKorDetail, type KorDetailItem } from '@/api/Detail/detail.api'; +import { useEffect, useState } from 'react'; +import { Badge, Image, Loader, ParkingTable } from '@/component'; const TravelSpotDetail = () => { const navigate = useNavigate(); const { contentId = '' } = useParams<{ contentId: string }>(); const formatCount = (n: number, cap = 999) => (n > cap ? `${cap}+` : `${n}`); - //추후 API로 대체 - const data = useMemo( - () => mockPlaceDetails.find((p) => p.id === contentId) ?? null, - [contentId], - ); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [errMsg, setErrMsg] = useState(null); const [liked, setLiked] = useState(false); const [likeCount, setLikeCount] = useState(0); @@ -37,16 +35,72 @@ const TravelSpotDetail = () => { const handleToggleBookmark = () => { setBookmarked((prev) => !prev); }; - useEffect(() => { - if (!data) return; - setLiked(data.liked); - setLikeCount(data.likeCount); - setBookmarked(data.bookmarked); - }, [data]); - if (!data) { - return
해당하는 id의 여행지가 존재하지 않습니다.
; + function mapKorToPlaceDetail(id: string, item: KorDetailItem): PlaceDetail { + const name = item.title ?? ''; + const normalize = (u?: string) => { + const s = u?.trim(); + if (!s) return ''; + return s.startsWith('http://') ? s.replace(/^http:\/\//, 'https://') : s; + }; + const thumbnail = normalize(item.firstimage) || normalize(item.firstimage2) || ''; + const address = item.addr1 ?? ''; + const description = item.overview ?? ''; + + const regionTag = '정보없음'; + return { + id, + name, + thumbnail, + address, + description, + //투두: 서버에 좋아요/북마크 API 붙이면 여기를 갱신 + liked: false, + likeCount: 0, + bookmarked: false, + //투두: 태그/지표는 추후 API 붙이기 + regionTag, + themeTag: '여행지', + serenity: 0, + extra: {}, + }; } + useEffect(() => { + if (!contentId) { + setErrMsg('contentId가 없습니다.'); + setLoading(false); + return; + } + let alive = true; + (async () => { + try { + setLoading(true); + setErrMsg(null); + const item = await getKorDetail(contentId); + if (!alive) return; + + if (!item) { + setErrMsg('해당 여행지 정보를 찾을 수 없습니다.'); + setData(null); + } else { + const mapped = mapKorToPlaceDetail(contentId, item); + setData(mapped); + //좋아요/북마크 초기값 세팅 + setLiked(mapped.liked); + setLikeCount(mapped.likeCount); + setBookmarked(mapped.bookmarked); + } + } catch (e: any) { + setErrMsg(e?.message || '여행지 정보를 불러오지 못했습니다.'); + setData(null); + } finally { + if (alive) setLoading(false); + } + })(); + return () => { + alive = false; + }; + }, [contentId]); return (
@@ -58,74 +112,89 @@ const TravelSpotDetail = () => { 여행지 상세조회
-
-
- {/*썸네일*/} -
- {data.thumbnail ? ( - {data.name} - ) : ( - - )} + {loading ? ( +
+
- {/*이름, 주소*/} -
-
-
{data.name}
-
{data.address}
-
+ ) : errMsg ? ( +
{errMsg}
+ ) : !data ? ( +
해당하는 id의 여행지가 존재하지 않습니다.
+ ) : ( + <> +
+ {/*썸네일*/} +
+ {data.thumbnail ? ( + {data.name} + ) : ( + + )} +
+ {/*이름, 주소*/} +
+
+
{data.name}
+
{data.address}
+
- {/*좋아요, 저장*/} -
- - - {formatCount(likeCount)} - + {/*좋아요, 저장*/} +
+ + + {formatCount(likeCount)} + - + +
+
-
-
- {/*태그 뱃지 영역*/} -
- - {data.regionTag} - - - {data.themeTag} - - - 한적함 - -
- {/*소개...*/} -
-
소개
-
{data.description}
-
- - {/*강릉시 한정 정보*/} - {/*AI 꿀팁 요약*/} - {data.extra?.aiSummary && ( -
-
- AI 꿀팁 요약 - + {/*태그 뱃지 영역*/} +
+ + {data.regionTag} + + + {data.themeTag} + + + 한적함 +
-
{data.extra.aiSummary}
-
- )} - {/*주차장 정보*/} - {data.extra?.parkings && ( - + {/*소개...*/} +
+
소개
+
{data.description}
+
+ + {/*강릉시 한정 정보*/} + {/*AI 꿀팁 요약*/} + {data.extra?.aiSummary && ( +
+
+ AI 꿀팁 요약 + +
+
{data.extra.aiSummary}
+
+ )} + {/*주차장 정보*/} + {data.extra?.parkings && ( + + )} + )}