diff --git a/src/frontend/public/media/floorplans/hall1.jpg b/src/frontend/public/media/floorplans/hall1.jpg new file mode 100644 index 0000000..b4e9953 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall1.jpg differ diff --git a/src/frontend/public/media/floorplans/hall10.jpg b/src/frontend/public/media/floorplans/hall10.jpg new file mode 100644 index 0000000..d9733d5 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall10.jpg differ diff --git a/src/frontend/public/media/floorplans/hall2.jpg b/src/frontend/public/media/floorplans/hall2.jpg new file mode 100644 index 0000000..d0e511b Binary files /dev/null and b/src/frontend/public/media/floorplans/hall2.jpg differ diff --git a/src/frontend/public/media/floorplans/hall3.jpg b/src/frontend/public/media/floorplans/hall3.jpg new file mode 100644 index 0000000..0ecf12c Binary files /dev/null and b/src/frontend/public/media/floorplans/hall3.jpg differ diff --git a/src/frontend/public/media/floorplans/hall4.jpg b/src/frontend/public/media/floorplans/hall4.jpg new file mode 100644 index 0000000..7559618 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall4.jpg differ diff --git a/src/frontend/public/media/floorplans/hall5.jpg b/src/frontend/public/media/floorplans/hall5.jpg new file mode 100644 index 0000000..57714b0 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall5.jpg differ diff --git a/src/frontend/public/media/floorplans/hall6.jpg b/src/frontend/public/media/floorplans/hall6.jpg new file mode 100644 index 0000000..d388b40 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall6.jpg differ diff --git a/src/frontend/public/media/floorplans/hall7.jpg b/src/frontend/public/media/floorplans/hall7.jpg new file mode 100644 index 0000000..8d36d0c Binary files /dev/null and b/src/frontend/public/media/floorplans/hall7.jpg differ diff --git a/src/frontend/public/media/floorplans/hall8.jpg b/src/frontend/public/media/floorplans/hall8.jpg new file mode 100644 index 0000000..e7379b3 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall8.jpg differ diff --git a/src/frontend/public/media/floorplans/hall9.jpg b/src/frontend/public/media/floorplans/hall9.jpg new file mode 100644 index 0000000..8322620 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall9.jpg differ diff --git a/src/frontend/public/media/floorplans/hall_outdoor1.jpg b/src/frontend/public/media/floorplans/hall_outdoor1.jpg new file mode 100644 index 0000000..111056b Binary files /dev/null and b/src/frontend/public/media/floorplans/hall_outdoor1.jpg differ diff --git a/src/frontend/public/media/floorplans/hall_outdoor2.jpg b/src/frontend/public/media/floorplans/hall_outdoor2.jpg new file mode 100644 index 0000000..3a1f402 Binary files /dev/null and b/src/frontend/public/media/floorplans/hall_outdoor2.jpg differ diff --git a/src/frontend/public/media/floorplans/kintex1_overview.jpg b/src/frontend/public/media/floorplans/kintex1_overview.jpg new file mode 100644 index 0000000..a834076 Binary files /dev/null and b/src/frontend/public/media/floorplans/kintex1_overview.jpg differ diff --git a/src/frontend/public/media/floorplans/kintex2_overview.jpg b/src/frontend/public/media/floorplans/kintex2_overview.jpg new file mode 100644 index 0000000..ef45a0c Binary files /dev/null and b/src/frontend/public/media/floorplans/kintex2_overview.jpg differ diff --git a/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx b/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx index e407db0..dd7d540 100644 --- a/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx +++ b/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx @@ -8,7 +8,8 @@ import { Skeleton } from '../../components/ui/States'; import { FloorplanCanvas, type CanvasLayer } from './FloorplanCanvas'; import { AutoLayoutDialog } from './AutoLayoutDialog'; import { ValidationPanel } from './ValidationPanel'; -import { sampleLayout } from './sampleLayout'; +import { sampleLayout, emptyLayout } from './sampleLayout'; +import { hallFloorplanUrl } from './hallFloorplan'; import type { AutoLayoutOption, ComplianceReport, LayoutDto } from '../../api/types'; import './editor.css'; @@ -40,6 +41,10 @@ export function BoothLayoutEditorPage() { if (err instanceof ApiRequestError && err.code === 'NOT_IMPLEMENTED') { return { layout: sampleLayout(eventId, hallId), degraded: true }; } + // 아직 저장된 배치안이 없는 홀(404) — 에러가 아니라 "AI 자동배치로 시작" 빈 캔버스 상태. + if (err instanceof ApiRequestError && err.code === 'NOT_FOUND') { + return { layout: emptyLayout(eventId, hallId), degraded: false }; + } throw err; } }, @@ -174,6 +179,7 @@ export function BoothLayoutEditorPage() { selectedBoothId={selectedBoothId} onSelectBooth={setSelectedBoothId} zoom={zoom} + floorplanUrl={hallFloorplanUrl(hallId)} /> ) )} diff --git a/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx b/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx index e88c065..237fd38 100644 --- a/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx +++ b/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import type { BoothDto } from '../../api/types'; import './canvas.css'; @@ -22,6 +22,8 @@ interface FloorplanCanvasProps { selectedBoothId: string | null; onSelectBooth: (boothId: string | null) => void; zoom: number; + /** 실측 홀 도면(크롤 JPG) 언더레이 URL — 없거나 로드 실패 시 기존 다크 서피스만. */ + floorplanUrl?: string | null; } const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4) @@ -45,8 +47,11 @@ export function FloorplanCanvas({ selectedBoothId, onSelectBooth, zoom, + floorplanUrl, }: FloorplanCanvasProps) { const [hw, hh] = hallDims; + const [drawingOk, setDrawingOk] = useState(true); + useEffect(() => setDrawingOk(true), [floorplanUrl]); const trenchLines = useMemo(() => { const lines: { x1: number; y1: number; x2: number; y2: number }[] = []; @@ -78,6 +83,20 @@ export function FloorplanCanvas({ rx={0.5} /> + {/* 실측 홀 도면 언더레이(크롤 JPG) — 부스/트렌치 아래, 클릭 통과 */} + {floorplanUrl && drawingOk && ( + setDrawingOk(false)} + /> + )} + {/* 트렌치 그리드 */} {layers.trench && ( diff --git a/src/frontend/src/screens/floorplan/canvas.css b/src/frontend/src/screens/floorplan/canvas.css index 7f5c455..a67614e 100644 --- a/src/frontend/src/screens/floorplan/canvas.css +++ b/src/frontend/src/screens/floorplan/canvas.css @@ -17,6 +17,13 @@ stroke-width: 0.6; } +/* 실측 홀 도면(JPG) 언더레이 — 다크 서피스 위 블루프린트 톤, 상호작용 통과 */ +.kx-canvas__drawing { + opacity: 0.3; + filter: saturate(0.35); + pointer-events: none; +} + .kx-canvas__trench line { stroke: var(--color-canvas-line); stroke-width: 0.15; diff --git a/src/frontend/src/screens/floorplan/hallFloorplan.ts b/src/frontend/src/screens/floorplan/hallFloorplan.ts new file mode 100644 index 0000000..ae52199 --- /dev/null +++ b/src/frontend/src/screens/floorplan/hallFloorplan.ts @@ -0,0 +1,13 @@ +/* + * 홀 ID → 실측 홀 도면(크롤 JPG) URL 매핑. + * 에셋은 docs/assets/floorplans 크롤본을 public/media/floorplans 로 빌드 내장(V46 포스터 패턴). + * 매핑 불가한 홀은 null — 캔버스는 기존 다크 서피스만 렌더(FloorplanCanvas onError 폴백과 동일). + */ +const AVAILABLE_HALLS = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + +export function hallFloorplanUrl(hallId: string | null | undefined): string | null { + const m = /^H(\d+)$/i.exec((hallId ?? '').trim()); + if (!m) return null; + const n = Number(m[1]); + return AVAILABLE_HALLS.has(n) ? `/media/floorplans/hall${n}.jpg` : null; +} diff --git a/src/frontend/src/screens/floorplan/sampleLayout.ts b/src/frontend/src/screens/floorplan/sampleLayout.ts index c8790d3..af1d774 100644 --- a/src/frontend/src/screens/floorplan/sampleLayout.ts +++ b/src/frontend/src/screens/floorplan/sampleLayout.ts @@ -5,6 +5,31 @@ import type { BoothDto, LayoutDto } from '../../api/types'; * ★ 실데이터 아님. 계약 shape과 동일 구조로 만들어 실 API 연결 시 그대로 대체된다. * 홀 126×90m 로컬 좌표. 부스 3×3m/6×3m 격자 배치. */ +/** + * 저장된 배치안이 없는 홀(GET 404)용 빈 배치 — 에러 화면 대신 "AI 자동배치로 시작" 상태로 진입. + * (2026-07-14 소유자 "도면을 불러오지 못했습니다" 제보 해소 — 404는 결함이 아니라 신규 시작점.) + */ +export function emptyLayout(eventId: string, hallId: string): LayoutDto { + return { + layoutId: `new-${eventId}-${hallId}`, + eventId, + hallId, + version: 0, + name: '새 배치안', + status: 'draft', + booths: [], + summary: { + boothCount: 0, + targetBoothCount: 0, + salesAreaM2: 0, + minAisleWidthM: 0, + violationBlock: 0, + violationWarn: 0, + }, + updatedAt: new Date().toISOString(), + }; +} + export function sampleLayout(eventId: string, hallId: string): LayoutDto { const booths: BoothDto[] = []; const cols = 12;