From cb9912b31d4e4917a96ffa04a77adb3c4601a676 Mon Sep 17 00:00:00 2001 From: sangwook yoo Date: Wed, 24 Jun 2026 11:29:35 +0900 Subject: [PATCH] =?UTF-8?q?docs(roof-shape):=20=EC=A7=80=EB=B6=95=ED=98=95?= =?UTF-8?q?=EC=83=81=20=ED=8C=A8=ED=84=B4=20=EB=A7=A4=EC=B2=98=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=EC=8A=A4=ED=8E=99=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [작업내용] : - matchRoofShapePattern(descriptor)→shapeNum|null 순수 함수 설계 - 매칭 키 flows 집합(1차) + type/ridge guard(모순 거부) - 폴백 null(4 비반환), confidence 무시 — UX 정책은 C 단계 위임 - 검증: scratchpad .mjs 불변식 하니스 8종 Co-Authored-By: Claude Opus 4.8 --- ...06-24-roof-shape-pattern-matcher-design.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-24-roof-shape-pattern-matcher-design.md diff --git a/docs/superpowers/specs/2026-06-24-roof-shape-pattern-matcher-design.md b/docs/superpowers/specs/2026-06-24-roof-shape-pattern-matcher-design.md new file mode 100644 index 00000000..0643c3bc --- /dev/null +++ b/docs/superpowers/specs/2026-06-24-roof-shape-pattern-matcher-design.md @@ -0,0 +1,112 @@ +# 지붕형상 패턴 매처(matchRoofShapePattern) — 설계 (Design Spec) + +- 일자: 2026-06-24 +- 브랜치: `feature/pv-btob` +- 상태: 설계 확정 (구현 대기) +- 선행: `docs/superpowers/specs/2026-06-24-roof-shape-pattern-catalog-design.md` (§5 매처 계약) + +## 1. 목적 / 배경 + +PDF 도면 분석 → 외벽선 자동확정 팝업 → **지붕형상 자동 매칭**으로 이어지는 본작업은 세 조각이다. + +- (A) `matchRoofShapePattern(descriptor) → shapeNum` 순수 매칭 함수 — **본 스펙** +- (B) PDF/Gemini 분석 확장 → `RoofShapeDescriptor` 산출 — 별도 작업 +- (C) 매칭 버튼 자동 클릭/적용(`setShapeNum → handleSave`) — 별도 작업 + +8패턴 카탈로그(`src/common/roofShapePattern.js`, 커밋 61ffcee9)는 (A)를 위해 만들어졌다. 본 스펙은 그 카탈로그를 룩업 테이블로 쓰는 **순수 매칭 함수 1개**만 다룬다. B·C는 범위 밖. + +### 범위 경계 (중요) +- **이번 작업(본 스펙)**: `matchRoofShapePattern` 순수 함수 + scratchpad 불변식 하니스 검증. +- **제외**: Gemini 스키마/프롬프트 확장(B), UI 자동 클릭 배선(C), descriptor 산출 로직. +- descriptor는 **입력 계약(스키마)**으로만 받는다 — 본 작업은 그 산출 방법을 모른다. + +## 2. 입력 계약 — RoofShapeDescriptor + +선행 스펙 §5에서 정의된 그대로(B가 산출할 형태): + +``` +RoofShapeDescriptor = { + type: 'hip' | 'gable' | 'shed' | 'manual', + ridge: 'hip' | 'horizontal' | 'vertical' | 'shed' | 'none', + flows: Array<'top' | 'bottom' | 'left' | 'right'>, // 정규 winding 화면축 + direction?: 'west' | 'east' | 'south' | 'north', // shed 일 때 + confidence?: number, // 0~1 +} +``` + +- `flows`는 **정규 winding**(`outerLines[0].direction === 'bottom'`) 기준 화면축 방향 집합. 동일 정규 프레임 생성은 B의 책임(선행 스펙 §2 전사 근거). +- `direction`·`confidence`는 매처가 **소비하지 않는다**(아래 §4 참조). + +## 3. 함수 위치 / 시그니처 + +- **위치**: `src/common/roofShapePattern.js` — 카탈로그와 동일 모듈. 매처는 카탈로그의 1차 소비자이고 외부 의존성이 0이라, 룩업·매처를 한 import 면에 모은다(응집도). 별도 파일 미생성. +- **시그니처**: `matchRoofShapePattern(descriptor) => number | null` + - 성공: 매칭된 `autoMatchable` 패턴의 `shapeNum` (1·2·3·5·6·7·8 중 하나). + - 실패: `null`. (호출자 C가 null → 수동(4) 폴백/재질의 등 UX 정책 결정.) +- shapeNum만 반환한다. direction 등 부가정보는 호출자가 `getRoofShapePatternByShapeNum(shapeNum)`으로 재조회(합성, YAGNI). + +## 4. 매칭 알고리즘 (flows 집합 + type/ridge 대조) + +``` +matchRoofShapePattern(descriptor): + 1. 정합성 검사 → 위반 시 null + - descriptor 가 비객체/null → null + - descriptor.flows 가 배열 아님 → null + - flows 원소 중 ROOF_FLOW 어휘(top/bottom/left/right) 밖 값 존재 → null + 2. flowKey = JSON.stringify(dedup(flows).sort()) // 집합 정규화 + 3. 후보 = ROOF_SHAPE_PATTERNS 중 autoMatchable === true 이고 + JSON.stringify([...p.flows].sort()) === flowKey 인 패턴 + - 후보 없음 → null (flows=[] 는 4(manual, autoMatchable=false)만 가지므로 자동매칭 후보 없음 → null) + 4. type/ridge guard: + - descriptor.type 이 존재하고 후보.type 과 불일치 → null + - descriptor.ridge 가 존재하고 후보.ridge 와 불일치 → null + (모순 descriptor 거부. type/ridge 결측이면 flows 만으로 통과 — 관대) + 5. 후보.shapeNum 반환 +``` + +### 설계 판단 (확정) +- **폴백 = `null`** (4 아님). 매처는 순수 구조매칭으로 정직하게 둔다. `autoMatchable:false`인 shapeNum 4는 매칭 결과로 **절대 반환하지 않는다**. "null → 수동(4) 열기/재질의" 정책은 C 단계로 위임. +- **confidence 무시**. 임계값 게이팅은 호출자(B/C) 책임. 매처는 confidence-agnostic. +- **type/ridge guard 적용**. flows 집합이 1차 키, type/ridge는 모순 검출용 guard. flows는 gable 시그니처인데 type='shed' 같은 모순 descriptor를 거부. 단, type/ridge가 **결측(falsy)**이면 guard를 건너뛴다(flows만으로 식별 — 8패턴 flows 집합이 상호 유일하므로 안전). + +### flows 집합 유일성 (카탈로그 불변식 재확인) +| shapeNum | flows 집합 | type | ridge | +|---|---|---|---| +| 1 | {top,bottom,left,right} | hip | hip | +| 2 | {top,bottom} | gable | horizontal | +| 3 | {left,right} | gable | vertical | +| 5 | {bottom} | shed | shed | +| 6 | {top} | shed | shed | +| 7 | {right} | shed | shed | +| 8 | {left} | shed | shed | +| 4 | {} (autoMatchable=false) | manual | none | + +autoMatchable 7패턴의 flows 집합이 상호 유일 → flows만으로 1차 식별 성립. + +## 5. 검증 (테스트 러너 미연동) + +CLAUDE.md: `package.json`에 test runner 없음. Task 1과 동일하게 scratchpad `.mjs` 불변식 하니스로 검증(별도 프레임워크 도입 금지). 소스 `.js`를 `.mjs`로 복사해 ESM import. + +불변식: +1. **자기 매칭**: autoMatchable 7패턴 각각 → `{type, ridge, flows}` descriptor가 자기 `shapeNum`으로 매칭. +2. **5~8 화면축↔버튼 매핑 정합**: flows `[bottom]→5`, `[top]→6`, `[right]→7`, `[left]→8`. +3. **모순 거부**: `{type:'shed', ridge:'shed', flows:['top','bottom']}`(flows는 gable인데 type=shed) → null. +4. **무효 입력**: `null`, `{}`, `{flows: 'x'}`, `{flows: ['up']}`(어휘 밖) → 전부 null. +5. **비자동매칭 회피**: `{type:'manual', ridge:'none', flows:[]}` → null (4를 반환하지 않음). +6. **flows 순서/중복 무관**: `['right','top','left','bottom']`, `['top','top','bottom','left','right']` → 1. +7. **type/ridge 결측 관대**: `{flows:['top','bottom']}`(type/ridge 없음) → 2. +8. **confidence 무시**: `{type:'hip', ridge:'hip', flows:[...4방], confidence:0.01}` → 1 (낮아도 매칭). + +추가: `yarn lint`(에러 0, 경고 최소화) + `yarn build` 통과(회귀 가드). Prettier: 싱글쿼트, **세미콜론 없음**, tabWidth 2, printWidth 150. + +## 6. 영향 범위 / 변경 파일 + +- **수정**: `src/common/roofShapePattern.js` — `matchRoofShapePattern` export 1개 추가. 기존 상수·접근자 무변경. +- **무변경**: `useRoofShapeSetting.js`, `RoofShapeSetting.jsx`, Gemini route, `usePdfImport.js` — 본 작업은 순수 함수 추가만. 아직 어디서도 import 안 됨(트리쉐이킹 대상). +- 문서: 본 스펙 + 구현 플랜. + +## 7. 리스크 / 미해결 + +- **descriptor 정규 프레임 의존성**: 매처는 정규 winding flows를 가정. 비정규 프레임 flows 산출은 B의 책임(선행 스펙 §2·§8에서 이관됨). +- **L자·凹凸 복합 footprint**: 깔끔한 8패턴 매칭은 직사각형 계열 가정. 복합형은 flows 집합이 8패턴 어느 것과도 안 맞아 자연히 null → C가 수동(4) 폴백. 폴백 판정 정책은 C. +- 본 매처는 **버튼 식별 전용**. 실제 지붕 생성은 기존 `handleSave`가 담당(선행 스펙 §4 원칙 유지).