dev #464

Merged
ysCha merged 27 commits from dev into prd-deploy 2025-12-13 17:14:18 +09:00
11 changed files with 1917 additions and 876 deletions

3
.gitignore vendored
View File

@ -42,4 +42,5 @@ next-env.d.ts
yarn.lock yarn.lock
package-lock.json package-lock.json
pnpm-lock.yaml pnpm-lock.yaml
certificates certificates
.ai

View File

@ -22,7 +22,7 @@
"chart.js": "^4.4.6", "chart.js": "^4.4.6",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"fabric": "^5.3.0", "fabric": "^5.5.2",
"framer-motion": "^11.2.13", "framer-motion": "^11.2.13",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"iron-session": "^8.0.2", "iron-session": "^8.0.2",

View File

@ -336,8 +336,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) { if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) {
// 용마루 -- straight-skeleton // 용마루 -- straight-skeleton
console.log('용마루 지붕') console.log('용마루 지붕')
drawRidgeRoof(this.id, this.canvas, textMode) //drawRidgeRoof(this.id, this.canvas, textMode)
//drawSkeletonRidgeRoof(this.id, this.canvas, textMode); drawSkeletonRidgeRoof(this.id, this.canvas, textMode);
} else if (isGableRoof(types)) { } else if (isGableRoof(types)) {
// A형, B형 박공 지붕 // A형, B형 박공 지붕
console.log('패턴 지붕') console.log('패턴 지붕')

View File

@ -32,6 +32,7 @@ import { useEvent } from '@/hooks/useEvent'
import { compasDegAtom } from '@/store/orientationAtom' import { compasDegAtom } from '@/store/orientationAtom'
import { hotkeyStore } from '@/store/hotkeyAtom' import { hotkeyStore } from '@/store/hotkeyAtom'
import { usePopup } from '@/hooks/usePopup' import { usePopup } from '@/hooks/usePopup'
import { outerLinePointsState } from '@/store/outerLineAtom'
export default function CanvasFrame() { export default function CanvasFrame() {
const canvasRef = useRef(null) const canvasRef = useRef(null)
@ -45,6 +46,7 @@ export default function CanvasFrame() {
const totalDisplay = useRecoilValue(totalDisplaySelector) // const totalDisplay = useRecoilValue(totalDisplaySelector) //
const { setIsGlobalLoading } = useContext(QcastContext) const { setIsGlobalLoading } = useContext(QcastContext)
const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState) const resetModuleStatisticsState = useResetRecoilState(moduleStatisticsState)
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
const resetMakersState = useResetRecoilState(makersState) const resetMakersState = useResetRecoilState(makersState)
const resetSelectedMakerState = useResetRecoilState(selectedMakerState) const resetSelectedMakerState = useResetRecoilState(selectedMakerState)
const resetSeriesState = useResetRecoilState(seriesState) const resetSeriesState = useResetRecoilState(seriesState)
@ -137,6 +139,7 @@ export default function CanvasFrame() {
const resetRecoilData = () => { const resetRecoilData = () => {
// resetModuleStatisticsState() // resetModuleStatisticsState()
resetOuterLinePoints()
resetMakersState() resetMakersState()
resetSelectedMakerState() resetSelectedMakerState()
resetSeriesState() resetSeriesState()

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react' import { useEffect, useRef, useState } from 'react'
import { useMessage } from '@/hooks/useMessage' import { useMessage } from '@/hooks/useMessage'
import WithDraggable from '@/components/common/draggable/WithDraggable' import WithDraggable from '@/components/common/draggable/WithDraggable'
import { useRecoilValue } from 'recoil' import { useRecoilValue } from 'recoil'
@ -15,8 +15,8 @@ export default function DormerOffset(props) {
const { closePopup } = usePopup() const { closePopup } = usePopup()
const [arrow1, setArrow1] = useState(null) const [arrow1, setArrow1] = useState(null)
const [arrow2, setArrow2] = useState(null) const [arrow2, setArrow2] = useState(null)
const arrow1LengthRef = useRef() const arrow1LengthRef = useRef(0)
const arrow2LengthRef = useRef() const arrow2LengthRef = useRef(0)
const [arrow1Length, setArrow1Length] = useState(0) const [arrow1Length, setArrow1Length] = useState(0)
const [arrow2Length, setArrow2Length] = useState(0) const [arrow2Length, setArrow2Length] = useState(0)
@ -59,12 +59,12 @@ export default function DormerOffset(props) {
name="" name=""
label="" label=""
className="input-origin block" className="input-origin block"
value={arrow1LengthRef.current.value} value={arrow1LengthRef.current.value ?? 0}
ref={arrow1LengthRef} ref={arrow1LengthRef}
onChange={(value) => setArrow1Length(value)} onChange={(value) => setArrow1Length(value)}
options={{ options={{
allowNegative: false, allowNegative: false,
allowDecimal: false allowDecimal: false,
}} }}
/> />
</div> </div>

View File

@ -264,7 +264,6 @@ export default function Simulator() {
style={{ width: '30%' }} style={{ width: '30%' }}
className="select-light" className="select-light"
value={pwrGnrSimType} value={pwrGnrSimType}
defaultValue={`D`}
onChange={(e) => { onChange={(e) => {
handleChartChangeData(e.target.value) handleChartChangeData(e.target.value)
setPwrGnrSimType(e.target.value) setPwrGnrSimType(e.target.value)
@ -334,33 +333,31 @@ export default function Simulator() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{moduleInfoList.length > 0 ? ( {moduleInfoList.length > 0 ? (
moduleInfoList.map((moduleInfo) => { moduleInfoList.map((moduleInfo) => {
return ( return (
<> <tr key={moduleInfo.itemId}>
<tr key={moduleInfo.itemId}> {/* 지붕면 */}
{/* 지붕면 */} <td>{moduleInfo.roofSurface}</td>
<td>{moduleInfo.roofSurface}</td> {/* 경사각 */}
{/* 경사각 */} <td>
<td> {convertNumberToPriceDecimal(moduleInfo.slopeAngle)}
{convertNumberToPriceDecimal(moduleInfo.slopeAngle)} {moduleInfo.classType == 0 ? '寸' : 'º'}
{moduleInfo.classType == 0 ? '寸' : 'º'} </td>
</td> {/* 방위각(도) */}
{/* 방위각(도) */} <td>{convertNumberToPriceDecimal(moduleInfo.azimuth)}</td>
<td>{convertNumberToPriceDecimal(moduleInfo.azimuth)}</td> {/* 태양전지모듈 */}
{/* 태양전지모듈 */} <td>
<td> <div className="overflow-lab">{moduleInfo.itemNo}</div>
<div className="overflow-lab">{moduleInfo.itemNo}</div> </td>
</td> {/* 매수 */}
{/* 매수 */} <td>{convertNumberToPriceDecimal(moduleInfo.amount)}</td>
<td>{convertNumberToPriceDecimal(moduleInfo.amount)}</td> </tr>
</tr> )
</> })
) ) : (
}) <tr>
) : ( <td colSpan={5}>{getMessage('common.message.no.data')}</td>
<tr>
<td colSpan={5}>{getMessage('common.message.no.data')}</td>
</tr> </tr>
)} )}
</tbody> </tbody>
@ -385,25 +382,23 @@ export default function Simulator() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{pcsInfoList.length > 0 ? ( {pcsInfoList.length > 0 ? (
pcsInfoList.map((pcsInfo) => { pcsInfoList.map((pcsInfo) => {
return ( return (
<> <tr key={pcsInfo.itemId}>
<tr key={pcsInfo.itemId}> {/* 파워컨디셔너 */}
{/* 파워컨디셔너 */} <td className="al-l">
<td className="al-l"> <div className="overflow-lab">{pcsInfo.itemNo}</div>
<div className="overflow-lab">{pcsInfo.itemNo}</div> </td>
</td> {/* 대 */}
{/* 대 */} <td>{convertNumberToPriceDecimal(pcsInfo.amount)}</td>
<td>{convertNumberToPriceDecimal(pcsInfo.amount)}</td> </tr>
</tr> )
</> })
) ) : (
}) <tr>
) : ( <td colSpan={2}>{getMessage('common.message.no.data')}</td>
<tr> </tr>
<td colSpan={2}>{getMessage('common.message.no.data')}</td>
</tr>
)} )}
</tbody> </tbody>
</table> </table>

View File

@ -648,6 +648,7 @@ export function useCommonUtils() {
lockMovementY: true, lockMovementY: true,
name: obj.name, name: obj.name,
editable: false, editable: false,
selectable: true, // 복사된 객체 선택 가능하도록 설정
id: uuidv4(), //복사된 객체라 새로 따준다 id: uuidv4(), //복사된 객체라 새로 따준다
}) })
@ -656,19 +657,25 @@ export function useCommonUtils() {
//배치면일 경우 //배치면일 경우
if (obj.name === 'roof') { if (obj.name === 'roof') {
clonedObj.setCoords() clonedObj.canvas = canvas // canvas 참조 설정
clonedObj.fire('modified')
// clonedObj.fire('polygonMoved')
clonedObj.set({ clonedObj.set({
direction: obj.direction, direction: obj.direction,
directionText: obj.directionText, directionText: obj.directionText,
roofMaterial: obj.roofMaterial, roofMaterial: obj.roofMaterial,
stroke: 'black', // 복사된 객체는 선택 해제 상태의 색상으로 설정
selectable: true, // 선택 가능하도록 설정
evented: true, // 마우스 이벤트를 받을 수 있도록 설정
isFixed: false, // containsPoint에서 특별 처리 방지
}) })
obj.lines.forEach((line, index) => { obj.lines.forEach((line, index) => {
clonedObj.lines[index].set({ attributes: line.attributes }) clonedObj.lines[index].set({ attributes: line.attributes })
}) })
clonedObj.fire('polygonMoved') // 내부 좌표 재계산 (points, pathOffset)
clonedObj.fire('modified')
clonedObj.setCoords() // 모든 속성 설정 후 좌표 업데이트
canvas.setActiveObject(clonedObj)
canvas.renderAll() canvas.renderAll()
addLengthText(clonedObj) //수치 추가 addLengthText(clonedObj) //수치 추가
drawDirectionArrow(clonedObj) //방향 화살표 추가 drawDirectionArrow(clonedObj) //방향 화살표 추가

View File

@ -30,6 +30,8 @@ import { QcastContext } from '@/app/QcastProvider'
import { usePlan } from '@/hooks/usePlan' import { usePlan } from '@/hooks/usePlan'
import { roofsState } from '@/store/roofAtom' import { roofsState } from '@/store/roofAtom'
import { useText } from '@/hooks/useText' import { useText } from '@/hooks/useText'
import { processEaveHelpLines } from '@/util/skeleton-utils'
import { QLine } from '@/components/fabric/QLine'
export function useRoofAllocationSetting(id) { export function useRoofAllocationSetting(id) {
const canvas = useRecoilValue(canvasState) const canvas = useRecoilValue(canvasState)
@ -372,11 +374,18 @@ export function useRoofAllocationSetting(id) {
setBasicSetting((prev) => { setBasicSetting((prev) => {
return { ...prev, selectedRoofMaterial: newRoofList.find((roof) => roof.selected) } return { ...prev, selectedRoofMaterial: newRoofList.find((roof) => roof.selected) }
}) })
const selectedRoofMaterial = newRoofList.find((roof) => roof.selected)
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF && obj.roofMaterial?.index === selectedRoofMaterial.index)
roofs.forEach((roof) => {
setSurfaceShapePattern(roof, roofDisplay.column, false, { ...selectedRoofMaterial }, true)
drawDirectionArrow(roof)
})
setRoofList(newRoofList) setRoofList(newRoofList)
setRoofMaterials(newRoofList) setRoofMaterials(newRoofList)
setRoofsStore(newRoofList) setRoofsStore(newRoofList)
const selectedRoofMaterial = newRoofList.find((roof) => roof.selected)
setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true) setSurfaceShapePattern(currentObject, roofDisplay.column, false, selectedRoofMaterial, true)
drawDirectionArrow(currentObject) drawDirectionArrow(currentObject)
modifyModuleSelectionData() modifyModuleSelectionData()
@ -449,6 +458,22 @@ export function useRoofAllocationSetting(id) {
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL) const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
roofBases.forEach((roofBase) => { roofBases.forEach((roofBase) => {
try { try {
const roofEaveHelpLines = canvas.getObjects().filter((obj) => obj.lineName === 'eaveHelpLine' && obj.roofId === roofBase.id)
if (roofEaveHelpLines.length > 0) {
if (roofBase.lines) {
// Filter out any eaveHelpLines that are already in lines to avoid duplicates
const existingEaveLineIds = new Set(roofBase.lines.map((line) => line.id))
const newEaveLines = roofEaveHelpLines.filter((line) => !existingEaveLineIds.has(line.id))
roofBase.lines = [...newEaveLines]
} else {
roofBase.lines = [...roofEaveHelpLines]
}
if (!roofBase.innerLines) {
roofBase.innerLines = []
}
}
if (roofBase.separatePolygon.length > 0) { if (roofBase.separatePolygon.length > 0) {
splitPolygonWithSeparate(roofBase.separatePolygon) splitPolygonWithSeparate(roofBase.separatePolygon)
} else { } else {

View File

@ -845,6 +845,8 @@ export const usePolygon = () => {
polygonLines.forEach((line) => { polygonLines.forEach((line) => {
line.need = true line.need = true
}) })
// 순서에 의존하지 않도록 모든 조합을 먼저 확인한 후 처리
const innerLineMapping = new Map() // innerLine -> polygonLine 매핑 저장
// innerLines와 polygonLines의 겹침을 확인하고 type 변경 // innerLines와 polygonLines의 겹침을 확인하고 type 변경
innerLines.forEach((innerLine) => { innerLines.forEach((innerLine) => {
@ -854,14 +856,28 @@ export const usePolygon = () => {
if (innerLine.attributes && polygonLine.attributes.type) { if (innerLine.attributes && polygonLine.attributes.type) {
// innerLine이 polygonLine보다 긴 경우 polygonLine.need를 false로 변경 // innerLine이 polygonLine보다 긴 경우 polygonLine.need를 false로 변경
if (polygonLine.length < innerLine.length) { if (polygonLine.length < innerLine.length) {
polygonLine.need = false if(polygonLine.lineName !== 'eaveHelpLine'){
polygonLine.need = false
}
} }
innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize // innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize
innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize // innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize
innerLine.attributes.type = polygonLine.attributes.type // innerLine.attributes.type = polygonLine.attributes.type
innerLine.direction = polygonLine.direction // innerLine.direction = polygonLine.direction
innerLine.attributes.isStart = true // innerLine.attributes.isStart = true
innerLine.parentLine = polygonLine // innerLine.parentLine = polygonLine
// 매핑된 innerLine의 attributes를 변경 (교차점 계산 전에 적용)
innerLineMapping.forEach((polygonLine, innerLine) => {
innerLine.attributes.planeSize = innerLine.attributes.planeSize ?? polygonLine.attributes.planeSize
innerLine.attributes.actualSize = innerLine.attributes.actualSize ?? polygonLine.attributes.actualSize
innerLine.attributes.type = polygonLine.attributes.type
innerLine.direction = polygonLine.direction
innerLine.attributes.isStart = true
innerLine.parentLine = polygonLine
})
} }
} }
}) })
@ -1371,7 +1387,7 @@ export const usePolygon = () => {
let representLine let representLine
// 지붕을 그리면서 기존 polygon의 line중 연결된 line을 찾는다. // 지붕을 그리면서 기존 polygon의 line중 연결된 line을 찾는다.
;[...polygonLines, ...innerLines].forEach((line) => { [...polygonLines, ...innerLines].forEach((line) => {
let startFlag = false let startFlag = false
let endFlag = false let endFlag = false
const startPoint = line.startPoint const startPoint = line.startPoint
@ -1567,52 +1583,126 @@ export const usePolygon = () => {
// ==== Dijkstra pathfinding ==== // ==== Dijkstra pathfinding ====
// function findShortestPath(start, end, graph, epsilon = 1) {
// const startKey = pointToKey(start, epsilon)
// const endKey = pointToKey(end, epsilon)
//
// const distances = {}
// const previous = {}
// const visited = new Set()
// const queue = [{ key: startKey, dist: 0 }]
//
// for (const key in graph) distances[key] = Infinity
// distances[startKey] = 0
//
// while (queue.length > 0) {
// queue.sort((a, b) => a.dist - b.dist)
// const { key } = queue.shift()
// if (visited.has(key)) continue
// visited.add(key)
//
// for (const neighbor of graph[key] || []) {
// const neighborKey = pointToKey(neighbor.point, epsilon)
// const alt = distances[key] + neighbor.distance
// if (alt < distances[neighborKey]) {
// distances[neighborKey] = alt
// previous[neighborKey] = key
// queue.push({ key: neighborKey, dist: alt })
// }
// }
// }
//
// const path = []
// let currentKey = endKey
//
// if (!previous[currentKey]) return null
//
// while (currentKey !== startKey) {
// const [x, y] = currentKey.split(',').map(Number)
// path.unshift({ x, y })
// currentKey = previous[currentKey]
// }
//
// const [sx, sy] = startKey.split(',').map(Number)
// path.unshift({ x: sx, y: sy })
//
// return path
// }
function findShortestPath(start, end, graph, epsilon = 1) { function findShortestPath(start, end, graph, epsilon = 1) {
const startKey = pointToKey(start, epsilon) const startKey = pointToKey(start, epsilon);
const endKey = pointToKey(end, epsilon) const endKey = pointToKey(end, epsilon);
const distances = {} // 거리와 이전 노드 추적
const previous = {} const distances = { [startKey]: 0 };
const visited = new Set() const previous = {};
const queue = [{ key: startKey, dist: 0 }] const visited = new Set();
for (const key in graph) distances[key] = Infinity // 우선순위 큐 (거리가 짧은 순으로 정렬)
distances[startKey] = 0 const queue = [{ key: startKey, dist: 0 }];
while (queue.length > 0) { // 모든 노드 초기화
queue.sort((a, b) => a.dist - b.dist) for (const key in graph) {
const { key } = queue.shift() if (key !== startKey) {
if (visited.has(key)) continue distances[key] = Infinity;
visited.add(key) }
}
for (const neighbor of graph[key] || []) { // 우선순위 큐에서 다음 노드 선택
const neighborKey = pointToKey(neighbor.point, epsilon) const getNextNode = () => {
const alt = distances[key] + neighbor.distance if (queue.length === 0) return null;
if (alt < distances[neighborKey]) { queue.sort((a, b) => a.dist - b.dist);
distances[neighborKey] = alt return queue.shift();
previous[neighborKey] = key };
queue.push({ key: neighborKey, dist: alt })
let current;
while ((current = getNextNode())) {
const currentKey = current.key;
// 목적지에 도달하면 종료
if (currentKey === endKey) break;
// 이미 방문한 노드는 건너뜀
if (visited.has(currentKey)) continue;
visited.add(currentKey);
// 인접 노드 탐색
for (const neighbor of graph[currentKey] || []) {
const neighborKey = pointToKey(neighbor.point, epsilon);
if (visited.has(neighborKey)) continue;
const alt = distances[currentKey] + neighbor.distance;
// 더 짧은 경로를 찾은 경우 업데이트
if (alt < (distances[neighborKey] || Infinity)) {
distances[neighborKey] = alt;
previous[neighborKey] = currentKey;
// 우선순위 큐에 추가
queue.push({ key: neighborKey, dist: alt });
} }
} }
} }
const path = [] // 경로 재구성
let currentKey = endKey const path = [];
let currentKey = endKey;
if (!previous[currentKey]) return null // 시작점에 도달할 때까지 역추적
while (previous[currentKey] !== undefined) {
while (currentKey !== startKey) { const [x, y] = currentKey.split(',').map(Number);
const [x, y] = currentKey.split(',').map(Number) path.unshift({ x, y });
path.unshift({ x, y }) currentKey = previous[currentKey];
currentKey = previous[currentKey]
} }
const [sx, sy] = startKey.split(',').map(Number) // 시작점 추가
path.unshift({ x: sx, y: sy }) if (path.length > 0) {
const [sx, sy] = startKey.split(',').map(Number);
path.unshift({ x: sx, y: sy });
}
return path return path.length > 0 ? path : null;
} }
// 최종 함수 // 최종 함수
function getPath(start, end, graph, epsilon = 1) { function getPath(start, end, graph, epsilon = 1) {
// startPoint와 arrivalPoint가 될 수 있는 점은 line.attributes.type이 'default' 혹은 null이 아닌 line인 경우에만 가능 // startPoint와 arrivalPoint가 될 수 있는 점은 line.attributes.type이 'default' 혹은 null이 아닌 line인 경우에만 가능

View File

@ -29,22 +29,39 @@ fabric.Rect.prototype.getCurrentPoints = function () {
/** /**
* fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용) * fabric.Group에 getCurrentPoints 메서드를 추가 (도머 그룹용)
* 그룹groupPoints를 다시 계산하여 반환 * 그룹 객체들의 점들을 수집하여 현재 월드 좌표를 반환
*/ */
fabric.Group.prototype.getCurrentPoints = function () { fabric.Group.prototype.getCurrentPoints = function () {
// groupPoints를 다시 계산 // 그룹 내 객체들로부터 실시간으로 점들을 계산
if (this._objects && this._objects.length > 0) {
let allPoints = []
// 그룹에 groupPoints가 있으면 해당 점들을 사용 (도머의 경우) // 그룹 내 모든 객체의 점들을 수집
if (this.groupPoints && Array.isArray(this.groupPoints)) { this._objects.forEach(function (obj) {
const matrix = this.calcTransformMatrix() if (obj.getCurrentPoints && typeof obj.getCurrentPoints === 'function') {
console.log('this.groupPoints', this.groupPoints) const objPoints = obj.getCurrentPoints()
return this.groupPoints.map(function (p) { allPoints = allPoints.concat(objPoints)
const point = new fabric.Point(p.x, p.y) } else if (obj.points && Array.isArray(obj.points)) {
return fabric.util.transformPoint(point, matrix) const pathOffset = obj.pathOffset || { x: 0, y: 0 }
const matrix = obj.calcTransformMatrix()
const transformedPoints = obj.points
.map(function (p) {
return new fabric.Point(p.x - pathOffset.x, p.y - pathOffset.y)
})
.map(function (p) {
return fabric.util.transformPoint(p, matrix)
})
allPoints = allPoints.concat(transformedPoints)
}
}) })
if (allPoints.length > 0) {
// Convex Hull 알고리즘을 사용하여 외곽 점들만 반환
return this.getConvexHull(allPoints)
}
} }
// groupPoints가 없으면 바운딩 박스를 사용 // 객체가 없으면 바운딩 박스를 사용
const bounds = this.getBoundingRect() const bounds = this.getBoundingRect()
const points = [ const points = [
{ x: bounds.left, y: bounds.top }, { x: bounds.left, y: bounds.top },

File diff suppressed because it is too large Load Diff