[작업내용] : - B(2 task): normalizeRoofShape 순수함수(TDD, roofShapePattern.js 동거) + route.js 스키마/프롬프트/반환 배선 - C(2 task): pdfRoofShapeDescriptorState atom 저장/리셋 + useRoofShapeSetting 마운트 consume-once pre-select - B→C 순차, 각 독립 커밋 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
231 lines
10 KiB
Markdown
231 lines
10 KiB
Markdown
# C — 지붕형상 자동 선택(pre-select) wiring 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 분석이 산출한 지붕형상 descriptor 를 매처에 통과시켜, `RoofShapeSetting` 마운트 시 매칭되는 8패턴 버튼을 자동 선택(pre-select)만 한다(적용은 사용자 [적용] 클릭).
|
|
|
|
**Architecture:** 신규 Recoil atom `pdfRoofShapeDescriptorState`(outerLineAtom.js)를 매개로, `PlacementShapeSetting.analyzePdfAndApply` 가 분석 결과의 `payload.roofShape` 를 atom 에 저장하고(새 분석 시작 시 null 리셋), `useRoofShapeSetting` 마운트 effect 가 atom 을 읽어 `matchRoofShapePattern` → 매칭되면 `setShapeNum(matched)` 만 호출하고 atom 을 리셋(consume-once)한다. `handleSave` 는 호출하지 않는다.
|
|
|
|
**Tech Stack:** JavaScript, React, Recoil, Next.js 14. 신규 의존성 없음.
|
|
|
|
**설계 근거 스펙:** `docs/superpowers/specs/2026-06-24-pdf-roof-shape-auto-detect-design.md` (§4·§5)
|
|
|
|
**선행 의존:** 플랜 B(응답에 `roofShape: RoofShapeDescriptor | null` 포함). 매처 `matchRoofShapePattern`(커밋 bcdd630a, 기출하).
|
|
|
|
## Global Constraints
|
|
|
|
- **Prettier**: 싱글쿼트, **세미콜론 없음**, tabWidth 2, printWidth 150, trailingComma all (`.prettierrc`). 커밋 전 `npx prettier --check`.
|
|
- **테스트 러너 없음**: C 는 UI 배선이라 자동 단위테스트 불가(매처는 기검증). 검증 = build 회귀 가드 + 앱 수동 시나리오(별도 테스트 프레임워크 도입 금지).
|
|
- **eslint 주의**: eslint 미설치 시 셋업 요구하면 임시 설치 금지·원복.
|
|
- **Path alias**: `@/* → ./src/*`.
|
|
- **alert 금지**: 직접 `alert()` 금지(이 플랜은 alert 추가 없음).
|
|
- **pre-select ONLY**: 매칭 시 `setShapeNum(matched)` 만 — `handleSave`(백엔드 POST + 다음 모달 전환)는 **절대 자동 호출하지 않는다**. 적용은 사용자 몫.
|
|
- **consume-once**: 매칭 시도 후 descriptor atom 을 null 로 리셋해 비-PDF 재진입 시 stale 적용을 막는다.
|
|
- **null 폴백**: 매처가 null(복합형/판별불가)이면 강제 선택 없이 기본값(`shapeNum` 초기값 1) 유지.
|
|
- `<SCRATCH>` = `/private/tmp/claude-501/-Users-devgrr-dev-interplug-hanasys-qcast-front/749df8d5-715e-499d-9009-832aa3cf4f4d/scratchpad`
|
|
|
|
---
|
|
|
|
### Task 1: descriptor atom + PlacementShapeSetting 저장/리셋
|
|
|
|
**Files:**
|
|
- Modify: `src/store/outerLineAtom.js` (끝 `:79` 뒤 atom 추가)
|
|
- Modify: `src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx` (import + setter + analyzePdfAndApply 리셋·저장)
|
|
|
|
**Interfaces:**
|
|
- Produces (Task 2 가 소비): `pdfRoofShapeDescriptorState` atom (default `null`, 값 = `RoofShapeDescriptor | null`).
|
|
- Consumes (플랜 B): 분석 응답 `payload.roofShape: RoofShapeDescriptor | null`.
|
|
|
|
- [ ] **Step 1: atom 추가**
|
|
|
|
`src/store/outerLineAtom.js` 끝(`autoOuterLineFixState` 다음, `:79` 뒤)에 추가:
|
|
|
|
```js
|
|
|
|
/**
|
|
* PDF 분석이 산출한 지붕형상 서술자(RoofShapeDescriptor | null).
|
|
* analyzePdfAndApply 가 저장 → useRoofShapeSetting 마운트 시 매칭·소비(consume-once).
|
|
*/
|
|
export const pdfRoofShapeDescriptorState = atom({
|
|
key: 'pdfRoofShapeDescriptorState',
|
|
default: null,
|
|
})
|
|
```
|
|
|
|
- [ ] **Step 2: PlacementShapeSetting import + setter 추가**
|
|
|
|
`PlacementShapeSetting.jsx` 의 import 블록에 추가(기존 `@/store/...` import 들 근처):
|
|
|
|
```js
|
|
import { pdfRoofShapeDescriptorState } from '@/store/outerLineAtom'
|
|
```
|
|
|
|
`useSetRecoilState` 는 이미 import 되어 있다(`:11`). 컴포넌트 내 setter 선언부(`setCurrentMenu` 선언 `:89` 근처)에 추가:
|
|
|
|
```js
|
|
const setPdfRoofShapeDescriptor = useSetRecoilState(pdfRoofShapeDescriptorState)
|
|
```
|
|
|
|
- [ ] **Step 3: 분석 시작 시 리셋**
|
|
|
|
`analyzePdfAndApply` 안에서 분석 시작 직전(`setPdfAnalyzing(true)` 줄 `:177` 앞)에 추가:
|
|
|
|
```js
|
|
setPdfRoofShapeDescriptor(null) // 이전 분석 잔여 제거(cross-run staleness 방지)
|
|
setPdfAnalyzing(true)
|
|
```
|
|
|
|
- [ ] **Step 4: payload 검증 통과 후 저장**
|
|
|
|
`analyzePdfAndApply` 의 최종 가드(`:238-240`) 통과 후, `applyOuterline` 호출(`:243`) **앞**에 추가:
|
|
|
|
```js
|
|
// 저신뢰 confirm 대기 중 언마운트된 경우까지 커버하는 최종 가드
|
|
if (pdfRunRef.current?.controller !== controller) {
|
|
return false
|
|
}
|
|
|
|
setPdfRoofShapeDescriptor(payload.roofShape ?? null) // 지붕형상 자동 선택용(C)
|
|
|
|
// 외벽선은 항상 미확정(autoFix:false)으로만 그린다 — 확정 여부는 이후 '외벽선 자동 확정' 팝업에서 분기한다.
|
|
const applied = await applyOuterline(payload.outerline, { unit: payload.unit || 'mm', autoFix: false })
|
|
```
|
|
|
|
- [ ] **Step 5: lint / build 검증**
|
|
|
|
```bash
|
|
npx prettier --check src/store/outerLineAtom.js src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx
|
|
yarn build
|
|
```
|
|
Expected: prettier 통과, build 통과(atom 은 아직 Task 2 에서 소비 — 저장만으로도 회귀 없음).
|
|
|
|
- [ ] **Step 6: 커밋**
|
|
|
|
```bash
|
|
git add src/store/outerLineAtom.js src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx
|
|
git commit -m "feat(roof-shape): PDF 지붕형상 descriptor atom 저장 배선
|
|
|
|
[작업내용] :
|
|
- pdfRoofShapeDescriptorState atom 추가(outerLineAtom)
|
|
- analyzePdfAndApply: 분석 시작 시 리셋 + payload.roofShape 저장
|
|
- 소비(매칭·pre-select)는 다음 task
|
|
|
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: useRoofShapeSetting 마운트 consume-once pre-select
|
|
|
|
**Files:**
|
|
- Modify: `src/hooks/roofcover/useRoofShapeSetting.js`
|
|
- import (`:3` recoil, `:9` outerLineAtom, 신규 roofShapePattern)
|
|
- hooks 선언부(`:50-51` 근처)
|
|
- 마운트 effect 가드 통과 직후(`:89` 뒤, `:90` `return () => {` 앞)
|
|
|
|
**Interfaces:**
|
|
- Consumes: `pdfRoofShapeDescriptorState`(Task 1), `matchRoofShapePattern(descriptor) => shapeNum | null`(기출하), `setShapeNum`(기존 useState).
|
|
- Produces: 없음(side effect — 마운트 시 매칭 버튼 pre-select).
|
|
|
|
- [ ] **Step 1: import 추가**
|
|
|
|
`:3` 의 recoil import 에 `useResetRecoilState` 추가:
|
|
|
|
```js
|
|
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
|
|
```
|
|
|
|
`:9` 의 outerLineAtom import 에 `pdfRoofShapeDescriptorState` 추가:
|
|
|
|
```js
|
|
import { outerLineFixState, pdfRoofShapeDescriptorState } from '@/store/outerLineAtom'
|
|
```
|
|
|
|
매처 import 추가(`:9` 다음 줄 등 import 블록 내):
|
|
|
|
```js
|
|
import { matchRoofShapePattern } from '@/common/roofShapePattern'
|
|
```
|
|
|
|
- [ ] **Step 2: hooks 선언 추가**
|
|
|
|
hooks 선언부(`outerLineFix` 선언 `:51` 근처)에 추가:
|
|
|
|
```js
|
|
const pdfRoofShapeDescriptor = useRecoilValue(pdfRoofShapeDescriptorState)
|
|
const resetPdfRoofShapeDescriptor = useResetRecoilState(pdfRoofShapeDescriptorState)
|
|
```
|
|
|
|
- [ ] **Step 3: 마운트 effect 에 consume-once pre-select 추가**
|
|
|
|
마운트 effect(`:83-124`)의 가드 통과 직후 — 가드 if 블록(`:85-89`) 다음, cleanup `return () => {`(`:90`) **앞**에 삽입:
|
|
|
|
```js
|
|
if (!canvas.outerLineFix || outerLines.length === 0) {
|
|
swalFire({ text: getMessage('wall.line.not.found') })
|
|
closePopup(id)
|
|
return
|
|
}
|
|
|
|
// PDF 분석 지붕형상 descriptor 가 있으면 매칭 버튼을 pre-select 한다(자동 선택만 — handleSave 는 사용자 [적용]).
|
|
if (pdfRoofShapeDescriptor) {
|
|
const matched = matchRoofShapePattern(pdfRoofShapeDescriptor)
|
|
if (matched != null) setShapeNum(matched)
|
|
resetPdfRoofShapeDescriptor() // consume-once: 비-PDF 재진입 시 stale 적용 방지
|
|
}
|
|
|
|
return () => {
|
|
```
|
|
|
|
(나머지 cleanup 본문은 무변경.)
|
|
|
|
- [ ] **Step 4: lint / build 검증**
|
|
|
|
```bash
|
|
npx prettier --check src/hooks/roofcover/useRoofShapeSetting.js
|
|
yarn build
|
|
```
|
|
Expected: prettier 통과, build 통과.
|
|
|
|
- [ ] **Step 5: 수동 시나리오 검증 (best-effort, 보고용)**
|
|
|
|
자동 테스트 불가 — 구현자는 build 통과까지 보장하고 아래를 "수동 검증 잔여"로 기록:
|
|
- 플랜 B 적용 + `NEXT_PUBLIC_ENABLE_LOGGING=true`, `yarn dev`.
|
|
- 切妻(가로 용마루) PDF → 외벽선 자동확정 ON → RoofShapeSetting 진입 시 **패턴A(shapeNum 2)** 버튼이 선택(하이라이트)되어 있는지. 寄棟→용마루(1), 片流れ→해당 방위(5~8) 확인.
|
|
- 복합형/판별불가 PDF → 아무 버튼도 강제 선택 안 됨(기본 1 유지).
|
|
- 적용은 자동 진행되지 않고 사용자가 [적용] 눌러야 다음 모달로 가는지(handleSave 비자동) 확인.
|
|
- 사이드바에서 RoofShapeSetting 재진입 시(비-PDF) pre-select 안 되는지(consume-once) 확인.
|
|
|
|
- [ ] **Step 6: 커밋**
|
|
|
|
```bash
|
|
git add src/hooks/roofcover/useRoofShapeSetting.js
|
|
git commit -m "feat(roof-shape): PDF 지붕형상 매칭 버튼 자동 선택(pre-select)
|
|
|
|
[작업내용] :
|
|
- 마운트 시 pdfRoofShapeDescriptorState → matchRoofShapePattern → setShapeNum(매칭 버튼 pre-select)
|
|
- pre-select only: handleSave 비자동(사용자 [적용])
|
|
- consume-once: 매칭 시도 후 atom 리셋(비-PDF 재진입 stale 방지)
|
|
- 매처 null(복합형) 시 강제 선택 없이 기본값 유지
|
|
- 런타임(실제 분기별 pre-select) 수동 검증 잔여
|
|
|
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>"
|
|
```
|
|
|
|
---
|
|
|
|
## Self-Review
|
|
|
|
**1. Spec coverage:**
|
|
- §4.1 atom 추가 → Task 1 Step 1 ✅
|
|
- §4.2 저장(payload.roofShape) + 새 분석 리셋 → Task 1 Step 3·4 ✅
|
|
- §4.3 consume-once pre-select(setShapeNum only, handleSave 미호출) → Task 2 Step 3 ✅
|
|
- §4.4 null 폴백(강제 선택 없음, 기본값 유지) → Task 2 Step 3(`if (matched != null)`) ✅
|
|
- §5 데이터 흐름(저장→AUTO_OUTERLINE_FIX→ON→RoofShapeSetting 마운트 매칭) → Task 1·2 합 ✅
|
|
- §6 에러/엣지(매처 null graceful, consume-once stale 차단) → Task 2 Step 3 ✅
|
|
- §7 C 검증(매처 기검증 + 앱 수동) → Task 2 Step 5 ✅
|
|
|
|
**2. Placeholder scan:** TBD/TODO 없음. 모든 코드 step 실제 코드 포함. Step 5 는 자동화 불가 항목의 명시적 수동 검증(placeholder 아님). ✅
|
|
|
|
**3. Type consistency:** atom 명 `pdfRoofShapeDescriptorState` 가 Task 1(선언·저장)·Task 2(소비)에서 동일. `matchRoofShapePattern(descriptor) => shapeNum|null` 시그니처가 매처 출하본과 일치(`!= null` 분기). `setShapeNum` 은 useRoofShapeSetting 기존 useState setter(`:26`). `payload.roofShape` 가 플랜 B 반환 필드명과 일치. recoil 훅 `useResetRecoilState` 추가 정합. ✅
|