# 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) 유지. - `` = `/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 " ``` --- ### 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 " ``` --- ## 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` 추가 정합. ✅