qcast-front/docs/superpowers/specs/2026-06-24-roof-shape-pattern-matcher-design.md
sangwook yoo cb9912b31d docs(roof-shape): 지붕형상 패턴 매처 설계 스펙 추가
[작업내용] :
- 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>
2026-06-24 11:29:35 +09:00

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.

불변식:

  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.jsmatchRoofShapePattern 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 원칙 유지).