docs(roof-shape): 지붕형상 패턴 매처 구현 계획서 추가

[작업내용] :
- matchRoofShapePattern 단일 Task TDD 플랜(하니스 8불변식)
- flows 집합 키 + type/ridge guard, 폴백 null
- Gemini 분석 확장·자동 클릭은 후속 작업으로 명시

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sangwook yoo 2026-06-24 11:34:53 +09:00
parent cb9912b31d
commit cfd8e3bdef

View File

@ -0,0 +1,190 @@
# 지붕형상 패턴 매처(matchRoofShapePattern) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** `RoofShapeDescriptor`를 8패턴 카탈로그와 대조해 매칭되는 `shapeNum`(없으면 `null`)을 반환하는 순수 함수 `matchRoofShapePattern`을 카탈로그 모듈에 추가한다.
**Architecture:** 기존 `src/common/roofShapePattern.js`(카탈로그 + `getRoofShapePatternByShapeNum`)에 export 1개를 추가한다. 외부 의존성 0의 순수 함수. flows 집합을 1차 매칭 키로, type/ridge를 모순 검출 guard로 사용. 매칭 실패·모순·무효 입력은 모두 `null`. 변 역할 적용(처마/케라바 그리기)은 기존 `useRoofShapeSetting`에 그대로 두고, 이 함수는 버튼 식별(shapeNum)만 한다.
**Tech Stack:** JavaScript (ESM `export const`), Next.js 14. 신규 의존성 없음.
**설계 근거 스펙:** `docs/superpowers/specs/2026-06-24-roof-shape-pattern-matcher-design.md` (선행: `...-roof-shape-pattern-catalog-design.md` §5)
## Global Constraints
- **Prettier**: 싱글쿼트, **세미콜론 없음**, tabWidth 2, printWidth 150, trailingComma all (`.prettierrc`).
- **테스트 러너 없음**: `package.json`에 test runner 미연동. 검증은 scratchpad의 node 불변식 하니스로 수행한다(별도 테스트 프레임워크 도입 금지).
- **eslint 주의**: eslint 미설치 시 `next lint`가 셋업을 요구할 수 있음 — 임시 설치하더라도 **커밋 금지·원복**. (메모리: `eslint-build-skips-lint`)
- **Path alias**: `@/* → ./src/*`. 단, 이 모듈은 import가 없다.
- **모듈 형태**: `export const` ESM. `package.json``"type":"module"` 없음 → 소스 `.js`는 CommonJS 취급되므로, 검증 시 `.mjs`로 복사해 import 한다(소스에 import가 없어 그대로 동작).
- **범위 경계**: Gemini 분석 확장(B)·UI 자동 클릭 배선(C)은 **이번 작업 제외**. 순수 매칭 함수만.
- `<SCRATCH>` = `/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/749df8d5-715e-499d-9009-832aa3cf4f4d/scratchpad`
---
### Task 1: matchRoofShapePattern 순수 함수 추가
**Files:**
- Modify: `src/common/roofShapePattern.js` (현재 1~106줄: 상수 4종 + `getRoofShapePatternByShapeNum`. 끝에 export 추가)
- Verify(scratchpad, 비커밋): `<SCRATCH>/check-matchRoofShapePattern.mjs`, `<SCRATCH>/roofShapePattern.mjs`
**Interfaces:**
- Consumes (기존 카탈로그 모듈에서):
- `ROOF_FLOW = { TOP:'top', BOTTOM:'bottom', LEFT:'left', RIGHT:'right' }`
- `ROOF_SHAPE_PATTERNS: Array<{ shapeNum, key, nameKey, type, ridge, flows, direction?, autoMatchable, note }>` (8개)
- Produces (다음 작업 C가 import):
- `matchRoofShapePattern(descriptor) => number | null`
- `descriptor = { type?, ridge?, flows, direction?, confidence? }`
- 성공 시 매칭된 `autoMatchable` 패턴의 `shapeNum`(1·2·3·5·6·7·8 중 하나), 실패 시 `null`.
- [ ] **Step 1: 검증 하니스 작성 (failing test)**
`<SCRATCH>/check-matchRoofShapePattern.mjs` 생성:
```js
import { matchRoofShapePattern, ROOF_SHAPE_PATTERNS } from './roofShapePattern.mjs'
const fail = (msg) => {
console.error('FAIL:', msg)
process.exit(1)
}
const eq = (got, want, label) => {
if (got !== want) fail(`${label}: expected ${want}, got ${got}`)
}
// 1. 자기 매칭 — autoMatchable 7패턴 각각 {type,ridge,flows} → 자기 shapeNum
for (const p of ROOF_SHAPE_PATTERNS.filter((x) => x.autoMatchable)) {
const got = matchRoofShapePattern({ type: p.type, ridge: p.ridge, flows: [...p.flows] })
eq(got, p.shapeNum, `self-match shapeNum ${p.shapeNum}`)
}
// 2. 5~8 화면축↔버튼 매핑 정합 (flows 단독)
eq(matchRoofShapePattern({ flows: ['bottom'] }), 5, 'flows[bottom]->5')
eq(matchRoofShapePattern({ flows: ['top'] }), 6, 'flows[top]->6')
eq(matchRoofShapePattern({ flows: ['right'] }), 7, 'flows[right]->7')
eq(matchRoofShapePattern({ flows: ['left'] }), 8, 'flows[left]->8')
// 3. 모순 거부 — flows 는 gable(top,bottom)인데 type=shed → null
eq(matchRoofShapePattern({ type: 'shed', ridge: 'shed', flows: ['top', 'bottom'] }), null, 'contradiction type')
// ridge 모순 — flows {top,bottom} 후보2 ridge=horizontal 인데 vertical → null
eq(matchRoofShapePattern({ type: 'gable', ridge: 'vertical', flows: ['top', 'bottom'] }), null, 'contradiction ridge')
// 4. 무효 입력 → 전부 null
eq(matchRoofShapePattern(null), null, 'null input')
eq(matchRoofShapePattern(undefined), null, 'undefined input')
eq(matchRoofShapePattern({}), null, 'no flows')
eq(matchRoofShapePattern({ flows: 'x' }), null, 'flows not array')
eq(matchRoofShapePattern({ flows: ['up'] }), null, 'flows out of vocab')
eq(matchRoofShapePattern({ flows: ['top', 'up'] }), null, 'flows partially out of vocab')
// 5. 비자동매칭 회피 — manual/none/[] 는 4를 반환하지 않고 null
eq(matchRoofShapePattern({ type: 'manual', ridge: 'none', flows: [] }), null, 'manual flows[] -> null')
eq(matchRoofShapePattern({ flows: [] }), null, 'empty flows -> null')
// 6. flows 순서/중복 무관 → 1(寄棟)
eq(matchRoofShapePattern({ flows: ['right', 'top', 'left', 'bottom'] }), 1, 'order-insensitive ->1')
eq(matchRoofShapePattern({ flows: ['top', 'top', 'bottom', 'left', 'right'] }), 1, 'dup-insensitive ->1')
// 7. type/ridge 결측 관대 → flows 만으로 매칭
eq(matchRoofShapePattern({ flows: ['top', 'bottom'] }), 2, 'missing type/ridge ->2')
eq(matchRoofShapePattern({ flows: ['left', 'right'] }), 3, 'missing type/ridge ->3')
// 8. confidence 무시 — 낮아도 매칭
eq(matchRoofShapePattern({ type: 'hip', ridge: 'hip', flows: ['top', 'bottom', 'left', 'right'], confidence: 0.01 }), 1, 'low confidence ->1')
console.log('OK: all matchRoofShapePattern invariants hold')
```
- [ ] **Step 2: 실패 확인 (red)**
Run:
```bash
SCRATCH=/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/749df8d5-715e-499d-9009-832aa3cf4f4d/scratchpad
cp src/common/roofShapePattern.js "$SCRATCH/roofShapePattern.mjs"
node "$SCRATCH/check-matchRoofShapePattern.mjs"
```
Expected: FAIL — `matchRoofShapePattern is not a function` (아직 export 없음).
- [ ] **Step 3: matchRoofShapePattern 추가 (minimal implementation)**
`src/common/roofShapePattern.js` 끝(`getRoofShapePatternByShapeNum` 다음 줄)에 추가:
```js
// descriptor(RoofShapeDescriptor) → 매칭되는 shapeNum | null.
// 매칭 키: flows 집합(1차) + type/ridge guard(모순 거부). confidence 는 무시(임계값 정책은 호출자).
// 실패·모순·무효 입력은 모두 null — autoMatchable:false(shapeNum 4)는 절대 반환하지 않는다.
export const matchRoofShapePattern = (descriptor) => {
if (!descriptor || typeof descriptor !== 'object') return null
const { type, ridge, flows } = descriptor
if (!Array.isArray(flows)) return null
const FLOW_VALUES = Object.values(ROOF_FLOW)
if (flows.some((flow) => !FLOW_VALUES.includes(flow))) return null
const flowKey = JSON.stringify([...new Set(flows)].sort())
const candidate = ROOF_SHAPE_PATTERNS.find(
(pattern) => pattern.autoMatchable && JSON.stringify([...pattern.flows].sort()) === flowKey,
)
if (!candidate) return null
// type/ridge 가 주어졌는데 후보와 모순이면 거부(결측이면 flows 만으로 통과 — 관대)
if (type && type !== candidate.type) return null
if (ridge && ridge !== candidate.ridge) return null
return candidate.shapeNum
}
```
- [ ] **Step 4: 통과 확인 (green)**
Run:
```bash
SCRATCH=/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/749df8d5-715e-499d-9009-832aa3cf4f4d/scratchpad
cp src/common/roofShapePattern.js "$SCRATCH/roofShapePattern.mjs"
node "$SCRATCH/check-matchRoofShapePattern.mjs"
```
Expected: `OK: all matchRoofShapePattern invariants hold`
- [ ] **Step 5: lint / build 검증 (sub-agent)**
서브에이전트로 lint·build 검증(프로젝트 컨벤션). lint는 best-effort(eslint 미설치 시 셋업 요구하면 임시 설치 금지·Prettier 규칙 수기 준수로 대체), build는 회귀 가드.
```bash
yarn lint # eslint 미설치 시 셋업 프롬프트면 중단·미설치 유지
yarn build # 통과 확인 (이 함수는 아직 import 안 됨 → 트리쉐이킹 대상이나 회귀 가드)
```
Expected: 신규 export에 대한 에러 0.
- [ ] **Step 6: 커밋**
```bash
git add src/common/roofShapePattern.js
git commit -m "feat(roof-shape): 지붕형상 패턴 매처 matchRoofShapePattern 추가
[작업내용] :
- descriptor(type/ridge/flows)→shapeNum|null 순수 매칭 함수
- flows 집합 1차 키 + type/ridge guard(모순 거부, 결측 관대)
- 폴백 null(autoMatchable=false인 4 비반환), confidence 무시
- Gemini 분석 확장(B)·자동 클릭 배선(C)은 다음 작업으로 분리
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
```
(scratchpad 검증 파일은 리포 밖이라 커밋 대상 아님.)
---
## Self-Review
**1. Spec coverage:**
- §3 위치(카탈로그 모듈)·시그니처(`shapeNum | null`) → Task 1 Step 3 ✅
- §4 알고리즘(정합성→flows 집합→type/ridge guard→shapeNum) → Step 3 구현 ✅
- §4 폴백 null(4 비반환) → Step 3 + 하니스 불변식 5 ✅
- §4 confidence 무시 → Step 3(미참조) + 불변식 8 ✅
- §4 type/ridge 결측 관대 → Step 3(`if (type && ...)`) + 불변식 7 ✅
- §5 검증 불변식 1~8 → Step 1 하니스 1~8 일대일 대응 ✅
- §5 lint+build → Step 5 ✅
- §6 무변경 대상(useRoofShapeSetting 등) → Step 3는 export 1개 추가만 ✅
**2. Placeholder scan:** TBD/TODO/"적절히 처리" 없음. 모든 코드 step에 실제 코드 포함. ✅
**3. Type consistency:** `matchRoofShapePattern(descriptor) => number | null` 시그니처가 Interfaces·Step 3·하니스에서 동일. `ROOF_FLOW`/`ROOF_SHAPE_PATTERNS` 소비명이 기존 모듈 export명과 일치(카탈로그 커밋 61ffcee9 확인). `autoMatchable`/`flows`/`shapeNum`/`type`/`ridge` 속성명이 카탈로그 데이터와 일치. ✅