feat(pdf-import): 외벽선 자동 확정 스위치를 분석 후 별도 팝업으로 분리
배치면 초기설정의 자동 확정 스위치를 제거하고, PDF 분석으로 외벽선을 미확정 상태로 그린 직후 '외벽선 자동 확정' 팝업(스위치+확인)을 띄워 분기한다. - ON: outerLineFix 확정 + saveCanvas 영속화 → 지붕형상 설정 직행 - OFF: 미확정 유지 → 외벽선 확정 팝업(WallLineSetting) 신규 메뉴 상수 AUTO_OUTERLINE_FIX + useMenu case 로 MenuDepth01 이 자동 표시 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
2e7c8769aa
commit
ca2b0c3dae
@ -56,7 +56,7 @@ A modal under `components/floor-plan/modal/<feature>` pairs with a hook under `h
|
||||
|
||||
**Plan persistence** (serialize/restore) is in `src/lib/canvas.js` and `src/util/canvas-util.js`. Recent commits (`a5e794b3`, `2aef32e0`) added a three-layer guard (entry / cache / save) and `SizeSetting` input guards against 0-area apertures/shadows/dormers — when touching plan load/save or size inputs, do not weaken or remove these guards without understanding the failure mode (`InvalidStateError` from corrupted plans).
|
||||
|
||||
**PDF 도면 임포트** — 배치면 초기설정 모달의 PDF 입력 모드 → `src/app/api/gemini/floor-plan/route.js`(Gemini 분석, 세션 인증 + 사용자별 분당 5회 레이트리밋) → `src/hooks/pdf-import/usePdfImport.js`. 임포트는 **외벽선 자동 확정(`autoOuterLineFixState`, 기본 ON) 옵션**에 따라 분기한다(`PlacementShapeSetting.handleSaveBtn` → `applyOuterline(mmPoints, { autoFix })`). **ON**: 수동 외벽선 확정(`useOuterLineWall.handleFix`) 직후와 동일한 상태를 만든다 — `useLine.addLine` 경유 outerLine 생성(attributes/planeSize/fontSize), `outerLinePoint` 정점 circle, `canvas.outerLineFix = true` + `outerLineFixState`. **OFF**: 외벽선만 그려둔 미확정(`outerLineFix=false`) 상태로 둔다. **메뉴/모달 전환**: `handleSaveBtn` 은 `setSelectedMenu('outline')` + `setCurrentMenu(ON→ROOF_SHAPE_SETTINGS / OFF→EXTERIOR_WALL_LINE)` 만 하면 된다 — `MenuDepth01` 이 `currentMenu` 변화를 감지해 `handleMenu` 로 해당 모달(RoofShapeSetting / WallLineSetting)을 자동으로 띄운다. **직접 `addPopup` 하지 말 것**(MenuDepth01 의 `closeAll` 과 충돌). ⚠️ `basicSettingSave({ skipSideEffects:true })` 는 메뉴를 건드리지 않아야 한다 — `setMenuByRoofSize` 가 `if (!skipSideEffects)` 밖에 있으면 async 저장(`await post`) 직후 `currentMenu` 를 `EXTERIOR_WALL_LINE` 로 덮어써, ON 인데도 WallLineSetting 으로 되돌아가 `useOuterLineWall` 이 `outerLineFix` 를 풀어버린다(2026-06-21 버그). **좌표에 축소 scale 을 곱하지 말 것** — 'QLine 길이×10 = 표시 mm' 불변식이 깨진다. 화면맞춤은 `viewportTransform` 줌 + `canvasZoomState` 동기화로만 처리한다.
|
||||
**PDF 도면 임포트** — 배치면 초기설정 모달의 PDF 입력 모드 → `src/app/api/gemini/floor-plan/route.js`(Gemini 분석, 세션 인증 + 사용자별 분당 5회 레이트리밋) → `src/hooks/pdf-import/usePdfImport.js`. **분석 직후 외벽선은 항상 미확정으로만 그린다**(`PlacementShapeSetting.handleSaveBtn` → `applyOuterline(mmPoints, { autoFix:false })` — `useLine.addLine` 경유 outerLine 생성(attributes/planeSize/fontSize) + `outerLinePoint` 정점 circle, `outerLineFix=false`). 그 후 `handleSaveBtn` 은 `setSelectedMenu('outline')` + `setCurrentMenu(MENU.ROOF_COVERING.AUTO_OUTERLINE_FIX)` 단일 전환만 한다 — `MenuDepth01` 이 `currentMenu` 변화를 감지해 `useMenu.handleMenu` 의 `AUTO_OUTERLINE_FIX` case 로 **'외벽선 자동 확정' 팝업(`AutoOuterLineFixSetting`)** 을 자동으로 띄운다. **확정 여부 분기는 이 팝업의 스위치(`autoOuterLineFixState`, 기본 ON) + [확인]** 에서 한다(`handleConfirm`): **ON** — 외벽선/정점/points 는 이미 `applyOuterline` 이 그려둔 상태라 `canvas.outerLineFix=true` + `setOuterLineFix(true)` 플래그만 켜면 수동 확정(`useOuterLineWall.handleFix`) 직후와 동일해진다(WALL/ROOF 폴리곤은 지붕형상 설정 적용 시점에 생성). 미확정으로 저장된 직전 상태를 덮어쓰도록 `saveCanvas` 한 뒤 `setCurrentMenu(ROOF_SHAPE_SETTINGS)`. **OFF** — 미확정 유지 + `setCurrentMenu(EXTERIOR_WALL_LINE)` 로 외벽선 확정 팝업(`WallLineSetting`)으로 보낸다. **팝업 전환은 `currentMenu` 메커니즘으로만** — 직접 `addPopup` 하지 말 것(MenuDepth01 의 `closeAll` 과 충돌). 신규 메뉴 `AUTO_OUTERLINE_FIX` 는 `subMenusState` 에 노출하지 않는 일회성 팝업이다. ⚠️ `basicSettingSave({ skipSideEffects:true })` 는 메뉴를 건드리지 않아야 한다 — `setMenuByRoofSize` 가 `if (!skipSideEffects)` 밖에 있으면 async 저장(`await post`) 직후 `currentMenu` 를 `EXTERIOR_WALL_LINE` 로 덮어써, AUTO_OUTERLINE_FIX 팝업 대신 WallLineSetting 으로 되돌아간다(2026-06-21 버그). **좌표에 축소 scale 을 곱하지 말 것** — 'QLine 길이×10 = 표시 mm' 불변식이 깨진다. 화면맞춤은 `viewportTransform` 줌 + `canvasZoomState` 동기화로만 처리한다.
|
||||
|
||||
### State — Recoil, split by domain
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ export const MENU = {
|
||||
INITIAL_CANVAS_SETTING: 'initialCanvasSetting', // 배치면 초기설정
|
||||
ROOF_COVERING: {
|
||||
EXTERIOR_WALL_LINE: 'exteriorWallLine', // 외벽선 그리기
|
||||
AUTO_OUTERLINE_FIX: 'autoOuterLineFix', // 외벽선 자동 확정(PDF 임포트 후 분기 팝업)
|
||||
ROOF_SHAPE_SETTINGS: 'roofShapeSettings', // 지붕형상 설정
|
||||
ROOF_SHAPE_PASSIVITY_SETTINGS: 'roofShapePassivitySettings', // 지붕형상 수동설정
|
||||
ROOF_SHAPE_EDITING: 'roofShapeEditing', // 지붕형상 편집
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
|
||||
import WithDraggable from '@/components/common/draggable/WithDraggable'
|
||||
import { useMessage } from '@/hooks/useMessage'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
|
||||
import { autoOuterLineFixState, outerLineFixState } from '@/store/outerLineAtom'
|
||||
import { canvasState, currentMenuState } from '@/store/canvasAtom'
|
||||
import { useCanvasMenu } from '@/hooks/common/useCanvasMenu'
|
||||
import { usePlan } from '@/hooks/usePlan'
|
||||
import { MENU } from '@/common/common'
|
||||
|
||||
/**
|
||||
* 외벽선 자동 확정 팝업 — PDF 임포트로 외벽선을 미확정(outerLineFix=false) 상태로 그린 직후 표시된다.
|
||||
* 스위치 + [확인] 으로 분기한다.
|
||||
* - ON: 수동 확정(useOuterLineWall.handleFix) 직후와 동일하게 외벽선을 확정하고 지붕형상 설정으로 진행한다.
|
||||
* - OFF: 미확정 상태 그대로 외벽선 확정 팝업(WallLineSetting) 으로 이동해 사용자가 직접 확정한다.
|
||||
*/
|
||||
export default function AutoOuterLineFixSetting({ id }) {
|
||||
const { getMessage } = useMessage()
|
||||
const { closePopup } = usePopup()
|
||||
const [autoOuterLineFix, setAutoOuterLineFix] = useRecoilState(autoOuterLineFixState)
|
||||
const setOuterLineFix = useSetRecoilState(outerLineFixState)
|
||||
const canvas = useRecoilValue(canvasState)
|
||||
const setCurrentMenu = useSetRecoilState(currentMenuState)
|
||||
const { setSelectedMenu } = useCanvasMenu()
|
||||
const { saveCanvas } = usePlan()
|
||||
|
||||
// currentMenu 를 바꾸면 MenuDepth01 이 handleMenu(closeAll 포함)로 이 팝업을 닫고 다음 모달을 띄운다.
|
||||
// 직접 addPopup/closePopup 으로 전환하지 않는다(closeAll 과 충돌).
|
||||
const handleConfirm = async () => {
|
||||
setSelectedMenu('outline')
|
||||
if (autoOuterLineFix) {
|
||||
// 외벽선/정점/points 는 이미 applyOuterline 이 그려둔 상태라 확정 플래그만 켜면 된다.
|
||||
// 이 플래그가 없으면 지붕형상 설정 진입이 'wall.line.not.found' 로 차단된다.
|
||||
// 미확정으로 저장된 직전 상태를 확정 상태로 덮어써 재로드 시 확정이 풀리지 않도록 한다.
|
||||
canvas.outerLineFix = true
|
||||
setOuterLineFix(true)
|
||||
await saveCanvas(false)
|
||||
setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS)
|
||||
} else {
|
||||
setCurrentMenu(MENU.ROOF_COVERING.EXTERIOR_WALL_LINE)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<WithDraggable isShow={true} pos={{ x: 50, y: 230 }} className="r">
|
||||
<WithDraggable.Header title={getMessage('modal.placement.initial.setting.size.pdf.autofix.label')} onClose={() => closePopup(id)} />
|
||||
<WithDraggable.Body>
|
||||
<div className="autofix-pop-wrap">
|
||||
<label className="toggle-btn">
|
||||
<input type="checkbox" checked={autoOuterLineFix} onChange={(e) => setAutoOuterLineFix(e.target.checked)} />
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
<div style={{ fontSize: '12px', color: '#bbb', marginTop: '8px', whiteSpace: 'pre-line' }}>
|
||||
{getMessage('modal.placement.initial.setting.size.pdf.autofix.info')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid-btn-wrap">
|
||||
<button className="btn-frame modal act" onClick={handleConfirm}>
|
||||
{getMessage('common.ok')}
|
||||
</button>
|
||||
</div>
|
||||
</WithDraggable.Body>
|
||||
</WithDraggable>
|
||||
)
|
||||
}
|
||||
@ -13,7 +13,6 @@ import { addedRoofsState, roofDisplaySelector, roofMaterialsAtom } from '@/store
|
||||
import { useCommonCode } from '@/hooks/common/useCommonCode'
|
||||
import QSelectBox from '@/components/common/select/QSelectBox'
|
||||
import { globalLocaleStore } from '@/store/localeAtom'
|
||||
import { autoOuterLineFixState } from '@/store/outerLineAtom'
|
||||
|
||||
import { getChonByDegree, getDegreeByChon } from '@/util/canvas-util'
|
||||
import { usePolygon } from '@/hooks/usePolygon'
|
||||
@ -108,8 +107,6 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
||||
const [pdfFacadePages, setPdfFacadePages] = useState('')
|
||||
const [pdfFloorPages, setPdfFloorPages] = useState('')
|
||||
const [pdfAnalyzing, setPdfAnalyzing] = useState(false)
|
||||
// 외벽선 자동 확정(auto fix) 전역 옵션 — 기본 ON
|
||||
const [autoOuterLineFix, setAutoOuterLineFix] = useRecoilState(autoOuterLineFixState)
|
||||
const { setIsGlobalLoading } = useContext(QcastContext)
|
||||
const pdfInputRef = useRef(null)
|
||||
// 진행 중인 PDF 분석 요청 추적 — 언마운트(라우팅 이탈) 시 abort 하고, 늦게 도착한 결과가
|
||||
@ -242,7 +239,8 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
||||
return false
|
||||
}
|
||||
|
||||
const applied = await applyOuterline(payload.outerline, { unit: payload.unit || 'mm', autoFix: autoOuterLineFix })
|
||||
// 외벽선은 항상 미확정(autoFix:false)으로만 그린다 — 확정 여부는 이후 '외벽선 자동 확정' 팝업에서 분기한다.
|
||||
const applied = await applyOuterline(payload.outerline, { unit: payload.unit || 'mm', autoFix: false })
|
||||
if (!applied) {
|
||||
swalFire({ text: getMessage('pdf.import.error.apply'), type: 'alert' })
|
||||
return false
|
||||
@ -466,16 +464,11 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
||||
|
||||
// roofSizeSet에 따라 메뉴 설정
|
||||
if (inputMode === INPUT_MODE.PDF) {
|
||||
// PDF 임포트: 외벽선을 미확정으로 그려둔 상태로 '외벽선 자동 확정' 팝업을 띄운다.
|
||||
// 그 팝업의 스위치(ON→지붕형상 / OFF→외벽선 확정)에서 확정 여부를 분기한다.
|
||||
// currentMenu 변경 시 MenuDepth01 이 handleMenu 로 AutoOuterLineFixSetting 모달을 자동으로 띄운다.
|
||||
setSelectedMenu('outline')
|
||||
if (autoOuterLineFix) {
|
||||
// 자동 확정 ON: 외곽선이 이미 확정(outerLineFix=true)된 상태이므로 지붕형상 설정으로 보낸다.
|
||||
// currentMenu 변경 시 MenuDepth01 이 handleMenu 로 RoofShapeSetting 모달을 자동으로 띄운다.
|
||||
setCurrentMenu(MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS)
|
||||
} else {
|
||||
// 자동 확정 OFF: 외벽선은 미확정(outerLineFix=false) 상태로 그려둔다 — 외벽선 그리기 단계로 보내면
|
||||
// MenuDepth01 이 WallLineSetting 을 띄워, 사용자가 검토 후 직접 [외벽선 확정] 할 수 있다. points 는 유지된다.
|
||||
setCurrentMenu(MENU.ROOF_COVERING.EXTERIOR_WALL_LINE)
|
||||
}
|
||||
setCurrentMenu(MENU.ROOF_COVERING.AUTO_OUTERLINE_FIX)
|
||||
} else if (currentRoof?.roofSizeSet === '2') {
|
||||
setSelectedMenu('surface')
|
||||
setCurrentMenu(MENU.BATCH_CANVAS.BATCH_DRAWING)
|
||||
@ -642,23 +635,6 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{getMessage('modal.placement.initial.setting.size.pdf.autofix.label')}</th>
|
||||
<td>
|
||||
<label className="toggle-btn">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoOuterLineFix}
|
||||
disabled={pdfAnalyzing}
|
||||
onChange={(e) => setAutoOuterLineFix(e.target.checked)}
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</label>
|
||||
<div style={{ fontSize: '12px', color: '#bbb', marginTop: '4px' }}>
|
||||
{getMessage('modal.placement.initial.setting.size.pdf.autofix.info')}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
<tr>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import WallLineSetting from '@/components/floor-plan/modal/outerlinesetting/WallLineSetting'
|
||||
import AutoOuterLineFixSetting from '@/components/floor-plan/modal/outerlinesetting/AutoOuterLineFixSetting'
|
||||
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
|
||||
import RoofShapePassivitySetting from '@/components/floor-plan/modal/roofShape/RoofShapePassivitySetting'
|
||||
import AuxiliaryDrawing from '@/components/floor-plan/modal/auxiliary/AuxiliaryDrawing'
|
||||
@ -45,6 +46,9 @@ export default function useMenu() {
|
||||
case MENU.ROOF_COVERING.EXTERIOR_WALL_LINE:
|
||||
addPopup(popupId, 1, <WallLineSetting id={popupId} />)
|
||||
break
|
||||
case MENU.ROOF_COVERING.AUTO_OUTERLINE_FIX:
|
||||
addPopup(popupId, 1, <AutoOuterLineFixSetting id={popupId} />)
|
||||
break
|
||||
case MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS:
|
||||
addPopup(popupId, 1, <RoofShapeSetting id={popupId} />)
|
||||
break
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"modal.placement.initial.setting.size.pdf.page.all.info": "全ページを解析するため処理時間が長くなる場合があります。図面内の立面図·平面図ページが特定できる場合はページ指定を推奨します。",
|
||||
"modal.placement.initial.setting.size.pdf.page.specify.info": "立面図·平面図が含まれるページ番号をそれぞれ入力してください。指定したページのみ読み込み素早く屋根を自動作成します。",
|
||||
"modal.placement.initial.setting.size.pdf.autofix.label": "外壁線の自動確定",
|
||||
"modal.placement.initial.setting.size.pdf.autofix.info": "オンにするとPDF解析後、外壁線を自動で確定し屋根形状設定へ進みます。",
|
||||
"modal.placement.initial.setting.size.pdf.autofix.info": "オンにすると外壁線を自動で確定し、屋根形状設定へ進みます。\nオフにすると外壁線の確定ステップに移動し、手動で確定できます。",
|
||||
"modal.placement.initial.setting.size.pdf.error.mime": "PDF ファイルのみアップロード可能です。",
|
||||
"modal.placement.initial.setting.size.pdf.error.too.large": "ファイルサイズが 20MB を超えています。",
|
||||
"modal.placement.initial.setting.size.pdf.error.no.file": "先に PDF ファイルをアップロードしてください。",
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"modal.placement.initial.setting.size.pdf.page.all.info": "모든 페이지를 분석하므로 처리 시간이 길어질 수 있습니다. 도면 내 입면도·평면도 페이지가 확인되면 페이지 지정을 권장합니다.",
|
||||
"modal.placement.initial.setting.size.pdf.page.specify.info": "입면도·평면도가 포함된 페이지 번호를 각각 입력해주세요. 지정한 페이지만 읽어 빠르게 지붕을 자동 작성합니다.",
|
||||
"modal.placement.initial.setting.size.pdf.autofix.label": "외벽선 자동 확정",
|
||||
"modal.placement.initial.setting.size.pdf.autofix.info": "켜면 PDF 분석 후 외벽선을 자동으로 확정하고 지붕형상 설정 단계로 넘어갑니다.",
|
||||
"modal.placement.initial.setting.size.pdf.autofix.info": "켜면 외벽선을 자동으로 확정하고 지붕형상 설정 단계로 넘어갑니다.\n끄면 외벽선 확정 단계로 이동해 직접 확정할 수 있습니다.",
|
||||
"modal.placement.initial.setting.size.pdf.error.mime": "PDF 파일만 업로드할 수 있습니다.",
|
||||
"modal.placement.initial.setting.size.pdf.error.too.large": "파일 크기가 20MB 를 초과합니다.",
|
||||
"modal.placement.initial.setting.size.pdf.error.no.file": "PDF 파일을 먼저 업로드해 주세요.",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user