# 지붕형상 패턴 매처(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 원칙 유지).