From 958a8888c3d3f8c5e55a38d8c96954ea2b16ea74 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 9 Sep 2024 11:03:30 +0900 Subject: [PATCH 01/16] =?UTF-8?q?=EC=98=A4=EB=B8=8C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98=20=EB=AA=A8=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/common.js | 2 + src/components/Roof2.jsx | 10 ++ src/components/ui/ObjectPlacement.jsx | 155 ++++++++++++++++++++++++ src/hooks/useMode.js | 164 +++++++++++++++++++++++++- src/store/canvasAtom.js | 12 ++ 5 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 src/components/ui/ObjectPlacement.jsx diff --git a/src/common/common.js b/src/common/common.js index 91a9583a..b1cca49e 100644 --- a/src/common/common.js +++ b/src/common/common.js @@ -12,6 +12,8 @@ export const Mode = { CELL_POWERCON: 'cellPowercon', //파워콘 DRAW_HELP_LINE: 'drawHelpLine', // 보조선 그리기 모드 지붕 존재해야함 ADSORPTION_POINT: 'adsorptionPoint', //흡착점 모드 + OPENING: 'opening', //개구 모드 + SHADOW: 'shadow', //그림자 생성 모드 DEFAULT: 'default', } diff --git a/src/components/Roof2.jsx b/src/components/Roof2.jsx index ecbc083b..d4ae8c1e 100644 --- a/src/components/Roof2.jsx +++ b/src/components/Roof2.jsx @@ -39,6 +39,7 @@ import GridSettingsModal from './GridSettingsModal' import { SurfaceShapeModal } from '@/components/ui/SurfaceShape' import { drawDirectionStringToArrow } from '@/util/qpolygon-utils' import ThumbnailList from '@/components/ui/ThumbnailLIst' +import ObjectPlacement from '@/components/ui/ObjectPlacement' export default function Roof2(props) { const { name, userId, email, isLoggedIn } = props @@ -753,6 +754,15 @@ export default function Roof2(props) { > 면형상 + {/**/} diff --git a/src/components/ui/ObjectPlacement.jsx b/src/components/ui/ObjectPlacement.jsx new file mode 100644 index 00000000..7bb10f14 --- /dev/null +++ b/src/components/ui/ObjectPlacement.jsx @@ -0,0 +1,155 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { Button, Input } from '@nextui-org/react' +import { useRecoilState, useSetRecoilState } from 'recoil' +import { modalState } from '@/store/modalAtom' +import { fabric } from 'fabric' +import { QPolygon } from '@/components/fabric/QPolygon' +import { modeState, objectPlacementModeState } from '@/store/canvasAtom' + +const BATCH_TYPE = { + OPENING: 'opening', + SHADOW: 'shadow', +} + +const INPUT_TYPE = { + FREE: 'free', + DIMENSION: 'dimension', +} + +const ObjectPlacement = ({ canvas }) => { + const [open, setOpen] = useRecoilState(modalState) + const [mode, setMode] = useRecoilState(modeState) + const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState) + const [width, setWidth] = useState(0) + const [height, setHeight] = useState(0) + const [areaBoundary, setAreaBoundary] = useState(true) + + // opening or shadow 개구 / 그림자 + const [batchType, setBatchType] = useState(BATCH_TYPE.OPENING) + + // free or dimension 프리 / 치수 + const [inputType, setInputType] = useState(INPUT_TYPE.FREE) + + const handleSave = () => { + setMode(batchType) + setOpen(false) + } + + return ( +
+
+ +
+ +
+

개구 · 그림자 배치

+
+ +
+
+ + +
+
+ +
+
설정
+ +
+ +
+ +
+ +
+ +
+
+ + { + setObjectPlacementModeState({ ...objectPlacementMode, width: e.target.value }) + }} + /> +
+ +
+ + { + setObjectPlacementModeState({ ...objectPlacementMode, height: e.target.value }) + }} + /> +
+
+ +
+ +
+ +
+ +
+
+
+ ) +} + +export default ObjectPlacement diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index b4d1e398..f575e954 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -27,6 +27,7 @@ import { guideLineState, horiGuideLinesState, vertGuideLinesState, + objectPlacementModeState, } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' @@ -74,6 +75,8 @@ export function useMode() { const [horiGuideLines, setHoriGuideLines] = useRecoilState(horiGuideLinesState) const [vertGuideLines, setVertGuideLines] = useRecoilState(vertGuideLinesState) + const [objectPlacementMode, setObjectPlacementModeState] = useRecoilState(objectPlacementModeState) + useEffect(() => { // if (!canvas) { // canvas?.setZoom(0.8) @@ -104,11 +107,7 @@ export function useMode() { }, [endPoint]) useEffect(() => { - canvas?.off('mouse:out', removeMouseLines) - canvas?.on('mouse:out', removeMouseLines) changeMode(canvas, mode) - canvas?.off('mouse:move') - canvas?.on('mouse:move', drawMouseLines) }, [mode, horiGuideLines, vertGuideLines]) useEffect(() => { @@ -423,6 +422,17 @@ export function useMode() { break case 'adsorptionPoint': canvas?.on('mouse:down', mouseEvent.adsorptionPoint) + break + case 'shadow': + canvas?.on('mouse:down', mouseEvent.shadowMode.down) + canvas?.on('mouse:move', mouseEvent.shadowMode.move) + canvas?.on('mouse:up', mouseEvent.shadowMode.up) + break + case 'opening': + canvas?.on('mouse:down', mouseEvent.openingMode.down) + canvas?.on('mouse:move', mouseEvent.openingMode.move) + canvas?.on('mouse:up', mouseEvent.openingMode.up) + break case 'default': canvas?.off('mouse:down') @@ -587,6 +597,9 @@ export function useMode() { const mouseAndkeyboardEventClear = () => { canvas?.off('mouse:down') + canvas?.off('mouse:move') + canvas?.off('mouse:up') + canvas?.off('mouse:out') Object.keys(mouseEvent).forEach((key) => { canvas?.off('mouse:down', mouseEvent[key]) document.removeEventListener('contextmenu', mouseEvent[key]) @@ -682,6 +695,9 @@ export function useMode() { changeMouseEvent(mode) changeKeyboardEvent(mode) + canvas?.on('mouse:move', drawMouseLines) + canvas?.on('mouse:out', removeMouseLines) + switch (mode) { case 'template': templateMode() @@ -992,6 +1008,146 @@ export function useMode() { canvas.add(circle) canvas.renderAll() }, + //면 형상 배치 모드 + surfaceShapeMode: (o) => {}, + // 그림자 모드 + shadowMode: { + rect: null, + isDown: false, + origX: 0, + origY: 0, + down: (o) => { + if (mode !== Mode.SHADOW) return + mouseEvent.shadowMode.isDown = true + const pointer = canvas.getPointer(o.e) + mouseEvent.shadowMode.origX = pointer.x + mouseEvent.shadowMode.origY = pointer.y + mouseEvent.shadowMode.rect = new fabric.Rect({ + fill: 'grey', + left: mouseEvent.shadowMode.origX, + top: mouseEvent.shadowMode.origY, + originX: 'left', + originY: 'top', + opacity: 0.3, + width: 0, + height: 0, + angle: 0, + transparentCorners: false, + }) + canvas.add(mouseEvent.shadowMode.rect) + }, + move: (e) => { + if (!mouseEvent.shadowMode.isDown) return + const pointer = canvas.getPointer(e.e) + if (mouseEvent.shadowMode.origX > pointer.x) { + mouseEvent.shadowMode.rect.set({ left: Math.abs(pointer.x) }) + } + if (mouseEvent.shadowMode.origY > pointer.y) { + mouseEvent.shadowMode.rect.set({ top: Math.abs(pointer.y) }) + } + + mouseEvent.shadowMode.rect.set({ width: Math.abs(mouseEvent.shadowMode.origX - pointer.x) }) + mouseEvent.shadowMode.rect.set({ height: Math.abs(mouseEvent.shadowMode.origY - pointer.y) }) + }, + up: (o) => { + mouseEvent.shadowMode.isDown = false + setMode(Mode.DEFAULT) + }, + }, + openingMode: { + rect: null, + isDown: false, + origX: 0, + origY: 0, + down: (o) => { + if (mode !== Mode.OPENING) return + mouseEvent.openingMode.isDown = true + const pointer = canvas.getPointer(o.e) + mouseEvent.openingMode.origX = pointer.x + mouseEvent.openingMode.origY = pointer.y + mouseEvent.openingMode.rect = new fabric.Rect({ + fill: 'white', + stroke: 'black', + strokeWidth: 1, + left: mouseEvent.openingMode.origX, + top: mouseEvent.openingMode.origY, + originX: 'left', + originY: 'top', + width: pointer.x - mouseEvent.openingMode.origX, + height: pointer.y - mouseEvent.openingMode.origY, + angle: 0, + transparentCorners: false, + }) + canvas.add(mouseEvent.openingMode.rect) + }, + move: (e) => { + if (!mouseEvent.openingMode.isDown) return + const pointer = canvas.getPointer(e.e) + if (mouseEvent.openingMode.origX > pointer.x) { + mouseEvent.openingMode.rect.set({ left: Math.abs(pointer.x) }) + } + if (mouseEvent.openingMode.origY > pointer.y) { + mouseEvent.openingMode.rect.set({ top: Math.abs(pointer.y) }) + } + + mouseEvent.openingMode.rect.set({ width: Math.abs(mouseEvent.openingMode.origX - pointer.x) }) + mouseEvent.openingMode.rect.set({ height: Math.abs(mouseEvent.openingMode.origY - pointer.y) }) + }, + up: (o) => { + mouseEvent.openingMode.isDown = false + + const { areaBoundary } = objectPlacementMode + + //roof의 내부에 있는지 확인 + if (!checkInsideRoof(mouseEvent.openingMode.rect)) { + setMode(Mode.DEFAULT) + } + + // 영역 교차인지 확인 + if (!areaBoundary) { + const isCross = checkCrossAreaBoundary(mouseEvent.openingMode.rect) + if (isCross) { + alert('영역이 교차되었습니다.') + canvas.remove(mouseEvent.openingMode.rect) + } + } + + mouseEvent.openingMode.rect.set({ name: 'opening' }) + }, + }, + } + + const checkCrossAreaBoundary = (rect) => { + const openings = canvas?._objects.filter((obj) => obj.name === 'opening') + if (openings.length === 0) { + return false + } + + for (let i = 0; i < openings.length; i++) { + const rect2 = openings[i] + // Check if one rectangle is to the left of the other + if (rect.x + rect.width <= rect2.x || rect2.x + rect2.width <= rect.x) { + return true + } + + // Check if one rectangle is above the other + if (rect.y + rect.height <= rect2.y || rect2.y + rect2.height <= rect.y) { + return true + } + } + + return false + } + + const checkInsideRoof = (rect) => { + let result = true + const roofs = canvas?._objects.filter((obj) => obj.name === 'roof') + if (roofs.length === 0) { + alert('지붕을 먼저 그려주세요') + canvas?.remove(rect) + return false + } + return result } const getInterSectPointByMouseLine = () => { diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index a675b508..d5720ca7 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -150,3 +150,15 @@ export const globalCompassState = atom({ default: 0, dangerouslyAllowMutability: true, }) + +// 면형상 배치 모드 +export const surfacePlacementModeState = atom({ + key: 'surfacePlacementMode', + default: { width: 0, height: 0, areaBoundary: true, inputType: 'free' }, +}) + +// 오브젝트 배치 모드 +export const objectPlacementModeState = atom({ + key: 'objectPlacementMode', + default: { width: 0, height: 0, areaBoundary: true, inputType: 'free', batchType: 'opening' }, +}) From 38754db30d33fe34ef7aae4ff7f5019477a4c9d9 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 9 Sep 2024 11:05:44 +0900 Subject: [PATCH 02/16] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useMode.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index f575e954..46709a76 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -600,10 +600,6 @@ export function useMode() { canvas?.off('mouse:move') canvas?.off('mouse:up') canvas?.off('mouse:out') - Object.keys(mouseEvent).forEach((key) => { - canvas?.off('mouse:down', mouseEvent[key]) - document.removeEventListener('contextmenu', mouseEvent[key]) - }) Object.keys(keyboardEvent).forEach((key) => { document.removeEventListener('keydown', keyboardEvent[key]) @@ -687,6 +683,7 @@ export function useMode() { const changeMode = (canvas, mode) => { mouseAndkeyboardEventClear() + addCommonMouseEvent() setMode(mode) setCanvas(canvas) @@ -695,9 +692,6 @@ export function useMode() { changeMouseEvent(mode) changeKeyboardEvent(mode) - canvas?.on('mouse:move', drawMouseLines) - canvas?.on('mouse:out', removeMouseLines) - switch (mode) { case 'template': templateMode() @@ -729,6 +723,12 @@ export function useMode() { } } + // 모든 모드에서 사용되는 공통 이벤트 추가 + const addCommonMouseEvent = () => { + canvas?.on('mouse:move', drawMouseLines) + canvas?.on('mouse:out', removeMouseLines) + } + const changeKeyboardEvent = (mode) => { if (mode === Mode.EDIT) { switch (mode) { From b81d69589830d495e3d55f84831817497f69e603 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 9 Sep 2024 12:37:27 +0900 Subject: [PATCH 03/16] =?UTF-8?q?polygon=20=EC=9D=B4=EB=8F=99=20=EC=8B=9C?= =?UTF-8?q?=20=EB=AC=B6=EC=97=AC=EC=9E=88=EB=8A=94=20arrow=EB=8F=84=20?= =?UTF-8?q?=EC=9E=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 3 +++ src/hooks/useMode.js | 44 +++++++++++++++++++++++-------- src/store/canvasAtom.js | 2 +- src/util/qpolygon-utils.js | 21 ++++++++++----- 4 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index b03a593d..1708c8d3 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -127,6 +127,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.on('modified', (e) => { this.addLengthText() + if (this.arrow) { + drawDirectionArrow(this) + } }) this.on('selected', () => { diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 46709a76..6639512b 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -32,7 +32,7 @@ import { import { QLine } from '@/components/fabric/QLine' import { fabric } from 'fabric' import { QPolygon } from '@/components/fabric/QPolygon' -import offsetPolygon from '@/util/qpolygon-utils' +import offsetPolygon, { inPolygon } from '@/util/qpolygon-utils' import { isObjectNotEmpty } from '@/util/common-utils' import * as turf from '@turf/turf' import { Mode } from '@/common/common' @@ -1061,8 +1061,25 @@ export function useMode() { origY: 0, down: (o) => { if (mode !== Mode.OPENING) return - mouseEvent.openingMode.isDown = true + const roofs = canvas?._objects.filter((obj) => obj.name === 'roof') + if (roofs.length === 0) { + alert('지붕을 먼저 그려주세요') + setMode(Mode.DEFAULT) + return + } const pointer = canvas.getPointer(o.e) + let selectRoof = null + roofs.forEach((roof) => { + if (roof.inPolygon({ x: pointer.x, y: pointer.y })) { + selectRoof = roof + } + }) + if (!selectRoof) { + alert('지붕 내부에만 생성 가능합니다.') + return + } + mouseEvent.openingMode.isDown = true + mouseEvent.openingMode.origX = pointer.x mouseEvent.openingMode.origY = pointer.y mouseEvent.openingMode.rect = new fabric.Rect({ @@ -1123,17 +1140,21 @@ export function useMode() { return false } + const rectPoints = [ + { x: rect.left, y: rect.top }, + { x: rect.left, y: rect.top + rect.height }, + { x: rect.left + rect.width, y: rect.top + rect.height }, + { x: rect.left + rect.width, y: rect.top }, + ] + for (let i = 0; i < openings.length; i++) { const rect2 = openings[i] - // Check if one rectangle is to the left of the other - if (rect.x + rect.width <= rect2.x || rect2.x + rect2.width <= rect.x) { - return true - } - - // Check if one rectangle is above the other - if (rect.y + rect.height <= rect2.y || rect2.y + rect2.height <= rect.y) { - return true - } + const rect2Points = [ + { x: rect2.left, y: rect2.top }, + { x: rect2.left, y: rect2.top + rect2.height }, + { x: rect2.left + rect2.width, y: rect2.top + rect2.height }, + { x: rect2.left + rect2.width, y: rect2.top }, + ] } return false @@ -1147,6 +1168,7 @@ export function useMode() { canvas?.remove(rect) return false } + return result } diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index 24697a02..ae020901 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -171,5 +171,5 @@ export const surfacePlacementModeState = atom({ // 오브젝트 배치 모드 export const objectPlacementModeState = atom({ key: 'objectPlacementMode', - default: { width: 0, height: 0, areaBoundary: true, inputType: 'free', batchType: 'opening' }, + default: { width: 0, height: 0, areaBoundary: false, inputType: 'free', batchType: 'opening' }, }) diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index 5e00b197..8d8a28ec 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -2714,6 +2714,12 @@ export const drawDirectionArrow = (polygon) => { if (!direction) { return } + + polygon.canvas + .getObjects() + .filter((obj) => obj.name === 'directionText' && obj.parent === polygon.arrow) + .forEach((obj) => polygon.canvas.remove(obj)) + let arrow = null let points = [] @@ -2721,13 +2727,13 @@ export const drawDirectionArrow = (polygon) => { polygon.canvas.remove(polygon.arrow) } - let centerPoint = polygon.getCenterPoint() + let centerPoint = { x: polygon.width / 2 + polygon.left, y: polygon.height / 2 + polygon.top } let stickeyPoint - const polygonMaxX = Math.max(...polygon.points.map((point) => point.x)) - const polygonMinX = Math.min(...polygon.points.map((point) => point.x)) - const polygonMaxY = Math.max(...polygon.points.map((point) => point.y)) - const polygonMinY = Math.min(...polygon.points.map((point) => point.y)) + const polygonMaxX = Math.max(...polygon.getCurrentPoints().map((point) => point.x)) + const polygonMinX = Math.min(...polygon.getCurrentPoints().map((point) => point.x)) + const polygonMaxY = Math.max(...polygon.getCurrentPoints().map((point) => point.y)) + const polygonMinY = Math.min(...polygon.getCurrentPoints().map((point) => point.y)) switch (direction) { case 'north': @@ -2800,6 +2806,7 @@ export const drawDirectionArrow = (polygon) => { polygon.arrow = arrow polygon.canvas.add(arrow) polygon.canvas.renderAll() + drawDirectionStringToArrow(polygon.canvas, 0) } /** @@ -2807,7 +2814,7 @@ export const drawDirectionArrow = (polygon) => { * @param canvas * @param compass */ -export const drawDirectionStringToArrow = (canvas, compass, fontSize) => { +export const drawDirectionStringToArrow = (canvas, compass = 0) => { const arrows = canvas?.getObjects().filter((obj) => obj.name === 'arrow') if (arrows.length === 0) { @@ -3000,8 +3007,10 @@ const addTextByArrows = (arrows, txt, canvas) => { originX: 'center', originY: 'center', name: 'directionText', + selectable: false, left: arrow.stickeyPoint.x, top: arrow.stickeyPoint.y, + parent: arrow, }) canvas.add(text) }) From 4d34dabe0e16d60ca67f35935bdb5c3a69b67260 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 9 Sep 2024 13:19:32 +0900 Subject: [PATCH 04/16] =?UTF-8?q?QPolygon=EC=A0=9C=EA=B1=B0=20=EC=8B=9C=20?= =?UTF-8?q?=EC=97=AE=EC=97=AC=EC=9E=88=EB=8A=94=20=ED=99=94=EC=82=B4?= =?UTF-8?q?=ED=91=9C=EC=99=80=20=ED=99=94=EC=82=B4=ED=91=9C=20=EB=B0=A9?= =?UTF-8?q?=ED=96=A5=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/fabric/QPolygon.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 1708c8d3..3c8b2148 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -147,6 +147,17 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { this.canvas.remove(text) }) this.texts = null + + if (this.arrow) { + this.canvas.remove(this.arrow) + this.canvas + .getObjects() + .filter((obj) => obj.name === 'directionText' && obj.parent === this.arrow) + .forEach((text) => { + this.canvas.remove(text) + }) + this.arrow = null + } }) // polygon.fillCell({ width: 50, height: 30, padding: 10 }) From be5731b92584e7fcd19f3602caf4e4238a412c10 Mon Sep 17 00:00:00 2001 From: leeyongjae Date: Mon, 9 Sep 2024 14:19:11 +0900 Subject: [PATCH 05/16] =?UTF-8?q?[=EB=A9=94=EC=8B=9C=EC=A7=80]=20=EA=B3=B5?= =?UTF-8?q?=ED=86=B5=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/ja.json | 89 ++++++++++++++++++++++++++++++++++++++++++++- src/locales/ko.json | 89 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/src/locales/ja.json b/src/locales/ja.json index a1725dfd..146bb78a 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1,3 +1,90 @@ { - "hi": "こんにちは" + "hi": "こんにちは", + "common.message.no.data": "No data", + "common.message.no.dataDown": "ダウンロードするデータがありません", + "common.message.noData": "表示するデータがありません", + "common.message.search": "search success", + "common.message.insert": "insert success", + "common.message.update": "update success", + "common.message.delete": "削除", + "common.message.restoration": "復元", + "common.message.cancel": "キャンセル", + "common.message.send": "メールを送信しました.", + "common.message.no.delete": "削除するデータがありません", + "common.message.save": "保存", + "common.message.transfer": "転送", + "common.message.batch.exec": "batch success", + "common.message.not.mov": "移動できません.", + "common.message.required.data": "{0} は入力必須項目となります。", + "common.message.save.error": "データの保存中にエラーが発生しました。 サイト管理者にお問い合わせください。", + "common.message.transfer.error": "データの転送中にエラーが発生しました。 サイト管理者にお問い合わせください。", + "common.message.delete.error": "データの削除中にエラーが発生しました。 サイト管理者にお問い合わせください。", + "common.message.batch.error": "バッチの実行中にエラーが発生しました。 サイト管理者に連絡してください。", + "common.message.send.error": "データの送信中にエラーが発生しました。サイト管理者にお問い合わせください", + "common.message.communication.error": "ネットワークエラーが発生しました。サイト管理者に連絡してください。", + "common.message.data.error": "{0} はデータ形式が無効です。", + "common.message.data.setting.error": "{0} は削除されたか、すでに構成されているデータです。", + "common.message.parameter.error": "パラメータエラー", + "common.message.product.parameter.error": "存在しない製品があります。", + "common.message.customer.parameter.error": "存在しない顧客があります。", + "common.message.file.exists.error": "ファイルが正常にアップロードされないためにエラーが発生しました", + "common.message.file.download.exists": "ファイルが存在しません。", + "common.message.file.download.error": "ァイルのダウンロードエラー", + "common.message.file.template.validation01": "フォルダをアップロードできません", + "common.message.file.template.validation02": "アップロードできるのはExcelファイルのみです。", + "common.message.file.template.validation03": "登録できない拡張子です", + "common.message.file.template.validation04": "容量を超えています アップロード可能な容量:{0} MB", + "common.message.file.template.validation05": "アップロードファイルを選択して下さい", + "common.message.multi.insert": "合計 {0} 件数 ({1}成功、 {2} 失敗 {3})", + "common.message.error": "エラーが発生しました。サイト管理者に連絡してください。", + "common.message.data.save": "保存しますか?", + "common.message.data.delete": " 削除しますか?", + "common.message.data.exists": "{0} はすでに存在するデータです。", + "common.message.data.no.exists": "{0} は存在しないデータです。", + "common.message.all": "All", + "common.message.tab.close.all": "すべてのタブを閉じますか?", + "common.message.transfer.save": "{0}件転送しますか?", + "common.message.confirm.save": "保存しますか?", + "common.message.confirm.confirm": "承認しますか?", + "common.message.confirm.request": "承認リクエストしますか?", + "common.message.confirm.delete": "削除しますか?", + "common.message.confirm.close": "閉じますか?", + "common.message.confirm.unclose": "クローズ中止しますか?", + "common.message.confirm.cancel": "キャンセルしますか?", + "common.message.confirm.uncancel": "キャンセル中止しますか?", + "common.message.confirm.copy": "コピーしますか?", + "common.message.confirm.createSo": "S/O作成しますか?", + "common.message.confirm.mark": "保存完了", + "common.message.confirm.mail": "メールを送信しますか?", + "common.message.confirm.printPriceItem": "価格を印刷しますか?", + "common.message.confirm.allAppr ": "Do you want to Batch approve the selected data?", + "common.message.confirm.deliveryFee": "送料を登録しますか?", + "common.message.success.delete": "削除完了", + "common.message.success.close": "閉じる", + "common.message.success.unclose": "キャンセルしました", + "common.message.validation.date": "終了日を開始日より前にすることはできません。 もう一度入力してください。", + "common.message.no.editfield": "フィールドを編集できません", + "common.message.success.rmmail": "リスク管理チームにメールを送信しました。", + "common.message.password.validation01": "パスワードの変更が一致しません。", + "common.message.password.validation02": "英語、数字、特殊文字を組み合わせた8桁以上を入力してください。", + "common.message.password.validation03": "パスワードをIDと同じにすることはできません。", + "common.message.menu.validation01": "注文を保存するメニューはありません.", + "common.message.menu.validation02": "The same sort order exists.", + "common.message.menuCode.check01": "登録可能", + "common.message.menuCode.check02": "登録できません", + "common.message.pleaseSelect": "{0}を選択してください", + "common.message.pleaseInput": "{0}を入力してください。", + "common.message.pleaseInputOr": "{0}または{1}を入力してください。", + "common.message.approved ": "承認済み", + "common.message.errorFieldExist": "エラー項目が存在します", + "common.message.storeIdExist ": "既に利用されている販売店IDです", + "common.message.userIdExist ": "すでに使用しているユーザーID。", + "common.message.noExists ": "削除された掲示物です", + "common.message.emailReqTo": "メール宛先が必要です", + "common.message.downloadPeriod": "ダウンロード検索期間を{0}日以内に選択してください。", + "common.message.backToSubmit": "販売店ブロック解除実行しますか?", + "common.message.backToG3": "Back to G3処理実行しますか?", + "common.message.writeToConfirm": "作成解除を実行しますか?", + "common.message.password.init.success": "パスワード [{0}] に初期化されました。", + "common.message.no.edit.save": "この文書は変更できません。" } diff --git a/src/locales/ko.json b/src/locales/ko.json index 282d9722..04b14c9f 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -1,3 +1,90 @@ { - "hi": "안녕하세요" + "hi": "안녕하세요", + "common.message.no.data": "No data", + "common.message.no.dataDown": "No data to download", + "common.message.noData": "No data to display", + "common.message.search": "search success", + "common.message.insert": "insert success", + "common.message.update": "update success", + "common.message.delete": "Deleted", + "common.message.restoration": "Restored", + "common.message.cancel": "Canceled", + "common.message.send": "The mail has been sent.", + "common.message.no.delete": "There is no data to delete.", + "common.message.save": "Saved.", + "common.message.transfer": "Transfered", + "common.message.batch.exec": "batch success", + "common.message.not.mov": "Its impossible to move.", + "common.message.required.data": "{0} is required input value.", + "common.message.save.error": "An error occurred while saving the data. Please contact site administrator.", + "common.message.transfer.error": "An error occurred while transfer the data. Please contact site administrator.", + "common.message.delete.error": "An error occurred while deleting data. Please contact site administrator.", + "common.message.batch.error": "An error occurred while executing the batch. Please contact site administrator.", + "common.message.send.error": "Error sending data, please contact your administrator.", + "common.message.communication.error": "Network error occurred. \n Please contact site administrator.", + "common.message.data.error": "{0} The data format is not valid.", + "common.message.data.setting.error": "{0} is data that has been deleted or already configured.", + "common.message.parameter.error": "Parameter Error", + "common.message.product.parameter.error": "존재하지 않는 제품이 있습니다.", + "common.message.customer.parameter.error": "존재하지 않는 고객이 있습니다.", + "common.message.file.exists.error": "Error due to file not uploading normally", + "common.message.file.download.exists": "File does not exist.", + "common.message.file.download.error": "File download error", + "common.message.file.template.validation01": "Unable to upload folder", + "common.message.file.template.validation02": "Only Excel files can be uploaded.", + "common.message.file.template.validation03": "Non-registerable extension", + "common.message.file.template.validation04": "Exceed capacity \n Uploadable capacity : {0} MB", + "common.message.file.template.validation05": "업로드 파일을 선택해주세요.", + "common.message.multi.insert": "Total {0} cases ({1} successes, {2} failures {3})", + "common.message.error": "Error occurred, please contact site administrator.", + "common.message.data.save": "Do you want to save it?", + "common.message.data.delete": "Do you want to delete it?", + "common.message.data.exists": "{0} is data that already exists.", + "common.message.data.no.exists": "{0} is data that does not exist.", + "common.message.all": "All", + "common.message.tab.close.all": "Close all tabs?", + "common.message.transfer.save": "Want to {0} transfer it?", + "common.message.confirm.save": "Want to save it?", + "common.message.confirm.confirm": "Want to approve?", + "common.message.confirm.request": "Would you like to request a Approval?", + "common.message.confirm.delete": "Do you want to delete it?", + "common.message.confirm.close": "Want to close?", + "common.message.confirm.unclose": "Do you want to cancel the close?", + "common.message.confirm.cancel": "Want to cancellation?", + "common.message.confirm.uncancel": "Do you want to cancel the cancellation?", + "common.message.confirm.copy": "Do you want to copy?", + "common.message.confirm.createSo": "Create Sales Order?", + "common.message.confirm.mark": "Saved.", + "common.message.confirm.mail": "Do you want to send mail?", + "common.message.confirm.printPriceItem": "Would you like to print item price?", + "common.message.confirm.allAppr ": "Do you want to Batch approve the selected data?", + "common.message.confirm.deliveryFee": "Do you want to register shipping fee?", + "common.message.success.delete": "Deleted.", + "common.message.success.close": "Closed.", + "common.message.success.unclose": "Cancel Closed.", + "common.message.validation.date": "The end date cannot be earlier than the start date. Please enter it again.", + "common.message.no.editfield": "Can not edit field", + "common.message.success.rmmail": "You have successfully sent mail to the Risk Management team.", + "common.message.password.validation01": "Change passwords do not match.", + "common.message.password.validation02": "Please enter at least 8 digits combining English, numbers, and special characters.", + "common.message.password.validation03": "Password cannot be the same as ID.", + "common.message.menu.validation01": "There is no menu to save the order.", + "common.message.menu.validation02": "The same sort order exists.", + "common.message.menuCode.check01": "Registerable", + "common.message.menuCode.check02": "Unable to register", + "common.message.pleaseSelect": "Please Select {0}", + "common.message.pleaseInput": "Please Input a {0}.", + "common.message.pleaseInputOr": "Please Input a {0} or {1}.", + "common.message.approved ": "Approved.", + "common.message.errorFieldExist": "Error Field Exist", + "common.message.storeIdExist ": "이미 사용하고 있는 판매점 ID 입니다.", + "common.message.userIdExist ": "이미 사용하고 있는 사용자 ID 입니다.", + "common.message.noExists ": "삭제된 게시물 입니다.", + "common.message.emailReqTo": "Email To is required", + "common.message.downloadPeriod": "Please select the download search period within {0} days.", + "common.message.backToSubmit": "판매점 블록 해제를 실행하시겠습니까?", + "common.message.backToG3": "Back to G3 처리를 실행하시겠습니까?", + "common.message.writeToConfirm": "작성 해제를 실행하시겠습니까?", + "common.message.password.init.success": "비밀번호 [{0}]로 초기화 되었습니다.", + "common.message.no.edit.save": "This document cannot be changed." } From 7eff8eefd8a40f938ef356b702c81af40436b20d Mon Sep 17 00:00:00 2001 From: changkyu choi Date: Mon, 9 Sep 2024 14:51:14 +0900 Subject: [PATCH 06/16] =?UTF-8?q?=EB=B0=B0=EC=B9=98=EB=A9=B4=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=20=EC=84=A4=EC=A0=95=20CRUD=20=ED=8C=9D=EC=97=85=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[locale]/initSettingsModal/page.jsx | 16 ++ src/components/Headers.jsx | 1 + src/components/InitSettingsModal.jsx | 194 +++++++++++++++----- src/components/Settings.jsx | 82 +++++++++ 4 files changed, 252 insertions(+), 41 deletions(-) create mode 100644 src/app/[locale]/initSettingsModal/page.jsx diff --git a/src/app/[locale]/initSettingsModal/page.jsx b/src/app/[locale]/initSettingsModal/page.jsx new file mode 100644 index 00000000..a081ef47 --- /dev/null +++ b/src/app/[locale]/initSettingsModal/page.jsx @@ -0,0 +1,16 @@ +import Hero from '@/components/Hero' +import InitSettingsModal from '@/components/InitSettingsModal' +import { initCheck } from '@/util/session-util' + +export default async function InitSettingsModalPage() { + await initCheck() + + return ( + <> + +
+ +
+ + ) +} diff --git a/src/components/Headers.jsx b/src/components/Headers.jsx index 331e899c..02859c96 100644 --- a/src/components/Headers.jsx +++ b/src/components/Headers.jsx @@ -10,6 +10,7 @@ export default function Headers() {
Intro Playground + Basic Settings Canvas Settings Roof Roof2 diff --git a/src/components/InitSettingsModal.jsx b/src/components/InitSettingsModal.jsx index a79c7a9e..1deb81a7 100644 --- a/src/components/InitSettingsModal.jsx +++ b/src/components/InitSettingsModal.jsx @@ -1,22 +1,31 @@ +'use client' + import { useEffect, useState, memo, useCallback } from 'react' import { Button, Checkbox, CheckboxGroup, RadioGroup, Radio, Input, Select, SelectItem } from '@nextui-org/react' import { useRecoilState, useRecoilValue } from 'recoil' import { modalContent, modalState } from '@/store/modalAtom' import { canvasSettingState } from '@/store/canvasAtom' import { useAxios } from '@/hooks/useAxios' +import { get, post } from '@/lib/Axios' export default function InitSettingsModal(props) { + const [objectNo, setObjectNo] = useState('test123240909001') // 후에 삭제 필요 const [open, setOpen] = useRecoilState(modalState) const [canvasSetting, setCanvasSetting] = useRecoilState(canvasSettingState) const [roofMaterials, setRoofMaterials] = useState([]) const [basicSetting, setBasicSettings] = useState({ - type: '1', - inputType: '1', - angleType: 'slope', + roofDrawingSet: '1', + roofSizeSet: '1', + roofAngleSet: 'slope', roofs: [], }) - const { get, post } = useAxios() + const modelProps = { + open, + setOpen, + } + + //const { get, post } = useAxios() useEffect(() => { get({ url: '/api/roof-material/roof-material-infos' }).then((res) => { @@ -26,6 +35,41 @@ export default function InitSettingsModal(props) { setRoofMaterials(res) }) + get({ url: `/api/canvas-management/canvas-basic-settings/by-object/${objectNo}` }).then((res) => { + if (!res) return + + // 'roofs' 배열을 생성하여 각 항목을 추가 + const roofsRow = res.map((item) => { + return { + roofDrawingSet: String(item.roofDrawingSet), + roofSizeSet: String(item.roofSizeSet), + roofAngleSet: item.roofAngleSet, + } + }) + + const roofsArray = res.map((item) => { + return { + roofSeq: String(item.roofSeq), + roofType: String(item.roofType), + roofWidth: String(item.roofWidth), + roofHeight: String(item.roofHeight), + roofGap: String(item.roofGap), + roofLayout: item.roofLayout, + } + }) + + // 나머지 데이터와 함께 'roofs' 배열을 patternData에 넣음 + const patternData = { + roofDrawingSet: roofsRow[0].roofDrawingSet, // 첫 번째 항목의 값을 사용 + roofSizeSet: roofsRow[0].roofSizeSet, // 첫 번째 항목의 값을 사용 + roofAngleSet: roofsRow[0].roofAngleSet, // 첫 번째 항목의 값을 사용 + roofs: roofsArray, // 만들어진 roofs 배열 + } + + // 데이터 설정 + setBasicSettings({ ...patternData }) + }) + if (!(Object.keys(canvasSetting).length === 0 && canvasSetting.constructor === Object)) { setBasicSettings({ ...canvasSetting }) } @@ -46,12 +90,12 @@ export default function InitSettingsModal(props) { //기본값 const newRoofSettings = { - id: basicSetting.roofs.length + 1, - roofId: '3', - width: '200', - height: '200', - gap: '0', - layout: 'parallel', + roofSeq: basicSetting.roofs.length + 1, + roofType: '3', + roofWidth: '200', + roofHeight: '200', + roofGap: '0', + roofLayout: 'parallel', } setBasicSettings((prevState) => ({ @@ -62,19 +106,45 @@ export default function InitSettingsModal(props) { //배열 값 변경 함수 const handleRoofSettings = (id, event) => { - const roof = basicSetting.roofs.map((roof, i) => (id === roof.id ? { ...roof, [event.target.name]: event.target.value } : roof)) + const roof = basicSetting.roofs.map((roof, i) => (id === roof.roofSeq ? { ...roof, [event.target.name]: event.target.value } : roof)) setBasicSettings((prevState) => ({ ...prevState, roofs: [...roof], })) } - const submitCanvasConfig = () => { - post({ url: '/api/canvas-config', data: basicSetting }).then((res) => { - if (!res) { - setCanvasSetting({ ...basicSetting }) - } - }) + //저장 + const submitCanvasConfig = async () => { + if (!objectNo) { + alert('object_no를 입력하세요.') + return + } + + const patternData = { + objectNo, + roofDrawingSet: basicSetting.roofDrawingSet, + roofSizeSet: basicSetting.roofSizeSet, + roofAngleSet: basicSetting.roofAngleSet, + roofMaterialsAddList: basicSetting.roofs, + } + await post({ url: `/api/canvas-management/canvas-basic-settings`, data: patternData }) + + //Recoil 설정 + setCanvasSetting({ ...basicSetting }) + + // 저장 후 재조회 + //await handleSelect() + } + + // 삭제버튼 클릭시 해당 요소 id를 targetId로 전달받음 + const onRemove = async (targetId) => { + console.log(targetId) + + setBasicSettings((prevState) => ({ + ...prevState, + roofs: prevState.roofs.filter((roof) => roof.roofSeq !== targetId), + })) + // setBasicSettings({ ...newRoofSettings }) // 삭제한 데이터 배열을 setData()에 상태를 변화시킴 } return ( @@ -84,7 +154,13 @@ export default function InitSettingsModal(props) {
- + 치수 입력에 의한 물건작성
@@ -92,7 +168,13 @@ export default function InitSettingsModal(props) {
- + 복사도 입력 실측값 입력 육지붕 @@ -102,13 +184,20 @@ export default function InitSettingsModal(props) {
- + 경사 각도
+
지붕재 추가(단위 : mm)
+ setObjectNo(e.target.value)} />
@@ -137,15 +227,16 @@ export default function InitSettingsModal(props) { const RoofSelectBox = (props) => { return (
+ 타입 : + 너비 : props.onChange(props.roof.id, e)} - /> - props.onChange(props.roof.id, e)} + onChange={(e) => props.onChange(props.roof.roofSeq, e)} /> mm - props.onChange(props.roof.id, e)} /> + 높이 : + props.onChange(props.roof.roofSeq, e)} + /> + mm + 서까래 간격 : + props.onChange(props.roof.roofSeq, e)} + /> mm
props.onChange(props.roof.id, e)} + onChange={(e) => props.onChange(props.roof.roofSeq, e)} > 병렬식 계단식
+
+ +
) } diff --git a/src/components/Settings.jsx b/src/components/Settings.jsx index 2d6e5142..43288dbe 100644 --- a/src/components/Settings.jsx +++ b/src/components/Settings.jsx @@ -6,12 +6,57 @@ import { Button } from '@nextui-org/react' import { get, post } from '@/lib/Axios' import { useRecoilState } from 'recoil' import { customSettingsState } from '@/store/canvasAtom' +import { modalContent, modalState } from '@/store/modalAtom' + +import ColorPicker from './common/color-picker/ColorPicker' export default function Settings() { const [objectNo, setObjectNo] = useState('test123240829010') const [error, setError] = useState(null) const [customSettings, setCustomSettings] = useRecoilState(customSettingsState) + const [color, setColor] = useState('#ff0000') + + const [open, setOpen] = useRecoilState(modalState) + const [contents, setContent] = useRecoilState(modalContent) + + const handleSavePopup = () => { + console.log('color ', color) + } + + const handleClosePopup = () => { + setContent('') + setOpen(false) + console.log('colorSetting ', color) + } + + const colorSetting = ( + <> +
+

React ColorPicker

+ +
{color}
+
+

+ +

+ + ) + + const customStyles = { + overlay: { + backgroundColor: 'rgba(0,0,0,0.5)', + }, + content: { + width: '300px', + height: '400px', + margin: 'auto', + borderRadius: '4px', + boxShadow: '0 2px 4px rgba(0,0,0,0.2)', + padding: '20px', + }, + } + // 상태를 하나의 객체로 관리 const [settings, setSettings] = useState({ display1: Array(11).fill(false), // 화면 표시1 @@ -48,6 +93,21 @@ export default function Settings() { // 클릭 시 상태 변경 함수 const handleToggle = (type, index) => { + // '실선 그리드' 클릭 시 팝업 열기 + if (type === 'gridSettings' && gridItems.gridSettings[index] === '실선 그리드') { + //openGridPopup() + } + + // '그리드 색 설정' 클릭 시 팝업 열기 + if (type === 'gridSettings' && gridItems.gridSettings[index] === '그리드 색 설정') { + //setSelectedGridSetting(gridItems.gridSettings[index]) + //setIsPopupOpen(true) + //return prevSettings // 설정은 변경하지 않음 + + setOpen(true) + setContent({ ...colorSetting }) + } + setSettings((prevSettings) => { // prevSettings[type]이 배열인지 확인하고, 그렇지 않은 경우 빈 배열로 초기화 let updated = Array.isArray(prevSettings[type]) ? [...prevSettings[type]] : [] @@ -61,6 +121,24 @@ export default function Settings() { }) } + // '실선 그리드' 클릭 시 팝업을 열기 위한 함수 + const openGridPopup = () => { + const popupWidth = 500 + const popupHeight = 300 + + // 팝업 창 위치를 화면 중앙으로 조정하기 위해 계산 + const left = window.innerWidth / 2 - popupWidth / 2 + const top = window.innerHeight / 2 - popupHeight / 2 + + // 새 창 열기 + window + .open + //'./components/intro', // 팝업으로 띄울 페이지의 URL + //'_blank', // 새 창으로 열기 + //`width=${popupWidth},height=${popupHeight},top=${top},left=${left}`, // 크기와 위치 지정 + () + } + // Canvas Setting 조회 및 초기화 const handleSelect = async () => { try { @@ -221,6 +299,10 @@ export default function Settings() {
흡착점 ON

[그리드 설정]

+
+ +
{color}
+
{gridItems.gridSettings.map((item, index) => (
Date: Mon, 9 Sep 2024 17:19:28 +0900 Subject: [PATCH 07/16] =?UTF-8?q?redo=20undo=EC=8B=9C=20=EB=B6=80=EB=AA=A8?= =?UTF-8?q?=EA=B0=80=20=EC=9E=88=EB=8A=94=20object=EC=9D=B8=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=20=EB=B6=80=EB=AA=A8=EA=B9=8C=EC=A7=80=20pop,=20add?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useCanvas.js | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index 2d99be3b..e6ae4dbf 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -138,12 +138,24 @@ export function useCanvas(id) { if (canvas) { if (canvas?._objects.length > 0) { const poppedObject = canvas?._objects.pop() + const group = [] + group.push(poppedObject) + + if (poppedObject.parent || poppedObject.parentId) { + canvas + ?.getObjects() + .filter((obj) => obj.parent === poppedObject.parent || obj.parentId === poppedObject.parentId || obj === poppedObject.parent) + .forEach((obj) => { + group.push(obj) + canvas?.remove(obj) + }) + } setHistory((prev) => { if (prev === undefined) { - return poppedObject ? [poppedObject] : [] + return poppedObject ? [group] : [] } - return poppedObject ? [...prev, poppedObject] : prev + return poppedObject ? [...prev, group] : prev }) canvas?.renderAll() } @@ -154,7 +166,13 @@ export function useCanvas(id) { if (canvas && history) { if (history.length > 0) { setIsLocked(true) - canvas?.add(history[history.length - 1]) + if (Array.isArray(history[history.length - 1])) { + history[history.length - 1].forEach((obj) => { + canvas?.add(obj) + }) + } else { + canvas?.add(history[history.length - 1]) + } const newHistory = history.slice(0, -1) setHistory(newHistory) } From 847c4cc37182206ea8941ef090f1172d1272dbbd Mon Sep 17 00:00:00 2001 From: basssy Date: Mon, 9 Sep 2024 17:45:43 +0900 Subject: [PATCH 08/16] =?UTF-8?q?qcast-100=20=EB=AC=BC=EA=B1=B4=ED=98=84?= =?UTF-8?q?=ED=99=A9=20..?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[locale]/management/stuff/detail/page.jsx | 15 + src/app/[locale]/management/stuff/page.jsx | 13 +- .../common/datepicker/RangeDatePicker.jsx | 1 + src/components/management/Stuff.jsx | 367 ++++++++++++++++- src/components/management/StuffDetail.jsx | 388 ++++++++++++++++++ src/components/management/StuffQGrid.jsx | 116 ++++++ .../management/StuffSearchCondition.jsx | 264 ++++++++++++ src/locales/ja.js | 11 + src/locales/ko.js | 11 + src/store/stuffAtom.js | 22 + src/styles/_test.scss | 8 +- 11 files changed, 1210 insertions(+), 6 deletions(-) create mode 100644 src/app/[locale]/management/stuff/detail/page.jsx create mode 100644 src/components/management/StuffDetail.jsx create mode 100644 src/components/management/StuffQGrid.jsx create mode 100644 src/components/management/StuffSearchCondition.jsx create mode 100644 src/store/stuffAtom.js diff --git a/src/app/[locale]/management/stuff/detail/page.jsx b/src/app/[locale]/management/stuff/detail/page.jsx new file mode 100644 index 00000000..8b84287a --- /dev/null +++ b/src/app/[locale]/management/stuff/detail/page.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import Hero from '@/components/Hero' +import StuffDetail from '@/components/management/StuffDetail' +export default function ManagementStuffDetailPage() { + return ( + <> +
+

물건정보

+
+
+ +
+ + ) +} diff --git a/src/app/[locale]/management/stuff/page.jsx b/src/app/[locale]/management/stuff/page.jsx index e2d3c1cd..7590a7cf 100644 --- a/src/app/[locale]/management/stuff/page.jsx +++ b/src/app/[locale]/management/stuff/page.jsx @@ -1,14 +1,19 @@ -import Hero from '@/components/Hero' +import StuffSearchCondition from '@/components/management/StuffSearchCondition' import Stuff from '@/components/management/Stuff' import { initCheck } from '@/util/session-util' - +import Hero from '@/components/Hero' export default async function ManagementStuffPage() { await initCheck() return ( <> - -
+ +
+
+ +
+
+
diff --git a/src/components/common/datepicker/RangeDatePicker.jsx b/src/components/common/datepicker/RangeDatePicker.jsx index ad802f49..de80221f 100644 --- a/src/components/common/datepicker/RangeDatePicker.jsx +++ b/src/components/common/datepicker/RangeDatePicker.jsx @@ -14,6 +14,7 @@ export default function RangeDatePicker(props) { setDateRange(update) }} isClearable={true} + // showMonthYearPicker={true} /> ) } diff --git a/src/components/management/Stuff.jsx b/src/components/management/Stuff.jsx index da7b834d..ba463744 100644 --- a/src/components/management/Stuff.jsx +++ b/src/components/management/Stuff.jsx @@ -1,7 +1,372 @@ +'use client' + +import React, { useEffect, useState, useRef } from 'react' +import { useRouter, usePathname } from 'next/navigation' +import { Button } from '@nextui-org/react' +import { useAxios } from '@/hooks/useAxios' +import { QToast } from '@/hooks/useToast' +import StuffQGrid from './StuffQGrid' +import { useI18n } from '@/locales/client' +import { useRecoilValue } from 'recoil' +import { stuffSearchState } from '@/store/stuffAtom' +import { queryStringFormatter } from '@/util/common-utils' +import dayjs from 'dayjs' +import isLeapYear from 'dayjs/plugin/isLeapYear' // 윤년 판단 플러그인 +dayjs.extend(isLeapYear) + export default function Stuff() { + const stuffSearchParams = useRecoilValue(stuffSearchState) + + const { get, del } = useAxios() + const gridRef = useRef() + const lang = useI18n() + + const [gridCount, setGridCount] = useState(0) + const [selectedRowData, setSelectedRowData] = useState([]) + const [selectedRowDataCount, setSelectedRowDataCount] = useState(0) + + const router = useRouter() + const pathname = usePathname() + + //그리드 내부 복사버튼 + const copyNo = async (value) => { + try { + await navigator.clipboard.writeText(value) + QToast({ + message: `${value}물건번호가 복사되었습니다`, + type: 'info', + }) + } catch (error) { + QToast({ + message: `클립보드 복사에 실패하였습니다`, + type: 'error', + }) + } + } + + const [gridProps, setGridProps] = useState({ + gridData: [], + isPageable: false, + // sets 10 rows per page (default is 100) + paginationPageSize: 100, + // allows the user to select the page size from a predefined list of page sizes + paginationPageSizeSelector: [100, 200, 300, 400], + gridColumns: [ + { + field: 'lastEditDatetime', + headerName: lang('stuff.gridHeader.lastEditDatetime'), + headerCheckboxSelection: true, + headerCheckboxSelectionCurrentPageOnly: true, //페이징시 현재 페이지만 체크되도록 + checkboxSelection: true, + showDisabledCheckboxes: true, + // headerClass: 'centered', //_test.scss에 추가 테스트 + // .centered { + // .ag-header-cell-label { + // justify-content: center !important; + // } + // } + cellStyle: { textAlign: 'center' }, + //suppressMovable: true, //헤더 못움직이게 + // width : 100 + // minWidth : 100 + // maxWidth : 100 + valueFormatter: function (params) { + if (params.value) { + return dayjs(params?.value).format('YYYY.MM.DD HH:mm:ss') + } else { + return null + } + }, + }, + { + field: 'objectNo', + headerName: lang('stuff.gridHeader.objectNo'), + // headerClass: 'centered', //_test.scss에 추가 테스트 + cellRenderer: function (params) { + if (params.data.objectNo) { + return ( +
+ + {params.value} +
+ ) + } + }, + cellRendererParams: { + onPress: copyNo, + }, + }, + { + field: 'planTotCnt', + headerName: lang('stuff.gridHeader.planTotCnt'), + cellStyle: { textAlign: 'right' }, + }, + { field: 'objectName', headerName: lang('stuff.gridHeader.objectName'), cellStyle: { textAlign: 'left' } }, + { + field: 'saleStoreId', + headerName: lang('stuff.gridHeader.saleStoreId'), + cellStyle: { textAlign: 'left' }, + }, + { field: 'saleStoreName', headerName: lang('stuff.gridHeader.saleStoreName'), cellStyle: { textAlign: 'left' } }, + { field: 'address', headerName: lang('stuff.gridHeader.address'), cellStyle: { textAlign: 'left' } }, + { field: 'dispCompanyName', headerName: lang('stuff.gridHeader.dispCompanyName'), cellStyle: { textAlign: 'left' } }, + { field: 'receiveUser', headerName: lang('stuff.gridHeader.receiveUser'), cellStyle: { textAlign: 'left' } }, + { + field: 'specDate', + headerName: lang('stuff.gridHeader.specDate'), + valueFormatter: function (params) { + if (params.value) { + return dayjs(params?.value).format('YYYY.MM.DD') + } else { + return null + } + }, + cellStyle: { textAlign: 'center' }, + }, + { + field: 'createDatetime', + headerName: lang('stuff.gridHeader.createDatetime'), + valueFormatter: function (params) { + if (params.value) { + return dayjs(params?.value).format('YYYY.MM.DD') + } else { + return null + } + }, + cellStyle: { textAlign: 'center' }, + }, + ], + gridCount: 0, + }) + + //그리드 더블클릭 + const getCellDoubleClicked = (event) => { + if (event.column.colId === 'objectNo') { + return + } else { + console.log(' 상세이동::::::::', event.data) + if (event.data.objectNo) { + router.push(`${pathname}/detail?objectNo=${event.data.objectNo.toString()}`) + } else { + QToast({ + message: '물건정보가 없습니다', + type: 'error', + }) + } + } + } + + //그리드 체크박스 선택시 + const getSelectedRowdata = (data) => { + // console.log('data:::', data) + // let delData = data.map((row) => { + // return row.objectNo + // }) + // console.log('delData::', delData) + setSelectedRowData(data) + setSelectedRowDataCount(data.length) + } + + //물건삭제 + const fnDeleteRowData = (data) => { + console.log('물건삭제:::::::::::') + if (data.length === 0) { + QToast({ + message: '삭제할 데이터를 선택하세요', + type: 'error', + }) + return false + } + let errCount = 0 + data.forEach((cell) => { + if (!cell.objectNo) { + // if (errCount === 0) { + // QToast({ + // message: `물건정보가 있는 행만 삭제 됩니다`, + // type: 'error', + // }) + // } + errCount++ + } + }) + + async function fetchDelete(data) { + console.log('물건삭제API호출!!!!!!!!!', data) + //행추가말고 api데이터만 보냄 + // let newData = data.filter((item) => item.company != null) + // console.log('삭제에 전송되는 데이타::', newData) + // await del({ url: '', data:newData }) + await get({ url: 'https://www.ag-grid.com/example-assets/space-mission-data.json' }) + // try { + // const res = await del({url:'', data:newData}) + + // if(!res || res.length === 0) { + + // } else { + fetchData() + // } + // } catch (error) { + // console.error('Data Delete error:', error); + // } + } + + // 삭제API 완료 후 fetchData Api호출 + async function fetchData() { + console.log('물건삭제후 조회API호출!!!!!!!!!!!!!', stuffSearchParams) + const data = await get({ url: 'https://www.ag-grid.com/example-assets/space-mission-data.json' }) + setGridProps({ ...gridProps, gridData: data, count: data.length }) + setGridCount(data.length) + //data.length = 10 + //setGridProps({ ...gridProps, gridData: data, count: data.length-1}) + //setGridCount(data.length - 1 ) + } + + if (errCount === 0) { + // console.log('errCount::::::::', errCount) + fetchDelete(data) + // fetchData() + } else { + QToast({ + message: `물건정보가 있는 행만 선택해주세요`, + type: 'error', + }) + } + } + + //행추가 + let newCount = 0 + const addRowItems = () => { + // console.log('girdRef::::::', gridRef.current.api) + const newItems = [ + { + mission: newCount + 1, + successful: true, + }, + ] + gridRef.current.api.applyTransaction({ + add: newItems, + addIndex: newCount, + }) + newCount++ + } + + //행삭제 + const removeRowItems = () => { + // console.log('selectedRowData::', selectedRowData) + let errCount = 0 + selectedRowData.forEach((cell) => { + if (!cell.company) { + let newSelectedRowData = selectedRowData.filter((item) => item.company == null) + gridRef.current.api.applyTransaction({ remove: newSelectedRowData }) + } else { + if (errCount === 0) { + QToast({ + message: `행추가로 추가 한 행만 삭제됩니다.`, + type: 'error', + }) + } + errCount++ + } + }) + } + + // 진입시 그리드 데이터 조회 + useEffect(() => { + if (stuffSearchParams?.code === 'S') { + const params = { + schObjectNo: '', + schSaleStoreId: '', + schAddress: '', + schObjectName: '', + schSaleStoreName: '', + schSpecDateYn: '', + schReceiveUser: '', + schDispCompanyName: '', + schDateType: 'U', + schFromDt: dayjs(new Date()).add(-1, 'year').format('YYYY-MM-DD'), + schToDt: dayjs(new Date()).format('YYYY-MM-DD'), + } + + async function fetchData() { + console.log('화면진입:::::::::::::', params) + const apiUrl = `/api/object/v1.0/object?saleStoreId=201TES01&${queryStringFormatter(params)}` + // console.log('apiUrl::', apiUrl) + + await get({ + url: apiUrl, + }).then((res) => { + if (res.length > 0) { + console.log('API결과:::::::', res) + setGridProps({ ...gridProps, gridData: res, count: res.length }) + setGridCount(res.length) + } + }) + } + fetchData() + } + }, []) + + useEffect(() => { + if (stuffSearchParams?.code === 'E') { + console.log('조회 눌럿을때 ::::::::::::::', stuffSearchParams) + async function fetchData() { + const apiUrl = `/api/object/v1.0/object?saleStoreId=201TES01&${queryStringFormatter(stuffSearchParams)}` + await get({ url: apiUrl }).then((res) => { + console.log('API결과:::::::', res) + setGridProps({ ...gridProps, gridData: res, count: res.length }) + setGridCount(res.length) + }) + } + fetchData() + } + }, [stuffSearchParams]) + return ( <> -

Management Stuff

+
+ 물건목록 + + 전체 : {gridCount} // 선택 : {selectedRowDataCount} + +
+ {/* */} + {/* + */} +
+
+ +
+
) } diff --git a/src/components/management/StuffDetail.jsx b/src/components/management/StuffDetail.jsx new file mode 100644 index 00000000..2f39b066 --- /dev/null +++ b/src/components/management/StuffDetail.jsx @@ -0,0 +1,388 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' +import { Input, RadioGroup, Radio, Button, Autocomplete, AutocompleteItem, Select, SelectItem, Checkbox, Textarea } from '@nextui-org/react' +import Link from 'next/link' +import { get } from '@/lib/Axios' +import { queryStringFormatter } from '@/util/common-utils' +import dayjs from 'dayjs' +export default function StuffDetail() { + const router = useRouter() + const searchParams = useSearchParams() + const [receiveUser, setReceiveUser] = useState('') //담당자 + const [name2, setName2] = useState('') //물건명 + const [name3, setName3] = useState('') //물건명후리가나 + const [zipCode, setZipCode] = useState('') //우편번호 + const [name5, setName5] = useState('') //수직적설량 + const [gubun, setGubun] = useState('NEW') //신축 기축 + const [sel, setSel] = useState('') //경칭선택 + const [sel2, setSel2] = useState('') //발전량시뮬레이션지역 + const [sel3, setSel3] = useState('') //기준풍속 + const [sel4, setSel4] = useState('') //설치높이 + + const [errors, setErrors] = useState({}) + const [isFormValid, setIsFormValid] = useState(false) //임시저장, 진짜저장 버튼 컨트롤 + const [testSelOption, setTestSelOption] = useState([]) // 테스트용 + const [autoSelectValue, setAutoSelectValue] = useState('') //판매점명 자동완성 + const [buttonValid, setButtonValid] = useState(true) //주소검색 활성화 컨트롤 + const [isSelected, setIsSelected] = useState(false) //한랭지대첵 체크박스 + const [isSelected2, setIsSelected2] = useState(false) //염해지역용아이템사용 체크박스 + const [gubun2, setGubun2] = useState('1') //면조도구분 라디오 + const [gubun3, setGubun3] = useState('A') //계약조건 라디오 + const [memo, setMemo] = useState('') //메모 + const objectNo = searchParams.get('objectNo') //url에서 물건번호 꺼내서 바로 set + + const [address1, setAddress1] = useState('') //우편API리턴 도도부현명 + const [address2, setAddress2] = useState('') //우편API리턴 시구정촌명 + const [address3, setAddress3] = useState('') //우편API리턴 마을 지역명 + const [prefcode, setPrefCode] = useState(1) //우편API prefcode + + const [editMode, setEditMode] = useState('NEW') + const [detailData, setDetailData] = useState({}) + + useEffect(() => { + // console.log('상세화면진입:::::::::', searchParams.get('objectNo')) + // console.log('물건번호::::', objectNo) + + if (objectNo) { + console.log('상세::') + setEditMode('EDIT') + //http://localhost:8080/api/object/v1.0/object/R201TES01240906007/1 + //일단 플랜번호 무조건 1로 + //API 호출 + get({ url: `/api/object/v1.0/object/${objectNo}/1` }).then((res) => { + if (res != null) { + // console.log('res:::::::', res) + setDetailData(res) + //setTestSelOption(res) + } + }) + } else { + console.log('신규:::') + } + }, [objectNo]) + + useEffect(() => { + validateForm() + }, [receiveUser, name2, name3, gubun, sel, autoSelectValue, zipCode, sel2, sel3, name5, sel4]) + + // 우편번호 숫자만 체크 + const textTypeHandler = (e) => { + //\D 숫자가 아닌것(특수문자포함)과 매치, [^0-9]와 동일 + if (!e.target.value.match(/\D/g)) { + setZipCode(e.target.value) + } + } + + // 수직적설량 숫자만 + const textTypeHandler2 = (e) => { + if (!e.target.value.match(/[^0-9]/g)) { + setName5(e.target.value) + } + } + const validateForm = () => { + let errors = {} + + if (!receiveUser || receiveUser.trim().length === 0) { + errors.receiveUser = '담당자 is required.' + } + + if (!name2 || name2.trim().length === 0) { + errors.name2 = '물건명 is required.' + } + + if (!name3 || name3.trim().length === 0) { + errors.name3 = '물건명후리가나 is required.' + } + + if (!sel) { + errors.sel = '경칭선택 is required' + } + + if (!sel2) { + errors.sel2 = '발전량시뮬레이션지역 is required' + } + + if (!sel3) { + errors.sel3 = '기준풍속 is required' + } + + if (!sel4) { + errors.sel4 = '설치높이 is required' + } + + if (!autoSelectValue) { + errors.autoSelectValue = '판매점ID자동완성 is required' + } + + if (!zipCode || zipCode.length != 7) { + errors.zipCode = '우편번호 is required.' + setButtonValid(true) + } else { + setButtonValid(false) + } + + if (!name5) { + errors.name5 = '수직적설량 is required.' + } + + // console.log('errors::', errors) + setErrors(errors) + setIsFormValid(Object.keys(errors).length === 0) + } + + // 우편번호 API + const onSearchPostNumber = () => { + if (!zipCode) { + return alert('우편번호 입력해') + } + const params = { + zipcode: zipCode, + } + + get({ url: `https://zipcloud.ibsnet.co.jp/api/search?${queryStringFormatter(params)}` }).then((res) => { + console.log('우편API RES::::::::', res) + if (res.status === 200) { + if (res.results.length > 0) { + setAddress1(res.results[0].address1) + setAddress2(res.results[0].address2) + setAddress3(res.results[0].address3) + setPrefCode(res.results[0].prefcode) + } else { + alert('등록된 우편번호에서 주소를 찾을 수 없습니다. 다시 입력해주세요.') + } + } else { + alert(res.message) + } + }) + } + + const onTempSave = () => { + console.log('임시저장::', isFormValid) + } + + const onSave = () => { + console.log('진짜저장isFormValid:::', isFormValid) + } + + const moveList = () => { + router.push('/management/stuff') + } + + const changeAddress2 = (e) => { + console.log('e:::::::', e.target.value) + } + + return ( + <> + {(editMode === 'NEW' &&
신규:::::::::::
) ||
상세:::::::::::
} +
+
+ 물건번호 + {objectNo} +
+
+ 사양확정일 + {detailData?.specDate ? dayjs(detailData.specDate).format('YYYY.MM.DD') : null} +
+
+ 갱신일시 + + {detailData?.lastEditDatetime + ? dayjs(detailData.lastEditDatetime).format('YYYY.MM.DD HH:mm:ss') + : detailData?.createDatetime + ? dayjs(detailData.createDatetime).format('YYYY.MM.DD HH:mm:ss') + : null} + +
+
+ 등록일 + +
+
+
(*필수 입력항목)
+
+ 담당자* + setReceiveUser(e.target.value)} /> +
+
+ 물건구분/물건명 * + { + setGubun(e.target.value) + }} + /> + + { + setGubun(e.target.value) + }} + /> + +
+ setName2(e.target.value)} /> +
+
+ +
+
+ 물건명 후리가나 + setName3(e.target.value)} /> +
+
+
+ 판매점명 /ID * +
+ + {(option) => {option.name}} + +
+
+
+ 우편번호* + + + *우편번호 7자리를 입력한 후, 주소검색 버튼을 클릭해 주십시오 +
+
+ 도도부현 / 주소* + {/* */} + +
+
+ 발전량시뮬레이션지역* + +
+
+ 기준풍속* + + m/s이하 +
+
+ 수직적설량* + cm + + 한랭지대책시행 + +
+
+ 면조도구분* + { + setGubun2(e.target.value) + }} + /> + + { + setGubun2(e.target.value) + }} + /> + + + 염해지역용아이템사용 + +
+
+ 설치높이* + + m +
+
+ 계약조건 + { + setGubun3(e.target.value) + }} + /> + + { + setGubun3(e.target.value) + }} + /> + +
+
+ 메모 +