docs(roof-shape): 지붕형상 패턴 카탈로그 구현 계획서 추가
[작업내용] : - 2026-06-24 카탈로그 모듈 구현 플랜(단일 태스크, node 불변식 검증 하니스) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
61ffcee949
commit
e4045142bf
271
docs/superpowers/plans/2026-06-24-roof-shape-pattern-catalog.md
Normal file
271
docs/superpowers/plans/2026-06-24-roof-shape-pattern-catalog.md
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
# 지붕형상 패턴 카탈로그 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:** PDF 분석→지붕형상 자동 매칭의 선행 단계로, 지붕형상 설정 팝업의 8개 패턴을 매칭 가능한 시그니처로 정의한 카탈로그 상수 모듈을 추가한다.
|
||||||
|
|
||||||
|
**Architecture:** 의존성 없는 순수 데이터 모듈 1개(`src/common/roofShapePattern.js`)를 신규 생성한다. 각 패턴 = `{shapeNum, key, nameKey, type, ridge, flows, direction?, autoMatchable, note}`. 매칭 키는 `type + ridge + flows(집합)`이며 `flows` 집합만으로 8패턴이 상호 유일하게 식별된다. 변 역할 적용(처마/케라바 실제 그리기) 로직은 기존 `useRoofShapeSetting`에 그대로 두고, 이 모듈은 **매칭 룩업 테이블** 역할만 한다.
|
||||||
|
|
||||||
|
**Tech Stack:** JavaScript (ESM `export const`), Next.js 14. 신규 의존성 없음.
|
||||||
|
|
||||||
|
**설계 근거 스펙:** `docs/superpowers/specs/2026-06-24-roof-shape-pattern-catalog-design.md`
|
||||||
|
|
||||||
|
## 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가 없다.
|
||||||
|
- **로깅/alert 금지 규칙** 해당 없음(순수 데이터 모듈).
|
||||||
|
- **모듈 형태**: `export const` ESM. `package.json`에 `"type":"module"` 없음 → 소스 `.js`는 CommonJS 취급되므로, 검증 시 `.mjs`로 복사해 import 한다(소스에 import가 없어 그대로 동작).
|
||||||
|
- **범위 경계**: 매칭 함수(`matchRoofShapePattern`)·PDF 분석 확장은 **이번 작업 제외**. 카탈로그 데이터 + 경량 접근자만.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 지붕형상 패턴 카탈로그 모듈 생성
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/common/roofShapePattern.js`
|
||||||
|
- Verify(scratchpad, 비커밋): `<SCRATCH>/check-roofShapePattern.mjs`, `<SCRATCH>/roofShapePattern.mjs`
|
||||||
|
|
||||||
|
> `<SCRATCH>` = `/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/15fa1013-fdbe-447b-aa10-6e3008bd6339/scratchpad`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 없음(순수 데이터).
|
||||||
|
- Produces (다음 매칭 작업이 import):
|
||||||
|
- `ROOF_FLOW = { TOP:'top', BOTTOM:'bottom', LEFT:'left', RIGHT:'right' }`
|
||||||
|
- `ROOF_SHAPE_TYPE = { HIP:'hip', GABLE:'gable', SHED:'shed', MANUAL:'manual' }`
|
||||||
|
- `ROOF_RIDGE = { HIP:'hip', HORIZONTAL:'horizontal', VERTICAL:'vertical', SHED:'shed', NONE:'none' }`
|
||||||
|
- `ROOF_SHAPE_PATTERNS: Array<{ shapeNum:number, key:string, nameKey:string, type:string, ridge:string, flows:string[], direction?:'west'|'east'|'south'|'north', autoMatchable:boolean, note?:string }>` (8개)
|
||||||
|
- `getRoofShapePatternByShapeNum(shapeNum:number) => pattern | null`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 검증 하니스 작성 (failing test)**
|
||||||
|
|
||||||
|
`<SCRATCH>/check-roofShapePattern.mjs` 생성:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
ROOF_SHAPE_PATTERNS,
|
||||||
|
ROOF_FLOW,
|
||||||
|
ROOF_SHAPE_TYPE,
|
||||||
|
ROOF_RIDGE,
|
||||||
|
getRoofShapePatternByShapeNum,
|
||||||
|
} from './roofShapePattern.mjs'
|
||||||
|
|
||||||
|
const fail = (msg) => {
|
||||||
|
console.error('FAIL:', msg)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 정확히 8개
|
||||||
|
if (ROOF_SHAPE_PATTERNS.length !== 8) fail(`expected 8 patterns, got ${ROOF_SHAPE_PATTERNS.length}`)
|
||||||
|
|
||||||
|
// 2. shapeNum 이 정확히 {1..8}
|
||||||
|
const nums = ROOF_SHAPE_PATTERNS.map((p) => p.shapeNum).sort((a, b) => a - b)
|
||||||
|
if (JSON.stringify(nums) !== JSON.stringify([1, 2, 3, 4, 5, 6, 7, 8])) fail(`shapeNum set mismatch: ${nums}`)
|
||||||
|
|
||||||
|
// 3. flows 집합 distinct (정렬 후 JSON 유일) — 매칭 핵심 불변식
|
||||||
|
const flowKeys = ROOF_SHAPE_PATTERNS.map((p) => JSON.stringify([...p.flows].sort()))
|
||||||
|
if (new Set(flowKeys).size !== 8) fail('flows signatures are not mutually distinct')
|
||||||
|
|
||||||
|
// 4. autoMatchable === false 는 오직 shapeNum 4
|
||||||
|
const nonMatch = ROOF_SHAPE_PATTERNS.filter((p) => !p.autoMatchable).map((p) => p.shapeNum)
|
||||||
|
if (JSON.stringify(nonMatch) !== JSON.stringify([4])) fail(`autoMatchable=false should be only [4], got ${nonMatch}`)
|
||||||
|
|
||||||
|
// 5. nameKey 비어있지 않음 / shed(type=SHED)만 direction 보유
|
||||||
|
for (const p of ROOF_SHAPE_PATTERNS) {
|
||||||
|
if (typeof p.nameKey !== 'string' || !p.nameKey) fail(`empty nameKey at shapeNum ${p.shapeNum}`)
|
||||||
|
const isShed = p.type === ROOF_SHAPE_TYPE.SHED
|
||||||
|
if (isShed && !p.direction) fail(`shed shapeNum ${p.shapeNum} missing direction`)
|
||||||
|
if (!isShed && p.direction) fail(`non-shed shapeNum ${p.shapeNum} has direction`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. flows 값이 ROOF_FLOW 어휘에 속함 / ridge·type 이 상수 어휘에 속함
|
||||||
|
const FLOW_VALUES = Object.values(ROOF_FLOW)
|
||||||
|
const TYPE_VALUES = Object.values(ROOF_SHAPE_TYPE)
|
||||||
|
const RIDGE_VALUES = Object.values(ROOF_RIDGE)
|
||||||
|
for (const p of ROOF_SHAPE_PATTERNS) {
|
||||||
|
if (!TYPE_VALUES.includes(p.type)) fail(`bad type at shapeNum ${p.shapeNum}: ${p.type}`)
|
||||||
|
if (!RIDGE_VALUES.includes(p.ridge)) fail(`bad ridge at shapeNum ${p.shapeNum}: ${p.ridge}`)
|
||||||
|
for (const f of p.flows) if (!FLOW_VALUES.includes(f)) fail(`bad flow at shapeNum ${p.shapeNum}: ${f}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 접근자
|
||||||
|
if (getRoofShapePatternByShapeNum(1)?.key !== 'ridge') fail('accessor(1) wrong')
|
||||||
|
if (getRoofShapePatternByShapeNum(99) !== null) fail('accessor(99) should be null')
|
||||||
|
|
||||||
|
console.log('OK: all roofShapePattern invariants hold')
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 실패 확인 (red)**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
SCRATCH=/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/15fa1013-fdbe-447b-aa10-6e3008bd6339/scratchpad
|
||||||
|
node "$SCRATCH/check-roofShapePattern.mjs"
|
||||||
|
```
|
||||||
|
Expected: FAIL — `Cannot find module '.../roofShapePattern.mjs'` (소스 모듈이 아직 없음).
|
||||||
|
|
||||||
|
- [ ] **Step 3: 카탈로그 모듈 작성 (minimal implementation)**
|
||||||
|
|
||||||
|
`src/common/roofShapePattern.js` 생성:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 지붕형상 설정 팝업(RoofShapeSetting)의 8개 패턴 카탈로그.
|
||||||
|
// PDF 분석→지붕형상 자동 매칭의 룩업 테이블. 매칭 키: type + ridge + flows(집합).
|
||||||
|
// 설계 근거: docs/superpowers/specs/2026-06-24-roof-shape-pattern-catalog-design.md
|
||||||
|
|
||||||
|
// 화면축 흐름방향 (방위적용 前 — 지붕형상 설정 시점에는 실제 방위가 없다)
|
||||||
|
export const ROOF_FLOW = { TOP: 'top', BOTTOM: 'bottom', LEFT: 'left', RIGHT: 'right' }
|
||||||
|
|
||||||
|
// 지붕 유형
|
||||||
|
export const ROOF_SHAPE_TYPE = { HIP: 'hip', GABLE: 'gable', SHED: 'shed', MANUAL: 'manual' }
|
||||||
|
|
||||||
|
// 용마루 방향
|
||||||
|
export const ROOF_RIDGE = { HIP: 'hip', HORIZONTAL: 'horizontal', VERTICAL: 'vertical', SHED: 'shed', NONE: 'none' }
|
||||||
|
|
||||||
|
// shapeNum = useRoofShapeSetting.shapeMenu 의 id 이자 자동 클릭 대상(setShapeNum 값).
|
||||||
|
// flows 집합만으로 8패턴이 상호 유일하게 식별된다.
|
||||||
|
// 5~8 화면축↔버튼 매핑은 useRoofShapeSetting case 4~8 에서 전사(정규 winding: outerLines[0].direction==='bottom').
|
||||||
|
export const ROOF_SHAPE_PATTERNS = [
|
||||||
|
{
|
||||||
|
shapeNum: 1,
|
||||||
|
key: 'ridge',
|
||||||
|
nameKey: 'modal.roof.shape.setting.ridge',
|
||||||
|
type: ROOF_SHAPE_TYPE.HIP,
|
||||||
|
ridge: ROOF_RIDGE.HIP,
|
||||||
|
flows: [ROOF_FLOW.TOP, ROOF_FLOW.BOTTOM, ROOF_FLOW.LEFT, ROOF_FLOW.RIGHT],
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '寄棟 — 전 변 처마, 사방 흐름',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shapeNum: 2,
|
||||||
|
key: 'patternA',
|
||||||
|
nameKey: 'modal.roof.shape.setting.patten.a',
|
||||||
|
type: ROOF_SHAPE_TYPE.GABLE,
|
||||||
|
ridge: ROOF_RIDGE.HORIZONTAL,
|
||||||
|
flows: [ROOF_FLOW.TOP, ROOF_FLOW.BOTTOM],
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '切妻 — 상하 처마/좌우 케라바, 가로 용마루',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shapeNum: 3,
|
||||||
|
key: 'patternB',
|
||||||
|
nameKey: 'modal.roof.shape.setting.patten.b',
|
||||||
|
type: ROOF_SHAPE_TYPE.GABLE,
|
||||||
|
ridge: ROOF_RIDGE.VERTICAL,
|
||||||
|
flows: [ROOF_FLOW.LEFT, ROOF_FLOW.RIGHT],
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '切妻 — 좌우 처마/상하 케라바, 세로 용마루',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shapeNum: 5,
|
||||||
|
key: 'west',
|
||||||
|
nameKey: 'commons.west',
|
||||||
|
type: ROOF_SHAPE_TYPE.SHED,
|
||||||
|
ridge: ROOF_RIDGE.SHED,
|
||||||
|
flows: [ROOF_FLOW.BOTTOM],
|
||||||
|
direction: 'west',
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '片流れ — 처마/흐름 bottom, 케라바 좌우',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shapeNum: 6,
|
||||||
|
key: 'east',
|
||||||
|
nameKey: 'commons.east',
|
||||||
|
type: ROOF_SHAPE_TYPE.SHED,
|
||||||
|
ridge: ROOF_RIDGE.SHED,
|
||||||
|
flows: [ROOF_FLOW.TOP],
|
||||||
|
direction: 'east',
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '片流れ — 처마/흐름 top, 케라바 좌우',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shapeNum: 7,
|
||||||
|
key: 'south',
|
||||||
|
nameKey: 'commons.south',
|
||||||
|
type: ROOF_SHAPE_TYPE.SHED,
|
||||||
|
ridge: ROOF_RIDGE.SHED,
|
||||||
|
flows: [ROOF_FLOW.RIGHT],
|
||||||
|
direction: 'south',
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '片流れ — 처마/흐름 right, 케라바 상하',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shapeNum: 8,
|
||||||
|
key: 'north',
|
||||||
|
nameKey: 'commons.north',
|
||||||
|
type: ROOF_SHAPE_TYPE.SHED,
|
||||||
|
ridge: ROOF_RIDGE.SHED,
|
||||||
|
flows: [ROOF_FLOW.LEFT],
|
||||||
|
direction: 'north',
|
||||||
|
autoMatchable: true,
|
||||||
|
note: '片流れ — 처마/흐름 left, 케라바 상하',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 변별로 설정 — 자동매칭 비대상(폴백). 깨끗한 매칭 실패 시 후보로만 표시.
|
||||||
|
shapeNum: 4,
|
||||||
|
key: 'side',
|
||||||
|
nameKey: 'modal.roof.shape.setting.side',
|
||||||
|
type: ROOF_SHAPE_TYPE.MANUAL,
|
||||||
|
ridge: ROOF_RIDGE.NONE,
|
||||||
|
flows: [],
|
||||||
|
autoMatchable: false,
|
||||||
|
note: '변별로 설정 — 자동매칭 비대상(폴백)',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// shapeNum 으로 카탈로그 항목 조회. 없으면 null.
|
||||||
|
export const getRoofShapePatternByShapeNum = (shapeNum) => ROOF_SHAPE_PATTERNS.find((pattern) => pattern.shapeNum === shapeNum) || null
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 통과 확인 (green)**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
```bash
|
||||||
|
SCRATCH=/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/15fa1013-fdbe-447b-aa10-6e3008bd6339/scratchpad
|
||||||
|
cp src/common/roofShapePattern.js "$SCRATCH/roofShapePattern.mjs"
|
||||||
|
node "$SCRATCH/check-roofShapePattern.mjs"
|
||||||
|
```
|
||||||
|
Expected: `OK: all roofShapePattern invariants hold`
|
||||||
|
|
||||||
|
- [ ] **Step 5: lint / build 검증 (sub-agent)**
|
||||||
|
|
||||||
|
서브에이전트로 lint·build 검증을 수행한다(프로젝트 컨벤션). lint는 best-effort(eslint 미설치 시 셋업 요구하면 임시 설치 금지·Prettier 규칙 수기 준수로 대체), build는 회귀 가드.
|
||||||
|
```bash
|
||||||
|
yarn lint # eslint 미설치 시 셋업 프롬프트면 중단·미설치 유지
|
||||||
|
```
|
||||||
|
Expected: 신규 파일에 대한 에러 0. (이 모듈은 아직 어디서도 import 되지 않아 build 트리쉐이킹 대상이지만, 컨벤션상 `yarn build` 통과도 확인.)
|
||||||
|
|
||||||
|
- [ ] **Step 6: 커밋**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/common/roofShapePattern.js
|
||||||
|
git commit -m "feat(roof-shape): 지붕형상 패턴 카탈로그 상수 모듈 추가
|
||||||
|
|
||||||
|
[작업내용] :
|
||||||
|
- ROOF_SHAPE_PATTERNS(8) + ROOF_FLOW/ROOF_SHAPE_TYPE/ROOF_RIDGE 상수
|
||||||
|
- 매칭 키 type+ridge+flows, flows 집합만으로 8패턴 유일 식별
|
||||||
|
- getRoofShapePatternByShapeNum 접근자
|
||||||
|
- 매칭 함수·PDF 분석 확장은 다음 작업으로 분리
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
||||||
|
```
|
||||||
|
(scratchpad 검증 파일은 리포 밖이라 커밋 대상 아님.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
**1. Spec coverage:**
|
||||||
|
- §3 데이터 모듈 스키마 → Task 1 Step 3 ✅
|
||||||
|
- §4 8패턴 카탈로그 내용 → Task 1 Step 3 (8항목, shapeNum/flows/direction 정합) ✅
|
||||||
|
- §4 경량 접근자 `getRoofShapePatternByShapeNum` → Step 3 ✅
|
||||||
|
- §6 distinct 불변식 검증 → Step 1·4 하니스(불변식 3) ✅
|
||||||
|
- §6 lint+build → Step 5 ✅
|
||||||
|
- §5 매칭 함수 미구현(계약만) → 본 플랜에서 의도적으로 제외(범위 경계) ✅
|
||||||
|
- §2 5~8 화면축↔버튼 매핑 → Step 3 데이터 + 모듈 주석에 반영 ✅
|
||||||
|
|
||||||
|
**2. Placeholder scan:** TBD/TODO/“적절히 처리” 없음. 모든 코드 step에 실제 코드 포함. ✅
|
||||||
|
|
||||||
|
**3. Type consistency:** `ROOF_SHAPE_PATTERNS` 항목 형태(`shapeNum/key/nameKey/type/ridge/flows/direction?/autoMatchable/note`)가 Interfaces·Step 3·하니스 검증에서 동일. 접근자명 `getRoofShapePatternByShapeNum` 일관. flows 값은 `ROOF_FLOW` 어휘로 통일. ✅
|
||||||
Loading…
x
Reference in New Issue
Block a user