diff --git a/reports/completion/TSK-013-01-tourism-info-data-consumption.md b/reports/completion/TSK-013-01-tourism-info-data-consumption.md index f3f5583..64dbdc2 100644 --- a/reports/completion/TSK-013-01-tourism-info-data-consumption.md +++ b/reports/completion/TSK-013-01-tourism-info-data-consumption.md @@ -5,18 +5,18 @@ - Scope-ID: `TSK-013-01` - Issue: `#421` - Parent Issue: `#420` -- Branch: `tourism-info-data-consumption-fix` -- Status: merged +- Branch: `tourism-info-data-consumption-fix`, `tourism-contract-field-consumption` +- Status: corrective PR ready ## Summary -KTO tourism place data is now consumed by the information sheet beyond title/address. The sheet renders image, summary, description, category, district, address, source, and homepage link when those fields are present. The tourism places request also receives an abort signal so a stalled `/api/tourism/places` response cannot leave the UI in an endless loading state. +KTO tourism place data is now consumed by the information sheet beyond name/address. The sheet renders image, summary, description, KTO content type label, district, address/roadAddress, source, and sourcePageUrl when those fields are present. The tourism places request also receives an abort signal so a stalled `/api/tourism/places` response cannot leave the UI in an endless loading state. ## Architecture Boundary Gate - Responsibility map: `tourismClient` owns the Worker consumer request; `useAppCoordinatorEffects` owns UI request lifecycle and timeout; `TourismInfoSheet` owns presentation of already-normalized `TourismPlaceItem` data. - Dependency direction: UI effect -> API client -> `fetchJson`; presentation component -> `TourismPlaceItem`; no browser code calls KTO/OpenAPI, Supabase, or admin import APIs directly. -- Test seam: unit tests assert rendered KTO fields at component boundary and `RequestInit.signal` forwarding at API client boundary. +- Test seam: unit tests assert rendered KTO contract fields at component boundary and `RequestInit.signal` forwarding at API client boundary. - Scope map: frontend data consumption and timeout guard only. API path, response shape, DB schema, OAuth flow, and curated place review/stamp flow are unchanged. - Architecture risk: adding UI fields can accidentally turn KTO information pins into curated place actions. The component continues to render information-only content without stamp/review/feed actions. @@ -36,8 +36,10 @@ KTO tourism place data is now consumed by the information sheet beyond title/add ## Remote Evidence -- PR: [#422](https://github.com/STH-1-Class-One-Group/JamIssue/pull/422) -- Main merge SHA: `729cbe8764f59f4ae7c9a3a845de9e0a6a53f83f` +- Initial PR: [#422](https://github.com/STH-1-Class-One-Group/JamIssue/pull/422) +- Initial main merge SHA: `729cbe8764f59f4ae7c9a3a845de9e0a6a53f83f` +- Corrective PR: TBD +- Corrective main merge SHA: TBD - CI: [27458352463](https://github.com/STH-1-Class-One-Group/JamIssue/actions/runs/27458352463) - production-smoke: [27458352467](https://github.com/STH-1-Class-One-Group/JamIssue/actions/runs/27458352467) - CodeQL: [27458352230](https://github.com/STH-1-Class-One-Group/JamIssue/actions/runs/27458352230) diff --git a/src/components/TourismInfoSheet.tsx b/src/components/TourismInfoSheet.tsx index d598caf..3020c9b 100644 --- a/src/components/TourismInfoSheet.tsx +++ b/src/components/TourismInfoSheet.tsx @@ -22,6 +22,22 @@ interface TourismInfoSheetProps { onCollapse: () => void; } +function getTourismPlaceTitle(place: TourismPlaceItem) { + return place.name || place.title || '관광정보'; +} + +function getTourismPlaceAddress(place: TourismPlaceItem) { + return place.address || place.roadAddress || null; +} + +function getTourismPlaceCategoryLabel(place: TourismPlaceItem) { + return place.ktoContentTypeLabel || place.category || place.ktoFacet || null; +} + +function getTourismPlaceSourceUrl(place: TourismPlaceItem) { + return place.sourcePageUrl || place.homepageUrl || null; +} + export function TourismInfoSheet({ place, isOpen, @@ -37,6 +53,10 @@ export function TourismInfoSheet({ const drawerState: DrawerState = sheetState === 'full' ? 'full' : 'partial'; const mapSheetState: MapSheetState = sheetState === 'full' ? 'full' : 'peek'; const sheetClassName = buildMapSheetClassName('place-drawer', mapSheetState, drawerState); + const title = getTourismPlaceTitle(place); + const address = getTourismPlaceAddress(place); + const categoryLabel = getTourismPlaceCategoryLabel(place); + const sourceUrl = getTourismPlaceSourceUrl(place); const primaryDescription = place.description && place.description !== place.summary ? place.description : null; const summary = place.summary || primaryDescription || '관광지 기본 정보를 확인할 수 있어요.'; @@ -54,14 +74,14 @@ export function TourismInfoSheet({
{place.imageUrl ? (
- {`${place.title} + {`${title}
) : null}

KTO INFO

-

{place.title}

+

{title}

{summary}

- {place.category ? {place.category} : null} + {categoryLabel ? {categoryLabel} : null} {place.district ? {place.district} : null}
@@ -83,14 +103,14 @@ export function TourismInfoSheet({ ) : null}
위치 -

{place.address || '주소 정보가 아직 제공되지 않았어요.'}

+

{address || '주소 정보가 아직 제공되지 않았어요.'}

출처

{place.sourceName || 'KTO 관광정보'}

- {place.homepageUrl ? ( - + {sourceUrl ? ( + 자세히 보기 ) : null} diff --git a/src/tourismTypes.ts b/src/tourismTypes.ts index 054d505..bcf58e8 100644 --- a/src/tourismTypes.ts +++ b/src/tourismTypes.ts @@ -8,7 +8,9 @@ */ export interface TourismFacetOption { - key: string; + key?: string; + id?: string; + name?: string; label: string; count: number; } @@ -28,16 +30,23 @@ export interface TourismCuratedPlaceLink { export interface TourismPlaceItem { id: string; - title: string; + name: string; + title?: string; category: string | null; + ktoContentTypeId?: string | null; + ktoContentTypeLabel?: string | null; + ktoFacet?: string | null; district: string | null; address: string | null; + roadAddress?: string | null; summary: string | null; description: string | null; latitude: number | null; longitude: number | null; imageUrl: string | null; - homepageUrl: string | null; + sourcePageUrl?: string | null; + homepageUrl?: string | null; + sourceUpdatedAt?: string | null; sourceName: string | null; isCurated: boolean; curatedPlace: TourismCuratedPlaceLink | null; diff --git a/test/e2e/tourism-map-layer.spec.ts b/test/e2e/tourism-map-layer.spec.ts index 8181c3f..897b2fd 100644 --- a/test/e2e/tourism-map-layer.spec.ts +++ b/test/e2e/tourism-map-layer.spec.ts @@ -1,19 +1,24 @@ import { expect, test } from '@playwright/test'; -import { createE2EAppState, installApiFixtures } from './fixtures'; import type { TourismPlaceItem } from '../../src/tourismTypes'; +import { createE2EAppState, installApiFixtures } from './fixtures'; const tourismPlace: TourismPlaceItem = { id: 'tourism-1', - title: 'KTO 정보 장소', - category: '관광지', + name: 'KTO 정보 장소', + category: 'tourism', + ktoContentTypeId: '12', + ktoContentTypeLabel: '관광지', + ktoFacet: 'tourism', district: '중구', - address: '대전 중구 테스트로 1', + address: null, + roadAddress: '대전 중구 테스트로 1', summary: 'KTO 정보성 장소입니다.', description: null, latitude: 36.35, longitude: 127.38, imageUrl: null, - homepageUrl: 'https://example.com', + sourcePageUrl: 'https://example.com', + sourceUpdatedAt: null, sourceName: 'KTO', isCurated: false, curatedPlace: null, diff --git a/test/unit/tourism-info-sheet.test.tsx b/test/unit/tourism-info-sheet.test.tsx index 6893178..d8047af 100644 --- a/test/unit/tourism-info-sheet.test.tsx +++ b/test/unit/tourism-info-sheet.test.tsx @@ -4,24 +4,29 @@ import { TourismInfoSheet } from '../../src/components/TourismInfoSheet'; import type { TourismPlaceItem } from '../../src/tourismTypes'; const tourismPlace: TourismPlaceItem = { - id: 'tourism-1', - title: '대전근현대사전시관', - category: '문화관광', - district: '중구', - address: '대전 중구 중앙로 101', - summary: '옛 충남도청 건물을 활용한 전시 공간입니다.', - description: '근현대 대전의 전시 변화를 전시와 사진 자료로 볼 수 있습니다.', - latitude: 36.327, - longitude: 127.421, + id: 'kto-content-1932079', + name: '굿모닝레지던스호텔휴', + category: 'lodging', + ktoContentTypeId: '32', + ktoContentTypeLabel: '숙박', + ktoFacet: 'lodging', + district: '서구', + address: null, + roadAddress: '대전 서구 둔산로73번길 21', + summary: '대전 서구에 있는 숙박 관광자원입니다.', + description: '객실과 편의시설을 갖춘 레지던스형 숙박시설입니다.', + latitude: 36.35, + longitude: 127.38, imageUrl: 'https://example.com/tourism.jpg', - homepageUrl: 'https://example.com/tourism', - sourceName: 'KTO', + sourcePageUrl: 'https://example.com/tourism', + sourceUpdatedAt: '2025-09-22T01:56:03+00:00', + sourceName: 'KTO TourAPI Daejeon Tourism', isCurated: false, curatedPlace: null, }; describe('TourismInfoSheet', () => { - it('renders the available KTO tourism data instead of only title and address', () => { + it('renders the available KTO tourism contract fields instead of only name and address', () => { render( { />, ); - expect(screen.getByRole('img', { name: '대전근현대사전시관 관광정보 이미지' })).toHaveAttribute( + expect(screen.getByRole('img', { name: '굿모닝레지던스호텔휴 관광정보 이미지' })).toHaveAttribute( 'src', tourismPlace.imageUrl, ); - expect(screen.getByText('옛 충남도청 건물을 활용한 전시 공간입니다.')).toBeInTheDocument(); - expect(screen.getByText('근현대 대전의 전시 변화를 전시와 사진 자료로 볼 수 있습니다.')).toBeInTheDocument(); - expect(screen.getByText('문화관광')).toBeInTheDocument(); - expect(screen.getByText('중구')).toBeInTheDocument(); - expect(screen.getByText('대전 중구 중앙로 101')).toBeInTheDocument(); - expect(screen.getByText('KTO')).toBeInTheDocument(); - expect(screen.getByRole('link', { name: '자세히 보기' })).toHaveAttribute('href', tourismPlace.homepageUrl); + expect(screen.getByRole('heading', { name: '굿모닝레지던스호텔휴' })).toBeInTheDocument(); + expect(screen.getByText('대전 서구에 있는 숙박 관광자원입니다.')).toBeInTheDocument(); + expect(screen.getByText('객실과 편의시설을 갖춘 레지던스형 숙박시설입니다.')).toBeInTheDocument(); + expect(screen.getByText('숙박')).toBeInTheDocument(); + expect(screen.getByText('서구')).toBeInTheDocument(); + expect(screen.getByText('대전 서구 둔산로73번길 21')).toBeInTheDocument(); + expect(screen.getByText('KTO TourAPI Daejeon Tourism')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: '자세히 보기' })).toHaveAttribute('href', tourismPlace.sourcePageUrl); }); });