diff --git a/docs/OWNER_FEEDBACK.md b/docs/OWNER_FEEDBACK.md index c89061b..5b9cb25 100644 --- a/docs/OWNER_FEEDBACK.md +++ b/docs/OWNER_FEEDBACK.md @@ -340,3 +340,7 @@ CAD 레이어 `5-트랜치`(표기 함정 — 트렌치≠트랜치)에서 K1 ### 세션 5 추가 — 기둥 추출·자동배치 회피 (2026-07-15, 소유자 지시 "기둥 추출해서 자동배치 회피 반영해") V62: hall_pillar 74개 시드 — K1은 CAD 4-COL-H 추출(공식 주석 54/117m와 일치 검증), K2 H7~H10은 공식 도면 치수 주석(midline 내부 기둥, ø2.3m), H6 미시드(근거 없음). 부수 교정: **V61 트렌치 y축 반전 보정**(도면 상단 y=0 기준 정합 — 캔버스/DWG 언더레이와 상하 일치). 자동배치는 기둥 반경+0.6m를 예약 영역으로 회피, 캔버스 구조 레이어에 기둥 원 표시. 게이트: gradle test·tsc·vite·XML 전부 통과 → push→배포. + +### 세션 5 추가 — V63 벽·계단·엘리베이터·문 인식 (2026-07-15, 소유자 지시 "V63 벽·계단 인식 진행해" + "엘리베이터,문,DRYWALL 까지 함께") + +hall_obstacle 60개 시드(K1 H1~H5) — 1m 셀 래스터·연결성분·행 스트립 분해로 벽/계단/엘리베이터/문 실측 추출. 자동배치가 문 1.0m·기타 0.3m 버퍼로 회피, 캔버스 구조 레이어 사각 표시. K2는 레이어 구조 상이로 보류(한계 기록). 게이트: gradle test·tsc·vite·XML 전부 통과 → push→배포. diff --git a/docs/analysis/cad-extraction.md b/docs/analysis/cad-extraction.md index f4870b4..216718e 100644 --- a/docs/analysis/cad-extraction.md +++ b/docs/analysis/cad-extraction.md @@ -107,3 +107,13 @@ H7/H8 블록 252m=126×2·폭 90m 정확 일치) + 회전 프로브 렌더 시 (RT 포인트가 CAD y 미반전으로 시드됨 — 도면 상단 y=0 기준으로 63-y 반전, 생성기도 정본 수정). - **엔진**: autoGenerate가 기둥 반경+0.6m 버퍼를 예약 영역으로 — 겹치는 부스 셀 배치 제외. - **캔버스**: 구조 레이어(기둥·비상구 토글)에 기둥 원 표시(HallInfo.pillars 계약 확장, NON_NULL 호환). + +## V63 벽·계단·엘리베이터·문 장애물 인식 (2026-07-15) + +- **추출(K1 H1~H5)**: 장애물 레이어(WALL/DRYWALL/조적/석고/판넬·STAIR/계단·ELEVATOR·DOOR/출입구)를 + 1m 셀 래스터 → 연결 성분 → **행 스트립 분해**(L자 벽 런의 bbox 뻥튀기 방지) → 60개 사각 영역. + 좌측 벽선(x0~1m)·우측 에지 문(x169~171)·H1 하단 부속실·H5 상단 부속실 벽 등. +- **V63**: hall_obstacle(kind: stair|elevator|door|wall) + 시드. 자동배치 회피 버퍼: 문 1.0m(접근 + 클리어)·기타 0.3m. 캔버스 구조 레이어에 사각 표시(문=녹색톤 구분). +- **한계**: ①벽선 기반 — 밀폐 실내 채움 아님(단, 외곽 여유 5m+벽 스트립 차단으로 실배치 유입 방지) + ②K2(H6~10)는 장애물 레이어 구조 상이(석고보드·경량철골 등 xref 평탄화)로 신뢰 추출 보류. diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java index dfc0d13..814e7fe 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java @@ -299,6 +299,13 @@ public class FloorplanServiceImpl implements FloorplanService { pillarZones.add(new double[]{p.x() - buf, p.y() - buf, p.x() + buf, p.y() + buf}); } } + // 벽·계단·엘리베이터·문 회피(V63 실측) — 문은 접근 클리어 1.0m, 그 외 0.3m 여유. + if (hall.obstacles() != null) { + for (HallInfo.Obstacle o : hall.obstacles()) { + double buf = "door".equals(o.kind()) ? 1.0 : 0.3; + pillarZones.add(new double[]{o.x0() - buf, o.y0() - buf, o.x1() + buf, o.y1() + buf}); + } + } List options = new ArrayList<>(); for (int i = 0; i < optionCount; i++) { @@ -592,9 +599,18 @@ public class FloorplanServiceImpl implements FloorplanService { nz(dbl(p.get("x"))), nz(dbl(p.get("y"))), nz(dbl(p.get("radiusM"))))); } } + List obstacles = new ArrayList<>(); + List> obs = hallMapper.findObstacles(hallId); + if (obs != null) { + for (Map o : obs) { + obstacles.add(new HallInfo.Obstacle(str(o.get("obstacleId")), str(o.get("kind")), + nz(dbl(o.get("x0"))), nz(dbl(o.get("y0"))), + nz(dbl(o.get("x1"))), nz(dbl(o.get("y1"))))); + } + } return new HallInfo(str(h.get("hallId")), str(h.get("label")), dims, dbl(h.get("heightM")), intOrNull(h.get("boothCapacity")), dbl(h.get("floorLoadTPerM2")), - Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits, pillars); + Boolean.TRUE.equals(bool(h.get("assumedTrench"))), trenches, exits, pillars, obstacles); } private int nextVersion(String eventId, String hallId) { diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/HallInfo.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/HallInfo.java index 871c130..0009787 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/HallInfo.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/HallInfo.java @@ -29,9 +29,20 @@ public record HallInfo( boolean assumedTrench, List trenches, List exits, - List pillars + List pillars, + List obstacles ) { + /** + * 벽·계단·엘리베이터·문 장애물(V63 hall_obstacle, CAD 실측 스트립) — 자동배치 회피·캔버스 표시. + * + * @param obstacleId 장애물 식별자 + * @param kind stair | elevator | door | wall + * @param x0,y0,x1,y1 홀 로컬 사각 영역(m, y=0 도면 상단) + */ + public record Obstacle(String obstacleId, String kind, double x0, double y0, double x1, double y1) { + } + /** * 기둥(구조 장애물) — 자동배치 회피·캔버스 표시 근거(V62 hall_pillar, CAD/공식 도면 실측). * diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/HallMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/HallMapper.java index 07012e6..1a8541e 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/HallMapper.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/HallMapper.java @@ -35,4 +35,7 @@ public interface HallMapper { /** 홀 기둥(V62 hall_pillar, 실측) — 자동배치 회피·캔버스 표시. */ List> findPillars(@Param("hallId") String hallId); + + /** 벽·계단·엘리베이터·문 장애물(V63 hall_obstacle, 실측) — 자동배치 회피·캔버스 표시. */ + List> findObstacles(@Param("hallId") String hallId); } diff --git a/src/backend/src/main/resources/db/migration/V63__hall_obstacle_seed.sql b/src/backend/src/main/resources/db/migration/V63__hall_obstacle_seed.sql new file mode 100644 index 0000000..ecce77b --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V63__hall_obstacle_seed.sql @@ -0,0 +1,78 @@ +-- V63: 벽·계단·엘리베이터 장애물 실측 시드(hall_obstacle) — 자동배치 회피 근거(소유자 지시 2026-07-15). +-- 생성기: tools/cad/extract_obstacle_seed.py (1m 셀 래스터 연결성분 bbox, 결정적 재생성). +-- K1 H1~H5 = CAD 추출. K2 = 장애물 레이어 신뢰 추출 불가(도면 구조 상이) — 미시드(한계, cad-extraction.md). + +CREATE TABLE IF NOT EXISTS hall_obstacle ( + id varchar(60) PRIMARY KEY, -- 예: H1-OB-1 + hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE, + kind varchar(20) NOT NULL, -- stair | elevator | door | wall(부속실·DRYWALL 포함) + x0 numeric(8,2) NOT NULL, y0 numeric(8,2) NOT NULL, + x1 numeric(8,2) NOT NULL, y1 numeric(8,2) NOT NULL -- 홀 로컬 m(y=0 도면 상단) +); +CREATE INDEX IF NOT EXISTS idx_hall_obstacle_hall ON hall_obstacle (hall_id); + +DELETE FROM hall_obstacle WHERE id LIKE 'H%-OB-%'; +INSERT INTO hall_obstacle (id, hall_id, kind, x0, y0, x1, y1) +VALUES + ('H1-OB-1', 'H1', 'wall', 0.0, 0.0, 1.0, 12.0), + ('H1-OB-2', 'H1', 'wall', 0.0, 14.0, 1.0, 27.0), + ('H1-OB-3', 'H1', 'door', 0.0, 34.0, 1.0, 52.0), + ('H1-OB-4', 'H1', 'door', 0.0, 52.0, 19.0, 54.0), + ('H1-OB-5', 'H1', 'door', 2.0, 61.0, 9.0, 62.0), + ('H1-OB-6', 'H1', 'door', 3.0, 62.0, 4.0, 63.0), + ('H1-OB-7', 'H1', 'door', 5.0, 62.0, 7.0, 63.0), + ('H1-OB-8', 'H1', 'door', 8.0, 54.0, 9.0, 61.0), + ('H1-OB-9', 'H1', 'door', 8.0, 62.0, 9.0, 63.0), + ('H1-OB-10', 'H1', 'door', 17.0, 54.0, 19.0, 61.0), + ('H1-OB-11', 'H1', 'door', 17.0, 61.0, 65.0, 62.0), + ('H1-OB-12', 'H1', 'door', 17.0, 62.0, 67.0, 63.0), + ('H1-OB-13', 'H1', 'wall', 70.0, 61.0, 84.0, 62.0), + ('H1-OB-14', 'H1', 'wall', 70.0, 62.0, 88.0, 63.0), + ('H1-OB-15', 'H1', 'wall', 89.0, 61.0, 91.0, 63.0), + ('H1-OB-16', 'H1', 'door', 96.0, 61.0, 104.0, 62.0), + ('H1-OB-17', 'H1', 'door', 96.0, 62.0, 105.0, 63.0), + ('H1-OB-18', 'H1', 'door', 106.0, 61.0, 137.0, 63.0), + ('H1-OB-19', 'H1', 'wall', 142.0, 61.0, 171.0, 63.0), + ('H1-OB-20', 'H1', 'door', 169.0, 0.0, 171.0, 1.0), + ('H1-OB-21', 'H1', 'door', 170.0, 1.0, 171.0, 2.0), + ('H1-OB-22', 'H1', 'wall', 170.0, 7.0, 171.0, 19.0), + ('H1-OB-23', 'H1', 'door', 170.0, 22.0, 171.0, 40.0), + ('H1-OB-24', 'H1', 'wall', 170.0, 41.0, 171.0, 55.0), + ('H2-OB-1', 'H2', 'wall', 0.0, 0.0, 1.0, 21.0), + ('H2-OB-2', 'H2', 'wall', 0.0, 39.0, 1.0, 63.0), + ('H2-OB-3', 'H2', 'door', 169.0, 0.0, 171.0, 1.0), + ('H2-OB-4', 'H2', 'door', 170.0, 1.0, 171.0, 2.0), + ('H2-OB-5', 'H2', 'door', 170.0, 7.0, 171.0, 28.0), + ('H2-OB-6', 'H2', 'door', 170.0, 31.0, 171.0, 49.0), + ('H2-OB-7', 'H2', 'wall', 170.0, 50.0, 171.0, 54.0), + ('H2-OB-8', 'H2', 'door', 170.0, 59.0, 171.0, 63.0), + ('H3-OB-1', 'H3', 'wall', 0.0, 0.0, 1.0, 21.0), + ('H3-OB-2', 'H3', 'wall', 0.0, 39.0, 1.0, 63.0), + ('H3-OB-3', 'H3', 'door', 169.0, 0.0, 171.0, 1.0), + ('H3-OB-4', 'H3', 'door', 170.0, 1.0, 171.0, 2.0), + ('H3-OB-5', 'H3', 'door', 170.0, 7.0, 171.0, 28.0), + ('H3-OB-6', 'H3', 'door', 170.0, 31.0, 171.0, 49.0), + ('H3-OB-7', 'H3', 'wall', 170.0, 50.0, 171.0, 54.0), + ('H3-OB-8', 'H3', 'door', 170.0, 59.0, 171.0, 63.0), + ('H4-OB-1', 'H4', 'wall', 0.0, 0.0, 1.0, 21.0), + ('H4-OB-2', 'H4', 'wall', 0.0, 39.0, 1.0, 63.0), + ('H4-OB-3', 'H4', 'door', 169.0, 0.0, 171.0, 1.0), + ('H4-OB-4', 'H4', 'door', 170.0, 1.0, 171.0, 2.0), + ('H4-OB-5', 'H4', 'door', 170.0, 7.0, 171.0, 20.0), + ('H4-OB-6', 'H4', 'wall', 170.0, 22.0, 171.0, 28.0), + ('H4-OB-7', 'H4', 'door', 170.0, 31.0, 171.0, 49.0), + ('H4-OB-8', 'H4', 'wall', 170.0, 50.0, 171.0, 54.0), + ('H4-OB-9', 'H4', 'door', 170.0, 59.0, 171.0, 63.0), + ('H5-OB-1', 'H5', 'wall', 0.0, 7.0, 19.0, 9.0), + ('H5-OB-2', 'H5', 'wall', 0.0, 9.0, 1.0, 21.0), + ('H5-OB-3', 'H5', 'wall', 0.0, 24.0, 1.0, 27.0), + ('H5-OB-4', 'H5', 'wall', 0.0, 34.0, 1.0, 63.0), + ('H5-OB-5', 'H5', 'wall', 17.0, 0.0, 19.0, 7.0), + ('H5-OB-6', 'H5', 'door', 170.0, 0.0, 171.0, 3.0), + ('H5-OB-7', 'H5', 'wall', 170.0, 6.0, 171.0, 20.0), + ('H5-OB-8', 'H5', 'wall', 170.0, 22.0, 171.0, 28.0), + ('H5-OB-9', 'H5', 'door', 170.0, 31.0, 171.0, 49.0), + ('H5-OB-10', 'H5', 'wall', 170.0, 50.0, 171.0, 54.0), + ('H5-OB-11', 'H5', 'door', 170.0, 59.0, 171.0, 63.0) +ON CONFLICT (id) DO UPDATE SET kind = EXCLUDED.kind, + x0 = EXCLUDED.x0, y0 = EXCLUDED.y0, x1 = EXCLUDED.x1, y1 = EXCLUDED.y1; diff --git a/src/backend/src/main/resources/mybatis/mapper/HallMapper.xml b/src/backend/src/main/resources/mybatis/mapper/HallMapper.xml index 6dc59b2..acc4db6 100644 --- a/src/backend/src/main/resources/mybatis/mapper/HallMapper.xml +++ b/src/backend/src/main/resources/mybatis/mapper/HallMapper.xml @@ -93,4 +93,14 @@ ORDER BY id + + + diff --git a/src/frontend/src/api/types.ts b/src/frontend/src/api/types.ts index 8349e2a..33224e9 100644 --- a/src/frontend/src/api/types.ts +++ b/src/frontend/src/api/types.ts @@ -170,6 +170,15 @@ export interface HallPillarDto { y: number; radiusM: number; } +/** 벽·계단·엘리베이터·문 장애물(V63 실측 스트립) — 자동배치 회피·캔버스 표시. */ +export interface HallObstacleDto { + obstacleId: string; + kind: 'stair' | 'elevator' | 'door' | 'wall'; + x0: number; + y0: number; + x1: number; + y1: number; +} /** 홀 마스터 정보(M2 LayoutDto.hall) — 캔버스 축척·경계 원천. dimsM=[폭, 깊이](m). */ export interface HallInfoDto { hallId: string; @@ -178,6 +187,7 @@ export interface HallInfoDto { ceilingM?: number | null; targetBoothCount?: number | null; pillars?: HallPillarDto[] | null; + obstacles?: HallObstacleDto[] | null; } export interface LayoutDto { layoutId: string; diff --git a/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx b/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx index 3c2ac26..bd8b1f4 100644 --- a/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx +++ b/src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx @@ -255,6 +255,7 @@ export function BoothLayoutEditorPage() { floorplanUrl={drawingTab === 'dwg' ? hallDwgUrl(hallId) : hallFloorplanUrl(hallId)} floorRegion={drawingTab === 'dwg' ? hallDwgRegion(hallId) : hallFloorRegion(hallId)} pillars={layout.hall?.pillars} + obstacles={layout.hall?.obstacles} /> ) )} diff --git a/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx b/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx index 89534e0..dacf6d9 100644 --- a/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx +++ b/src/frontend/src/screens/floorplan/FloorplanCanvas.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import type { BoothDto, HallPillarDto } from '../../api/types'; +import type { BoothDto, HallObstacleDto, HallPillarDto } from '../../api/types'; import './canvas.css'; const ZOOM_MIN = 0.3; @@ -39,6 +39,8 @@ interface FloorplanCanvasProps { onZoomChange?: (zoom: number) => void; /** 홀 기둥(V62 실측) — 구조 레이어에 원으로 표시(자동배치는 서버가 회피). */ pillars?: HallPillarDto[] | null; + /** 벽·계단·엘리베이터·문 장애물(V63 실측) — 구조 레이어에 사각으로 표시. */ + obstacles?: HallObstacleDto[] | null; } const TRENCH_SPACING_M = 9; // 가정 트렌치 그리드 간격(실측 대체 — R4) @@ -66,6 +68,7 @@ export function FloorplanCanvas({ floorRegion, onZoomChange, pillars, + obstacles, }: FloorplanCanvasProps) { const [hw, hh] = hallDims; @@ -200,6 +203,22 @@ export function FloorplanCanvas({ /> )} + {/* 벽·계단·엘리베이터·문 장애물(V63 실측) — 구조 레이어 */} + {layers.structure && obstacles && obstacles.length > 0 && ( + + )} + {/* 기둥(V62 실측) — 구조 레이어 */} {layers.structure && pillars && pillars.length > 0 && (