diff --git a/.env.development b/.env.development new file mode 100644 index 00000000..161467af --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +NEXT_PUBLIC_TEST="테스트변수입니다. development" + +NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080" \ No newline at end of file diff --git a/.env.production b/.env.production new file mode 100644 index 00000000..efb12105 --- /dev/null +++ b/.env.production @@ -0,0 +1,3 @@ +NEXT_PUBLIC_TEST="테스트변수입니다. production" + +NEXT_PUBLIC_API_SERVER_PATH="http://localhost:8080" \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 8cc4c040..15466bd2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,15 +1,15 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - reactStrictMode: false, + reactStrictMode: true, webpack: (config) => { config.externals.push({ - "utf-8-validate": "commonjs utf-8-validate", - bufferutil: "commonjs bufferutil", - canvas: "commonjs canvas", - }); + 'utf-8-validate': 'commonjs utf-8-validate', + bufferutil: 'commonjs bufferutil', + canvas: 'commonjs canvas', + }) // config.infrastructureLogging = { debug: /PackFileCache/ }; - return config; + return config }, -}; +} -export default nextConfig; +export default nextConfig diff --git a/package.json b/package.json index a5c429a7..d4cdd9bb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@nextui-org/react": "^2.4.2", "@prisma/client": "^5.17.0", + "axios": "^1.7.3", "fabric": "^5.3.0", "framer-motion": "^11.2.13", "mathjs": "^13.0.2", diff --git a/postcss.config.mjs b/postcss.config.mjs index 1a69fd2a..0dc456ad 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -3,6 +3,6 @@ const config = { plugins: { tailwindcss: {}, }, -}; +} -export default config; +export default config diff --git a/src/app/changelog/changelog.module.css b/src/app/changelog/changelog.module.css new file mode 100644 index 00000000..273a4a34 --- /dev/null +++ b/src/app/changelog/changelog.module.css @@ -0,0 +1,4 @@ +.test { + @apply bg-red-500; + @apply text-2xl; +} diff --git a/src/app/changelog/page.jsx b/src/app/changelog/page.jsx index 821254ec..1dcc8d0e 100644 --- a/src/app/changelog/page.jsx +++ b/src/app/changelog/page.jsx @@ -1,19 +1,22 @@ 'use client' +import { Button, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from '@nextui-org/react' import Hero from '@/components/Hero' -import { - Table, - TableBody, - TableCell, - TableColumn, - TableHeader, - TableRow, -} from '@nextui-org/react' +import QSelect from '@/components/ui/QSelect' +import styles from './changelog.module.css' +import { get } from '@/lib/Axios' export default function changelogPage() { + const testVar = process.env.NEXT_PUBLIC_TEST + + const handleUsers = async () => { + const users = await get('/api/user/find-all') + console.log(users) + } return ( <> +
이 영역은 테스트입니다.
@@ -35,6 +38,15 @@ export default function changelogPage() {
+
+ +
+
{testVar}
+
+
+ +
+
) } diff --git a/src/app/globals.css b/src/app/globals.css index 1c95a542..33e09403 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -30,10 +30,11 @@ body { .text-balance { text-wrap: balance; } -} */ +} .archivo-black-regular { font-family: 'Archivo Black', sans-serif; font-weight: 400; font-style: normal; } +*/ diff --git a/src/components/Roof2.jsx b/src/components/Roof2.jsx index e3009432..da41280b 100644 --- a/src/components/Roof2.jsx +++ b/src/components/Roof2.jsx @@ -6,7 +6,7 @@ import QRect from '@/components/fabric/QRect' import RangeSlider from './ui/RangeSlider' import { useRecoilState, useRecoilValue } from 'recoil' -import { canvasSizeState, fontSizeState, roofState, sortedPolygonArray } from '@/store/canvasAtom' +import { canvasSizeState, fontSizeState, roofMaterialState, roofState, sortedPolygonArray } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' import { getCanvasState, insertCanvasState } from '@/lib/canvas' import { calculateIntersection } from '@/util/canvas-util' @@ -31,7 +31,8 @@ export default function Roof2() { const [showControl, setShowControl] = useState(false) - const roof = useRecoilValue(roofState) + //지붕재 + const roofMaterial = useRecoilValue(roofMaterialState) const { mode, @@ -138,12 +139,12 @@ export default function Roof2() { { x: 100, y: 400 }, ] const type2 = [ - { x: 100, y: 100 }, - { x: 100, y: 1000 }, - { x: 1000, y: 1000 }, - { x: 1000, y: 600 }, - { x: 550, y: 600 }, - { x: 550, y: 100 }, + { x: 200, y: 100 }, + { x: 200, y: 1000 }, + { x: 1100, y: 1000 }, + { x: 1100, y: 600 }, + { x: 650, y: 600 }, + { x: 650, y: 100 }, ] const type3 = [ @@ -257,20 +258,23 @@ export default function Roof2() { ] const types = [type1, type2, type3, type4, type1A, type1B, eightPoint, eightPoint2, eightPoint3, eightPoint4, twelvePoint] - - const polygon = new QPolygon(type1B, { + const newP = [ + { x: 450, y: 450 }, + { x: 650, y: 250 }, + { x: 675, y: 275 }, + { x: 450, y: 850 }, + ] + const polygon = new QPolygon(type2, { fill: 'transparent', stroke: 'black', strokeWidth: 1, - selectable: true, + selectable: false, fontSize: fontSize, - name: 'QPolygon1', + name: 'wall', }) canvas?.add(polygon) - handleOuterlinesTest2(polygon) - // const lines = togglePolygonLine(polygon) // togglePolygonLine(lines[0]) } @@ -360,6 +364,70 @@ export default function Roof2() { handleClear() } + const drawRoofMaterial = () => { + const { width, height, roofStyle } = roofMaterial + + const wallPolygon = canvas?.getObjects().find((obj) => obj.name === 'wall') + + wallPolygon.set('strokeDashArray', [10, 5, 2, 5]) + wallPolygon.set('stroke', 'blue') + wallPolygon.set('strokeWidth', 1) + + const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof') + + roofs.forEach((roof) => { + let maxLengthLine = roof.lines.reduce((acc, cur) => { + return acc.length > cur.length ? acc : cur + }) + + const roofRatio = window.devicePixelRatio || 1 + + // 패턴 소스를 위한 임시 캔버스 생성 + const patternSourceCanvas = document.createElement('canvas') + if (roofStyle === 1) { + if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') { + patternSourceCanvas.width = width * roofRatio + patternSourceCanvas.height = height * roofRatio + } else { + patternSourceCanvas.width = height * roofRatio + patternSourceCanvas.height = width * roofRatio + } + } else if (roofStyle === 2) { + if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') { + patternSourceCanvas.width = width * 2 + patternSourceCanvas.height = height * 2 + } else { + patternSourceCanvas.width = height * 2 + patternSourceCanvas.height = width * 2 + } + } + + const ctx = patternSourceCanvas.getContext('2d') + + ctx.scale(roofRatio, roofRatio) + ctx.strokeStyle = 'green' + ctx.lineWidth = 0.4 + // 벽돌 패턴 그리기 + if (roofStyle === 1) { + ctx.strokeRect(0, 0, 50, 30) + } else if (roofStyle === 2) { + // 지그재그 + ctx.strokeRect(0, 0, 200, 100) + ctx.strokeRect(100, 100, 200, 100) + } + + // 패턴 생성 + const pattern = new fabric.Pattern({ + source: patternSourceCanvas, + repeat: 'repeat', + }) + roof.set('fill', null) + + roof.set('fill', pattern) + canvas?.renderAll() + }) + } + /** * canvas 내용 불러오기 */ @@ -380,6 +448,21 @@ export default function Roof2() { makeRoofPatternPolygon(roofStyle) } + const createRoofRack = () => { + const roofs = canvas?.getObjects().filter((obj) => obj.name === 'roof') + roofs.forEach((roof) => { + let maxLengthLine = roof.lines.reduce((acc, cur) => { + return acc.length > cur.length ? acc : cur + }) + + if (maxLengthLine.direction === 'right' || maxLengthLine.direction === 'left') { + roof.fillCell({ width: 50, height: 100, padding: 0 }) + } else { + roof.fillCell({ width: 100, height: 50, padding: 0 }) + } + }) + } + return ( <> {canvas && ( @@ -486,6 +569,12 @@ export default function Roof2() { + + diff --git a/src/components/fabric/QLine.js b/src/components/fabric/QLine.js index 2d1e87df..e6a2bc65 100644 --- a/src/components/fabric/QLine.js +++ b/src/components/fabric/QLine.js @@ -10,8 +10,9 @@ export const QLine = fabric.util.createClass(fabric.Line, { length: 0, direction: null, idx: 0, + area: 0, initialize: function (points, options, canvas) { - this.callSuper('initialize', points, options) + this.callSuper('initialize', points, { ...options, selectable: options.selectable ?? false }) if (options.id) { this.id = options.id } else { @@ -23,16 +24,9 @@ export const QLine = fabric.util.createClass(fabric.Line, { point = Math.round(point) }) - const scaleX = this.scaleX - const scaleY = this.scaleY - const x1 = this.left - const y1 = this.top - const x2 = this.left + this.width * scaleX - const y2 = this.top + this.height * scaleY this.idx = options.idx ?? 0 - const dx = x2 - x1 - const dy = y2 - y1 - this.length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(0)) + + this.setLength() this.direction = options.direction ?? getDirection({ x: this.x1, y: this.y1 }, { x: this.x2, y: this.y2 }) @@ -67,9 +61,22 @@ export const QLine = fabric.util.createClass(fabric.Line, { }) }, + setLength() { + const scaleX = this.scaleX + const scaleY = this.scaleY + const x1 = this.left + const y1 = this.top + const x2 = this.left + this.width * scaleX + const y2 = this.top + this.height * scaleY + const dx = x2 - x1 + const dy = y2 - y1 + this.length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(0)) + }, + addLengthText() { const thisText = this.canvas.getObjects().find((obj) => obj.name === 'lengthText' && obj.parentId === this.id) + this.setLength() const scaleX = this.scaleX const scaleY = this.scaleY const x1 = this.left @@ -79,13 +86,10 @@ export const QLine = fabric.util.createClass(fabric.Line, { if (thisText) { thisText.set({ text: this.length.toFixed(0).toString(), left: (x1 + x2) / 2, top: (y1 + y2) / 2 }) + this.text = thisText return } - const dx = x2 - x1 - const dy = y2 - y1 - this.length = Number(Math.sqrt(dx * dx + dy * dy).toFixed(0)) - const text = new fabric.Textbox(this.length.toFixed(0).toString(), { left: (x1 + x2) / 2, top: (y1 + y2) / 2, @@ -113,4 +117,12 @@ export const QLine = fabric.util.createClass(fabric.Line, { setCanvas(canvas) { this.canvas = canvas }, + + setViewLengthText(bool) { + const thisText = this.canvas.getObjects().find((obj) => obj.name === 'lengthText' && obj.parentId === this.id) + if (thisText) { + thisText.set({ visible: bool }) + } + return this + }, }) diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js index 1817862a..685d83ee 100644 --- a/src/components/fabric/QPolygon.js +++ b/src/components/fabric/QPolygon.js @@ -2,7 +2,7 @@ import { fabric } from 'fabric' import { v4 as uuidv4 } from 'uuid' import { QLine } from '@/components/fabric/QLine' import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util' -import { drawHelpLineInHexagon } from '@/util/qpolygon-utils' +import { calculateAngle, dividePolygon, drawHelpLineInHexagon } from '@/util/qpolygon-utils' export const QPolygon = fabric.util.createClass(fabric.Polygon, { type: 'QPolygon', @@ -19,10 +19,25 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { point.x = Math.round(point.x) point.y = Math.round(point.y) }) - if (points.length <= 8) { + options.sort = options.sort ?? true + if (!options.sort && points.length <= 8) { points = sortedPointLessEightPoint(points) } else { - points = sortedPoints(points) + let isDiagonal = false + points.forEach((point, i) => { + if (isDiagonal) { + return + } + const nextPoint = points[(i + 1) % points.length] + const angle = calculateAngle(point, nextPoint) + if (!(Math.abs(angle) === 0 || Math.abs(angle) === 180)) { + isDiagonal = true + } + }) + + if (!isDiagonal) { + points = sortedPoints(points) + } } this.callSuper('initialize', points, options) @@ -77,6 +92,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), { type: this.type, text: this.text, + hips: this.hips, + ridges: this.ridges, + connectRidges: this.connectRidges, }) }, init: function () { @@ -119,6 +137,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { direction: getDirectionByPoint(point, nextPoint), idx: i, }) + line.startPoint = point + line.endPoint = nextPoint this.lines.push(line) }) }, @@ -201,6 +221,8 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { //센터 정렬시에 쓴다 체크박스가 존재함 TODO: if문 추가해서 정렬해야함 let tmpWidth = (boundingBoxWidth - (rectWidth + cell.padding) * cols) / 2 + const drawCellsArray = [] //그려진 셀의 배열 + for (let i = 0; i < cols; i++) { for (let j = 0; j < rows; j++) { const rectLeft = minX + i * (rectWidth + cell.padding) + tmpWidth @@ -212,7 +234,6 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { { x: rectLeft, y: rectTop + rectHeight }, { x: rectLeft + rectWidth, y: rectTop + rectHeight }, ] - const allPointsInside = rectPoints.every((point) => this.inPolygon(point)) if (allPointsInside) { @@ -222,23 +243,25 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { width: rectWidth, height: rectHeight, fill: '#BFFD9F', + stroke: 'black', selectable: true, // 선택 가능하게 설정 lockMovementX: true, // X 축 이동 잠금 lockMovementY: true, // Y 축 이동 잠금 lockRotation: true, // 회전 잠금 lockScalingX: true, // X 축 크기 조정 잠금 lockScalingY: true, // Y 축 크기 조정 잠금 - opacity: 0.6, + opacity: 0.8, }) - + drawCellsArray.push(rect) //배열에 넣어서 반환한다 this.canvas.add(rect) } } } this.canvas?.renderAll() + return drawCellsArray }, inPolygon(point) { - const vertices = this.getCurrentPoints() + const vertices = this.points let intersects = 0 for (let i = 0; i < vertices.length; i++) { @@ -326,5 +349,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, { text.set({ visible: isView }) }) }, - divideLine() {}, + divideLine() { + dividePolygon(this) + }, }) diff --git a/src/components/ui/QSelect.jsx b/src/components/ui/QSelect.jsx new file mode 100644 index 00000000..6ebf2bd6 --- /dev/null +++ b/src/components/ui/QSelect.jsx @@ -0,0 +1,33 @@ +import { Select, SelectItem } from '@nextui-org/react' +import styles from './QSelect.module.css' + +const animals = [ + { key: 'cat', label: 'Cat' }, + { key: 'dog', label: 'Dog' }, + { key: 'elephant', label: 'Elephant' }, + { key: 'lion', label: 'Lion' }, + { key: 'tiger', label: 'Tiger' }, + { key: 'giraffe', label: 'Giraffe' }, + { key: 'dolphin', label: 'Dolphin' }, + { key: 'penguin', label: 'Penguin' }, + { key: 'zebra', label: 'Zebra' }, + { key: 'shark', label: 'Shark' }, + { key: 'whale', label: 'Whale' }, + { key: 'otter', label: 'Otter' }, + { key: 'crocodile', label: 'Crocodile' }, +] + +export default function QSelect() { + return ( + <> +
+ +
+
test
+ + ) +} diff --git a/src/components/ui/QSelect.module.css b/src/components/ui/QSelect.module.css new file mode 100644 index 00000000..c0802771 --- /dev/null +++ b/src/components/ui/QSelect.module.css @@ -0,0 +1,3 @@ +.test { + @apply bg-blue-500; +} diff --git a/src/hooks/useCanvas.js b/src/hooks/useCanvas.js index c1ba723a..31b223bf 100644 --- a/src/hooks/useCanvas.js +++ b/src/hooks/useCanvas.js @@ -7,6 +7,8 @@ import { canvasSizeState, fontSizeState } from '@/store/canvasAtom' import { QLine } from '@/components/fabric/QLine' import QRect from '@/components/fabric/QRect' import { QPolygon } from '@/components/fabric/QPolygon' +import { defineQLine } from '@/util/qline-utils' +import { defineQPloygon } from '@/util/qpolygon-utils' export function useCanvas(id) { const [canvas, setCanvas] = useState() @@ -115,22 +117,8 @@ export function useCanvas(id) { QPolygon.prototype.canvas = canvas QLine.prototype.canvas = canvas QRect.prototype.canvas = canvas - - fabric.QLine.fromObject = function (object, callback) { - function _callback(instance) { - delete instance.points - callback && callback(instance) - } - - const options = fabric.util.object.clone(object, true) - options.points = [object.x1, object.y1, object.x2, object.y2] - - fabric.Object._fromObject('QLine', options, _callback, 'points') - } - - fabric.QPolygon.fromObject = function (object, callback) { - fabric.Object._fromObject('QPolygon', object, callback, 'points') - } + defineQLine() + defineQPloygon() } /** diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js index 4533d7a2..c60fba2b 100644 --- a/src/hooks/useMode.js +++ b/src/hooks/useMode.js @@ -5,6 +5,7 @@ import { useRecoilState } from 'recoil' import { canvasSizeState, + drewRoofCellsState, fontSizeState, roofPolygonArrayState, roofPolygonPatternArrayState, @@ -18,17 +19,16 @@ import { fabric } from 'fabric' import { QPolygon } from '@/components/fabric/QPolygon' export const Mode = { - DRAW_LINE: 'drawLine', // 기준선 긋기모드 + DRAW_LINE: 'drawLine', // 기준선 긋기모드` EDIT: 'edit', TEMPLATE: 'template', PATTERNA: 'patterna', PATTERNB: 'patternb', TEXTBOX: 'textbox', DRAW_RECT: 'drawRect', - ROOF_PATTERN: 'roofPattern', - MODULE: 'module', - ROOF_TRESTLE: 'roofTrestle', - FILL_CELLS: 'fillCells', + ROOF_PATTERN: 'roofPattern', //지붕패턴 모드 + ROOF_TRESTLE: 'roofTrestle', //지붕가대 모드 + FILL_CELLS: 'fillCells', //태양광셀 모드 DEFAULT: 'default', } @@ -57,6 +57,7 @@ export function useMode() { const [canvasSize] = useRecoilState(canvasSizeState) const [selectedCellRoofArray, setSelectedCellRoofArray] = useState([]) + const [drewRoofCells, setDrewRoofCells] = useRecoilState(drewRoofCellsState) useEffect(() => { // 이벤트 리스너 추가 @@ -641,6 +642,9 @@ export function useMode() { * a : 시작점, b : 끝점 */ const drawLineWithLength = (a, b) => { + if (!a || !b) { + return + } const line = new QLine([a.left, a.top, b.left, b.top], { stroke: 'black', strokeWidth: 2, @@ -700,7 +704,6 @@ export function useMode() { stroke: 'black', fill: 'transparent', viewLengthText: true, - selectable: true, fontSize: fontSize, }, canvas, @@ -1174,6 +1177,7 @@ export function useMode() { setRoof(roof) roof.drawHelpLine() + roof.divideLine() } const togglePolygonLine = (obj) => { @@ -3175,23 +3179,27 @@ export function useMode() { canvas?.renderAll() } + /** + * 지붕 패턴 생성 로직 + * @param roofStyle + */ const makeRoofPatternPolygon = (roofStyle) => { if (Object.keys(roofPolygonPattern).length === 0 && roofPolygonPattern.constructor === Object) { alert('객체가 비어있습니다.') return } - //내부 선 점선으로 변경 + //내부 선 점선으로 변경 추후에 다시 되돌리는 로직 필요 roofPolygonPattern.lines.forEach((line, index) => { line.line.set('strokeDashArray', [10, 5, 2, 5]) line.line.set('stroke', 'blue') line.line.set('strokeWidth', 1) }) - var ratio = window.devicePixelRatio || 1 + const ratio = window.devicePixelRatio || 1 - let inputPatternSize = { width: 30, height: 20 } //임시 사이즈 - let patternSize = { ...inputPatternSize } // 입력된 값을 뒤집기 위해 + const inputPatternSize = { width: 30, height: 20 } //임시 사이즈 + const patternSize = { ...inputPatternSize } // 입력된 값을 뒤집기 위해 if (templateType === 2) { //세로형이면 width height를 바꿈 @@ -3237,6 +3245,7 @@ export function useMode() { fill: pattern, selectable: false, fontSize: 15, // fontSize는 필요에 따라 조정 + sort: false, lockMovementX: true, lockMovementY: true, lockRotation: true, @@ -3246,6 +3255,7 @@ export function useMode() { let polygonArray = [] + //패턴 폴리곤을 생성 후 배열에 담음 roofPolygonPattern.roofPatternPolygonArray.forEach((patternPolygon, index) => { const drawPolygon = new QPolygon(patternPolygon, commonOption) canvas.add(drawPolygon) @@ -3255,9 +3265,13 @@ export function useMode() { }) canvas?.renderAll() + //지붕 폴리곤 recoil에 담음 setRoofPolygonArray(polygonArray) } + /** + * 가대 생성 로직 + */ const makeRoofTrestle = () => { if (Object.keys(roofPolygonPattern).length === 0 && roofPolygonPattern.constructor === Object) { alert('객체가 비어있습니다.') @@ -3278,8 +3292,13 @@ export function useMode() { strokeWidth: 3, } + /** + * 지붕가대 생성 후 가대 선택 이벤트를 추가하는 로직 + * @param polygon + */ function toggleSelection(polygon) { if (polygon.strokeWidth === defualtStrokeStyle.strokeWidth) { + //기본 선택이랑 스트로크 굵기가 같으면 선택 안됨으로 봄 polygon.set({ stroke: selectedStrokeStyle.stroke, strokeWidth: selectedStrokeStyle.strokeWidth, @@ -3288,6 +3307,7 @@ export function useMode() { canvas.discardActiveObject() // 객체의 활성 상태 해제 selectedAreaArray.push(polygon) } else { + //선택후 재선택하면 선택안됨으로 변경 polygon.set({ stroke: defualtStrokeStyle.stroke, strokeWidth: defualtStrokeStyle.strokeWidth, @@ -3295,6 +3315,7 @@ export function useMode() { }) canvas.discardActiveObject() // 객체의 활성 상태 해제 + //폴리곤에 커스텀 인덱스를 가지고 해당 배열 인덱스를 찾아 삭제함 const removeIndex = polygon.customIndex const removeArrayIndex = selectedAreaArray.findIndex((x) => x.customIndex === removeIndex) selectedAreaArray.splice(removeArrayIndex, 1) @@ -3302,6 +3323,7 @@ export function useMode() { canvas?.renderAll() } + //외각선을 안쪽으로 그려 가대선을 그린다. polygons.forEach((polygon, index) => { const trestlePolygon = handleOuterlinesTest(polygon, -12) trestlePolygon.setViewLengthText(false) //얘는 set으로 안먹는다... @@ -3315,25 +3337,45 @@ export function useMode() { lockScalingX: true, lockScalingY: true, bringToFront: true, - customIndex: polygon.customIndex, + customIndex: polygon.customIndex, //가대 폴리곤의 임시 인덱스를 넣어줌 }) + /** + * 가대 선택 이벤트 + */ trestlePolygon.on('mousedown', function () { - const customIndex = polygon.get('customIndex') toggleSelection(trestlePolygon) }) }) setSelectedCellRoofArray(selectedAreaArray) } + /** + * 가대 선택 후 셀 채우기 + */ const makeRoofFillCells = () => { - // const selectedCellRoofs = selectedCellRoofArray + const drawCellsArray = [] + if (selectedCellRoofArray.length === 0) { + //배열에 선택된 가대 셀이 없으면 리턴 alert('선택된 영역이 없습니다.') setMode(Mode.DEFAULT) //default 모드로 변경 return } + if (drewRoofCells.length > 0) { + //리코일에 + if (confirm('패널이 초기화 됩니다.')) { + drewRoofCells.forEach((cells, index) => { + cells.drawCells.forEach((cell) => { + canvas?.remove(cell) + }) + }) + setDrewRoofCells([]) + canvas?.renderAll() + } + } + const inputCellSize = { width: 172, height: 113 } const cellSize = { ...inputCellSize } //기본으로 가로형으로 넣고 @@ -3342,8 +3384,11 @@ export function useMode() { } selectedCellRoofArray.forEach((polygon, index) => { - polygon.fillCell({ width: cellSize.width, height: cellSize.height, padding: 10 }) + const drawCells = polygon.fillCell({ width: cellSize.width, height: cellSize.height, padding: 10 }) + drawCellsArray.push({ roofIndex: polygon.customIndex, drawCells: drawCells }) }) + + setDrewRoofCells(drawCellsArray) setMode(Mode.DEFAULT) //default 모드로 변경 } diff --git a/src/lib/Axios.js b/src/lib/Axios.js new file mode 100644 index 00000000..cae33f3e --- /dev/null +++ b/src/lib/Axios.js @@ -0,0 +1,56 @@ +'use client' + +import axios from 'axios' + +axios.defaults.baseURL = process.env.NEXT_PUBLIC_API_SERVER_PATH + +const axiosInstance = axios.create({ + // baseURL: process.env.API_SERVER_URL, + headers: { + Accept: 'application/json', + }, +}) + +axiosInstance.interceptors.request.use((config) => { + // config['Authorization'] = localStorage.getItem('token') + //TODO: 인터셉터에서 추가 로직 구현 + return config +}) + +axiosInstance.interceptors.request.use(undefined, (error) => { + //TODO: 인터셉터에서 에러 처리 로직 구현 + // if (error.isAxiosError && e.response?.status === 401) { + // localStorage.removeItem('token') + // } +}) + +export const get = (url) => + axiosInstance + .get(url) + .then((res) => res.data) + .catch(console.error) + +export const post = (url, data) => + axiosInstance + .post(url, data) + .then((res) => res.data) + .catch(console.error) + +export const put = (url, data) => + axiosInstance + .put(url, data) + .then((res) => res.data) + .catch(console.error) + +export const patch = (url, data) => + axiosInstance + .patch(url, data) + + .then((res) => res.data) + .catch(console.error) + +export const del = (url) => + axiosInstance + .delete(url) + .then((res) => res.data) + .catch(console.error) diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js index c51a7061..48eb0281 100644 --- a/src/store/canvasAtom.js +++ b/src/store/canvasAtom.js @@ -53,3 +53,17 @@ export const templateTypeState = atom({ default: 1, //1:모임지붕, 2:A타입, 3:B타입 dangerouslyAllowMutability: true, }) + +//셀 그린 이후에 생성하는 state +export const drewRoofCellsState = atom({ + key: 'drewRoofCells', + default: [], + dangerouslyAllowMutability: true, +}) + +// 지붕재 width, height, rafter(서까래), roofStyle을 갖고있고 roofStyle 1은 정방향, 2는 지그재그 +export const roofMaterialState = atom({ + key: 'roofMaterial', + default: { width: 20, height: 10, rafter: 0, roofStyle: 2 }, + dangerouslyAllowMutability: true, +}) diff --git a/src/util/qline-utils.js b/src/util/qline-utils.js index 77821886..6443efb2 100644 --- a/src/util/qline-utils.js +++ b/src/util/qline-utils.js @@ -2,8 +2,14 @@ import { fabric } from 'fabric' import { QLine } from '@/components/fabric/QLine' export const defineQLine = () => { - /*fabric.QLine = QLine - fabric.QLine.fromObject = (object, callback) => { - return new fabric.QLine([object.x1, object.y1, object.x2, object.y2], object) - }*/ + fabric.QLine.fromObject = function (object, callback) { + function _callback(instance) { + delete instance.points + callback && callback(instance) + } + const options = fabric.util.object.clone(object, true) + options.points = [object.x1, object.y1, object.x2, object.y2] + + fabric.Object._fromObject('QLine', options, _callback, 'points') + } } diff --git a/src/util/qpolygon-utils.js b/src/util/qpolygon-utils.js index af5d676e..36eadd4d 100644 --- a/src/util/qpolygon-utils.js +++ b/src/util/qpolygon-utils.js @@ -1,6 +1,7 @@ import { fabric } from 'fabric' import { QLine } from '@/components/fabric/QLine' -import { calculateIntersection, distanceBetweenPoints, findClosestPoint } from '@/util/canvas-util' +import { calculateIntersection, distanceBetweenPoints, findClosestPoint, getDirectionByPoint } from '@/util/canvas-util' +import { QPolygon } from '@/components/fabric/QPolygon' export const defineQPloygon = () => { fabric.QPolygon.fromObject = function (object, callback) { @@ -19,7 +20,7 @@ export const drawHelpLineInHexagon = (polygon, chon) => { const ridgeStartPoints = [] const ridgeEndPoints = [] - const centerInterSectionPoints = [] + let centerInterSectionPoints = [] // polygon.lines = polygon.lines.sort((a, b) => a.length - b.length) polygon.wall.lines = getOneSideLines(polygon.wall) @@ -101,9 +102,9 @@ export const drawHelpLineInHexagon = (polygon, chon) => { : nextLine.startPoint line.connectedPoint = { interSectionPoint, area, startPoint, endPoint } - line.connectedPoints.push(interSectionPoint) + line.connectedPoints.push({ interSectionPoint, area, startPoint, endPoint }) nextLine.connectedPoint = { interSectionPoint, area, startPoint, endPoint } - nextLine.connectedPoints.push(interSectionPoint) + nextLine.connectedPoints.push({ interSectionPoint, area, startPoint, endPoint }) } } }) @@ -150,6 +151,12 @@ export const drawHelpLineInHexagon = (polygon, chon) => { name: 'hip', }) + line.startPoint = point.startPoint + line.endPoint = point.interSectionPoint + + line2.startPoint = point.endPoint + line2.endPoint = point.interSectionPoint + polygon.hips.push(line) polygon.hips.push(line2) @@ -165,20 +172,26 @@ export const drawHelpLineInHexagon = (polygon, chon) => { uniqueInterSectionPoints.forEach((point) => { const interSectionPoint = point.interSectionPoint - if (connectedPoint.x === interSectionPoint.x && connectedPoint.y === interSectionPoint.y) { + if (connectedPoint.interSectionPoint.x === interSectionPoint.x && connectedPoint.interSectionPoint.y === interSectionPoint.y) { removedIdx.push(line.idx) } }) }) }) - const notIntersectedLines = helpLines.filter((line) => !removedIdx.includes(line.idx)) + let notIntersectedLines = helpLines.filter((line) => !removedIdx.includes(line.idx)) + + notIntersectedLines = notIntersectedLines.map((line) => { + return { ...line, centerInterSectionPoints: [] } + }) notIntersectedLines.forEach((line) => { centerLines.forEach((centerLine) => { const interSectionPoint = calculateIntersection(line, centerLine) if (interSectionPoint && polygon.inPolygon(interSectionPoint) && polygon.wall.inPolygon(interSectionPoint)) { + line.centerInterSectionPoints.push(interSectionPoint) + interSectionPoint.lineIdx = line.idx centerInterSectionPoints.push(interSectionPoint) } }) @@ -187,11 +200,12 @@ export const drawHelpLineInHexagon = (polygon, chon) => { // centerInterSectionPoints에서 ridgeStartPoints와 x가 같거나 y가 같은것중 가장 가까운 점들을 찾는다. ridgeStartPoints.forEach((point) => { const xPoints = centerInterSectionPoints.filter((centerPoint) => Math.abs(centerPoint.x - point.x) < 2) + const yPoints = centerInterSectionPoints.filter((centerPoint) => Math.abs(centerPoint.y - point.y) < 2) let closestPoint if (xPoints.length === 0) { closestPoint = findClosestPoint(point, yPoints) - } else { + } else if (yPoints.length === 0) { closestPoint = findClosestPoint(point, xPoints) } @@ -200,44 +214,119 @@ export const drawHelpLineInHexagon = (polygon, chon) => { stroke: 'purple', fontSize: polygon.fontSize, name: 'ridge', + direction: getDirectionByPoint(point, closestPoint), }) + + line.startPoint = point + line.endPoint = closestPoint + polygon.ridges.push(line) polygon.canvas.add(line) ridgeEndPoints.push(closestPoint) + + notIntersectedLines = notIntersectedLines.filter((line) => line.idx !== closestPoint.lineIdx) } }) + centerInterSectionPoints = [] + notIntersectedLines.forEach((line) => { + centerInterSectionPoints.push(...line.centerInterSectionPoints) + }) + // ridgeEndPoints끼리 이어준다. - const remainingPoints = ridgeEndPoints + const remainingPoints = [...ridgeEndPoints] + + // ridgeEndPoint에서 centerInterSectionPoints와 45도인 점을 찾아 이어준다. + + ridgeEndPoints.forEach((ridgePoint) => { + const filteredCenterInterSectionPoints = centerInterSectionPoints.filter((centerPoint) => { + const degree = calculateAngle(ridgePoint, centerPoint) + return Math.abs(degree) === 45 || Math.abs(degree) === 135 + })[0] + + if (filteredCenterInterSectionPoints) { + const line = new QLine([ridgePoint.x, ridgePoint.y, filteredCenterInterSectionPoints.x, filteredCenterInterSectionPoints.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'hip', + }) + + line.startPoint = ridgePoint + line.endPoint = filteredCenterInterSectionPoints + + polygon.hips.push(line) + polygon.canvas.add(line) + + ridgeStartPoints.push(filteredCenterInterSectionPoints) + + polygon.points.forEach((point) => { + const degree = calculateAngle(ridgePoint, point) + + if (Math.abs(degree) % 45 < 1) { + const line = new QLine([ridgePoint.x, ridgePoint.y, point.x, point.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'hip', + }) + + polygon.hips.push(line) + polygon.canvas.add(line) + } + }) + } + }) + + // ridgeEndPoint끼리 연결한다. + while (remainingPoints.length > 1) { + const startPoint = remainingPoints.shift() + const endPoint = remainingPoints.shift() + + const line = new QLine([startPoint.x, startPoint.y, endPoint.x, endPoint.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'connectRidge', + }) + + line.startPoint = startPoint + line.endPoint = endPoint + + polygon.connectRidges.push(line) - remainingPoints.forEach((ridgePoint) => { polygon.points.forEach((point) => { - const degree = calculateAngle(ridgePoint, point) + const degree = calculateAngle(startPoint, point) - if (Math.abs(degree) % 45 < 1) { - const line = new QLine([ridgePoint.x, ridgePoint.y, point.x, point.y], { + if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { + const line = new QLine([startPoint.x, startPoint.y, point.x, point.y], { stroke: 'purple', fontSize: polygon.fontSize, name: 'hip', }) + line.startPoint = startPoint + line.endPoint = point + polygon.hips.push(line) polygon.canvas.add(line) } }) - }) - while (remainingPoints.length > 0) { - const point = remainingPoints.shift() - const closestPoint = findClosestPoint(point, remainingPoints) - if (!closestPoint) continue - // 마루끼리 연결 - const line = new QLine([point.x, point.y, closestPoint.x, closestPoint.y], { - stroke: 'purple', - fontSize: polygon.fontSize, - name: 'connectRidge', + polygon.points.forEach((point) => { + const degree = calculateAngle(endPoint, point) + + if (Math.abs(degree) === 45 || Math.abs(degree) === 135) { + const line = new QLine([endPoint.x, endPoint.y, point.x, point.y], { + stroke: 'purple', + fontSize: polygon.fontSize, + name: 'hip', + }) + + line.startPoint = endPoint + line.endPoint = point + + polygon.hips.push(line) + polygon.canvas.add(line) + } }) - polygon.connectRidges.push(line) polygon.canvas.add(line) } @@ -246,7 +335,7 @@ export const drawHelpLineInHexagon = (polygon, chon) => { export const drawCenterLines = (polygon) => { const centerLines = [] - const oneSideLines = getOneSideLines(polygon) + const oneSideLines = polygon.lines.map((line) => getOneSideLine(line)) const horizontalLines = oneSideLines.filter((line) => line.direction === 'right') const verticalLines = oneSideLines.filter((line) => line.direction === 'bottom') @@ -261,9 +350,6 @@ export const drawCenterLines = (polygon) => { horizontalLines.forEach((line, index) => { const nextLine = horizontalLines[(index + 1) % horizontalLines.length] - line.set({ strokeWidth: 5 }) - nextLine.set({ strokeWidth: 5 }) - polygon.canvas.renderAll() const startCenterX = Math.min(line.x1, nextLine.x1) @@ -319,6 +405,8 @@ const getOneSideLines = (polygon) => { line.x2 = newX2 line.y2 = newY2 line.direction = 'bottom' + line.startPoint = { x: newX1, y: newY1 } + line.endPoint = { x: newX2, y: newY2 } } else if (line.direction === 'left') { newX1 = line.x2 newY1 = line.y2 @@ -330,11 +418,13 @@ const getOneSideLines = (polygon) => { line.x2 = newX2 line.y2 = newY2 line.direction = 'right' + line.startPoint = { x: newX1, y: newY1 } + line.endPoint = { x: newX2, y: newY2 } } return line }) } -const calculateAngle = (point1, point2) => { +export const calculateAngle = (point1, point2) => { const deltaX = point2.x - point1.x const deltaY = point2.y - point1.y const angleInRadians = Math.atan2(deltaY, deltaX) @@ -375,3 +465,214 @@ const calculateTriangleArea = (point1, point2, point3) => { return Math.abs(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2 } + +// polygon을 나눈다. +export const dividePolygon = (polygon) => { + let hips = polygon.hips + const ridges = polygon.ridges.map((ridge) => getOneSideLine(ridge)) + const connectRidges = polygon.connectRidges + const polygonLines = polygon.lines + + hips.forEach((hip) => { + // hips의 startPoint와 endPoint를 polygon의 points와 비교하여 같은 점이 endPoint일 경우 startPoint로 변경한다. + const startPoint = polygon.points.find((point) => point.x === hip.endPoint.x && point.y === hip.endPoint.y) + if (startPoint) { + const temp = hip.startPoint + hip.startPoint = hip.endPoint + hip.endPoint = temp + } + }) + hips = [...hips, ...connectRidges] + polygon.setViewLengthText(false) + + polygonLines.forEach((line) => { + let ridge + + const startPoint = line.startPoint + const endPoint = line.endPoint + let polygonPoints = [] + + polygonPoints.push(startPoint) + + polygonPoints.push(endPoint) + + const startHip = hips.find((hip) => hip.startPoint.x === startPoint.x && hip.startPoint.y === startPoint.y) + const endHip = hips.find((hip) => hip.startPoint.x === endPoint.x && hip.startPoint.y === endPoint.y) + + if (!startHip || !endHip) { + return + } + + if (startHip && endHip && startHip.endPoint.x === endHip.endPoint.x && startHip.endPoint.y === endHip.endPoint.y) { + polygonPoints.push(startHip.endPoint) + + const newPolygon = new QPolygon(polygonPoints, { + fontSize: polygon.fontSize, + id: polygon.id, + name: 'roof', + selectable: false, + stroke: 'black', + fill: 'transparent', + strokeWidth: 3, + }) + + polygon.canvas.add(newPolygon) + return + } + + let connectedRidge = ridges.find( + (ridge) => + (ridge.startPoint.x === startHip.endPoint.x && ridge.startPoint.y === startHip.endPoint.y) || + (ridge.endPoint.x === startHip.endPoint.x && ridge.endPoint.y === startHip.endPoint.y), + ) + + const hipStartPoint = startHip.endPoint + const hipEndPoint = endHip.endPoint + const restRidgeConnection = connectRidges[0] + + if (connectedRidge.startPoint.x === hipStartPoint.x && connectedRidge.startPoint.y === hipStartPoint.y) { + if (connectedRidge.endPoint.x === hipEndPoint.x && connectedRidge.endPoint.y === hipEndPoint.y) { + polygonPoints.push(connectedRidge.endPoint) + polygonPoints.push(connectedRidge.startPoint) + + const newPolygon = new QPolygon(polygonPoints, { + fontSize: polygon.fontSize, + id: polygon.id, + name: 'roof', + selectable: false, + stroke: 'black', + fill: 'transparent', + strokeWidth: 3, + }) + + polygon.canvas.add(newPolygon) + return + } + } else if (connectedRidge.endPoint.x === hipStartPoint.x && connectedRidge.endPoint.y === hipStartPoint.y) { + if (connectedRidge.startPoint.x === hipEndPoint.x && connectedRidge.startPoint.y === hipEndPoint.y) { + polygonPoints.push(connectedRidge.startPoint) + polygonPoints.push(connectedRidge.endPoint) + + const newPolygon = new QPolygon(polygonPoints, { + fontSize: polygon.fontSize, + id: polygon.id, + name: 'roof', + selectable: false, + stroke: 'black', + fill: 'transparent', + strokeWidth: 3, + sort: true, + }) + + polygon.canvas.add(newPolygon) + return + } + } + + // 지붕이 꺾여있는 경우 + + if ( + (restRidgeConnection.startPoint.x === startHip.endPoint.x && restRidgeConnection.startPoint.y === startHip.endPoint.y) || + (restRidgeConnection.endPoint.x === startHip.endPoint.x && restRidgeConnection.endPoint.y === startHip.endPoint.y) + ) { + polygonPoints = [startPoint, startHip.endPoint] + let lastPoint + + if (restRidgeConnection.startPoint.x === startHip.endPoint.x && restRidgeConnection.startPoint.y === startHip.endPoint.y) { + lastPoint = restRidgeConnection.endPoint + polygonPoints.push(restRidgeConnection.endPoint) + } else { + lastPoint = restRidgeConnection.startPoint + polygonPoints.push(restRidgeConnection.startPoint) + } + + connectedRidge = ridges.find( + (ridge) => + (ridge.startPoint.x === lastPoint.x && ridge.startPoint.y === lastPoint.y) || + (ridge.endPoint.x === lastPoint.x && ridge.endPoint.y === lastPoint.y), + ) + + if (connectedRidge.startPoint.x === lastPoint.x && connectedRidge.startPoint.y === lastPoint.y) { + polygonPoints.push(connectedRidge.endPoint) + } else { + polygonPoints.push(connectedRidge.startPoint) + } + + polygonPoints.push(endPoint) + } else { + polygonPoints = [endPoint, endHip.endPoint] + let lastPoint + + if (restRidgeConnection.startPoint.x === endHip.endPoint.x && restRidgeConnection.startPoint.y === endHip.endPoint.y) { + lastPoint = restRidgeConnection.endPoint + polygonPoints.push(restRidgeConnection.endPoint) + } else { + lastPoint = restRidgeConnection.startPoint + polygonPoints.push(restRidgeConnection.startPoint) + } + + connectedRidge = ridges.find( + (ridge) => + (ridge.startPoint.x === lastPoint.x && ridge.startPoint.y === lastPoint.y) || + (ridge.endPoint.x === lastPoint.x && ridge.endPoint.y === lastPoint.y), + ) + + if (connectedRidge.startPoint.x === startHip.endPoint.x && connectedRidge.startPoint.y === startHip.endPoint.y) { + lastPoint = connectedRidge.startPoint + polygonPoints.push(connectedRidge.startPoint) + } else { + lastPoint = connectedRidge.endPoint + polygonPoints.push(connectedRidge.endPoint) + } + + polygonPoints.push(startPoint) + } + + const newPolygon = new QPolygon(polygonPoints, { + fontSize: polygon.fontSize, + id: polygon.id, + name: 'roof', + selectable: false, + stroke: 'black', + fill: 'transparent', + strokeWidth: 3, + }) + + polygon.canvas.add(newPolygon) + }) +} + +const getOneSideLine = (line) => { + // left, top 방향의 line은 right, bottom 방향의 line으로 변경한다. + const newLine = { ...line } + let newX1, newY1, newX2, newY2 + if (newLine.direction === 'top') { + newX1 = newLine.x2 + newY1 = newLine.y2 + newX2 = newLine.x1 + newY2 = newLine.y1 + + newLine.x1 = newX1 + newLine.y1 = newY1 + newLine.x2 = newX2 + newLine.y2 = newY2 + newLine.direction = 'bottom' + newLine.startPoint = { x: newX1, y: newY1 } + newLine.endPoint = { x: newX2, y: newY2 } + } else if (line.direction === 'left') { + newX1 = newLine.x2 + newY1 = newLine.y2 + newX2 = newLine.x1 + newY2 = newLine.y1 + + newLine.x1 = newX1 + newLine.y1 = newY1 + newLine.x2 = newX2 + newLine.y2 = newY2 + newLine.direction = 'right' + newLine.startPoint = { x: newX1, y: newY1 } + newLine.endPoint = { x: newX2, y: newY2 } + } + + return newLine +} diff --git a/yarn.lock b/yarn.lock index f34efa44..5eec9ca2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2181,6 +2181,15 @@ asynckit@^0.4.0: resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== +axios@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.3.tgz#a1125f2faf702bc8e8f2104ec3a76fab40257d85" + integrity sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" @@ -2539,6 +2548,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + foreground-child@^3.1.0: version "3.2.1" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz" @@ -3233,6 +3247,11 @@ prisma@^5.17.0: dependencies: "@prisma/engines" "5.17.0" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz"