@@ -92,7 +208,13 @@ export default function InitSettingsModal(props) {
{gridItems.gridSettings.map((item, index) => (
)
}
diff --git a/src/components/fabric/QPolygon.js b/src/components/fabric/QPolygon.js
index b03a593d..3c8b2148 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', () => {
@@ -144,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 })
diff --git a/src/components/management/Stuff.jsx b/src/components/management/Stuff.jsx
index da7b834d..e26b11a3 100644
--- a/src/components/management/Stuff.jsx
+++ b/src/components/management/Stuff.jsx
@@ -1,7 +1,342 @@
+'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 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)
+ alert('물건번호가 복사되었습니다.')
+ } catch (error) {
+ alert('물건번호 복사에 실패했습니다.')
+ }
+ }
+
+ 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()}`)
+ }
+ }
+ }
+
+ //그리드 체크박스 선택시
+ const getSelectedRowdata = (data) => {
+ setSelectedRowData(data)
+ setSelectedRowDataCount(data.length)
+ }
+
+ //물건삭제
+ const fnDeleteRowData = (data) => {
+ console.log('물건삭제:::::::::::')
+ if (data.length === 0) {
+ return alert('삭제할 데이터를 선택하세요')
+ }
+ let errCount = 0
+ data.forEach((cell) => {
+ if (!cell.objectNo) {
+ if (errCount === 0) {
+ alert('물건정보가 있는 행만 삭제 됩니다')
+ }
+ 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 {
+ alert('물건정보가 있는 행만 선택해주세요')
+ }
+ }
+
+ //행추가
+ 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) {
+ alert('행추가로 추가 한 행만 삭제됩니다.')
+ }
+ 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)
+ }}
+ />
+
+
+
+ 메모
+
+
+ {!isFormValid ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+ >
+ )}
+
+
+
+ {/*
*/}
+ >
+ )
+}
diff --git a/src/components/management/StuffQGrid.jsx b/src/components/management/StuffQGrid.jsx
new file mode 100644
index 00000000..8139991b
--- /dev/null
+++ b/src/components/management/StuffQGrid.jsx
@@ -0,0 +1,116 @@
+import React from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { AgGridReact } from 'ag-grid-react'
+
+import 'ag-grid-community/styles/ag-grid.css'
+import 'ag-grid-community/styles/ag-theme-quartz.css'
+
+export default function StuffQGrid(props) {
+ const { gridData, gridColumns, isPageable = true, count, gridRef } = props
+ /**
+ * 행 데이터를 설정할 때 useState을 사용하여 렌더링 전반에 걸쳐 일관된 배열 참조를 유지하는 것이 좋습니다
+ */
+ const [rowData, setRowData] = useState(null)
+
+ /**
+ * Column Definitions를 설정할 때는 useMemo 또는 useState를 사용하여 렌더 간에 일관된 참조를 유지하십시오.
+ * 응용 프로그램이 Column Definitions를 동적으로 변경하는 경우에도 렌더링 간에 일관된 참조를 유지하려면 useMemo 또는 useState를 사용하십시오.
+ */
+ const [colDefs, setColDefs] = useState(
+ gridColumns ?? [
+ { field: 'mission', filter: true },
+ { field: 'company' },
+ { field: 'location' },
+ { field: 'date' },
+ { field: 'price', valueFormatter: (params) => `₩ ${params.value.toLocaleString()}` },
+ { field: 'successful' },
+ { field: 'rocket' },
+ ],
+ )
+
+ /**
+ * defaultColDef 속성을 제공할 때 이 인라인 또는 구성 요소의 단순 개체로 정의하지 마십시오. 이렇게 하면 모든 렌더링에서 새 인스턴스가 생성됩니다.
+ * 대신 or useState 를 사용하여 useMemo 렌더 간에 일관된 참조가 유지되도록 합니다.
+ */
+ const defaultColDef = useMemo(() => {
+ return {
+ filter: false,
+ flex: 1,
+ sortable: false,
+ suppressMovable: true,
+ resizable: false,
+ suppressSizeToFit: false,
+ headerClass: 'centered', //_test.scss에 추가 테스트
+ }
+ }, [])
+
+ /**
+ * 단순 유형(string, boolean 및 number)의 속성은 렌더링 간에 값으로 비교되므로 후크를 사용할 필요가 없습니다.
+ */
+ const rowBuffer = 100
+
+ /**
+ * 모든 렌더링에서 그리드 상태를 재설정하지 않도록 useCallback을 사용하는 것이 좋습니다.
+ * api데이타 해당 컬럼에 따라 로우 체크박스 체크 가능여부 등 컨트롤
+ */
+ const isRowSelectable = useCallback(
+ (params) => {
+ return !!params.data
+ },
+ [count],
+ )
+
+ // 체크박스 체크시
+ const onSelectionChanged = useCallback((event) => {
+ props.getSelectedRowdata(event.api.getSelectedRows())
+ }, [])
+
+ //더블클릭
+ const onCellDoubleClicked = useCallback((event) => {
+ // if (event.column.colId === 'company') {
+ // return
+ // } else {
+ props.getCellDoubleClicked(event)
+ // }
+ }, [])
+
+ //컨텐츠에 따라 컬럼넓이 자동조절
+ const autoSizeStrategy = useMemo(() => {
+ return {
+ type: 'fitCellContents',
+ }
+ }, [])
+
+ const onGridReady = useCallback((event) => {
+ // 헤더 사이즈 조정 컬럼에 width값으로 계산
+ event.api.sizeColumnsToFit()
+ }, [])
+
+ // Fetch data & update rowData state
+ useEffect(() => {
+ gridData ? setRowData(gridData) : ''
+ }, [gridData])
+
+ return (
+
+ )
+}
diff --git a/src/components/management/StuffSearchCondition.jsx b/src/components/management/StuffSearchCondition.jsx
new file mode 100644
index 00000000..f0d665d0
--- /dev/null
+++ b/src/components/management/StuffSearchCondition.jsx
@@ -0,0 +1,264 @@
+'use client'
+
+import React, { useEffect } from 'react'
+import { useState } from 'react'
+import { Input, RadioGroup, Radio, Button } from '@nextui-org/react'
+import RangeDatePicker from '@/components/common/datepicker/RangeDatePicker'
+import { useRecoilState, useResetRecoilState } from 'recoil'
+import { stuffSearchState } from '@/store/stuffAtom'
+import dayjs from 'dayjs'
+import isLeapYear from 'dayjs/plugin/isLeapYear' // 윤년 판단 플러그인
+dayjs.extend(isLeapYear)
+import Link from 'next/link'
+export default function StuffSearchCondition() {
+ //달력 props 관련 날짜 셋팅
+ const [dateRange, setDateRange] = useState([dayjs(new Date()).add(-1, 'year').format('YYYY-MM-DD'), dayjs(new Date()).format('YYYY-MM-DD')])
+ const [startRangeDate, endRangeDate] = dateRange
+
+ const rangeDatePickerProps = {
+ startRangeDate, //시작일
+ endRangeDate, //종료일
+ setDateRange,
+ }
+
+ //여기서 선택한 검색조건들을 recoil로 관리
+ const resetStuffRecoil = useResetRecoilState(stuffSearchState)
+ const [stuffSearch, setStuffSearch] = useRecoilState(stuffSearchState)
+ const [objectNo, setObjectNo] = useState('') //물건번호
+ const [saleStoreId, setSaleStoreId] = useState('') //판매대리점ID
+ const [address, setAddress] = useState('') //물건주소
+ const [objectName, setobjectName] = useState('') //물건명
+ const [saleStoreName, setSaleStoreName] = useState('') //판매대리점명
+ const [specDateYn, setSpecDateYn] = useState('') //사양 확인('', 'Y', 'N')
+ const [receiveUser, setReceiveUser] = useState('') //담당자
+ const [dispCompanyName, setDispCompanyName] = useState('') //견적처
+ const [dateType, setDateType] = useState('U') //갱신일(U)/등록일(R)
+
+ // 조회
+ const onSubmit = () => {
+ let diff = dayjs(endRangeDate).diff(startRangeDate, 'day')
+ if (diff > 366) {
+ return alert('최대1년 조회 가능합니다.')
+ }
+ setStuffSearch({
+ schObjectNo: stuffSearch?.schObjectNo ? stuffSearch.schObjectNo : objectNo,
+ schSaleStoreId: stuffSearch?.schSaleStoreId ? stuffSearch.schSaleStoreId : saleStoreId,
+ schAddress: stuffSearch?.schAddress ? stuffSearch.schAddress : address,
+ schObjectName: stuffSearch?.schObjectName ? stuffSearch.schObjectName : objectName,
+ schSaleStoreName: stuffSearch?.schSaleStoreName ? stuffSearch.schSaleStoreName : saleStoreName,
+ schSpecDateYn: stuffSearch?.schSpecDateYn ? stuffSearch.schSpecDateYn : specDateYn,
+ schReceiveUser: stuffSearch?.schReceiveUser ? stuffSearch.schReceiveUser : receiveUser,
+ schDispCompanyName: stuffSearch?.schDispCompanyName ? stuffSearch.schDispCompanyName : dispCompanyName,
+ schDateType: stuffSearch?.schDateType ? stuffSearch.schDateType : dateType,
+ schFromDt: dayjs(startRangeDate).format('YYYY-MM-DD'),
+ schToDt: dayjs(endRangeDate).format('YYYY-MM-DD'),
+ code: 'E',
+ })
+ }
+
+ //초기화
+ const resetRecoil = () => {
+ setObjectNo('')
+ setSaleStoreId('')
+ setAddress('')
+ setobjectName('')
+ setSaleStoreName('')
+ setSpecDateYn('')
+ setReceiveUser('')
+ setDispCompanyName('')
+ setDateType('U')
+ setDateRange([dayjs(new Date()).add(-1, 'year').format('YYYY-MM-DD'), dayjs(new Date()).format('YYYY-MM-DD')])
+ resetStuffRecoil()
+ }
+
+ //x로 날짜 비웠을때 기본값으로 셋팅
+ useEffect(() => {
+ if (!startRangeDate && !endRangeDate) {
+ setDateRange([dayjs(new Date()).add(-1, 'year').format('YYYY-MM-DD'), dayjs(new Date()).format('YYYY-MM-DD')])
+ }
+ }, [startRangeDate, endRangeDate])
+
+ useEffect(() => {
+ setDateRange([
+ stuffSearch?.schFromDt ? stuffSearch.schFromDt : dayjs(new Date()).add(-1, 'year').format('YYYY-MM-DD'),
+ stuffSearch?.schToDt ? stuffSearch.schToDt : dayjs(new Date()).format('YYYY-MM-DD'),
+ ])
+ }, [stuffSearch])
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {Array(4)
+ .fill()
+ .map((_, i) => {
+ if (i === 0) {
+ return (
+
+ {
+ setObjectNo(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schObjectNo: e.target.value })
+ }}
+ />
+ {
+ setSaleStoreId(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schSaleStoreId: e.target.value })
+ }}
+ />
+ {
+ setAddress(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', address: e.target.value })
+ }}
+ />
+
+ )
+ } else if (i === 1) {
+ return (
+
+
{
+ setobjectName(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schObjectName: e.target.value })
+ }}
+ />
+
{
+ setSaleStoreName(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schSaleStoreName: e.target.value })
+ }}
+ />
+
+ {
+ setSpecDateYn(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schSpecDateYn: e.target.value })
+ }}
+ />
+
+
+
+ {
+ setSpecDateYn(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schSpecDateYn: e.target.value })
+ }}
+ />
+
+
+
+ {
+ setSpecDateYn(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schSpecDateYn: e.target.value })
+ }}
+ />
+
+
+
+ )
+ } else if (i === 2) {
+ return (
+
+ {
+ setReceiveUser(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schReceiveUser: e.target.value })
+ }}
+ />
+ {
+ setDispCompanyName(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schDispCompanyName: e.target.value })
+ }}
+ />
+
+ )
+ } else {
+ return (
+
+
+ {
+ setDateType(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schDateType: e.target.value })
+ }}
+ />
+
+
+
+ {
+ setDateType(e.target.value)
+ setStuffSearch({ ...stuffSearch, code: 'S', schDateType: e.target.value })
+ }}
+ />
+
+
+
+
+ )
+ }
+ })}
+
+ >
+ )
+}
diff --git a/src/components/ui/ObjectPlacement.jsx b/src/components/ui/ObjectPlacement.jsx
new file mode 100644
index 00000000..f7a73a5c
--- /dev/null
+++ b/src/components/ui/ObjectPlacement.jsx
@@ -0,0 +1,146 @@
+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'
+import { BATCH_TYPE, INPUT_TYPE } from '@/common/common'
+
+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 (
+
+
+
+
+
+
+
개구 · 그림자 배치
+
+
+
+
+
+
+
+
+
+
+
설정
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default ObjectPlacement
diff --git a/src/components/ui/SurfaceShape.jsx b/src/components/ui/SurfaceShape.jsx
index ac30138f..706729a3 100644
--- a/src/components/ui/SurfaceShape.jsx
+++ b/src/components/ui/SurfaceShape.jsx
@@ -13,7 +13,7 @@ import { getIntersectionPoint } from '@/util/canvas-util'
* @constructor
*/
export const SurfaceShapeModal = ({ canvas }) => {
- const [type, setType] = useState(0)
+ const [type, setType] = useState(1)
const setOpen = useSetRecoilState(modalState)
const fontSize = useRecoilValue(fontSizeState)
//지붕재
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)
}
diff --git a/src/hooks/useMode.js b/src/hooks/useMode.js
index b4d1e398..7e1efe4a 100644
--- a/src/hooks/useMode.js
+++ b/src/hooks/useMode.js
@@ -27,14 +27,15 @@ import {
guideLineState,
horiGuideLinesState,
vertGuideLinesState,
+ objectPlacementModeState,
} from '@/store/canvasAtom'
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'
+import { INPUT_TYPE, Mode } from '@/common/common'
export function useMode() {
const [mode, setMode] = useRecoilState(modeState)
@@ -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,10 +597,9 @@ export function useMode() {
const mouseAndkeyboardEventClear = () => {
canvas?.off('mouse:down')
- Object.keys(mouseEvent).forEach((key) => {
- canvas?.off('mouse:down', mouseEvent[key])
- document.removeEventListener('contextmenu', mouseEvent[key])
- })
+ canvas?.off('mouse:move')
+ canvas?.off('mouse:up')
+ canvas?.off('mouse:out')
Object.keys(keyboardEvent).forEach((key) => {
document.removeEventListener('keydown', keyboardEvent[key])
@@ -674,6 +683,7 @@ export function useMode() {
const changeMode = (canvas, mode) => {
mouseAndkeyboardEventClear()
+ addCommonMouseEvent()
setMode(mode)
setCanvas(canvas)
@@ -713,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) {
@@ -992,6 +1008,214 @@ 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
+ 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.origX = pointer.x
+ mouseEvent.openingMode.origY = pointer.y
+ if (objectPlacementMode.inputType === INPUT_TYPE.FREE) {
+ mouseEvent.openingMode.isDown = true
+
+ 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,
+ })
+ canvas.add(mouseEvent.openingMode.rect)
+ } else if (objectPlacementMode.inputType === INPUT_TYPE.DIMENSION) {
+ 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: Number(objectPlacementMode.width),
+ height: Number(objectPlacementMode.height),
+ })
+ canvas.add(mouseEvent.openingMode.rect)
+ canvas.off('mouse:move')
+ }
+ },
+ 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' })
+ setMode(Mode.DEFAULT)
+ },
+ },
+ }
+
+ const checkCrossAreaBoundary = (rect) => {
+ const openings = canvas?._objects.filter((obj) => obj.name === 'opening')
+ if (openings.length === 0) {
+ 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 },
+ ]
+
+ const rect1Corners = {
+ minX: Math.min(...rectPoints.map((point) => point.x)),
+ maxX: Math.max(...rectPoints.map((point) => point.x)),
+ minY: Math.min(...rectPoints.map((point) => point.y)),
+ maxY: Math.max(...rectPoints.map((point) => point.y)),
+ }
+ let isCross = true
+ for (let i = 0; i < openings.length; i++) {
+ if (i !== 0 && isCross) {
+ break
+ }
+ const rect2 = openings[i]
+ 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 },
+ ]
+
+ const rect2Corners = {
+ minX: Math.min(...rect2Points.map((point) => point.x)),
+ maxX: Math.max(...rect2Points.map((point) => point.x)),
+ minY: Math.min(...rect2Points.map((point) => point.y)),
+ maxY: Math.max(...rect2Points.map((point) => point.y)),
+ }
+
+ // Check if one rectangle is to the left of the other
+ if (
+ rect1Corners.maxX < rect2Corners.minX ||
+ rect2Corners.maxX < rect1Corners.minX ||
+ rect1Corners.maxY < rect2Corners.minY ||
+ rect2Corners.maxY < rect1Corners.minY
+ ) {
+ isCross = false
+ continue
+ } else {
+ isCross = true
+ break
+ }
+ }
+
+ return isCross
+ }
+
+ 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/locales/ja.js b/src/locales/ja.js
index 2b7df6bd..5ae1b171 100644
--- a/src/locales/ja.js
+++ b/src/locales/ja.js
@@ -4,4 +4,87 @@ export default {
hello: 'こんにちは',
welcome: 'こんにちは {name}!',
locale: '現在のロケールは {locale} です。',
+ common: {
+ require: '필수',
+ },
+ site: {
+ name: 'Q.CAST III',
+ sub_name: '태양광 발전 시스템 도면관리 사이트',
+ },
+ login: {
+ login: 'Login',
+ init_password: {
+ btn: '비밀번호 초기화',
+ title: '비밀번호 초기화',
+ sub_title: '비밀번호를 초기화할 아이디와 이메일 주소를 입력해 주세요.',
+ },
+ },
+ join: {
+ title: 'Q.CAST3 로그인ID 발행 신청',
+ sub1: {
+ title: '판매대리점 정보',
+ comment: '※ 등록되는 리셀러의 회사 이름을 입력하십시오. (2차점은 「○○판매주식회사(2차점:××설비주식회사)」로 기입해 주세요.)',
+ storeQcastNm: '판매대리점명',
+ storeQcastNm_placeholder: '株式会社エネルギア・ソリューション・アンド・サービス(2次店:山口住機販売有限会社)',
+ storeQcastNmKana: '판매대리점명 후리가나',
+ storeQcastNmKana_placeholder: 'カブシキガイシャエネルギア・ソリューション・アン',
+ postCd: '우편번호',
+ postCd_placeholder: '숫자 7자리',
+ addr: '주소',
+ addr_placeholder: '전각50자이내',
+ telNo: '전화번호',
+ telNo_placeholder: '00-0000-0000',
+ fax: 'FAX 번호',
+ fax_placeholder: '00-0000-0000',
+ },
+ sub2: {
+ title: '담당자 정보',
+ userNm: '담당자명',
+ userNmKana: '담당자명 후리가나',
+ userId: '신청 ID',
+ email: '이메일 주소',
+ telNo: '전화번호',
+ telNo_placeholder: '00-0000-0000',
+ fax: 'FAX 번호',
+ fax_placeholder: '00-0000-0000',
+ category: '부서명',
+ },
+ sub3: {
+ title: '견적서 제출용 회사정보',
+ qtCompNm: '회사명',
+ qtPostCd: '우편번호',
+ qtPostCd_placeholder: '숫자 7자리',
+ qtAddr: '주소',
+ qtAddr_placeholder: '전각50자이내',
+ qtEmail: '이메일 주소',
+ qtTelNo: '전화번호',
+ qtTelNo_placeholder: '00-0000-0000',
+ qtFax: 'FAX 번호',
+ qtFax_placeholder: '00-0000-0000',
+ },
+ btn: {
+ approval_request: 'ID 승인요청',
+ },
+ complete: {
+ title: 'Q.CAST3 로그인ID 발행신청 완료',
+ contents: '※ 신청한 ID가 승인되면, 담당자 정보에 입력한 이메일 주소로 로그인 관련 안내 메일이 전송됩니다.',
+ email_comment: '담당자 이메일 주소',
+ email: 'test@naver.com',
+ },
+ },
+ stuff: {
+ gridHeader: {
+ lastEditDatetime: '갱신일시',
+ objectNo: '물건번호',
+ planTotCnt: '플랜 수',
+ objectName: '물건명',
+ saleStoreId: '대리점ID',
+ saleStoreName: '대리점명',
+ address: '물건주소',
+ dispCompanyName: '견적처',
+ receiveUser: '담당자',
+ specDate: '사양확인',
+ createDatetime: '등록일',
+ },
+ },
}
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.js b/src/locales/ko.js
index 40e17ba2..c01b913c 100644
--- a/src/locales/ko.js
+++ b/src/locales/ko.js
@@ -4,4 +4,88 @@ export default {
hello: '안녕',
welcome: '안녕 {name}!',
locale: '현재 로케일은 {locale}입니다.',
+ common: {
+ require: '필수',
+ },
+ site: {
+ name: 'Q.CAST III',
+ sub_name: '태양광 발전 시스템 도면관리 사이트',
+ },
+ login: {
+ login: '로그인',
+ init_password: {
+ btn: '비밀번호 초기화',
+ title: '비밀번호 초기화',
+ sub_title: '비밀번호를 초기화할 아이디와 이메일 주소를 입력해 주세요.',
+ complete_message: '비밀번호가 초기화 되었습니다. 초기화된 비밀번호는 아이디와 같습니다.',
+ },
+ },
+ join: {
+ title: 'Q.CAST3 로그인ID 발행 신청',
+ sub1: {
+ title: '판매대리점 정보',
+ comment: '※ 등록되는 리셀러의 회사 이름을 입력하십시오. (2차점은 「○○판매주식회사(2차점:××설비주식회사)」로 기입해 주세요.)',
+ storeQcastNm: '판매대리점명',
+ storeQcastNm_placeholder: '주식회사 에너지 기어 솔루션 앤 서비스 (2차점: 야마구치 주기 판매 유한회사)',
+ storeQcastNmKana: '판매대리점명 후리가나',
+ storeQcastNmKana_placeholder: '주식회사 에너지 기어 솔루션',
+ postCd: '우편번호',
+ postCd_placeholder: '숫자 7자리',
+ addr: '주소',
+ addr_placeholder: '전각50자이내',
+ telNo: '전화번호',
+ telNo_placeholder: '00-0000-0000',
+ fax: 'FAX 번호',
+ fax_placeholder: '00-0000-0000',
+ },
+ sub2: {
+ title: '담당자 정보',
+ userNm: '담당자명',
+ userNmKana: '담당자명 후리가나',
+ userId: '신청 ID',
+ email: '이메일 주소',
+ telNo: '전화번호',
+ telNo_placeholder: '00-0000-0000',
+ fax: 'FAX 번호',
+ fax_placeholder: '00-0000-0000',
+ category: '부서명',
+ },
+ sub3: {
+ title: '견적서 제출용 회사정보',
+ qtCompNm: '회사명',
+ qtPostCd: '우편번호',
+ qtPostCd_placeholder: '숫자 7자리',
+ qtAddr: '주소',
+ qtAddr_placeholder: '전각50자이내',
+ qtEmail: '이메일 주소',
+ qtTelNo: '전화번호',
+ qtTelNo_placeholder: '00-0000-0000',
+ qtFax: 'FAX 번호',
+ qtFax_placeholder: '00-0000-0000',
+ },
+ btn: {
+ approval_request: 'ID 승인요청',
+ },
+ complete: {
+ title: 'Q.CAST3 로그인ID 발행신청 완료',
+ contents: '※ 신청한 ID가 승인되면, 담당자 정보에 입력한 이메일 주소로 로그인 관련 안내 메일이 전송됩니다.',
+ email_comment: '담당자 이메일 주소',
+ email: 'test@naver.com',
+ },
+ },
+ stuff: {
+ gridHeader: {
+ lastEditDatetime: '갱신일시',
+ objectNo: '물건번호',
+ planTotCnt: '플랜 수',
+ objectName: '물건명',
+ saleStoreId: '대리점ID',
+ saleStoreName: '대리점명',
+ address: '물건주소',
+ dispCompanyName: '견적처',
+ receiveUser: '담당자',
+ specDate: '사양확인',
+ createDatetime: '등록일',
+ },
+ },
}
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."
}
diff --git a/src/store/canvasAtom.js b/src/store/canvasAtom.js
index 664e42cb..ae020901 100644
--- a/src/store/canvasAtom.js
+++ b/src/store/canvasAtom.js
@@ -161,3 +161,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: false, inputType: 'free', batchType: 'opening' },
+})
diff --git a/src/store/stuffAtom.js b/src/store/stuffAtom.js
new file mode 100644
index 00000000..a122543c
--- /dev/null
+++ b/src/store/stuffAtom.js
@@ -0,0 +1,22 @@
+import { atom } from 'recoil'
+import dayjs from 'dayjs'
+import isLeapYear from 'dayjs/plugin/isLeapYear' // 윤년 판단 플러그인
+dayjs.extend(isLeapYear)
+export const stuffSearchState = atom({
+ key: 'stuffSearchState',
+ default: {
+ schObjectNo: '', //물건번호
+ schSaleStoreId: '', //판매대리점ID
+ schAddress: '', //물건주소
+ schObjectName: '', //물건명
+ schSaleStoreName: '', //판매대리점명
+ schSpecDateYn: '', //사양타입 ('', 'Y', 'N')
+ schReceiveUser: '', //담당자
+ schDispCompanyName: '', //견적처
+ schDateType: 'U', //갱신일(U)/등록일(R)
+ schFromDt: dayjs(new Date()).add(-1, 'year').format('YYYY-MM-DD'), //시작일
+ schToDt: dayjs(new Date()).format('YYYY-MM-DD'), //종료일
+ code: 'S',
+ },
+ dangerouslyAllowMutability: true,
+})
diff --git a/src/styles/_test.scss b/src/styles/_test.scss
index 55440362..9e542bbd 100644
--- a/src/styles/_test.scss
+++ b/src/styles/_test.scss
@@ -41,7 +41,7 @@
.grid-item {
width: 100%;
height: 100%;
- border: 1px solid black; /* 그리드 외각선 */
+ border: 1px solid black; /* 그리드 외각선 */
text-align: center; /* 그리드 내 가운데 정렬 */
}
@@ -79,3 +79,9 @@
background-color: white;
color: black;
}
+
+.centered {
+ .ag-header-cell-label {
+ justify-content: center !important;
+ }
+}
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)
})
diff --git a/yarn.lock b/yarn.lock
index 6bd3608f..ed3490ac 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5515,6 +5515,11 @@ react-dom@^18:
loose-envify "^1.1.0"
scheduler "^0.23.2"
+react-hook-form@^7.53.0:
+ version "7.53.0"
+ resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.53.0.tgz#3cf70951bf41fa95207b34486203ebefbd3a05ab"
+ integrity sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==
+
react-icons@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c"