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>
This commit is contained in:
parent
e4045142bf
commit
cb9912b31d
@ -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 원칙 유지).
|
||||||
Loading…
x
Reference in New Issue
Block a user