[작업내용] : - 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>
10 KiB
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 가 소비):
pdfRoofShapeDescriptorStateatom (defaultnull, 값 =RoofShapeDescriptor | null). -
Consumes (플랜 B): 분석 응답
payload.roofShape: RoofShapeDescriptor | null. -
Step 1: atom 추가
src/store/outerLineAtom.js 끝(autoOuterLineFixState 다음, :79 뒤)에 추가:
/**
* 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 들 근처):
import { pdfRoofShapeDescriptorState } from '@/store/outerLineAtom'
useSetRecoilState 는 이미 import 되어 있다(:11). 컴포넌트 내 setter 선언부(setCurrentMenu 선언 :89 근처)에 추가:
const setPdfRoofShapeDescriptor = useSetRecoilState(pdfRoofShapeDescriptorState)
- Step 3: 분석 시작 시 리셋
analyzePdfAndApply 안에서 분석 시작 직전(setPdfAnalyzing(true) 줄 :177 앞)에 추가:
setPdfRoofShapeDescriptor(null) // 이전 분석 잔여 제거(cross-run staleness 방지)
setPdfAnalyzing(true)
- Step 4: payload 검증 통과 후 저장
analyzePdfAndApply 의 최종 가드(:238-240) 통과 후, applyOuterline 호출(:243) 앞에 추가:
// 저신뢰 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 검증
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: 커밋
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 (
:3recoil,:9outerLineAtom, 신규 roofShapePattern) - hooks 선언부(
:50-51근처) - 마운트 effect 가드 통과 직후(
:89뒤,:90return () => {앞)
- import (
Interfaces:
-
Consumes:
pdfRoofShapeDescriptorState(Task 1),matchRoofShapePattern(descriptor) => shapeNum | null(기출하),setShapeNum(기존 useState). -
Produces: 없음(side effect — 마운트 시 매칭 버튼 pre-select).
-
Step 1: import 추가
:3 의 recoil import 에 useResetRecoilState 추가:
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
:9 의 outerLineAtom import 에 pdfRoofShapeDescriptorState 추가:
import { outerLineFixState, pdfRoofShapeDescriptorState } from '@/store/outerLineAtom'
매처 import 추가(:9 다음 줄 등 import 블록 내):
import { matchRoofShapePattern } from '@/common/roofShapePattern'
- Step 2: hooks 선언 추가
hooks 선언부(outerLineFix 선언 :51 근처)에 추가:
const pdfRoofShapeDescriptor = useRecoilValue(pdfRoofShapeDescriptorState)
const resetPdfRoofShapeDescriptor = useResetRecoilState(pdfRoofShapeDescriptorState)
- Step 3: 마운트 effect 에 consume-once pre-select 추가
마운트 effect(:83-124)의 가드 통과 직후 — 가드 if 블록(:85-89) 다음, cleanup return () => {(:90) 앞에 삽입:
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 검증
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: 커밋
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 추가 정합. ✅