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 ? (
-
+
) : 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);
});
});