diff --git a/apps/sn/app/components/ui/fileUploadForm.tsx b/apps/sn/app/components/ui/fileUploadForm.tsx index e111d8362..3c57bb812 100644 --- a/apps/sn/app/components/ui/fileUploadForm.tsx +++ b/apps/sn/app/components/ui/fileUploadForm.tsx @@ -7,13 +7,14 @@ import React, { useState } from 'react' const FileUploadForm = ({ title }: { title: string }) => { const [files, setFiles] = useState(null); const [selectedYear, setSelectedYear] = useState(''); + const [selectedQuarter, setSelectedQuarter] = useState(''); // 현재 년도부터 10년 전까지의 년도 옵션 생성 const yearOptions = Array.from({ length: 10 }, (_, i) => { const year = new Date().getFullYear() - i; return { text: `${year}년`, val: year.toString() }; }); - + const quater = [{text: '1분기', val: '1'}, {text: '2분기', val: '2'}, {text: '3분기', val: '3'}, {text: '4분기', val: '4'}] return (
@@ -32,8 +33,8 @@ const FileUploadForm = ({ title }: { title: string }) => { className="w-50" selectClass="rounded-lg bg-white" label="분기" - selectArray={yearOptions} - onSelectValue={(value: string | number) => setSelectedYear(String(value))} + selectArray={quater} + onSelectValue={(value: string | number) => setSelectedQuarter(String(value))} />
@@ -44,7 +45,7 @@ const FileUploadForm = ({ title }: { title: string }) => { className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" onClick={() => { - if (!files?.[0] || !selectedYear) { + if (!files?.[0] || !selectedYear || !selectedQuarter) { alert('파일과 등록년도를 선택해주세요.'); return; } diff --git a/apps/sn/app/components/ui/modal.tsx b/apps/sn/app/components/ui/modal.tsx index bc8085f71..e860ed7b9 100644 --- a/apps/sn/app/components/ui/modal.tsx +++ b/apps/sn/app/components/ui/modal.tsx @@ -13,7 +13,7 @@ const Modal: React.FC = ({ isModalOpen, setIsModalOpen, title, child return ( - + {title ? {title} : null} diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx index 36451bfbd..3d6b10d91 100644 --- a/apps/sn/app/components/ui/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -8,7 +8,7 @@ type UiTableProps = { headlist: UiHeadProps[]; pageSize: number; total: number; - children: React.ReactNode | undefined; + children: Record | undefined; cellClick: ((index: number | undefined, headId: string) => void) | undefined; onPageChange: (page: number) => void; className?: string; @@ -82,7 +82,7 @@ export function VerticalTable({ {/* 데이터 값 또는 입력 필드 */} - {isEditable ? ( + {children && children[head.id] ? children[head.id] : isEditable ? ( head.type === "select" ? (
@@ -48,7 +48,7 @@ const IrSetting = () => {
- + { if (date) setSelectedDate(date) }} mode={"single"} /> diff --git a/apps/sn/app/ii/rdii/page.tsx b/apps/sn/app/ii/rdii/page.tsx index 9a04102e7..7db1bfe56 100644 --- a/apps/sn/app/ii/rdii/page.tsx +++ b/apps/sn/app/ii/rdii/page.tsx @@ -138,7 +138,7 @@ export default function RTKForm() {
- + { if (date) setSelectedDate(date) }} mode={"single"} /> diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index f42f2ab29..a3a036518 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -29,6 +29,20 @@ const tableColumn = [ { id: 'date', title: '분석일자' }, { id: 'usr', title: '분석자' } ] +const tableColumn2 = [ + { id: 'no', title: '번호' }, + { id: 'hasu', title: '하수처리장' }, + { id: 'guyeok', title: '처리구역' }, + { id: 'sangsu', title: '상수 사용량' }, + { id: 'rate', title: '오수 전환율' }, + { id: 'rtx', title: 'RTX계수출처' }, + { id: 'rdii', title: 'RDII 기여면적' }, + { id: 'water', title: '순오수량' }, + { id: 'measureWater', title: '강우유출량' }, + { id: 'ii', title: 'RDII(m2/d)' }, + { id: 'date', title: '분석일자' }, + { id: 'usr', title: '분석자' } +] const tableInfoData = [{ 'no': '1', 'hasu': '양평하수처리장', 'guyeok': '양평', 'sangsu': '1000', 'rate': '0.5', 'water': '500', 'measureWater': '1000', 'ii': '500', 'date': '2023-10-01', 'usr': '홍길동' }] const Result = () => { const form = useForm>({ @@ -43,7 +57,7 @@ const Result = () => { } }) return ( - { }} graphBtn={true}> + { }} graphBtn={true}> {[
○ 침입수 산정결과 현황
{ cellClick={(e) => { }} onPageChange={(e) => { }} /> -
]} +
, +
+ { }} + onPageChange={(e) => { }} + /> +
+ ]} ) } diff --git a/apps/sn/app/map/components/common/index.ts b/apps/sn/app/map/components/common/index.ts index 1d7efa7b2..043c85f42 100644 --- a/apps/sn/app/map/components/common/index.ts +++ b/apps/sn/app/map/components/common/index.ts @@ -1,21 +1,34 @@ +import MsSwagePpl from "../popupComponent/MsSwagePpl"; +import sewagePump from "../popupComponent/sewagePump"; import smh from "../popupComponent/smh"; import Ssc from "../popupComponent/Ssc"; +import StormWaterPump from "../popupComponent/stormWaterPump"; import SwtrmtFclt from "../popupComponent/SwtrmtFclt"; +type LayerComponentProps = { featureData: any }; +type LayerContentMap = { + [key: string]: { + component?: React.FC; + props?: any; + }; +}; // 레이어 이름과 컴포넌트를 매핑 -const layerContentMap: { [key: string]: { component?: React.FC ; props?: any; } } = { +// 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 +const layerContentMap: LayerContentMap = { // TODO: gis데이터 추가되면 추가 작업 필요 "하수처리시설": { - component: SwtrmtFclt, + component: MsSwagePpl, }, "하수처리구역": {component: Ssc, }, "중점우수토실":{component: Ssc,}, - "하수관로":{component: SwtrmtFclt,}, + "하수관로":{component: MsSwagePpl,}, "하수맨홀":{component: smh,}, + "빗물펌프":{component:StormWaterPump}, + "오수펌프":{component:sewagePump}, }; // 동적 컴포넌트 반환 함수 -export const getLayerContentComponent = (layerNm: string): React.FC => { +export const getLayerContentComponent = (layerNm: string): React.FC | null => { const component = layerContentMap[layerNm]; if (!component || !component.component) { // component가 없으면 null 반환 diff --git a/apps/sn/app/map/components/common/table.ts b/apps/sn/app/map/components/common/table.ts index a64e610d3..b72d3e81d 100644 --- a/apps/sn/app/map/components/common/table.ts +++ b/apps/sn/app/map/components/common/table.ts @@ -49,3 +49,25 @@ export const SwtrmtPrcsZoneCol = [ { id: 'sggNm', title: '시군구명' }, { id: 'swtrmtFcltNm', title: '하수처리시설명' }, ] + +// 하수맨홀 +export const MsSwageMhlCol = [ + { id: 'fid', title: 'FID' }, + { id: 'geom', title: 'geometry' }, + { id: 'ftreSmblCd', title: '지형지물부호코드' }, + { id: 'mngNo', title: '관리번호' }, + { id: 'pbadmsEmdCd', title: '행정읍면동코드' }, + { id: 'mapshtNo', title: '도엽번호' }, + { id: 'mngInstCd', title: '관리기관코드' }, + { id: 'instlYmd', title: '설치일자' }, + { id: 'lastDrdgnYmd', title: '최종준설일자' }, + { id: 'usgCd', title: '용도코드' }, + { id: 'fcltyShpCd', title: '시설물형태코드' }, + { id: 'mhlKndCd', title: '맨홀종류코드' }, + { id: 'swageMhDiam', title: '하수맨홀구경' }, + { id: 'wdthLen', title: '가로길이' }, + { id: 'vrtcLen', title: '세로길이' }, + { id: 'mqltCd', title: '재질코드' }, + { id: 'altdVl', title: '표고값' }, + { id: 'dpthlVl', title: '심도값' }, + { id: 'instMngNo', title:'기관관리번호'}] diff --git a/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx b/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx new file mode 100644 index 000000000..f847cceb0 --- /dev/null +++ b/apps/sn/app/map/components/popupComponent/MsSwagePpl.tsx @@ -0,0 +1,46 @@ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React, { useState } from 'react' +import InputSection from '../InputSection'; +import { handleInputChange } from '../common'; +// 하수관로 +// TODO: RDII 버튼인가 그거 팝업 두개 구현 필요 +const inputColumn = [ + { id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, + { id: "swtrmt_zone_cd", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, + { id: "basu", title: "배수구역", type: "input" }, + { id: "mltv_dtdv_nm", title: "배수분구", type: "input" }, + { id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] }, + { id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] }, + { id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] }, +]; +const SwtrmtFclt = ({ featureData }: { featureData: any }) => { + console.log("featureData", featureData); + const [repairData, setRepairData] = useState([ + { year: "2025", type: "개대체" }, + { year: "2025", type: "보수" }, + ]); + const [cleaningData, setCleaningData] = useState([ + { year: "2025", type: "청소" }, + { year: "2025", type: "준설" }, + ]); + const [inspectionData, setInspectionData] = useState([ + { year: "2025", type: "관로기술진단" }, + { year: "2025", type: "관로기술진단" }, + ]); + const dataMap = { + repair: { data: repairData, setData: setRepairData }, + cleaning: { data: cleaningData, setData: setCleaningData }, + inspection: { data: inspectionData, setData: setInspectionData }, + }; + return ( +
+ + handleInputChange(section, index, field, value, dataMap)} /> + handleInputChange(section, index, field, value, dataMap)} /> + handleInputChange(section, index, field, value, dataMap)} />
+ ) +} + +export default SwtrmtFclt \ No newline at end of file diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx index a6e783ac9..e085251f7 100644 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -12,7 +12,7 @@ const headlist = [ { id: 'startrain', title: '강우시작일시' }, { id: 'endrain', title: '강우종료일시' }, { id: '선행건기', title: '선행건기(h)' }, { id: '강우정보', title: '강우정보' }, { id: '누적강우량', title: '누적강우량(mm)' }, { id: '지속시간', title: '지속시간(h)' }, { id: '강우강도', title: '강우강도(mm/h)' }, ] const headlist3 = [{ id: 'nm', title: '회차' }, { id: 'date', title: '측정일자' }, { id: 'measure', title: '수량(m)' }, { id: 'bod', title: 'BOD(mg/L)' }, { id: 'toc', title: 'TOC(mg/L)' }, { id: '오염부하량', title: '오염부하량(kg)' }] -const Ssc = () => { +const Ssc = ({ featureData }: { featureData: any }) => { const [isModalOpen, setIsModalOpen] = React.useState(false); const openModal = () => { setIsModalOpen(true); diff --git a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx index b6d785414..eef14ea5e 100644 --- a/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx +++ b/apps/sn/app/map/components/popupComponent/SwtrmtFclt.tsx @@ -1,44 +1,48 @@ +import Modal from '@/components/ui/modal'; import { VerticalTable } from '@/components/ui/verticalTable'; +import { UiTable } from '@common/business_components/ui'; +import { Button, TableCell } from '@common/components/ui'; import React, { useState } from 'react' -import InputSection from '../InputSection'; -import { handleInputChange } from '../common'; + // 하수처리시설 -// TODO: 컬럼에 보고서 엉 ? 이거 버튼누르면 팝업으로 상세화면 띄워주는 건지 확인 필요 -const inputColumn = [ - { id: "title", title: "설치주제", type: "select", options: ["BTL", "재정사업"] }, - { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, - { id: "basu", title: "배수구역", type: "input" }, - { id: "bungu", title: "배수분구", type: "input" }, - { id: "wltjs", title: "지선/간선", type: "select", options: ["지선", "간선"] }, - { id: "flow", title: "흐름방식", type: "select", options: ["자연유하", "압송"] }, - { id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] }, -]; +const headList = [ + { id: "title", title: "하수도정비 기본계획보고서" }, + { id: "cad", title: "하수도대장 평면도" },] +const modalHeadList = [ + { id: "no", title: "번호" }, + { id: "year", title: "승인연도" }, + { id: "cad", title: '평면도'}, + { id: "fileNm", title: "파일명" }, + { id: "date", title: "입력시간" }, +] + const SwtrmtFclt = () => { - const [repairData, setRepairData] = useState([ - { year: "2025", type: "개대체" }, - { year: "2025", type: "보수" }, - ]); - const [cleaningData, setCleaningData] = useState([ - { year: "2025", type: "청소" }, - { year: "2025", type: "준설" }, - ]); - const [inspectionData, setInspectionData] = useState([ - { year: "2025", type: "관로기술진단" }, - { year: "2025", type: "관로기술진단" }, - ]); - const dataMap = { - repair: { data: repairData, setData: setRepairData }, - cleaning: { data: cleaningData, setData: setCleaningData }, - inspection: { data: inspectionData, setData: setInspectionData }, + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalTitle, setModalTitle] = useState(''); + const openModal = (title:string) => { + setIsModalOpen(true); + setModalTitle(title) + }; + const closeModal = () => { + setIsModalOpen(false); }; return (
- openModal('하수도정비 기본계획보고서')}>보고서, + cad: , + }} headName='디지털 자료관 연계정보' tableData={[]} headlist={headList} pageSize={20} total={0} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { throw new Error("Function not implemented."); }}> - handleInputChange(section, index, field, value, dataMap)} /> - handleInputChange(section, index, field, value, dataMap)} /> - handleInputChange(section, index, field, value, dataMap)} />
+ +
+ +
+
+
) } diff --git a/apps/sn/app/map/components/popupComponent/sewagePump.tsx b/apps/sn/app/map/components/popupComponent/sewagePump.tsx new file mode 100644 index 000000000..9c62762d9 --- /dev/null +++ b/apps/sn/app/map/components/popupComponent/sewagePump.tsx @@ -0,0 +1,18 @@ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React from 'react' +const inputColumn = [ + { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, + { id: "bungu", title: "배수분구", type: "input" }, + { id: "freq", title: "설계빈도", type: "input" }, +] +// 오수펌프 +const sewagePump = ({ featureData }: { featureData: any }) => { + return ( +
+
+ ) +} + +export default sewagePump diff --git a/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx b/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx new file mode 100644 index 000000000..3a6cdee2d --- /dev/null +++ b/apps/sn/app/map/components/popupComponent/stormWaterPump.tsx @@ -0,0 +1,17 @@ +import { VerticalTable } from '@/components/ui/verticalTable'; +import React from 'react' +const inputColumn = [ + { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, + { id: "bungu", title: "배수분구", type: "input" }, +] +// 빗물펌프 +const StormWaterPump = ({ featureData }: { featureData: any }) => { + return ( +
+
+ ) +} + +export default StormWaterPump \ No newline at end of file diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx index 834cfd6dc..223bebaa3 100644 --- a/apps/sn/app/map/featureClickEventPopup.tsx +++ b/apps/sn/app/map/featureClickEventPopup.tsx @@ -1,7 +1,7 @@ 'use client' import { VerticalTable } from "@/components/ui/verticalTable"; import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from "./components/common/table"; import { useGisContext } from "@common/business_components/ui/map/Gis"; import { getData } from "@/components/lib/api"; @@ -16,7 +16,9 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, const [layerNm, setLayerNm] = useState(""); // 레이어 이름 상태 const [tableData, setTableData] = useState(null); // 피처 데이터 상태 const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 - let [buttonChk, setButtonChk] = useState(true) + const [buttonChk, setButtonChk] = useState(true) + const [fetchData, setfetchData] = useState(); // api 호출여부 + const contentRef = useRef(null); // 피쳐데이터의 현재 레이어 이름 가져오는 함수 const getSelectedLayers = () => { let result = null; @@ -45,11 +47,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 setLayerNm(selectedLayerName); // 상태 업데이트 - const data = await getData(`v1/map/features/${featureId}`); + const data = await getData(`v1/fclts/${featureId}`); + setfetchData(data) switch (selectedLayerName) { case '하수처리시설': setHeadlist(SwtrmtFcltCol); - setButtonChk(false) + setButtonChk(true) break; case '하수처리구역': setHeadlist(SwtrmtPrcsZoneCol) @@ -64,7 +67,10 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, case '하수관로': setButtonChk(true) break; - case '하수맨홀': + case '오수펌프': + setButtonChk(true) + break; + case '빗물펌프': setButtonChk(true) break; default: @@ -75,6 +81,24 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, } fetchData(); }, [featureId]); + // TODO: api 호출작업 필요(사용자 입력부분) + const btnClickEvent = () => { + if (buttonChk && contentRef.current) { + const inputElements = contentRef.current.querySelectorAll('input'); + const selectElements = contentRef.current.querySelectorAll('select'); + // select 태그의 값 가져오기 + console.log("Select Elements:", selectElements); + console.log("Input Elements:", inputElements); + selectElements.forEach((select) => { + console.log("Select Value:", select.value); + }); + + // input 태그의 값 가져오기 + inputElements.forEach((input) => { + console.log("Input Value:", input.value); + }); + } + } const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null; return ( -
+
- {buttonChk ? : null}
- {ContentComponent && } + {ContentComponent && }
) diff --git a/apps/sn/app/page.tsx b/apps/sn/app/page.tsx index db70f7dfa..fbf788346 100644 --- a/apps/sn/app/page.tsx +++ b/apps/sn/app/page.tsx @@ -5,9 +5,13 @@ import { Button } from "@common/components/ui"; export default function Home() { return (
+

하수관망도

+
+ +
+

운영정보 관리

-
diff --git a/packages/common/business_components/src/ui/excel_download/index.tsx b/packages/common/business_components/src/ui/excel_download/index.tsx index d1370b95c..58ecf8a52 100644 --- a/packages/common/business_components/src/ui/excel_download/index.tsx +++ b/packages/common/business_components/src/ui/excel_download/index.tsx @@ -1,16 +1,84 @@ import * as XLSX from 'xlsx'; type ExcelDownloadProps = { - data : JSON | JSON[]; - fileName : string; - sheetNameArray : string[]; + data: JSON | JSON[]; + fileName: string; + sheetNameArray: string[]; } -export const downloadExcel = ({ data, fileName = new Date().getTime().toString()+'.xlsx', sheetNameArray = ["Sheet1"] }: ExcelDownloadProps) => { - const worksheet = XLSX.utils.json_to_sheet(Array.isArray(data) ? data : [data]); - const workbook = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(workbook, worksheet, sheetNameArray[0]); - //let buffer = XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }); - //XLSX.write(workbook, { bookType: "xlsx", type: "binary" }); - XLSX.writeFile(workbook, fileName+'.xlsx'); - }; \ No newline at end of file +export const downloadExcel = ({ data, fileName = new Date().getTime().toString() + '.xlsx', sheetNameArray = ["Sheet1"] }: ExcelDownloadProps) => { + const worksheet = XLSX.utils.json_to_sheet(Array.isArray(data) ? data : [data]); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, sheetNameArray[0]); + //let buffer = XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }); + //XLSX.write(workbook, { bookType: "xlsx", type: "binary" }); + XLSX.writeFile(workbook, fileName + '.xlsx'); +}; + + + +export const filePicked=(oFile: File, headerNum:number=0):Promise =>{ + // Create A File Reader HTML5 + const reader = new FileReader(); + return new Promise((resolve, reject) => { + // Ready The Event For When A File Gets Selected + reader.onload = function (e) { + const data = e.target?.result; + if (!data) { + console.error("Failed to read file: e.target.result is null"); + resolve(undefined); + return; + } + const wb = XLSX.read(data, { type: 'binary' }); + let jsonXls: JSON | undefined; + + //TODO 시트가 2개이상일 경우 마지막 시트의 데이터만 표출됨 + // Loop Over Each Sheet + wb.SheetNames.forEach(function (sheetName) { + // Obtain The Current Row As CSV + const sheet = wb.Sheets[sheetName]; + + if (sheet) { + jsonXls = XLSX.utils.sheet_to_json(sheet, { range: headerNum }) as unknown as JSON; + } else { + console.error(`Sheet "${sheetName}" is undefined.`); + } + }); + resolve(jsonXls); + }; + + reader.onerror = function () { + console.error("Error reading file"); + reject(undefined); + }; + + // Tell JS To Start Reading The File.. You could delay this if desired + reader.readAsBinaryString(oFile); + }); +} + + + +export const filePickedBlob=(oFile: ArrayBuffer, data:[][],fileNm:string, headerNum:number=0):Promise =>{ + // Create A File Reader HTML5 + return new Promise(() => { + // Ready The Event For When A File Gets Selected + const wb = XLSX.read(oFile, { type: 'binary' }); + // Loop Over Each Sheet + wb.SheetNames.forEach(function (sheetName, index) { + // Obtain The Current Row As CSV + if(index==0){ + const sheet = wb.Sheets[sheetName]; + if (sheet) { + //오리진 확인 test 필요 + // const sheet = XLSX.utils.json_to_sheet(data); + XLSX.utils.sheet_add_aoa(sheet, data, { origin: 'A'+headerNum }); + // XLSX.utils.book_append_sheet(wb, sheet, 'test'); + XLSX.writeFile(wb, `${fileNm}.xlsx`); + } else { + console.error(`Sheet "${sheetName}" is undefined.`); + } + } + }); + }); + } \ No newline at end of file diff --git a/packages/common/business_components/src/ui/file-upload/index.tsx b/packages/common/business_components/src/ui/file-upload/index.tsx index 3d581ed05..4c6dcec00 100644 --- a/packages/common/business_components/src/ui/file-upload/index.tsx +++ b/packages/common/business_components/src/ui/file-upload/index.tsx @@ -6,14 +6,22 @@ const dropZoneConfig = { maxSize: 1024 * 1024 * 4, multiple: true, }; + + + interface FileUploadProps { + files: File[]; // 파일 배열 + setFiles: (files: File[] | null) => void; // 파일 배열 세터 + fileTypes?: string; // 옵션 (파일 확장자 문자열) + } + export const FileUpload = -(({ ...props }) => { +({ files, setFiles, fileTypes }: FileUploadProps) => { return (

- Click to upload -   or drag and drop + 클릭 후 업로드 +   혹은 파일을 드래그 해주세요

- {!props?.fileTypes?"SVG, PNG, JPG, GIF":props?.fileTypes} + {!fileTypes?"SVG, PNG, JPG, GIF":fileTypes}

- {props.files && - props.files.length > 0 && - props.files.map((file:File, i:number) => ( + {files&& + files.length > 0 && + files.map((file:File, i:number) => ( {file.name} @@ -43,4 +51,4 @@ export const FileUpload = ) -}) \ No newline at end of file +} \ No newline at end of file diff --git a/packages/common/business_components/src/ui/index.ts b/packages/common/business_components/src/ui/index.ts index e2273e8e5..e591be5b5 100644 --- a/packages/common/business_components/src/ui/index.ts +++ b/packages/common/business_components/src/ui/index.ts @@ -1,20 +1,21 @@ export * from "./chart"; export * from "./date_picker"; +export * from "./dialog_ui"; +export * from "./excel_download"; export * from "./file-upload"; export * from "./form"; export * from "./form_ui"; +export * from "./list_div"; +export * from "./main_content_div"; export * from "./scroll"; +export * from "./search_base_form"; export * from "./search_form"; +export * from "./search_form_left"; +export * from "./search_form_right"; export * from "./search_input"; export * from "./search_input_select"; export * from "./select"; -export * from "./table"; -export * from "./main_content_div"; -export * from "./title"; -export * from "./search_form_left"; -export * from "./search_form_right"; -export * from "./table_div"; -export * from "./list_div"; export * from "./tab"; -export * from "./dialog_ui"; -export * from "./excel_download"; \ No newline at end of file +export * from "./table"; +export * from "./table_div"; +export * from "./title"; diff --git a/packages/common/business_components/src/ui/map/Gis.js b/packages/common/business_components/src/ui/map/Gis.js index ce416bfd2..f45ffce3f 100644 --- a/packages/common/business_components/src/ui/map/Gis.js +++ b/packages/common/business_components/src/ui/map/Gis.js @@ -27,7 +27,7 @@ function Gis({ treeData = [], isSearch = false }) { const [featureData,setFeatureData] = useState(null) // 선택된 피쳐 데이터 const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태 const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []); - //const [treeData, setTreeData] = useState([]); + const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref const checkedLayerIds = useRef([]); let gisMap = null; const closePopup = () => { @@ -45,6 +45,7 @@ function Gis({ treeData = [], isSearch = false }) { useEffect(() => { if (!mapRef.current) return; + if (mapInstanceRef.current) return; // 이미 생성된 맵이 있다면 중단 // EPSG:5179 등록 proj4.defs([ @@ -55,7 +56,7 @@ function Gis({ treeData = [], isSearch = false }) { ]); register(proj4); - //맵 생성성 + //맵 생성 const map = new OlMap({ layers: [gisCore.baseLayers['base'], gisCore.baseLayers['satellite']], renderer: 'webgl', @@ -99,10 +100,10 @@ function Gis({ treeData = [], isSearch = false }) { gisCore.scaleControl(); gisCore.changeBaseLayer('base'); setMapObject({ map: map }); + mapInstanceRef.current = map; // 생성 완료 플래그 설정 }, []); useEffect(() => { - //if (!locgovCd) return null; if (!treeData || treeData.length === 0) return; gisCore.deleteAllOverlay(); @@ -141,7 +142,6 @@ function Gis({ treeData = [], isSearch = false }) { } } - return ( <> @@ -156,9 +156,9 @@ function Gis({ treeData = [], isSearch = false }) { /> )}
- + {/* - + */}
diff --git a/packages/common/business_components/src/ui/map/GisContent.js b/packages/common/business_components/src/ui/map/GisContent.js index 686a3ae70..93edcd12c 100644 --- a/packages/common/business_components/src/ui/map/GisContent.js +++ b/packages/common/business_components/src/ui/map/GisContent.js @@ -8,7 +8,7 @@ import './lib/css/gis.edit.css'; import MapToggleBtn from './MapToggleBtn'; import GisEdit from './GisEdit'; -export default function GisContent({ edit = false,onFeatureSelect }) { +export default function GisContent({ edit = false, onFeatureSelect }) { const { gisMap, gisCore } = useGisContext(); const [visibleComponents, setVisibleComponents] = useState({}); const addressResultLayer = useRef(null); @@ -51,11 +51,10 @@ export default function GisContent({ edit = false,onFeatureSelect }) { return ( <> {/**/} - {/* */} - {/* { */} - {/* */} + + + - {/* {edit && } */} ); } diff --git a/packages/common/business_components/src/ui/map/GisSelect.js b/packages/common/business_components/src/ui/map/GisSelect.js index b9e1191b6..4aa8f3049 100644 --- a/packages/common/business_components/src/ui/map/GisSelect.js +++ b/packages/common/business_components/src/ui/map/GisSelect.js @@ -1,15 +1,29 @@ -import React from 'react'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import GisApp from './lib/gis.core'; import GisOlSelect from './lib/gis.select'; -import { useContext } from 'react'; import { GisContext } from './Gis'; + export default React.memo(function GisSelect({ onFeatureSelect }) { + const { gisMap } = useContext(GisContext); + + + // useEffect(() => { + // if (!gisMap) return; // 아직 초기화 안 됐으면 아무것도 안 함 + + // const select = new GisOlSelect(gisMap); + // GisApp.Module.select = select; + + // select.getSelectedInfo(function (features) { + // if (features?.[0]) { + // // onFeatureClick?.(features[0]); // props로 넘긴 함수 실행 + // onFeatureSelect(layer.getProperties()); //피쳐 속성데이터 상위컴포넌트로 전달 + // } + // }); + // }, [gisMap, onFeatureSelect]); + const select = new GisOlSelect(gisMap); GisApp.Module.select = select; - const reset = () => { - select.reset(); - }; select.getSelectedInfo(function (features) { //clickFeature(features[0]); @@ -18,6 +32,7 @@ export default React.memo(function GisSelect({ onFeatureSelect }) { onFeatureSelect(layer); //피쳐 속성데이터 상위컴포넌트로 전달 } }); + return ( <> {/*
diff --git a/packages/common/business_components/src/ui/map/GisTree.js b/packages/common/business_components/src/ui/map/GisTree.js index 01a3fabc3..d01786187 100644 --- a/packages/common/business_components/src/ui/map/GisTree.js +++ b/packages/common/business_components/src/ui/map/GisTree.js @@ -11,7 +11,7 @@ import { import { Tabs, TabsList, TabsTrigger, TabsContent } from "@common/components/ui/tabs"; export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => {} }) { - const { gisCore, treeData, checkedLayerIds } = useGisContext(); + const { gisCore, treeData } = useGisContext(); const [checked, setChecked] = useState([]); const [expanded, setExpanded] = useState([]); const [nodes, setNodes] = useState([]); @@ -43,7 +43,6 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes const node = nodes[i]; if (node.data.initEprssYn === 'Y') { values.push(node.data.lyrSn); - checkedLayerIds?.current.push(node.data.lyrSn + ''); } } return values; @@ -94,7 +93,6 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes } const allIds = nodes.map((n) => n.id); const unchecked = allIds.filter((nid) => !newChecked.includes(nid)); - checkedLayerIds.current = newChecked; gisCore.setLayerOnMap(newChecked, true); gisCore.setLayerOnMap(unchecked, false); return newChecked; diff --git a/packages/common/business_components/src/ui/map/lib/gis.select.js b/packages/common/business_components/src/ui/map/lib/gis.select.js index 973ae6f95..7f52ab2a3 100644 --- a/packages/common/business_components/src/ui/map/lib/gis.select.js +++ b/packages/common/business_components/src/ui/map/lib/gis.select.js @@ -345,6 +345,7 @@ GisOlSelect.prototype = { this.selectInteraction.on('select', function (e) { if (!e.auto) { let features = me.selectInteraction.getFeatures(); + if(features.getArray().length == 0) return; let layers = []; for (let i in features.array_) { let feature = features.array_[0]; diff --git a/packages/common/business_components/src/ui/search_base_form/index.tsx b/packages/common/business_components/src/ui/search_base_form/index.tsx new file mode 100644 index 000000000..9dc1bf735 --- /dev/null +++ b/packages/common/business_components/src/ui/search_base_form/index.tsx @@ -0,0 +1,142 @@ +import { Button } from "@common/components/ui"; +import { useCallback, useEffect, useState } from "react"; +import { SearchFormLeft, type EnvPartSelectDataType } from "../search_form_left"; +import { SearchFormRight } from "../search_form_right"; +import type { SelectDataType } from "../search_input_select"; +type SearchBaseType = { + leftChildren:React.ReactNode; + rightChildren:React.ReactNode; + selectSidoData?: SelectDataType[] | []; + selectSigunData?: SelectDataType[] | []; + selectRegionData?: SelectDataType[] | []; + selectUnitWaterShedData?: SelectDataType[] | []; + selectMiddleRegionData?: SelectDataType[] | []; + selectEnvPartData?: EnvPartSelectDataType[] | []; + queryClick:(returnType:ReturnType) => void; + excelName?:string; + excelClick:(returnType:ReturnType) => void; + initClick:() => void; + btnClass:string; + ref:React.RefObject; +} +export type ReturnType = { + partValue: string; + sidoValue: string; + sigunValue: string; + regionValue: string; + unitWaterShedValue: string; + middleRegionValue: string; + envPartValue: string; +} + + +function SearchBaseForm ({ + leftChildren, + rightChildren, + selectSidoData, + selectSigunData, + selectRegionData, + selectUnitWaterShedData, + selectMiddleRegionData, + selectEnvPartData, + btnClass, + excelName, + queryClick, + excelClick, + initClick, + ref + }: SearchBaseType) { + const [partValue, setPartValue] = useState("A"); + const [sidoValue, setSidoValue] = useState("0"); + const [sigunValue, setSigunValue] = useState("0"); + const [regionValue, setRegionValue] = useState("0"); + const [untitWaterShedValue, setUntitWaterShedValue] = useState("0"); + const [middleRegionValue, setMiddleRegionValue] = useState("0"); + const [envPartValue, setEnvPartValue] = useState("0"); + const [resetTrigger, setResetTrigger] = useState(false); + const queryVal = () => { + console.log("조회버튼 클릭"); + queryClick({ + partValue: partValue, + sidoValue: sidoValue, + sigunValue: sigunValue, + regionValue: regionValue, + unitWaterShedValue: untitWaterShedValue, + middleRegionValue: middleRegionValue, + envPartValue: envPartValue, + }); + } + const initVal = useCallback(() => { + setResetTrigger(true); + initClick(); + }, [setResetTrigger, initClick]); + useEffect(() => { + if (resetTrigger) { + setResetTrigger(false); + } + }, [resetTrigger]); + const excelDownload = () => { + excelClick({ + partValue: partValue, + sidoValue: sidoValue, + sigunValue: sigunValue, + regionValue: regionValue, + unitWaterShedValue: untitWaterShedValue, + middleRegionValue: middleRegionValue, + envPartValue: envPartValue, + }) + }; + useEffect(() => { + if (ref && ref.current) { + const customRef = ref.current; + customRef.partValue = partValue; + customRef.sidoValue = sidoValue; + customRef.sigunValue = sigunValue; + customRef.regionValue = regionValue; + customRef.unitWaterShedValue = untitWaterShedValue; + customRef.middleRegionValue = middleRegionValue; + customRef.envPartValue = envPartValue; + } + }, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]); + return ( +
+ + {leftChildren} + + + +
+ + +
+ {rightChildren} +
+
+ ) + } + + export { SearchBaseForm }; + \ No newline at end of file diff --git a/packages/common/business_components/src/ui/search_form_left/index.tsx b/packages/common/business_components/src/ui/search_form_left/index.tsx index 248e8f625..ec9e814f5 100644 --- a/packages/common/business_components/src/ui/search_form_left/index.tsx +++ b/packages/common/business_components/src/ui/search_form_left/index.tsx @@ -41,38 +41,41 @@ type SearchFormLeftProps = { resetTrigger?: boolean; } -type EnvPartSelectDataType = SelectDataType & { +export type EnvPartSelectDataType = SelectDataType & { adstDliStrCd: string, } const selectPartData:SelectDataType[] = [ - { text: "행정별", val: '0' }, - { text: "유역별", val: '01' }, - { text: "환경청별", val: '02' }, + { text: "행정별", val: 'A' }, + { text: "유역별", val: 'R' }, + { text: "환경청별", val: 'G' }, ]; function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) { - const [partValue, setPartValue] = useState('0'); - const [showPart, setShowPart] = useState(''); + /******************선택된 데이터******************/ + const [partValue, setPartValue] = useState('A'); + //행정별 시도 시군구 const [sidoValue, setSidoValue] = useState('0'); const [sigunValue, setSigunValue] = useState('0'); + //유역별 const [regionValue, setRegionValue] = useState('0'); const [unitWaterShedValue, setUnitWaterShedValue] = useState('0'); const [middleRegionValue, setMiddleRegionValue] = useState('0'); + //환경청별 시도 시군구 const [envPartValue, setEnvPartValue] = useState('0'); - const [envPartSidoValue, setEnvPartSidoValue] = useState('0'); - const [envPartSigunValue, setEnvPartSigunValue] = useState('0'); + // 환경청 + const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); + /****************** 셀렉트 리스트 데이터. ******************/ + // 조회된 조건에 맞는 시도 시군 유역 const [filteredSigunData, setFilteredSigunData] = useState([{ text: "전체", val: '0' }]); const [filteredUnitWaterShedData, setFilteredUnitWaterShedData] = useState([{ text: "전체", val: '0' }]); const [filteredMiddleRegionData, setFilteredMiddleRegionData] = useState([{ text: "전체", val: '0' }]); - + // 조회된 조건에 맞는 환경청 시도 시군 const [filteredEnvPartData, setFilteredEnvPartData] = useState([{ text: "전체", val: '0', adstDliStrCd:'' }]); const [filteredEnvPartSidoData, setFilteredEnvPartSidoData] = useState([{ text: "전체", val: '0', adstDliStrCd:'' }]); const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState([{ text: "전체", val: '0' }]); - const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState([]); - // 시군구 세팅 const initializeSigunData = (sidoCd: string) => { if (sidoCd === '0') { @@ -154,12 +157,10 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel setRtnPartValue(val); } // 구분 - if(val == '01') { - setShowPart('01'); + if(val == 'R') { initializeMiddleRegionData('0'); initializeUnitWaterShedData('0'); - } else if(val == '02') { - setShowPart('02'); + } else if(val == 'G') { setFilteredEnvPartData([ { text: "전체", val: '0', adstDliStrCd:'' }, ...selectEnvPartData.filter((item: EnvPartSelectDataType) => item.val !== null && item.adstDliStrCd !== '') @@ -167,7 +168,6 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel setEnvPartValue('0'); initializeCityDataByEnvPartSido('0'); } else { - setShowPart('0'); initializeSigunData('0'); onSelectValue(selectSigunData[0]?.val || '0', 'sigun'); } @@ -225,7 +225,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel onSelectValue(filteredEnvPartSidoData[0]?.val || '0', 'envPartSido'); break; case 'envPartSido' : - setEnvPartSidoValue(val); + setSidoValue(val); if (setRtnEnvPartSidoValue) { setRtnEnvPartSidoValue(val); } @@ -233,7 +233,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel onSelectValue(filteredEnvPartSigunData[0]?.val || '0', 'envPartSigun'); break; case 'envPartSigun' : - setEnvPartSigunValue(val); + setSigunValue(val); if (setRtnEnvPartSigunValue) { setRtnEnvPartSigunValue(val); } @@ -243,16 +243,13 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel // 리셋 함수 const resetAllValues = () => { - setPartValue('0'); - setShowPart(''); + setPartValue('A'); setSidoValue('0'); setSigunValue('0'); setRegionValue('0'); setUnitWaterShedValue('0'); setMiddleRegionValue('0'); setEnvPartValue('0'); - setEnvPartSidoValue('0'); - setEnvPartSigunValue('0'); setFilteredSigunData([{ text: "전체", val: '0' }]); setFilteredUnitWaterShedData([{ text: "전체", val: '0' }]); @@ -261,7 +258,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd: '' }]); setFilteredEnvPartSigunData([{ text: "전체", val: '0' }]); - if (setRtnPartValue) setRtnPartValue('0'); + if (setRtnPartValue) setRtnPartValue('A'); if (setRtnSidoValue) setRtnSidoValue('0'); if (setRtnSigunValue) setRtnSigunValue('0'); if (setRtnRegionValue) setRtnRegionValue('0'); @@ -287,17 +284,17 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel isShowPart ? (
onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/> - {showPart === '01' ? ( + {partValue === 'R' ? ( <> onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/> onSelectValue(val.toString(), 'unitWaterShed')} value={unitWaterShedValue} selectClass={undefined}/> onSelectValue(val.toString(), 'middleRegion')} value={middleRegionValue} selectClass={undefined}/> - ) : showPart === '02' ? ( + ) : partValue === 'G' ? ( <> onSelectValue(val.toString(), 'envPart')} value={envPartValue} selectClass={undefined}/> - onSelectValue(val.toString(), 'envPartSido')} value={envPartSidoValue} selectClass={undefined}/> - onSelectValue(val.toString(), 'envPartSigun')} value={envPartSigunValue} selectClass={undefined}/> + onSelectValue(val.toString(), 'envPartSido')} value={sidoValue} selectClass={undefined}/> + onSelectValue(val.toString(), 'envPartSigun')} value={sigunValue} selectClass={undefined}/> ) : ( <> @@ -315,4 +312,4 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel ) } -export { SearchFormLeft }; \ No newline at end of file +export { SearchFormLeft }; diff --git a/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx new file mode 100644 index 000000000..dc859a55d --- /dev/null +++ b/packages/common/business_components/src/ui/table/HeadMakeColSpan.tsx @@ -0,0 +1,64 @@ +import { TableHead, TableRow } from "@common/components/ui"; + +interface TableUpperProps { + id: string; + title: string; + upName?: string; + upSequnce?: number; + row? : number; + rowSpan?: number; + colSpan?: number; +} + +const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => { + const maxRow = upHeadList.reduce((max, head) => Math.max(max, head.row || 0), 0); + + if(maxRow == 0){ + return ( + <> + + {upHeadList.map((head: TableUpperProps) => { + return ( + + {head.title} + + ); + })} + + + ); + }else{ + return ( + <> + {Array.from({ length: maxRow + 1 }, (_, rowIndex) => ( + + {upHeadList.map((head: TableUpperProps) => { + if (head.row === rowIndex) { + return ( + + {head.title} + + ); + } + })} + + ))} + + ); + } + +} + +export { HeadMakeColSpan }; diff --git a/packages/common/business_components/src/ui/table/index.tsx b/packages/common/business_components/src/ui/table/index.tsx index 994b9a827..45748fb37 100644 --- a/packages/common/business_components/src/ui/table/index.tsx +++ b/packages/common/business_components/src/ui/table/index.tsx @@ -1,18 +1,19 @@ -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; +import { Table, TableBody, TableCell, TableHeader, TableRow } from "@common/components"; import { useEffect, useState } from "react"; +import { HeadMakeColSpan } from "./HeadMakeColSpan"; import UiPageNations from "./uiPageNations"; type UiTableProps = { headName: React.ReactNode | undefined; - tableData: { [key: string]: string | number | boolean | object}[]; + tableData: { [key: string]:any}[]; headlist: UiHeadProps[]; pageSize: number; total: number; - children: React.ReactNode | undefined; cellClick : ((index:number|undefined, headId:string)=>void )| undefined; onPageChange: (page: number) => void; className?: string; resetPageNo?: boolean; + children?: React.ReactNode | undefined; } type UiHeadProps = { @@ -21,7 +22,7 @@ type UiHeadProps = { align?: string; } -export function UiTable({ tableData, headlist, pageSize, total, children, headName, cellClick, onPageChange, className = '', resetPageNo = false }: UiTableProps) { +export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false }: UiTableProps) { const [cur, setCur] = useState(1); const [idx, setIdx] = useState(-1); @@ -43,7 +44,7 @@ export function UiTable({ tableData, headlist, pageSize, total, children, headNa setIdx(index); } }; - + return ( <> {headName ?
@@ -52,30 +53,28 @@ export function UiTable({ tableData, headlist, pageSize, total, children, headNa
- {children ? <>{children} : - {headlist?.map((head) => ( - {head.title} - ))} - } - - - {tableData?.map((el: { [key: string]: string | number | boolean | object}, index) => ( - - {headlist?.map((head) => ( - handleCellClick(index, head.id)} - key={head.id + index} - > - {typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')} - - ))} - - ))} - + {children ? children : } + + + {tableData.length > 0 ? tableData?.map((el: { [key: string]: any}, index) => ( + + {headlist?.map((head) => ( + head.id && head.id.length > 1 && ( + handleCellClick(index, head.id)} + key={`cell-${head.id}-${index}`} + > + {typeof el[head.id] === 'number' ? String(el[head.id]).replace(/\B(?=(\d{3})+(?!\d))/g, ",") : String(el[head.id] ?? '')} + + ) + ))} + + )) : <>} +
{total <= 0 ? <> : }