[작업내용] : - matchRoofShapePattern(descriptor)→shapeNum|null 순수 함수 설계 - 매칭 키 flows 집합(1차) + type/ridge guard(모순 거부) - 폴백 null(4 비반환), confidence 무시 — UX 정책은 C 단계 위임 - 검증: scratchpad .mjs 불변식 하니스 8종 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.0 KiB
7.0 KiB
지붕형상 패턴 매처(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.
불변식:
- 자기 매칭: autoMatchable 7패턴 각각 →
{type, ridge, flows}descriptor가 자기shapeNum으로 매칭. - 5~8 화면축↔버튼 매핑 정합: flows
[bottom]→5,[top]→6,[right]→7,[left]→8. - 모순 거부:
{type:'shed', ridge:'shed', flows:['top','bottom']}(flows는 gable인데 type=shed) → null. - 무효 입력:
null,{},{flows: 'x'},{flows: ['up']}(어휘 밖) → 전부 null. - 비자동매칭 회피:
{type:'manual', ridge:'none', flows:[]}→ null (4를 반환하지 않음). - flows 순서/중복 무관:
['right','top','left','bottom'],['top','top','bottom','left','right']→ 1. - type/ridge 결측 관대:
{flows:['top','bottom']}(type/ridge 없음) → 2. - 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—matchRoofShapePatternexport 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 원칙 유지).