qcast-front/docs/외벽선 확정(handleFix) 흐름.md
sangwook yoo 2e7c8769aa ```
docs(outerline): 외벽선 확정(handleFix) 흐름 및 PDF 자동 확정 가이드 문서 추가
```
- 외벽선 확정 (`handleFix`) 로직의 상세 흐름과 확정 후 캔버스/Recoil 불변 조건을 문서화합니다.
- PDF 도면 분석을 통한 외벽선 자동 확정 기능 구현 시 참고할 설계 가이드와 체크리스트를 포함합니다.
- `handleFix` 본체와 `useEffect`, `ccwCheck` 등 관련 로직의 역할 및 주의사항을 명확히 합니다.
```
2026-06-22 10:48:29 +09:00

285 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 외벽선 확정 (`handleFix`) 흐름
> Q.CAST Floor Plan — **지붕덮개 → 외벽선 그리기** 모달의 **「외벽선 확정」** 버튼이 호출하는 로직 정리.
> PDF/Gemini 도면 분석 후 캔버스에 외벽선을 그린 뒤, 옵션에 따라 이 확정 단계를 자동 실행할 때 참고한다.
코드 단일 진실 공급원:
| 역할 | 경로 |
|---|---|
| UI (버튼) | `src/components/floor-plan/modal/outerlinesetting/WallLineSetting.jsx` |
| 핵심 로직 | `src/hooks/roofcover/useOuterLineWall.js``handleFix`, `ccwCheck`, `points` `useEffect` |
| 상태 | `src/store/outerLineAtom.js``outerLinePointsState`, `outerLineFixState` |
| 라인 생성 | `src/hooks/useLine.js``addLine` (QLine + `attributes.planeSize`) |
| PDF 임포트 대응 구현 | `src/hooks/pdf-import/usePdfImport.js``applyOuterline` |
| 확정 후 다음 단계 | `src/components/floor-plan/modal/roofShape/RoofShapeSetting.jsx` |
---
## 0. 한눈에 보기
```
[외벽선 그리기 메뉴] → WallLineSetting 모달
↓ (클릭/Enter)
SweetAlert 확인 (modal.cover.outline.fix.confirm)
↓ confirm
handleFix() — 닫힘 정점 추가 + isFix.current = true
↓ (points useEffect)
outerLine QLine 재생성 + outerLinePoint circle
↓ isFix.current === true
outerLineFix 플래그 ON → 모달 닫기 → ccwCheck → 지붕형상 설정 모달 오픈
```
**확정 직후 캔버스/Recoil이 만족해야 하는 불변 조건** (PDF 자동 확정도 동일):
1. `canvas` 객체 중 `name === 'outerLine'` 인 QLine ≥ 2 (닫힌 다각형)
2. `name === 'outerLinePoint'` 인 fabric.Circle (정점, `visible: false`)
3. `canvas.outerLineFix === true` **및** Recoil `outerLineFixState === true`
4. 외벽선 정점 순서는 **반시계(CCW)**`ccwCheck` 또는 동등 전처리
5. QLine 은 반드시 `useLine.addLine` 경유 — `attributes.planeSize`, `fontSize` 없으면 지붕형상·처마/케라바·저장 복원 깨짐
6. 좌표에 **축소 scale 을 곱하지 않음**`QLine.length × 10 = 표시 mm` 불변식 유지
---
## 1. UI 진입점
### 1-1. 모달·버튼
- 컴포넌트: `WallLineSetting` (`WithDraggable`, class `r`)
- 메뉴 진입: `useMenu``currentMenu === EXTERIOR_WALL_LINE``addPopup(..., <WallLineSetting />)`
- 버튼 DOM: `div.grid-btn-wrap > button.btn-frame.modal.act`
- i18n 키: `modal.cover.outline.fix` (ko: 「외벽선 확정」)
### 1-2. 클릭 핸들러
확정 버튼은 **즉시** `handleFix` 를 부르지 않고, 확인 다이얼로그를 거친다.
```jsx
// WallLineSetting.jsx
swalFire({
type: 'confirm',
text: getMessage('modal.cover.outline.fix.confirm'),
confirmFn: handleFix,
})
```
동일 확인은 `useOuterLineWall.enterCheck` (Enter 키) 에서도 사용한다.
> **자동 확정 구현 시**: 사용자 확인이 필요 없으면 `swalFire` 를 생략하고 `handleFix` 또는 아래 §5 의 「확정 후처리」만 직접 호출하면 된다.
---
## 2. `handleFix` 본체
파일: `useOuterLineWall.js`
### 2-1. 사전 조건·검증
| 단계 | 조건 | 실패 시 |
|---|---|---|
| 1 | `points.length >= 3` | `return` (무발화) |
| 2 | 모든 꼭짓점이 첫 점 기준 직각(각도 % 90 === 0) | `return` — 축정렬 직사각형만으로 닫힌 경우 확정 불가 |
검증 통과 후:
```javascript
setPoints((prev) => [...prev, { x: prev[0].x, y: prev[0].y }]) // 폐곡선: 마지막 = 첫 점
isFix.current = true
```
- `points` 는 Recoil `outerLinePointsState`
- `isFix.current` 는 ref — **다음 `points` useEffect 한 번**에서만 소비된다 (자동으로 false 로 리셋되지 않음; 훅 unmount 전까지 true 유지)
### 2-2. `handleFix` 가 직접 하지 **않는** 일
다음은 모두 **`useEffect([points])`** 와 **`ccwCheck`** 에 위임된다.
- Fabric `outerLine` / `outerLinePoint` 생성
- `outerLineFixState` / `canvas.outerLineFix` 설정
- 마우스·키보드 이벤트 해제
- `WallLineSetting` 모달 닫기
- `RoofShapeSetting` 모달 열기
---
## 3. `points` 변경 시 useEffect (확정의 실질 후처리)
`points` 가 바뀔 때마다 실행된다. 확정 **이전**에도 그리기 미리보기 역할을 한다.
### 3-1. 공통 (매 `points` 변경)
1. 기존 `outerLine`, `helpGuideLine` 제거 (`removeLine`)
2. `startPoint` 제거
3. `points.length === 0``setOuterLineFix(true)` 후 return (초기화 경로)
4. `points.length === 1` → 시작점 빨간 circle (`startPoint`)
5. `points.length >= 2`:
- `setOuterLineFix(false)`**그리는 중에는 미확정**
- `outerLinePoint` circle 생성 (`visible: false`)
- 인접 정점 쌍마다 `drawLine``addLine` (`name: 'outerLine'`, `strokeWidth: 3`, …)
- 3점 이상이면 닫힘 가이드 (`helpGuideLine`, 회색 점선)
### 3-2. 확정 분기 (`isFix.current === true`)
```javascript
if (isFix.current) {
removeAllMouseEventListeners()
removeAllDocumentEventListeners()
canvas?.renderAll()
setOuterLineFix(true)
canvas.outerLineFix = true
closePopup(id) // WallLineSetting 닫기
ccwCheck() // §4
addPopup(propertiesId, 1, <RoofShapeSetting id={propertiesId} />)
}
```
**수동 플로우 확정 후 사용자가 보는 화면**: 외벽선 그리기 모달 종료 → **지붕형상 설정** 모달 자동 오픈.
---
## 4. `ccwCheck` — 정점 순서 보정
확정 직후 호출. 외벽선이 **시계 방향**이면 **반시계**로 뒤집는다.
1. `canvas.getObjects()` 에서 `name === 'outerLine'` 필터
2. `outerLines.length < 2``wall.line.not.found` 알림 후 return
3. Shoelace signed area 로 CCW 판별 (`signedArea > 0` → CW)
4. CW 이면 각 라인을 `[x2,y2,x1,y1]`**역방향 재생성** (`addLine` + 기존 line `remove`)
PDF 임포트(`usePdfImport.ensureCounterClockwise`)는 **정점 배열** 단계에서 동일 목적을 수행한다. 캔버스에 이미 그려진 뒤라면 `ccwCheck` 와 결과가 같아야 한다.
---
## 5. PDF 자동 확정 — `handleFix` 와의 대응
현재 `usePdfImport.applyOuterline`**수동 확정 직후 캔버스 상태**까지는 맞추지만, **`handleFix` 전체를 호출하지는 않는다**.
| 수동 `handleFix` 후 | `applyOuterline` 현재 | 자동 확정 옵션 추가 시 |
|---|---|---|
| SweetAlert 확인 | PDF 덮어쓰기 확인만 | 옵션으로 생략/유지 선택 |
| `points` 에 닫힘점 추가 | `closedPoints = [...pts, pts[0]]` + `setOuterLinePoints` | 동일 |
| `useEffect` 로 QLine 재그리기 | `drawOuterLines` (`addLine` 동일 옵션) | 동일 |
| `outerLinePoint` circle | `drawOuterLinePoints` | 동일 |
| `canvas.outerLineFix` + `outerLineFixState` | ✅ 설정함 | ✅ 필수 |
| `ccwCheck` | `ensureCounterClockwise` (정점 단계) | 라인 그린 뒤 CW 이면 `ccwCheck` 또는 정점 보정 |
| `closePopup(WallLineSetting)` | 해당 모달 없음 | 불필요 |
| `RoofShapeSetting` 모달 오픈 | ❌ **안 함** | 옵션 `autoFix: true``addPopup` 으로 직행 |
| `fitViewport` + `canvasZoomState` | ✅ PDF 전용 | 수동 확정에는 없음 — PDF 유지 |
| 마우스 리스너 제거 | 그리기 모드 미사용 | 불필요 |
`PlacementShapeSetting` PDF 분기는 메뉴만 `ROOF_SHAPE_SETTINGS` 로 보내고, **지붕형상 모달 자동 오픈은 하지 않는다** — 자동 확정 옵션은 별도 구현이 필요하다.
참고: `CLAUDE.md` PDF 임포트 절 — 「수동 외벽선 확정(`useOuterLineWall.handleFix`) 직후와 동일한 상태」.
---
## 6. 자동 확정 구현 가이드 (권장)
### 6-1. `handleFix` 를 그대로 호출하지 **말** 것
`handleFix``useOuterLineWall` 훅 내부에 묶여 있고, 전제 조건이 다르다.
- Recoil `outerLinePointsState`**미닫힌** 정점 배열이어야 함 (마지막 ≠ 첫 점)
- `WallLineSetting` 모달이 마ounted 되어 `propertiesId``RoofShapeSetting` 을 열어야 함
- 직각-only 다각형이면 조용히 실패
PDF 경로는 이미 **닫힌 폴리곤 + 라인 객체**가 있으므로, 훅 밖에서 후처리하는 편이 안전하다.
### 6-2. 권장: 공통 「확정 후처리」 추출 (미래 리팩터)
아래를 `finalizeOuterLine({ canvas, points, options })` 같은 유틸/훅으로 묶으면 수동·PDF·자동 옵션이 한 경로를 탄다.
```text
입력: closedPoints (첫점=끝점 포함 가능, drawOuterLines 기준 인접쌍)
1. clearExisting outerLine / outerLinePoint (필요 시)
2. drawOuterLines(closedPoints) // useLine.addLine
3. drawOuterLinePoints(closedPoints)
4. CCW 보정 (정점 또는 ccwCheck)
5. canvas.outerLineFix = true; setOuterLineFix(true)
6. canvas.renderAll()
7. [옵션] fitViewport
8. [옵션] addPopup(RoofShapeSetting)
```
### 6-3. 최소 변경 (현재 코드베이스)
`applyOuterline``proceed()` 끝에 옵션 분기만 추가:
```javascript
// options.autoOpenRoofShape === true 일 때
const roofShapeId = uuidv4()
addPopup(roofShapeId, 1, <RoofShapeSetting id={roofShapeId} pos={{ x: 50, y: 230 }} />)
setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS) // 메뉴 탭 동기화
```
`usePopup` / `useMenu` 의존은 `usePdfImport` 또는 호출측(`PdfImportModal`, `PlacementShapeSetting`)에서 주입.
### 6-4. 검증 체크리스트 (자동 확정 후)
- [ ] `canvas.getObjects().filter(o => o.name === 'outerLine').length >= 3`
- [ ] 각 QLine 에 `attributes.planeSize` 존재
- [ ] `canvas.outerLineFix === true`
- [ ] `useRoofShapeSetting` 마운트 시 `wall.line.not.found` 미발생
- [ ] 치수 라벨(mm)이 실측과 일치 (scale 미적용)
- [ ] 외벽선 그리기 메뉴 재진입 시 `setOuterLineFix(false)` 로 **재편집 모드**로 떨어지지 않도록 — PDF 플로우는 `ROOF_SHAPE_SETTINGS` 메뉴 유지 (`PlacementShapeSetting` 참고)
---
## 7. 관련 Recoil / Canvas 플래그
| 키 | 타입 | 확정 전 (그리기 중) | 확정 후 |
|---|---|---|---|
| `outerLinePointsState` | `{x,y}[]` | 미닫힘 정점들 | 닫힘(마지막=첫점) 또는 동일 좌표의 closed ring |
| `outerLineFixState` | `boolean` | `false` (2점 이상) | `true` |
| `canvas.outerLineFix` | `boolean` (canvas 커스텀) | `false` | `true` — 직렬화/가드에 사용 (`common.js` `'outerLineFix'`) |
`canvasAtom` 의 selector 는 `outerLineFix === false` 이거나 points 비어 있으면 특정 UI를 잠근다.
---
## 8. 확정 이후 파이프라인
```
handleFix 완료
→ RoofShapeSetting (useRoofShapeSetting)
→ WALL/ROOF 폴리곤 생성, pitchText, 처마/케라바 타입
→ (이후) 보조선, 처마·케라바 편집, 배치면 …
```
`useRoofShapeSetting` 진입 가드:
```javascript
if (!canvas.outerLineFix || outerLines.length === 0) {
swalFire({ text: getMessage('wall.line.not.found') })
closePopup(id)
}
```
자동 확정 없이 `outerLineFix` 만 켜 두면 메뉴는 지붕형상으로 가도 **모달을 열어 주지 않으면** 사용자는 수동으로 메뉴를 눌러야 한다.
---
## 9. 유사 이름 주의 (`handleFix` 동명이인)
| 훅 | 용도 |
|---|---|
| **`useOuterLineWall.handleFix`** | **본 문서 대상** — 외벽선 그리기 확정 |
| `usePropertiesSetting.handleFix` | 외벽선 **속성**(처마/케라바) 확정 — 다른 모달 |
| `usePlacementShapeDrawing.handleFix` | 배치면 그리기 확정 |
| `useRoofLinePropertySetting.handleFix` | 지붕선 속성 확정 |
| `useAuxiliaryDrawing.handleFix` | 보조선 확정 |
DOM class `btn-frame modal act` + 「외벽선 확定/외벽선 확정」은 **`WallLineSetting`** 과 **`PlacementShapeDrawing`** 등에서 공유 메시지 키를 쓸 수 있다. DOM path `modal-pop-wrap r` + `useOuterLineWall` 이면 **본 문서** 대상이다.
---
## 10. 참고 문서·코드
- `docs/PDF Gemini 평면도 분석 기능 구현 계획.md` — PDF → 캔버스 반영 전체
- `docs/Canvas Status.md` — §2.1 외벽선 그리기, §2.2 지붕형상 설정
- `CLAUDE.md` — PDF 임포트 불변식 (scale, `addLine`, `outerLineFix`)
---
*작성: 2026-06-19 — PDF 자동 외벽선 확정 기능 설계용*