From b02ad92fca41e2fbfb206fa47598c751a682e5e1 Mon Sep 17 00:00:00 2001 From: "hyojun.choi" Date: Mon, 9 Sep 2024 17:19:28 +0900 Subject: [PATCH 01/10] =?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 02/10] =?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) + }} + /> + +
+
+ 메모 +