qcast-front/docs/superpowers/plans/2026-06-24-roof-shape-C-auto-preselect.md
sangwook yoo 4ebe02186f docs(roof-shape): 지붕형상 자동판별 B·C 구현 계획서 추가
[작업내용] :
- 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>
2026-06-24 14:40:12 +09:00

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 가 소비): pdfRoofShapeDescriptorState atom (default null, 값 = 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 (: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 추가:

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 추가 정합.