Compare commits
2 Commits
7b861a4af2
...
8719d110f3
| Author | SHA1 | Date | |
|---|---|---|---|
| 8719d110f3 | |||
| 7a735a4c71 |
BIN
src/frontend/public/media/floorplans/hall1.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
src/frontend/public/media/floorplans/hall10.jpg
Normal file
|
After Width: | Height: | Size: 87 KiB |
BIN
src/frontend/public/media/floorplans/hall2.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
src/frontend/public/media/floorplans/hall3.jpg
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
src/frontend/public/media/floorplans/hall4.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
src/frontend/public/media/floorplans/hall5.jpg
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
src/frontend/public/media/floorplans/hall6.jpg
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
src/frontend/public/media/floorplans/hall7.jpg
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
src/frontend/public/media/floorplans/hall8.jpg
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
src/frontend/public/media/floorplans/hall9.jpg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
src/frontend/public/media/floorplans/hall_outdoor1.jpg
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
src/frontend/public/media/floorplans/hall_outdoor2.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
src/frontend/public/media/floorplans/kintex1_overview.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
src/frontend/public/media/floorplans/kintex2_overview.jpg
Normal file
|
After Width: | Height: | Size: 121 KiB |
@ -3,8 +3,9 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
/* 페이지-영역 스크롤(.kx-page 모델). 고정 100% 높이 셸을 제거해 표가 잘리지 않고
|
||||||
min-height: 0;
|
* 내용 높이만큼 늘어나며, 넘칠 때만 이 루트가 스크롤(내부 그리드 스크롤 없음). */
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
.kx-aq__head {
|
.kx-aq__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -99,16 +100,16 @@
|
|||||||
.kx-aq__body {
|
.kx-aq__body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
/* overflow:hidden 제거 — 본문이 내용 높이만큼 늘어나고 루트(.kx-aq)가 스크롤. */
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
.kx-aq__main {
|
.kx-aq__main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
padding: var(--space-4) var(--space-5);
|
padding: var(--space-4) var(--space-5);
|
||||||
overflow: auto;
|
/* overflow:auto 제거 — 표 내부 세로 스크롤 대신 페이지 흐름으로 전체 노출. */
|
||||||
}
|
}
|
||||||
.kx-aq__main-head {
|
.kx-aq__main-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -180,13 +181,17 @@
|
|||||||
/* side */
|
/* side */
|
||||||
.kx-aq__side {
|
.kx-aq__side {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
|
flex: none;
|
||||||
border-left: var(--border-card);
|
border-left: var(--border-card);
|
||||||
background: var(--color-neutral-050);
|
background: var(--color-neutral-050);
|
||||||
padding: var(--space-5);
|
padding: var(--space-5);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
overflow-y: auto;
|
/* 표 높이에 스트레치되지 않고, 페이지 스크롤 시 상단에 고정 유지. */
|
||||||
|
align-self: flex-start;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
.kx-aq__card {
|
.kx-aq__card {
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
|
|||||||
@ -8,7 +8,8 @@ import { Skeleton } from '../../components/ui/States';
|
|||||||
import { FloorplanCanvas, type CanvasLayer } from './FloorplanCanvas';
|
import { FloorplanCanvas, type CanvasLayer } from './FloorplanCanvas';
|
||||||
import { AutoLayoutDialog } from './AutoLayoutDialog';
|
import { AutoLayoutDialog } from './AutoLayoutDialog';
|
||||||
import { ValidationPanel } from './ValidationPanel';
|
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 type { AutoLayoutOption, ComplianceReport, LayoutDto } from '../../api/types';
|
||||||
import './editor.css';
|
import './editor.css';
|
||||||
|
|
||||||
@ -40,6 +41,10 @@ export function BoothLayoutEditorPage() {
|
|||||||
if (err instanceof ApiRequestError && err.code === 'NOT_IMPLEMENTED') {
|
if (err instanceof ApiRequestError && err.code === 'NOT_IMPLEMENTED') {
|
||||||
return { layout: sampleLayout(eventId, hallId), degraded: true };
|
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;
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -174,6 +179,7 @@ export function BoothLayoutEditorPage() {
|
|||||||
selectedBoothId={selectedBoothId}
|
selectedBoothId={selectedBoothId}
|
||||||
onSelectBooth={setSelectedBoothId}
|
onSelectBooth={setSelectedBoothId}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
|
floorplanUrl={hallFloorplanUrl(hallId)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import type { BoothDto } from '../../api/types';
|
import type { BoothDto } from '../../api/types';
|
||||||
import './canvas.css';
|
import './canvas.css';
|
||||||
|
|
||||||
@ -22,6 +22,8 @@ interface FloorplanCanvasProps {
|
|||||||
selectedBoothId: string | null;
|
selectedBoothId: string | null;
|
||||||
onSelectBooth: (boothId: string | null) => void;
|
onSelectBooth: (boothId: string | null) => void;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
|
/** 실측 홀 도면(크롤 JPG) 언더레이 URL — 없거나 로드 실패 시 기존 다크 서피스만. */
|
||||||
|
floorplanUrl?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4)
|
||||||
@ -45,8 +47,11 @@ export function FloorplanCanvas({
|
|||||||
selectedBoothId,
|
selectedBoothId,
|
||||||
onSelectBooth,
|
onSelectBooth,
|
||||||
zoom,
|
zoom,
|
||||||
|
floorplanUrl,
|
||||||
}: FloorplanCanvasProps) {
|
}: FloorplanCanvasProps) {
|
||||||
const [hw, hh] = hallDims;
|
const [hw, hh] = hallDims;
|
||||||
|
const [drawingOk, setDrawingOk] = useState(true);
|
||||||
|
useEffect(() => setDrawingOk(true), [floorplanUrl]);
|
||||||
|
|
||||||
const trenchLines = useMemo(() => {
|
const trenchLines = useMemo(() => {
|
||||||
const lines: { x1: number; y1: number; x2: number; y2: number }[] = [];
|
const lines: { x1: number; y1: number; x2: number; y2: number }[] = [];
|
||||||
@ -78,6 +83,20 @@ export function FloorplanCanvas({
|
|||||||
rx={0.5}
|
rx={0.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 실측 홀 도면 언더레이(크롤 JPG) — 부스/트렌치 아래, 클릭 통과 */}
|
||||||
|
{floorplanUrl && drawingOk && (
|
||||||
|
<image
|
||||||
|
href={floorplanUrl}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={hw}
|
||||||
|
height={hh}
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
className="kx-canvas__drawing"
|
||||||
|
onError={() => setDrawingOk(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 트렌치 그리드 */}
|
{/* 트렌치 그리드 */}
|
||||||
{layers.trench && (
|
{layers.trench && (
|
||||||
<g className="kx-canvas__trench">
|
<g className="kx-canvas__trench">
|
||||||
|
|||||||
@ -17,6 +17,13 @@
|
|||||||
stroke-width: 0.6;
|
stroke-width: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 실측 홀 도면(JPG) 언더레이 — 다크 서피스 위 블루프린트 톤, 상호작용 통과 */
|
||||||
|
.kx-canvas__drawing {
|
||||||
|
opacity: 0.3;
|
||||||
|
filter: saturate(0.35);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.kx-canvas__trench line {
|
.kx-canvas__trench line {
|
||||||
stroke: var(--color-canvas-line);
|
stroke: var(--color-canvas-line);
|
||||||
stroke-width: 0.15;
|
stroke-width: 0.15;
|
||||||
|
|||||||
13
src/frontend/src/screens/floorplan/hallFloorplan.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -5,6 +5,31 @@ import type { BoothDto, LayoutDto } from '../../api/types';
|
|||||||
* ★ 실데이터 아님. 계약 shape과 동일 구조로 만들어 실 API 연결 시 그대로 대체된다.
|
* ★ 실데이터 아님. 계약 shape과 동일 구조로 만들어 실 API 연결 시 그대로 대체된다.
|
||||||
* 홀 126×90m 로컬 좌표. 부스 3×3m/6×3m 격자 배치.
|
* 홀 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 {
|
export function sampleLayout(eventId: string, hallId: string): LayoutDto {
|
||||||
const booths: BoothDto[] = [];
|
const booths: BoothDto[] = [];
|
||||||
const cols = 12;
|
const cols = 12;
|
||||||
|
|||||||