Merge pull request 'dev' (#958) from dev into prd-deploy

Reviewed-on: #958
This commit is contained in:
ysCha 2026-07-10 08:21:34 +09:00
commit db07b11220
7 changed files with 323 additions and 12 deletions

View File

@ -247,3 +247,67 @@ overlap 을 판정하는데, 이 블록은 원래 **변별 경로(`drawRoofByAtt
- **박공**(`drawGableRoof`): split 미사용 → 무영향. - **박공**(`drawGableRoof`): split 미사용 → 무영향.
**재현 검증 체크리스트**: ① 육각형 용마루 6변 전부 등록(소실 없음) ② 단순 사각형 우진각 4변 유지(회귀 없음) ③ 변별 지붕 외곽선·추녀 정상 ④ 한쪽흐름 경사변 actual=plane/cos 유지. (캔버스 인터랙션이라 런타임 확인은 재현 필요 — 정적 분석상 ①만 동작 변화, ②③④ 불변 예상.) **재현 검증 체크리스트**: ① 육각형 용마루 6변 전부 등록(소실 없음) ② 단순 사각형 우진각 4변 유지(회귀 없음) ③ 변별 지붕 외곽선·추녀 정상 ④ 한쪽흐름 경사변 actual=plane/cos 유지. (캔버스 인터랙션이라 런타임 확인은 재현 필요 — 정적 분석상 ①만 동작 변화, ②③④ 불변 예상.)
---
## 11. 수동설정 & 동선이동/형올림내림 — 외곽 지붕선/폴리곤 숨김 정합 (2026-07-08 추가)
지금까지(§1~§10)는 `drawHelpLine`**부르는** 경로에서 외곽 지붕선을 편집 가능한 innerLine 으로 만드는 작업이었다. §11 은 그 반대편 — `drawHelpLine`/`split` 결과가 **누락되거나 덮어써져** 원본 지붕 폴리곤이 화면에 남는 두 기능을 정합한다.
### 11-1. 배경 / 두 증상
| 기능 | 담당 훅 | 파란 외곽 지붕선 | 원본 폴리곤 |
|---|---|---|---|
| **지붕형상 수동설정** | `useRoofShapePassivitySetting.js` | ❌ 없음 | 검은 외곽선 노출 |
| **동선이동 / 형올림내림** | `useMovementSetting.js` (`MENU.ROOF_COVERING.MOVEMENT_SHAPE_UPDOWN`) | ✅ 있음 | **파란** 외곽선 노출 |
### 11-2. 근본 원인 (정적 분석 + 색상 실측)
- 원본 지붕 폴리곤은 `stroke:'black'` / `fill:'transparent'` / `strokeWidth:3` 로 생성(`usePolygon.js:2346`). 즉 보이는 것은 **검은 외곽선**(면 아님).
- 폴리곤을 숨기는 `roof.set({ stroke: 'transparent' })` 는 코드 전체에서 **딱 두 곳**`splitRoofLinesToInnerLines`(`qpolygon-utils.js:5775`), `drawGableRoof`(`qpolygon-utils.js:1709`). `skeleton-utils.js` 에는 없음(용마루 경로도 `split:1288` 경유로만 숨김).
- **수동설정**: `handleLineToPolygon``drawRoofPolygon(wall)`(`:288`,`:296`)만 호출하고 `drawHelpLine`/`split`**안 부름** → 외곽 지붕선 미생성 + 폴리곤 stroke 검정 그대로. (외곽선 `attributes.type` 은 완료 검증(`:208`)으로 세팅돼 있음.)
- **동선이동/형올림내림**: 저장 시 `drawHelpLine()`(`:1002`) → `split`(`:5775`)이 `transparent` 로 정상 숨김. 그러나 팝업 cleanup 의 `restoreLineStyles`(`:222`)가 `roof.set({ stroke: '#1083E3' })`**되살림** — "평상 stroke" 를 파란색으로 오인(주석 `:217`). cleanup 은 팝업 닫을 때마다 무조건 실행(`:159`)되어 **항상** 발생(파란색 실측 확인).
### 11-3. 설계 (v2 — 확정)
**변경 1 — 수동설정: `split` 직접 호출** (`drawHelpLine` 아님)
`useRoofShapePassivitySetting.js` `handleLineToPolygon` 의 최종 `drawRoofPolygon(wall)`(`:296`) 직후:
```js
import { splitRoofLinesToInnerLines } from '@/util/qpolygon-utils'
...
const roof = drawRoofPolygon(wall)
addLengthText(roof)
roof.innerLines = roof.innerLines || []
splitRoofLinesToInnerLines(roof, roof.innerLines, canvas, 'plane') // 외곽 지붕선 + 폴리곤 숨김
```
- `innerLines` **빈 배열** → 분할점 없어 각 `roof.lines` 변이 통짜 `drawRoofLine`(외곽 지붕선)으로 그려짐. **마루·추녀 없음**(수동설정은 외곽 지붕선만 필요 — 도메인 확정).
- 형상 자동 분기(`drawHelpLine`) 미사용 → 사용자 수동 지정 존중.
- `split` 내부 `:5775` 가 폴리곤 숨김(`stroke:'transparent'`)을 동반.
- `textMode``'plane'`(이 훅엔 `settingModalFirstOptions` 없음).
- ⚠️ 구현 시 확인: `drawRoofPolygon``:288`·`:296` 두 번 호출됨 → 최종 화면에 남는 roof 인스턴스에 `split` 을 붙일 것.
**변경 2 — 동선이동/형올림내림: `restoreLineStyles` 정합**
`useMovementSetting.js:222`:
```diff
- roof.set({ stroke: '#1083E3' })
+ roof.set({ stroke: 'transparent' }) // 평상 상태 = 폴리곤 숨김
```
- `innerLines` 파란색(`:223``baseLines` 숨김(`:231`)은 **유지**(지붕선은 파란색이 맞음).
- 저장/취소 both 정상 — 취소(이동 없이 닫기) 시에도 진입 `:90` 의 검정 stroke 를 올바르게 원복.
### 11-4. 범위 밖 (YAGNI)
`drawHelpLine` 최상위에 폴리곤 숨김을 일원화(형상 분기·SK 예외와 무관하게 항상 `stroke:'transparent'`)하는 안을 검토했으나 **제외**. 관측된 두 버그와 직접 무관하고(수동설정=`split` 직접, 동선이동=cleanup 덮어쓰기), 4경로 회귀 부담만 늘어남. 세 기능 모두 `split`(`:5775`)/`gable`(`:1709`) 숨김을 타므로 불필요.
### 11-5. 검증 기준
1. 수동설정 완료 → 외곽 지붕선(파란) 생성 + 검은 폴리곤 사라짐. 마루/추녀는 없음(의도).
2. 동선이동/형올림내림 저장 → 폴리곤 안 보임 + 지붕선 파란 유지.
3. 동선이동/형올림내림 **취소**(이동 없이 팝업 닫기) → 폴리곤 안 보임(원복).
4. 회귀: 템플릿설정(`useRoofShapeSetting`) 4경로 지붕 정상, 동선이동 마루이동(FLOW_LINE)·형올림내림(UP_DOWN) 양쪽 동작.

View File

@ -0,0 +1,148 @@
# 수동설정 & 동선이동/형올림내림 외곽 지붕선/폴리곤 숨김 정합 — 구현 계획
> **For agentic workers:** 이 계획은 task 단위로 실행한다. 각 step 은 checkbox(`- [ ]`)로 추적한다.
> **검증 방식:** 이 저장소는 test runner 가 없다(CLAUDE.md). 각 task 는 `yarn dev` 컴파일 성공 + 런타임 재현 체크리스트로 검증한다. **커밋은 재영이 나중에 일괄 수행** — 이 계획은 커밋 step 을 두지 않는다.
**Goal:** 지붕형상 수동설정과 동선이동/형올림내림 이후에도 원본 지붕 폴리곤이 화면에 남지 않고, 외곽 지붕선(파란 QLine)만 보이도록 정합한다.
**Architecture:** 설계 근거는 `docs/plans/2026-06-01-skeleton-roofline-edit-design.md` §11. 두 지점을 수정한다 — (1) 수동설정은 중복 `drawRoofPolygon` 호출을 제거하고 최종 roof 에 `splitRoofLinesToInnerLines` 를 직접 호출, (2) 동선이동/형올림내림은 팝업 cleanup 의 `restoreLineStyles` 가 폴리곤 stroke 를 파란색으로 되살리던 것을 `transparent` 로 정합.
**Tech Stack:** Next.js 14, Fabric.js 5.5.2, `@/util/qpolygon-utils``splitRoofLinesToInnerLines`.
---
## 파일 구조
| 파일 | 책임 | 변경 |
|---|---|---|
| `src/hooks/roofcover/useRoofShapePassivitySetting.js` | 지붕형상 수동설정 | import 추가 + `handleLineToPolygon` 중복 제거 + split 호출 |
| `src/hooks/roofcover/useMovementSetting.js` | 동선이동/형올림내림 | `restoreLineStyles` 1줄 정합 |
전제(확정된 사실):
- `splitRoofLinesToInnerLines(roof, innerLines, canvas, textMode, opts={})``@/util/qpolygon-utils``export` 되어 있다.
- `innerLines` 빈 배열로 부르면 분할점이 없어 각 `roof.lines` 변이 통짜 외곽 지붕선(`drawRoofLine`)으로 그려지고, 함수 끝(`:5775`)에서 `roof.set({ stroke: 'transparent' })` 로 원본 폴리곤을 숨긴다. 마루/추녀는 생성되지 않는다(수동설정 요구사항).
- `drawRoofPolygon``makePolygon`(`useMode.js:1387` `canvas.add`) 경유로 roof 를 캔버스에 추가하고 기존 roof 를 제거하지 않는다. 그래서 수동설정의 `:288`·`:296` 이중 호출은 roof 2개를 남긴다(중복 확정).
---
## Task 1: 수동설정 — 중복 `drawRoofPolygon` 제거 + `split` 직접 호출
**Files:**
- Modify: `src/hooks/roofcover/useRoofShapePassivitySetting.js` (import 라인, `handleLineToPolygon` `:286-298`)
- [ ] **Step 1: `splitRoofLinesToInnerLines` import 추가**
파일 상단 import 블록(`:1-13`)에 아래 한 줄을 추가한다(기존 `@/util/canvas-util` import 아래 등 적절한 위치):
```js
import { splitRoofLinesToInnerLines } from '@/util/qpolygon-utils'
```
- [ ] **Step 2: `handleLineToPolygon` 의 중복 세트 제거 + split 삽입**
현재 코드(`:286-298`):
```js
// 완료 한 경우에는 지붕까지 그려줌
addPitchTextsByOuterLines()
const roof = drawRoofPolygon(wall)
addLengthText(roof)
if (isFix.current) {
// 완료 한 경우에는 지붕까지 그려줌
addPitchTextsByOuterLines()
// 지붕 그리기 직전(라인 세팅 완료) 상태를 스냅샷 → undo 시 지붕이 그려지기 전 화면으로 복귀
saveSnapshot(null, { force: true })
const roof = drawRoofPolygon(wall)
addLengthText(roof)
}
```
아래로 교체한다(블록 밖 첫 세트 3줄 제거, `if` 블록 내 split 2줄 추가):
```js
if (isFix.current) {
// 완료 한 경우에는 지붕까지 그려줌
addPitchTextsByOuterLines()
// 지붕 그리기 직전(라인 세팅 완료) 상태를 스냅샷 → undo 시 지붕이 그려지기 전 화면으로 복귀
saveSnapshot(null, { force: true })
const roof = drawRoofPolygon(wall)
addLengthText(roof)
// [2026-07-08] 지붕모양(roof.lines)을 따라 외곽 지붕선 생성 + 원본 폴리곤 숨김(transparent).
// innerLines 빈 배열 → 분할점 없어 각 변이 통짜 지붕선(drawRoofLine). 마루/추녀는 생성 안 함(수동설정은 외곽선만).
roof.innerLines = roof.innerLines || []
splitRoofLinesToInnerLines(roof, roof.innerLines, canvas, 'plane')
}
```
근거: `isFix=false` 경로는 이 지점 이전 `:228`에서 롤백 후 `return` 하므로 제거한 첫 세트는 저장 경로에서만 실행되던 중복이다. 제거로 `saveSnapshot` 이 비로소 '지붕 그리기 전' 상태를 찍게 되어 주석과 코드가 일치한다.
- [ ] **Step 3: dev 컴파일 검증**
Run: `yarn dev`
Expected: 컴파일 에러 없이 로컬 서버 기동(`localhost:3000`). `splitRoofLinesToInnerLines` import 미해결/문법 에러 없음.
(주의: dev 실행 중 `yarn build` 금지 — `.next` 오염.)
- [ ] **Step 4: 런타임 재현 검증 (수동설정)**
브라우저에서 지붕형상 **수동설정**으로 외곽선 타입을 지정하고 완료.
Expected:
- 외곽 지붕선(파란 QLine)이 그려진다.
- 검은/파란 **원본 폴리곤 외곽선이 남지 않는다**(겹친 roof 없음).
- 마루/추녀는 생성되지 않는다(의도).
- 이후 지붕면 할당 메뉴로 정상 전환(`:216`).
---
## Task 2: 동선이동/형올림내림 — `restoreLineStyles` 폴리곤 stroke 정합
**Files:**
- Modify: `src/hooks/roofcover/useMovementSetting.js:222`
- [ ] **Step 1: cleanup 의 폴리곤 stroke 복원값을 transparent 로 변경**
현재 `restoreLineStyles`(`:219-224`):
```js
const restoreLineStyles = () => {
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
roofs.forEach((roof) => {
roof.set({ stroke: '#1083E3' })
roof.innerLines.forEach((line) => line.set({ selectable: true, strokeWidth: 2, stroke: '#1083E3' }))
})
```
`:222` 한 줄만 변경한다:
```js
roof.set({ stroke: 'transparent' })
```
`roof.innerLines`(`:223`) 의 파란색·selectable 복원은 **그대로 유지**한다(지붕선은 파란색이 맞음). 근거: 평상 상태의 원본 폴리곤 stroke 는 `split`(`:5775`)이 만든 `transparent` 이며, cleanup 이 이를 파란색으로 되살리던 것이 폴리곤이 항상 보이던 원인이다.
- [ ] **Step 2: dev 컴파일 검증**
Run: `yarn dev`
Expected: 컴파일 에러 없음.
- [ ] **Step 3: 런타임 재현 검증 (동선이동/형올림내림)**
브라우저에서:
- **동선이동(FLOW_LINE)** 마루 이동 저장 → 팝업 닫힘 후 원본 폴리곤이 **안 보이고** 외곽 지붕선(파란)만 남는다.
- **형올림내림(UP_DOWN)** 저장 → 동일하게 폴리곤 안 보임.
- **취소**(이동 없이 팝업 닫기) → 폴리곤 안 보임(진입 시 `:90` 에서 검게 만든 stroke 가 올바르게 원복).
---
## 최종 회귀 검증
- [ ] **템플릿설정(`useRoofShapeSetting`) 4경로 회귀**: 용마루/박공/한쪽흐름/변별 지붕을 템플릿으로 생성 → 외곽 지붕선·폴리곤 숨김 기존과 동일(회귀 없음).
- [ ] **동선이동 2회 연속**: 마루이동 → 다시 마루이동 시 지붕선/폴리곤 정상.
- [ ] **수동설정 → 동선이동 연계**: 수동설정으로 만든 지붕을 동선이동/형올림내림 → 폴리곤 안 보임 유지.
---
## 범위 밖 (YAGNI)
- `drawHelpLine` 최상위 폴리곤 숨김 일원화 — 제외(설계 §11-4). 두 버그와 직접 무관.
- `addPitchTextsByOuterLines` 는 Task 1 에서 첫 세트 제거로 자연히 1회만 남는다. 별도 정리 불필요.

View File

@ -180,7 +180,6 @@ export default function PassivityCircuitAllocation(props) {
.filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.pcs && obj.pcs.id === selectedPcs.id) .filter((obj) => obj.name === POLYGON_TYPE.MODULE && obj.pcs && obj.pcs.id === selectedPcs.id)
// 1. , // 1. ,
const targetModuleInfos = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && targetModules.includes(obj.id)) const targetModuleInfos = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE && targetModules.includes(obj.id))
debugger
const newTargetModuleGroup = [...new Set(targetModuleInfos.concat(originHaveThisPcsModules).map((obj) => obj.moduleInfo.northModuleYn))] const newTargetModuleGroup = [...new Set(targetModuleInfos.concat(originHaveThisPcsModules).map((obj) => obj.moduleInfo.northModuleYn))]
if (newTargetModuleGroup.length > 1) { if (newTargetModuleGroup.length > 1) {

View File

@ -650,6 +650,15 @@ export function useModuleBasicSetting(tabNum) {
} }
}, [isManualModuleSetup, isManualModuleLayoutSetup]) }, [isManualModuleSetup, isManualModuleLayoutSetup])
// [MANUAL-CHIDORI-BY-FACE 2026-07-08] 千鳥(치도리) 가부는 지붕재(면)별로 다르다(plvrYn).
// 千鳥 라디오가 전역 ON이어도 커서 밑 면이 不可(plvrYn==='N')면 그 면에선 千鳥 스냅을 끄고 정렬로 배치한다.
const isChidoriAllowedForFace = (face) => {
const roofIndex = face?.roofMaterial?.index
if (roofIndex == null) return true
const plvrYn = moduleSelectionData?.roofConstructions?.find((c) => Number(c.roofIndex) === Number(roofIndex))?.construction?.plvrYn
return plvrYn !== 'N'
}
/** /**
* trestle에서 영역을 가져와 mouse:move 이벤트로 해당 영역에 진입했을때 booleanPointInPolygon 진입여부를 확인 * trestle에서 영역을 가져와 mouse:move 이벤트로 해당 영역에 진입했을때 booleanPointInPolygon 진입여부를 확인
* 확인 셀을 이동시킴 * 확인 셀을 이동시킴
@ -679,6 +688,14 @@ export function useModuleBasicSetting(tabNum) {
return return
} }
// [MANUAL-SURFACE-GATE 2026-07-08] 자동배치와 동일하게 선택된 설치면이 없으면 배치 불가 (선택 없이는 千鳥 판정 대상도 없음)
if (moduleSetupSurface.length === 0) {
swalFire({ text: getMessage('module.place.no.surface'), icon: 'warning' })
setIsManualModuleSetup(false)
setManualSetupMode(`manualSetup_false`)
return
}
const batchObjects = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.OBJECT_SURFACE) //도머s 객체 const batchObjects = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.OBJECT_SURFACE) //도머s 객체
//수동모드 모듈 설치면 선택 잠금 //수동모드 모듈 설치면 선택 잠금
moduleSetupSurfaces.forEach((obj) => { moduleSetupSurfaces.forEach((obj) => {
@ -843,7 +860,7 @@ export function useModuleBasicSetting(tabNum) {
tempModule.left = holdCellCenterX - toFixedWithoutRounding(width / 2, 2) tempModule.left = holdCellCenterX - toFixedWithoutRounding(width / 2, 2)
} }
if (isChidori) { if (isChidori && isChidoriAllowedForFace(moduleSetupSurfaces[i])) {
//움직이는 모듈왼쪽 -> 가운데 //움직이는 모듈왼쪽 -> 가운데
if (Math.abs(smallLeft - holdCellCenterX) < snapDistance) { if (Math.abs(smallLeft - holdCellCenterX) < snapDistance) {
tempModule.left = holdCellCenterX + intvHor / 2 tempModule.left = holdCellCenterX + intvHor / 2
@ -886,7 +903,7 @@ export function useModuleBasicSetting(tabNum) {
tempModule.top = holdCellCenterY - toFixedWithoutRounding(height / 2, 2) tempModule.top = holdCellCenterY - toFixedWithoutRounding(height / 2, 2)
} }
if (isChidori) { if (isChidori && isChidoriAllowedForFace(moduleSetupSurfaces[i])) {
//위쪽 -> 가운데 //위쪽 -> 가운데
if (Math.abs(smallTop - holdCellCenterY) < snapDistance) { if (Math.abs(smallTop - holdCellCenterY) < snapDistance) {
tempModule.top = holdCellCenterY + intvHor / 2 tempModule.top = holdCellCenterY + intvHor / 2
@ -986,6 +1003,19 @@ export function useModuleBasicSetting(tabNum) {
// } // }
} }
// [MANUAL-CHIDORI-BY-FACE 2026-07-08] 千鳥는 「가능면 + 한다」일 때만 허용. 그 외(불가면 전부 / 가능면이어도 「하지 않는다」)는 手動 드래그도 정렬 강제.
// 사용자가 반칸 어긋난 위치에 놓아도 기존 모듈 열/행 격자(pitch)에 흡착시켜 치도리를 제거한다.
if (!(isChidori && isChidoriAllowedForFace(moduleSetupSurfaces[i])) && manualDrawModules && manualDrawModules.length > 0) {
const gridRef = manualDrawModules[0]
if (flowDirection === 'south' || flowDirection === 'north') {
const pitchX = width + intvHor
if (pitchX > 0) tempModule.left = gridRef.left + Math.round((tempModule.left - gridRef.left) / pitchX) * pitchX
} else {
const pitchY = height + intvVer
if (pitchY > 0) tempModule.top = gridRef.top + Math.round((tempModule.top - gridRef.top) / pitchY) * pitchY
}
}
tempModule.setCoords() tempModule.setCoords()
canvas?.renderAll() canvas?.renderAll()
inside = true inside = true
@ -1194,6 +1224,14 @@ export function useModuleBasicSetting(tabNum) {
return return
} }
// [MANUAL-SURFACE-GATE 2026-07-08] 자동배치와 동일하게 선택된 설치면이 없으면 배치 불가
if (moduleSetupSurface.length === 0) {
swalFire({ text: getMessage('module.place.no.surface'), icon: 'warning' })
setIsManualModuleLayoutSetup(false)
setManualSetupMode(`manualLayoutSetup_false`)
return
}
const batchObjects = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.OBJECT_SURFACE) //도머s 객체 const batchObjects = canvas?.getObjects().filter((obj) => obj.name === POLYGON_TYPE.OBJECT_SURFACE) //도머s 객체
//수동모드 모듈 설치면 선택 잠금 //수동모드 모듈 설치면 선택 잠금
moduleSetupSurfaces.forEach((obj) => { moduleSetupSurfaces.forEach((obj) => {
@ -1396,7 +1434,7 @@ export function useModuleBasicSetting(tabNum) {
tempModule.left = holdCellCenterX - toFixedWithoutRounding(width / 2, 2) tempModule.left = holdCellCenterX - toFixedWithoutRounding(width / 2, 2)
} }
if (isChidori) { if (isChidori && isChidoriAllowedForFace(moduleSetupSurfaces[i])) {
//움직이는 모듈왼쪽 -> 가운데 //움직이는 모듈왼쪽 -> 가운데
if (Math.abs(smallLeft - holdCellCenterX) < snapDistance) { if (Math.abs(smallLeft - holdCellCenterX) < snapDistance) {
tempModule.left = holdCellCenterX + intvHor / 2 tempModule.left = holdCellCenterX + intvHor / 2
@ -1439,7 +1477,7 @@ export function useModuleBasicSetting(tabNum) {
tempModule.top = holdCellCenterY - toFixedWithoutRounding(width / 2, 2) tempModule.top = holdCellCenterY - toFixedWithoutRounding(width / 2, 2)
} }
if (isChidori) { if (isChidori && isChidoriAllowedForFace(moduleSetupSurfaces[i])) {
//위쪽 -> 가운데 //위쪽 -> 가운데
if (Math.abs(smallTop - holdCellCenterY) < snapDistance) { if (Math.abs(smallTop - holdCellCenterY) < snapDistance) {
tempModule.top = holdCellCenterY + intvHor / 2 tempModule.top = holdCellCenterY + intvHor / 2
@ -1587,6 +1625,9 @@ export function useModuleBasicSetting(tabNum) {
if (tempModule) { if (tempModule) {
saveSnapshot() saveSnapshot()
// [MANUAL-CHIDORI-BY-FACE 2026-07-08] 지정배치 그리드 대상 면(trestlePolygon)이 千鳥 不可(plvrYn==='N')면 千鳥 오프셋 없이 정렬 배치.
// 아래 그리드 생성부의 isChidori 는 이 면 기준 값을 따른다(전역 라디오가 ON이어도 不可면 정렬).
const isChidori = moduleSetupOption.isChidori && isChidoriAllowedForFace(trestlePolygon)
let startX, startY let startX, startY
let installedLastHeightCoord = 0 //마지막으로 설치된 모듈의 좌표 let installedLastHeightCoord = 0 //마지막으로 설치된 모듈의 좌표
let installedHeightModuleCount = 0 let installedHeightModuleCount = 0

View File

@ -3384,18 +3384,75 @@ export const useTrestle = () => {
const surfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE) const surfaces = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE_SETUP_SURFACE)
const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE) const modules = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE)
let case1Cnt = 0
let case2Cnt = 0
const perSurfaceGroups = []
surfaces.forEach((surface) => { surfaces.forEach((surface) => {
const modules = surface.modules const modules = surface.modules
// 1. 현재 surface의 모듈들을 그룹화 // 1. 현재 surface의 모듈들을 그룹화
const groupInSurface = groupPoints(modules, surface) const groupInSurface = groupPoints(modules, surface)
const surfaceCnt = countMatchingCircuitNumbers(groupInSurface)
cnt += countMatchingCircuitNumbers(groupInSurface) perSurfaceGroups.push({
surfaceId: surface.id,
direction: surface.direction,
moduleIntvlHor: surface.trestleDetail?.moduleIntvlHor,
moduleIntvlVer: surface.trestleDetail?.moduleIntvlVer,
addedCnt: surfaceCnt,
groups: groupInSurface.map((g) =>
g.map((m) => {
const c = m.getCenterPoint()
return { id: m.id, circuitNumber: m.circuitNumber, x: +c.x.toFixed(2), y: +c.y.toFixed(2), w: m.width, h: m.height }
}),
),
})
cnt += surfaceCnt
case1Cnt += surfaceCnt
}) })
const groupByCircuitAndSurfaceCnt = groupByCircuitAndSurface(modules) const groupByCircuitAndSurfaceCnt = groupByCircuitAndSurface(modules)
Object.keys(groupByCircuitAndSurfaceCnt).forEach((key) => { Object.keys(groupByCircuitAndSurfaceCnt).forEach((key) => {
cnt += groupByCircuitAndSurfaceCnt[key] - 1 cnt += groupByCircuitAndSurfaceCnt[key] - 1
case2Cnt += groupByCircuitAndSurfaceCnt[key] - 1
})
// [DBL-CABLE-PROD] 운영 1회 진단용 — env 게이트 없이 브라우저 콘솔에 전량 출력.
// 진단 후 제거. 이 한 줄만 복사해서 전달하면 원인(Case1 면내분리 / Case2 면교차) 확정 가능.
console.log('[DBL-CABLE-PROD]', {
total: cnt,
case1_sameSurfaceSplit: case1Cnt,
case2_crossSurface: case2Cnt,
groupByCircuitAndSurfaceCnt,
roofSizeSet,
modules: modules.map((m) => {
const c = m.getCenterPoint()
return {
id: m.id,
circuitNumber: m.circuitNumber,
surfaceId: m.surfaceId,
x: +c.x.toFixed(2),
y: +c.y.toFixed(2),
w: m.width,
h: m.height,
northModuleYn: m.moduleInfo?.northModuleYn,
moduleTpCd: m.moduleInfo?.moduleTpCd,
pcsItemId: m.pcsItemId,
pcsTpCd: m.pcs?.pcsTpCd,
}
}),
surfacesSummary: surfaces.map((s) => ({
id: s.id,
direction: s.direction,
moduleCount: s.modules?.length,
pitch: s.roofMaterial?.pitch,
roofMatlCd: s.roofMaterial?.roofMatlCd,
moduleIntvlHor: s.trestleDetail?.moduleIntvlHor,
moduleIntvlVer: s.trestleDetail?.moduleIntvlVer,
})),
perSurfaceGroups,
}) })
return cnt return cnt

View File

@ -219,7 +219,9 @@ export function useMovementSetting(id) {
const restoreLineStyles = () => { const restoreLineStyles = () => {
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF) const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
roofs.forEach((roof) => { roofs.forEach((roof) => {
roof.set({ stroke: '#1083E3' }) // [2026-07-08] 평상 상태의 원본 폴리곤 stroke 는 split(:5775)이 만든 transparent.
// 파란색으로 복원하면 지붕 폴리곤이 화면에 남으므로 transparent 로 되돌린다. (innerLines 는 파란색 유지)
roof.set({ stroke: 'transparent' })
roof.innerLines.forEach((line) => line.set({ selectable: true, strokeWidth: 2, stroke: '#1083E3' })) roof.innerLines.forEach((line) => line.set({ selectable: true, strokeWidth: 2, stroke: '#1083E3' }))
}) })
const walls = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) const walls = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)

View File

@ -11,6 +11,7 @@ import { useSwal } from '@/hooks/useSwal'
import { usePopup } from '@/hooks/usePopup' import { usePopup } from '@/hooks/usePopup'
import { getChonByDegree } from '@/util/canvas-util' import { getChonByDegree } from '@/util/canvas-util'
import { useUndoRedo } from '@/hooks/useUndoRedo' import { useUndoRedo } from '@/hooks/useUndoRedo'
import { splitRoofLinesToInnerLines } from '@/util/qpolygon-utils'
//지붕형상 수동 설정 //지붕형상 수동 설정
export function useRoofShapePassivitySetting(id) { export function useRoofShapePassivitySetting(id) {
@ -283,11 +284,6 @@ export function useRoofShapePassivitySetting(id) {
const wall = addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' }) const wall = addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' })
wall.lines = [...lines] wall.lines = [...lines]
// 완료 한 경우에는 지붕까지 그려줌
addPitchTextsByOuterLines()
const roof = drawRoofPolygon(wall)
addLengthText(roof)
if (isFix.current) { if (isFix.current) {
// 완료 한 경우에는 지붕까지 그려줌 // 완료 한 경우에는 지붕까지 그려줌
addPitchTextsByOuterLines() addPitchTextsByOuterLines()
@ -295,6 +291,10 @@ export function useRoofShapePassivitySetting(id) {
saveSnapshot(null, { force: true }) saveSnapshot(null, { force: true })
const roof = drawRoofPolygon(wall) const roof = drawRoofPolygon(wall)
addLengthText(roof) addLengthText(roof)
// [2026-07-08] 지붕모양(roof.lines)을 따라 외곽 지붕선 생성 + 원본 폴리곤 숨김(transparent).
// innerLines 빈 배열 → 분할점 없어 각 변이 통짜 지붕선(drawRoofLine). 마루/추녀는 생성 안 함(수동설정은 외곽선만).
roof.innerLines = roof.innerLines || []
splitRoofLinesToInnerLines(roof, roof.innerLines, canvas, 'plane')
} }
// 외벽선은 WALL/ROOF 폴리곤 바로 위에 배치 — 맨 앞(bringToFront)으로 올리면 // 외벽선은 WALL/ROOF 폴리곤 바로 위에 배치 — 맨 앞(bringToFront)으로 올리면
// offset 0인 변에서 같은 좌표에 그려진 보조선의 클릭을 가로채 선택을 막는다 // offset 0인 변에서 같은 좌표에 그려진 보조선의 클릭을 가로채 선택을 막는다