From a7216dfcc631f08b9fc6ad991e88a4f6b6a35ba9 Mon Sep 17 00:00:00 2001 From: swcho Date: Thu, 5 Jun 2025 17:13:25 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20-=20=EC=B9=A8=EC=9E=85=EC=88=98=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=20=ED=99=94=EB=A9=B4=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84,=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C,=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=A4=91=20-=20=EC=A4=91=EC=A0=90=EC=9A=B0?= =?UTF-8?q?=EC=88=98=ED=86=A0=EC=8B=A4=20=EC=83=81=EC=84=B8=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84=20-=20verti?= =?UTF-8?q?calTable.tsx=EC=97=90=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=20contentLayout.tsx=EC=97=90=20className=20prop?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20-=20fileDownload.ts=EC=97=90=20BASE=5FU?= =?UTF-8?q?RL=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/components/lib/api/index.ts | 2 +- .../app/components/lib/utils/fileDownload.ts | 2 + apps/sn/app/components/ui/contentLayout.tsx | 3 +- apps/sn/app/components/ui/verticalTable.tsx | 79 ++++++- apps/sn/app/ii/BaseLayout.tsx | 189 ++++++++++------ apps/sn/app/ii/irsetting/page.tsx | 2 +- apps/sn/app/ii/result/page.tsx | 210 +++++++++++++----- apps/sn/app/map/components/common/index.ts | 10 +- apps/sn/app/map/components/common/table.ts | 59 ++++- .../app/map/components/popupComponent/Ssc.tsx | 173 ++++++++++----- apps/sn/app/map/featureClickEventPopup.tsx | 91 ++++++-- 11 files changed, 606 insertions(+), 214 deletions(-) diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts index 84f38002f..d7db93f77 100644 --- a/apps/sn/app/components/lib/api/index.ts +++ b/apps/sn/app/components/lib/api/index.ts @@ -43,7 +43,7 @@ export const putData = async (url: string, data: object): Promise => { return resData; }; -export const getData = async (url: string): Promise => { +export const getData = async (url: string, data?: object): Promise => { const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, { method: "GET", }); diff --git a/apps/sn/app/components/lib/utils/fileDownload.ts b/apps/sn/app/components/lib/utils/fileDownload.ts index 681203196..ffe47eced 100644 --- a/apps/sn/app/components/lib/utils/fileDownload.ts +++ b/apps/sn/app/components/lib/utils/fileDownload.ts @@ -1,3 +1,5 @@ +const BASE_URL = process.env.NEXT_PUBLIC_SERVER_API_URL || ''; + export const handleDownload = async (api:string, fileNm:string) => { try { const response = await fetch(api, { diff --git a/apps/sn/app/components/ui/contentLayout.tsx b/apps/sn/app/components/ui/contentLayout.tsx index 75d61235f..de4406efd 100644 --- a/apps/sn/app/components/ui/contentLayout.tsx +++ b/apps/sn/app/components/ui/contentLayout.tsx @@ -5,8 +5,9 @@ type LayoutNode = { children : ReactNode title : string isLoading?: boolean + className?: string } -const ContentLayout = ({children, title, isLoading} : LayoutNode) => { +const ContentLayout = ({children, title, isLoading, className} : LayoutNode) => { return ( diff --git a/apps/sn/app/components/ui/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx index ae3068a67..e5e506796 100644 --- a/apps/sn/app/components/ui/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; type UiTableProps = { headName: React.ReactNode | undefined; + tableName1?: React.ReactNode | undefined; tableData: { [key: string]: string | number | boolean | object }[]; headlist: UiHeadProps[]; pageSize: number; @@ -14,6 +15,18 @@ type UiTableProps = { className?: string; resetPageNo?: boolean; isEditable?: boolean; + tableName2?: React.ReactNode | undefined; + tableData2?: { [key: string]: string | number | boolean | object }[]; + headlist2?: UiHeadProps[]; + pageSize2?: number; + total2?: number; + children2?: Record<string, React.ReactNode> | undefined; + cellClick2?: ((index: number | undefined, headId: string) => void) | undefined; + onPageChange2?: (page: number) => void; + className2?: string; + resetPageNo2?: boolean; + isEditable2?: boolean; + showSecondTable?: boolean; }; type UiHeadProps = { @@ -25,17 +38,30 @@ type UiHeadProps = { }; export function VerticalTable({ + headName, + tableName1, tableData, headlist, pageSize, total, children, - headName, cellClick, onPageChange, className = "", resetPageNo = false, isEditable = false, + tableName2, + tableData2, + headlist2, + pageSize2, + total2, + children2, + cellClick2, + onPageChange2, + className2 = "", + resetPageNo2 = false, + isEditable2 = false, + showSecondTable = false, }: UiTableProps) { const [cur, setCur] = useState<number>(1); const [formData, setFormData] = useState<{ [key: string]: string | number | boolean }>({}); @@ -72,6 +98,9 @@ export function VerticalTable({ </div> ) : null} <div className={`card !m-[1.2rem] !p-0r ${className}`}> + {tableName1 && ( // tableName prop이 있으면 표시 + <div className="text-xl font-semibold my-2 mx-3">{tableName1}</div> + )} <Table variant="secondary"> <TableBody className="text-center"> {headlist?.map((head) => ( @@ -115,6 +144,54 @@ export function VerticalTable({ ))} </TableBody> </Table> + {tableName2 && ( // tableName prop이 있으면 표시 + <div className="text-xl font-semibold mt-8 my-2 mx-3">{tableName2}</div> + )} + {showSecondTable && ( + <Table variant="secondary"> + <TableBody className="text-center"> + {headlist2?.map((head) => ( + <TableRow key={head.id}> + {/* 컬럼 제목 */} + <TableCell className="font-bold bg-blue-100 text-xs w-fit whitespace-nowrap"> + {head.title} + </TableCell> + {/* 데이터 값 또는 입력 필드 */} + <TableCell className="text-xs"> + {children && children[head.id] ? children[head.id] : isEditable2 ? ( + head.type === "select" ? ( + <select + id={head.id} + value={formData[head.id] ?? "-"} + onChange={(e) => handleInputChange(head.id, e.target.value)} + className="w-full border border-gray-300 rounded-sm px-2 py-1 text-xl border-radius-0" + > + {head.options?.map((option) => ( + <option key={option} value={option}> + {option} + </option> + ))} + </select> + ) : head.type === "input" ? ( + <input + id={head.id} + type="text" + value={formData[head.id] ?? ""} + onChange={(e) => handleInputChange(head.id, e.target.value)} + className="w-full text-xl border-radius-0 border border-gray-300 rounded-sm px-2 py-1" + /> + ) : ( + <span>{formData[head.id] ?? "-"}</span> + ) + ) : ( + <span>{formData[head.id] ?? "-"}</span> + )} + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + )} {total > 0 && ( <UiPageNations total={total} diff --git a/apps/sn/app/ii/BaseLayout.tsx b/apps/sn/app/ii/BaseLayout.tsx index 3974b2407..308f5e350 100644 --- a/apps/sn/app/ii/BaseLayout.tsx +++ b/apps/sn/app/ii/BaseLayout.tsx @@ -1,13 +1,13 @@ 'use client' -import { getData } from '@/components/lib/api' +import { getData, deleteData } from '@/components/lib/api' import ContentLayout from '@/components/ui/contentLayout' import { GovSidoSigunguSearchForm } from '@/components/ui/govSidoSigunguSearchForm' -import { Form, SearchFormRight, SelectBox } from '@common/business_components/ui' -import { SidoSigunguSelectBox } from '../components/ui/SidoSigunguSelectBox' -import { SelectBox2 } from '@common/business_components/ui/select/index-2' +import { DatePicker, Form, SearchFormRight, SelectBox } from '@common/business_components/ui' import ListDiv from '@common/business_components/ui/list_div' +import { SelectBox2 } from '@common/business_components/ui/select/index-2' import { Button, Label } from '@common/components/ui' import { useEffect, useState, type ReactNode } from 'react' +import { handleDownload } from '@/components/lib/utils/fileDownload' type TitleName = { mainTitle: string @@ -17,7 +17,8 @@ type TitleName = { type SelectBox = { labelNm?: Label searchChk?: boolean - title?: string // SearchDiv의 제목 (선택 사항) + title?: string; + onSearchClick?: () => Promise<void>; } type Label = { first: string @@ -29,7 +30,7 @@ type Label = { interface ApiSelectOption { text: string; val: string; -} +} type BaseLayout = { children?: ReactNode[] titleName: TitleName @@ -38,6 +39,7 @@ type BaseLayout = { graphBtn?: boolean firstDivBtn?: boolean searchBox?: SelectBox + showDatePicker?: boolean; enoCd?: string | number setEnoCd?: (enoCd: string) => void ctpvCd?: string | number @@ -48,6 +50,8 @@ type BaseLayout = { onSwageTreatmentPlantChange?: (value: string | number) => void; selectedSwageProcessingZone?: string | number; onSwageProcessingZoneChange?: (value: string | number) => void; + aplcnYm?: Date + setAplcnYm?: (aplcnYm: Date | undefined) => void; } // TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ? /** @@ -59,23 +63,26 @@ type BaseLayout = { * @param graphBtn : 하단에 그래프 버튼 유무 처리 * @returns */ -const BaseLayout = ({ - children, - titleName, - form, - onSubmitEvent, - graphBtn = false, +const BaseLayout = ({ + children, + titleName, + form, + onSubmitEvent, + graphBtn = false, searchBox, + showDatePicker = false, enoCd, setEnoCd, ctpvCd, setCtpvCd, - sigungu, + sigungu, setSigungu, selectedSwageTreatmentPlant, onSwageTreatmentPlantChange, selectedSwageProcessingZone, onSwageProcessingZoneChange, + aplcnYm, + setAplcnYm }: BaseLayout) => { const defaultOption: ApiSelectOption = { val: '0', text: '선택' }; @@ -83,6 +90,9 @@ const BaseLayout = ({ const [processingZoneOptions, setProcessingZoneOptions] = useState<ApiSelectOption[]>([]); const [treatmentFacilityOptions, setTreatmentFacilityOptions] = useState<ApiSelectOption[]>([]); const [isLoadingAreaData, setIsLoadingAreaData] = useState<boolean>(false); + const [atflId, setAtflId] = useState('') + const [fileNm, setFileNm] = useState('') + const [dcrpPplMtncBizSn, setDcrpPplMtncBizSn] = useState('') useEffect(() => { const fetchData = async () => { @@ -159,66 +169,101 @@ const BaseLayout = ({ fetchData(); }, [sigungu]); // sigungu 상태가 변경될 때마다 실행 - return ( - <ContentLayout title={titleName.mainTitle}> - <ListDiv> - {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} - {searchBox?.title && ( - <div className="card-header"> - <h3 className="text-2xl font-medium">{searchBox.title}</h3> - <div> - {graphBtn ? <Button className="!mr-[1.2rem]">다운로드</Button> : null} - {/* 그래프 있을 시 */} - {graphBtn ? <Button className="!mr-[1.2rem]">삭제</Button> : null} - </div> - </div> - )} - <div className="flex items-center w-full"> - <GovSidoSigunguSearchForm - isShowPart={true} - setRtnEnvPartValue={(val) => setEnoCd?.(val)} - setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} - setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 - > - <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> - <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> - </GovSidoSigunguSearchForm> - {searchBox?.searchChk ? <SearchFormRight><Button>조회</Button></SearchFormRight> : null} - - </div> - </ListDiv> - <ListDiv> - <div className="card-header"> - <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> - <div> - {graphBtn ? <Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size='sm'>다운로드</Button> : null} - {/* 그래프 있을 시 */} - {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>삭제</Button> : null} - </div> - </div> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmitEvent)}> - <div className="card !m-[1.2rem]"> - {/* 폼 정보 입력 칸 */} - {children?.[0]} - </div> - </form> - </Form> - </ListDiv> - { - children?.[1] != null ? <ListDiv> - <div className="card-header flex items-center justify-between"> - <h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3> - <div> - {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>다운로드</Button> : null} - {/* 그래프 있을 시 */} - {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" size='sm'>삭제</Button> : null} - </div> - </div> - <div className="card !m-[1.2rem]">{children?.[1]}</div> - </ListDiv> : null + // 파일 다운로드 + const isChckFile = () => { + if (fileNm === '' || fileNm === undefined) { + alert('파일을 선택해주세요'); + return false + } + handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm) + return true + } + // 파일 삭제 + const isDeleteFile = async () => { + if (dcrpPplMtncBizSn === '' || dcrpPplMtncBizSn === undefined) { + alert('삭제할 파일을 선택해주세요'); + return false + } + const data = await deleteData(`/attach-service/api/v1/atfl/attachments/${atflId}`) + if (data) { + const delData = await deleteData(`v1/dcrpPpl/${dcrpPplMtncBizSn}`); + if (delData.status == 200) { + alert('삭제되었습니다.') + searchBox?.onSearchClick?.(); + } else { + alert('삭제에 실패했습니다.') } - </ContentLayout > + } + } + + return ( + <Form {...form} className="min-h-screen flex flex-col"> + <ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden"> + <ListDiv> + {/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */} + {searchBox?.title && ( + <div className="card-header"> + <h3 className="text-2xl font-medium">{searchBox.title}</h3> + </div> + )} + <div className="flex items-center w-full"> + {showDatePicker && ( // showDatePicker 값에 따라 DatePicker 렌더링 + <> + <Label className="ml-4 w-20 text-xl text-gray-600 whitespace-nowrap">분석기간</Label> + <DatePicker + mode={"single"} + field={{ + value: aplcnYm ?? new Date(), + onChange: setAplcnYm ?? ((date: Date | undefined) => console.log("DatePicker onChange:", date)) + }} + selectClass="ml-5 date-picker-trigger" /> + </> + )} + <GovSidoSigunguSearchForm + isShowPart={true} + setRtnEnvPartValue={(val) => setEnoCd?.(val)} + setRtnEnvPartSidoValue={(val) => setCtpvCd?.(val)} + setRtnEnvPartSigunValue={setSigungu} // 선택된 시군구 값을 받을 콜백 함수 전달 + > + <SelectBox2 label={"하수처리장"} value={selectedSwageTreatmentPlant} selectArray={treatmentFacilityOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageTreatmentPlantChange?.(val)} /> + <SelectBox2 label={"하수처리구역"} value={selectedSwageProcessingZone} selectArray={processingZoneOptions} className="col-span-3" selectClass="rounded-lg bg-white truncate" onSelectValue={(val) => onSwageProcessingZoneChange?.(val)} /> + </GovSidoSigunguSearchForm> + {searchBox?.searchChk ? <SearchFormRight><Button onClick={searchBox.onSearchClick}>조회</Button></SearchFormRight> : null} + </div> + </ListDiv> + <ListDiv> + <div className="card-header"> + <h3 className="text-2xl font-medium">{titleName.firstCardTitle}</h3> + <div> + {graphBtn ? <Button className="px-4 py-2 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" onClick={isChckFile}>파일 다운로드</Button> : null} + {/* 그래프 있을 시 */} + {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]" onClick={isDeleteFile}>삭제</Button> : null} + </div> + </div> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmitEvent)} className="flex-grow flex flex-col"> + <div className="card !m-[1.2rem] flex-grow flex flex-col"> + {/* 폼 정보 입력 칸 */} + {children?.[0]} + </div> + </form> + </Form> + </ListDiv> + { + children?.[1] != null ? <ListDiv> + <div className="card-header flex items-center justify-between"> + <h3 className="text-2xl font-medium">{titleName?.secondCardTitle}</h3> + <div> + {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]">파일 다운로드</Button> : null} + {/* 그래프 있을 시 */} + {graphBtn ? <Button className="px-4 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700 !mr-[1.2rem]">삭제</Button> : null} + </div> + </div> + <div className="card !m-[1.2rem]">{children?.[1]}</div> + </ListDiv> : null + } + </ContentLayout > + </Form> ) } diff --git a/apps/sn/app/ii/irsetting/page.tsx b/apps/sn/app/ii/irsetting/page.tsx index be24229a0..2b9085ccd 100644 --- a/apps/sn/app/ii/irsetting/page.tsx +++ b/apps/sn/app/ii/irsetting/page.tsx @@ -196,7 +196,7 @@ const IrSetting = () => { swageTmplntCd: select4.toString(), // 하수처리장코드 swtrmtZoneCd: select5.toString(), // 하수처리구역코드 aplcnYm: new Date(selectedDate).toISOString().substring(0, 7).replace("-", ""), // 적용연월 - dtrwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) + drtwSwtcRt: parseFloat(input1) || null, // 오수전환율 (숫자로 변환) anlsInfo: input2, // 분석정보 atflCd: file1?.atflCd, // 첨부파일CD (오수관망도) atflId: file1?.atflId, // 첨부파일ID (오수관망도) diff --git a/apps/sn/app/ii/result/page.tsx b/apps/sn/app/ii/result/page.tsx index 6e1d1b067..fe1ca129c 100644 --- a/apps/sn/app/ii/result/page.tsx +++ b/apps/sn/app/ii/result/page.tsx @@ -1,89 +1,181 @@ 'use client' -import React, { useState } from 'react' -import { FormField, SelectBox, UiTable } from '@common/business_components/ui' -import { Button, DatePicker, Input } from '@common/components/ui' -import { z } from 'zod' +import { getData } from '@/components/lib/api' +import { UiTable } from '@common/business_components/ui' import { zodResolver } from '@hookform/resolvers/zod' +import { useState } from 'react' import { useForm } from 'react-hook-form' -import FormSection from '@/components/ui/formSection' +import { z } from 'zod' import BaseLayout from '../BaseLayout' -import { title } from 'process' + + +interface ApiSelectOption { + text: string; + val: string; +} + const formSchema = z.object({ - managenm: z.string(), - region: z.string(), - suyeonga: z.number(), - inputdate: z.coerce.date(), - rtkSource: z.string(), - suyeongaPlace: z.string() + enoCd: z.string().default('0'), // 환경청코드 + ctpvCd: z.string().default('0'), // 시도코드 + sggCd: z.object({ // 시군구 + val: z.string().default('0'), + text: z.string().default('전체'), + }).default({ val: '0', text: '전체' }), + swageTmplntCd: z.string().default('0'), // 하수처리장 + swtrmtZoneCd: z.string().default('0'), // 하수처리구역 + aplcnYm: z.date().default(new Date()), // 분석일자 (DatePicker와 연동) }) + +type ResultFormValues = z.infer<typeof formSchema> + // TODO: 분석구분에 따라서 테이블 컬럼 변경필요 const tableColumn = [ - { id: 'no', title: '번호' }, - { id: 'hasu', title: '하수처리장' }, - { id: 'guyeok', title: '처리구역' }, - { id: 'sangsu', title: '상수 사용량' }, - { id: 'rate', title: '오수 전환율' }, - { id: 'water', title: '순오수량' }, - { id: 'measureWater', title: '측정하수량' }, - { id: 'ii', title: '침입수량' }, - { id: 'date', title: '분석일자' }, - { id: 'usr', title: '분석자' } + // { id: 'no', title: '번호' }, + { id: 'inftwtAnlsSn', title: '분석번호' }, + { id: 'swageTmplntNm', title: '하수처리장' }, + { id: 'swtrmtZoneNm', title: '처리구역' }, + { id: 'aplcnYm', title: '적용연월' }, + { id: 'drtwSwtcRt', title: '오수 전환율' }, + { id: 'anlsInfo', title: '침입수 분석정보' }, + { + id: 'regDt', + title: '분석일자', + }, + { + id: 'rgtrNm', + title: '분석자', + } ] const tableColumn2 = [ { id: 'no', title: '번호' }, + { id: 'analysis no', title: '분석번호' }, { id: 'hasu', title: '하수처리장' }, { id: 'guyeok', title: '처리구역' }, { id: 'sangsu', title: '상수 사용량' }, { id: 'rate', title: '오수 전환율' }, - { id: 'rtx', title: 'RTX계수출처' }, + { id: 'rtx', title: 'RTX 계수 출처' }, { id: 'rdii', title: 'RDII 기여면적' }, - { id: 'water', title: '순오수량' }, - { id: 'measureWater', title: '강우유출량' }, - { id: 'ii', title: 'RDII(m2/d)' }, + { id: 'rdii analysis info', title: 'RDII 분석정보' }, { 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<z.infer<typeof formSchema>>({ + const form = useForm<ResultFormValues>({ resolver: zodResolver(formSchema), defaultValues: { - managenm: "", - region: "", - suyeonga: 0, - inputdate: new Date(), - rtkSource: "", - suyeongaPlace: "" + enoCd: '0', + ctpvCd: '0', + sggCd: { val: '0', text: '전체' }, + swageTmplntCd: '0', + swtrmtZoneCd: '0', + aplcnYm: new Date(), } }) + + + const [tableData, setTableData] = useState<any[]>([]); // API 조회 결과 저장 상태 + const [totalData, setTotalData] = useState(0) //총 데이터 수 + const [rdiiResultData, setRdiiResultData] = useState<any[]>([]); // RDII 결과 저장 상태 (필요시) + const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태 + + const handleSearchButtonClick = async () => { + setIsLoadingResult(true); + const formValues = form.getValues(); // react-hook-form에서 현재 값 가져오기 + + const inftwtAnlsSearchDto = { + "aplcnYm": formValues.aplcnYm.toISOString().substring(0, 7).replace("-", ""), + "enoCd": formValues.enoCd === '0' ? null : formValues.enoCd, + "ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd, + "sggCd": formValues.sggCd.val === '0' ? null : formValues.sggCd.val, + // "swageTmplntCd": formValues.swageTmplntCd === '0' ? null : formValues.swageTmplntCd, + "swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd, + }; + + const queryParams = new URLSearchParams({ + page: '0', + size: '10', + sort: 'inftwtAnlsSn' + }); + Object.entries(inftwtAnlsSearchDto).forEach(([key, value]) => { + if (value !== undefined && value !== null && String(value).trim() !== '') { // undefined, null, 빈 문자열이 아닌 경우에만 추가 + queryParams.append(key, String(value)); + } + }); + const queryString = queryParams.toString(); + + console.log("API: ", `v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); // 디버깅용 로그 + try { + const data = await getData(`v1/inftwtAnls${queryString ? `?${queryString}` : ''}`); + setTableData(data.content || []); + setTotalData(tableData.length); + console.log("response:", data); + console.log("response data:", data.content || []); + } catch (error) { + console.error("Error fetching analysis result data:", error); + alert("분석결과 조회 중 오류가 발생했습니다."); + setTableData([]); + setTotalData(0); + } finally { + setIsLoadingResult(false); + } + }; + return ( - <BaseLayout searchBox={{labelNm: {first: "분석기간",second: "분석구분",third: "하수처리장",fourth: "하수처리구역",},searchChk: true,}} titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle:'RDII 산정결과 현황'}} form={form} onSubmitEvent={() => { }} graphBtn={true}> - {[<div key='table1'> + <BaseLayout + searchBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }} + titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle: 'RDII 산정결과 현황' }} + form={form} // form 인스턴스 전달 + onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit + graphBtn={true} + showDatePicker={true} + enoCd={form.watch("enoCd")} + setEnoCd={(val) => form.setValue("enoCd", val)} + ctpvCd={form.watch("ctpvCd")} + setCtpvCd={(val) => form.setValue("ctpvCd", val)} + sigungu={form.watch("sggCd")} + setSigungu={(val) => form.setValue("sggCd", val)} + selectedSwageTreatmentPlant={form.watch("swageTmplntCd")} + onSwageTreatmentPlantChange={(val) => form.setValue("swageTmplntCd", String(val))} + selectedSwageProcessingZone={form.watch("swtrmtZoneCd")} + onSwageProcessingZoneChange={(val) => form.setValue("swtrmtZoneCd", String(val))} + aplcnYm={form.watch("aplcnYm")} + setAplcnYm={(val) => { + if (val instanceof Date) { + form.setValue("aplcnYm", val) + } else if (val) { + form.setValue("aplcnYm", new Date(val)) + } + }} + // DatePicker는 BaseLayout 내부에서 form.watch("aplcnYm")과 form.setValue("aplcnYm", ...)을 사용하도록 수정 필요 + > + {[<div key='table1' className="h-[55vh] flex flex-col p-4 border rounded-lg bg-white shadow-md"> <div className="mb-[1.2rem] text-xl font-medium">○ 침입수 산정결과 현황</div> - <UiTable - headName='' - headlist={tableColumn} - tableData={tableInfoData} - total={0} - pageSize={10} - children={undefined} - cellClick={(e) => { }} - onPageChange={(e) => { }} - /> + <div className="flex-grow overflow-auto"> + <UiTable + headName='' + headlist={tableColumn} + tableData={tableData} + total={totalData} + pageSize={10} + children={undefined} + cellClick={(e) => { }} + onPageChange={(e) => { }} + /> + </div> </div>, - <div key='table2'> - <UiTable - headName='' - headlist={tableColumn2} - tableData={[]} - total={0} - pageSize={10} - children={undefined} - cellClick={(e) => { }} - onPageChange={(e) => { }} - /> - </div> - ]} + // <div key='table2'> + // <UiTable + // headName='' + // headlist={tableColumn2} + // tableData={rdiiResultData} + // total={rdiiResultData.length} + // pageSize={10} + // children={undefined} + // cellClick={(e) => { }} + // onPageChange={(e) => { }} + // /> + // </div> + ]} </BaseLayout> ) } diff --git a/apps/sn/app/map/components/common/index.ts b/apps/sn/app/map/components/common/index.ts index c3e1a4a37..b72ec26d5 100644 --- a/apps/sn/app/map/components/common/index.ts +++ b/apps/sn/app/map/components/common/index.ts @@ -5,7 +5,11 @@ import Ssc from "../popupComponent/Ssc"; import StormWaterPump from "../popupComponent/stormWaterPump"; import SwtrmtFclt from "../popupComponent/SwtrmtFclt"; -type LayerComponentProps = { featureData: any; chkMainSsc?: boolean }; +type LayerComponentProps = { + featureData: any; + chkMainSsc?: boolean; + additionalData?: any; +}; type LayerContentMap = { [key: string]: { component?: React.FC<LayerComponentProps>; @@ -16,9 +20,7 @@ type LayerContentMap = { // 레이어 선택시 변경되는 팝업 컨텐츠 동적으로 변경하도록 작업 const layerContentMap: LayerContentMap = { // TODO: gis데이터 추가되면 추가 작업 필요 - "하수처리시설": { - component: SwtrmtFclt, - }, + "하수처리시설": { component: SwtrmtFclt, }, "하수처리구역": { }, "우수토실":{component: Ssc,}, "중점우수토실":{component: Ssc, }, // 중점우수토실 여부 설정 diff --git a/apps/sn/app/map/components/common/table.ts b/apps/sn/app/map/components/common/table.ts index 148f5ef0f..6c641f9be 100644 --- a/apps/sn/app/map/components/common/table.ts +++ b/apps/sn/app/map/components/common/table.ts @@ -48,7 +48,7 @@ export const SwtrmtPrcsZoneCol = [ // { id: 'sggCd', title: '시군구코드' }, { id: 'sggNm', title: '시군구명' }, { id: 'swtrmtFcltNm', title: '하수처리시설명' }, -] +]; // 하수맨홀 export const MsSwageMhlCol = [ @@ -70,4 +70,59 @@ export const MsSwageMhlCol = [ // { id: 'mqltCd', title: '재질코드' }, { id: 'altdVl', title: '표고값' }, { id: 'dpthlVl', title: '심도값' }, - { id: 'instMngNo', title:'기관관리번호'}] + { id: 'instMngNo', title:'기관관리번호'} +]; + +// 중점우수토실 측정지점 +export const untrtSwgRainCol1 = [ + { id: 'manage_no', title: '관리번호' }, + { id: 'swr_ppl_eqp_nm', title: '처리구역' }, + { id: 'swr_ppl_trt_nm', title: '처리분구' }, + { id: 'addr', title: '인근주소' }, + { id: 'chmbr_xcnts', title: '토실 위도' }, + { id: 'chmbr_ydnts', title: '토실 경도' } +]; + +// 중점우수토실 측정지점 시설현황 +export const untrtSwgRainCol2 = [ + { id: 'flwmtr_mesure_tp_etc', title: '구분' }, + { id: 'discharge_pipe_spec', title: '방류관로 규격' }, + { id: 'swr_ppl_eqp_ar', title: '배수구역 면적(m²)' }, + { id: 'swr_trt_popl', title: '하수처리 인구(인)' }, + { id: 'screen_instl_at', title: '스크린설치 여부' }, + { id: 'weir_hg', title: '위어 높이(m)' }, + { id: 'weir_lt', title: '위어 길이(m)' } +]; + +// 중점우수토실 강우정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol3 = [ + { id: 'rain_begin_dt', title: '강우시작일시' }, + { id: 'rain_end_dt', title: '강우종료일시' }, + { id: 'precdng_dry', title: '선행건기(h)' }, + { id: 'rain_info', title: '강우정보' }, + { id: 'rain_fall', title: '누적강우량(mm)' }, + { id: 'cntnc_time', title: '지속시간(h)' }, + { id: 'rain_in', title: '강우강도(mm/h)' } +]; + +// 중점우수토실 측정정보 (id값 틀릴 수 있음) +export const untrtSwgRainCol4 = [ + { id: 'mesure_begin_dt', title: '측정시작일시' }, + { id: 'mesure_end_dt', title: '측정종료일시' }, + { id: 'mesure_intrvl', title: '측정간격(분)' }, + { id: 'mesure_mhrls_instl_lc', title: '측정기기 설치위치' }, + { id: 'flwmtr_combined', title: '유량계 종류 및 모델명' }, // 합쳐진 컬럼 + { id: 'qltwtr_begin_dt', title: '채수시작일시' }, + { id: 'qltwtr_end_dt', title: '채수종료일시' }, + { id: 'qltwtr_co', title: '채수횟수' } +]; + +// 중점우수토실 수량/수질측정 정보 +export const untrtSwgRainCol5 = [ + { id: 'odr', title: '회차' }, + { id: 'mesureDt', title: '측정일자' }, + { id: 'mesureQy', title: '수량(m)' }, + { id: 'mesureBod', title: 'BOD(mg/L)' }, + { id: 'mesureToc', title: 'TOC(mg/L)' }, + { id: 'mesureLoad', title: '오염부하량(kg)' } +]; \ 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 06b975739..aa20261ab 100644 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -1,18 +1,16 @@ // 우수토실 -import Modal from '@/components/ui/modal'; -import { VerticalTable } from '@/components/ui/verticalTable'; -import { UiTable } from '@common/business_components/ui'; +import { UiModal, UiTable } from '@common/business_components/ui'; import { Button, TableHead, TableRow } from '@common/components/ui'; -import React from 'react' -const inputColumn = [ - { id: "swtrmt_zone_cd", title: "처리구역", type: "input" }, - { id: "mainSscYn", title: "중점우수토실 대상여부", type: "select", options: ['Y', 'N'] }, -]; -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 = ({ featureData, chkMainSsc }: { featureData: any, chkMainSsc?: boolean}) => { +import React from 'react'; +import { untrtSwgRainCol3, untrtSwgRainCol4, untrtSwgRainCol5 } from '../common/table'; + +const Ssc = ({ featureData, chkMainSsc, additionalData }: { + featureData: any, + chkMainSsc?: boolean, + additionalData?: any +}) => { + console.log('Additional data received:', additionalData); // 추가 데이터 확인용 + const [isModalOpen, setIsModalOpen] = React.useState(false); const openModal = () => { setIsModalOpen(true); @@ -20,45 +18,120 @@ const Ssc = ({ featureData, chkMainSsc }: { featureData: any, chkMainSsc?: boole const closeModal = () => { setIsModalOpen(false); }; + + // 데이터 전처리: 합쳐진 필드 추가 + const preprocessedData = featureData ? [{ + ...featureData, + flwmtr_combined: `${featureData.flwmtr_mesure_tp_etc || '-'} / ${featureData.flwmtr_knd || '-'}` + }] : []; + + const renderModalBody = () => ( + <div className='grid grid-cols-1 gap-4 p-4'> + {/* 강우정보 테이블 */} + <UiTable + headName='강우정보' + tableData={preprocessedData} + headlist={untrtSwgRainCol3} + pageSize={10} + total={preprocessedData.length} + children={undefined} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Rain data page change:', page); + }} + /> + + {/* 측정정보 테이블 */} + <UiTable + headName='측정정보' + tableData={preprocessedData} + headlist={untrtSwgRainCol4} + pageSize={10} + total={preprocessedData.length} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Measure data page change:', page); + }} + > + {/* 커스텀 헤더 */} + <TableRow> + <TableHead colSpan={5} className="text-center bg-blue-100 font-bold">수량</TableHead> + <TableHead colSpan={3} className="text-center bg-blue-100 font-bold">수질</TableHead> + </TableRow> + <TableRow> + <TableHead className="text-center">측정시작일시</TableHead> + <TableHead className="text-center">측정종료일시</TableHead> + <TableHead className="text-center">측정간격(분)</TableHead> + <TableHead className="text-center">측정기기설치위치</TableHead> + <TableHead className="text-center">유량계 종류 및 모델명</TableHead> + <TableHead className="text-center">채수시작일시</TableHead> + <TableHead className="text-center">채수종료일시</TableHead> + <TableHead className="text-center">채수횟수</TableHead> + </TableRow> + </UiTable> + + {/* 수량/수질측정 정보 테이블 */} + <UiTable + headName='수량/수질측정 정보' + tableData={additionalData} + headlist={untrtSwgRainCol5} + pageSize={10} + total={additionalData.length} + children={undefined} + cellClick={undefined} + onPageChange={function (page: number): void { + console.log('Quality data page change:', page); + }} + /> + </div> + ); + return ( - <div><VerticalTable headName='자체 입력정보' tableData={[]} headlist={inputColumn} pageSize={0} total={0} children={undefined} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }} />{!chkMainSsc ? null : - <div> - <div className="card-header"> - <h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3> + <div> + {/* <VerticalTable + headName='자체 입력정보' + tableData={[]} + headlist={inputColumn} + pageSize={0} + total={0} + children={undefined} + cellClick={undefined} + isEditable={true} + onPageChange={function (page: number): void { + throw new Error('Function not implemented.'); + }} + /> */} + + {!chkMainSsc ? null : + <div> + <div className="card-header"> + <h3 className="text-2xl mx-3 font-medium text-label">중점우수토실 운영정보</h3> + </div> + <Button + className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600' + onClick={openModal} + > + 상세보기 + </Button> + + <UiModal + open={isModalOpen} + onOpenChange={setIsModalOpen} + bodyClass={` + h-[100vh] + w-[60vw] + max-w-none + max-h-none + overflow-visible + z-[101] + `} + body={renderModalBody()} + noHeaderYn='Y' + noFooterYn='N' + // footerContent={renderFooterContent()} + /> </div> - <Button className='mx-auto block w-64 mt-3 py-2 text-white rounded-sm text-sm font-medium hover:bg-blue-700 bg-blue-600' onClick={openModal}>상세보기</Button> - <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}> - <div className='grid grid-col-3'><UiTable headName='강우정보' tableData={[]} headlist={headlist} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }}></UiTable> - <UiTable headName='측정정보' tableData={[]} headlist={[]} pageSize={0} total={0} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }}> {/* 커스텀 헤더 */} - <TableRow> - <TableHead colSpan={5} className="text-center bg-blue-100 font-bold"> - 수량 - </TableHead> - <TableHead colSpan={3} className="text-center bg-blue-100 font-bold"> - 수질 - </TableHead> - </TableRow> - <TableRow> - <TableHead className="text-center">측정시작일시</TableHead> - <TableHead className="text-center">측정종료일시</TableHead> - <TableHead className="text-center">측정간격(분)</TableHead> - <TableHead className="text-center">측정기기설치위치</TableHead> - <TableHead className="text-center">유량계 종류 및 모델명</TableHead> - <TableHead className="text-center">채수시작일시</TableHead> - <TableHead className="text-center">채수종료일시</TableHead> - <TableHead className="text-center">채수횟수</TableHead> - </TableRow></UiTable> - <UiTable headName='수량/수질측정 정보' tableData={[]} headlist={headlist3} pageSize={0} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error('Function not implemented.'); - }}></UiTable></div> - </Modal> - </div>} + } </div> ) } diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx index 2f70adbc3..9b431f23c 100644 --- a/apps/sn/app/map/featureClickEventPopup.tsx +++ b/apps/sn/app/map/featureClickEventPopup.tsx @@ -1,12 +1,12 @@ 'use client' -import { VerticalTable } from "@/components/ui/verticalTable"; -import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; -import { useEffect, useRef, useState } from "react"; -import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from "./components/common/table"; -import { useGisContext } from "@common/business_components/ui/map/Gis"; import { getData, putData } from "@/components/lib/api"; -import { getLayerContentComponent } from "./components/common"; import Modal from "@/components/ui/modal"; +import { VerticalTable } from "@/components/ui/verticalTable"; +import { useGisContext } from "@common/business_components/ui/map/Gis"; +import { Button } from "@common/components/ui"; +import { useEffect, useRef, useState } from "react"; +import { getLayerContentComponent } from "./components/common"; +import { SwtrmtFcltCol, SwtrmtPrcsZoneCol, untrtSwgRainCol1, untrtSwgRainCol2 } from "./components/common/table"; // TODO: const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject<number[]>; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; @@ -15,13 +15,16 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, ? featureData.getId() : featureData.fid; const [layerNm, setLayerNm] = useState<string>(""); // 레이어 이름 상태 - const [tableData, setTableData] = useState<any>(null); // 피처 데이터 상태 + const [tableData, setTableData] = useState<any>(null); // 피처 데이터 + const [mntrngsData, setMntrngsData] = useState(false); // 수량/수질 측정 정보 const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + const [headlist2, setHeadlist2] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 const [buttonChk, setButtonChk] = useState(true) const [fetchData, setfetchData] = useState<any>(); // api 호출여부 const contentRef = useRef<HTMLDivElement>(null); const [isModalOpen, setIsModalOpen] = useState(false); // 모달 열기 상태 const [chkMainSsc, setChkMainSsc] = useState(false); // 중점우수토실 여부 + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 const getSelectedLayers = () => { let result = null; @@ -38,19 +41,21 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, if (featureLayer.get('lyrNm') === null) { if (featureData.lyrTypeNm === 'tb_il_swage_prcs_fclt') result = '하수처리시설'; else if (featureData.lyrTypeNm === 'tb_ms_swage_prcs_zone') result = '하수처리구역'; + else if (featureData.lyrTypeNm === 'om_dt_untrt_swg_rain') result = '중점우수토실'; } else result = featureLayer.get('lyrNm'); - + return result; // return featureLayer.get('lyrNm'); // 레이어 이름 출력 }; + useEffect(() => { const fetchData = async () => { if (featureId) { const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 setLayerNm(selectedLayerName); // 상태 업데이트 const layerId = featureId.split('.')[0] - const data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`); + let data = await getData(`v1/fclts/${featureId}?lyrTypeNm=${layerId}`); setfetchData(data) switch (selectedLayerName) { case '하수처리시설': @@ -62,8 +67,20 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, setButtonChk(false) break; case '중점우수토실': + setHeadlist(untrtSwgRainCol1) + setHeadlist2(untrtSwgRainCol2) setButtonChk(false) setChkMainSsc(true) + + // 데이터 전처리: 합쳐진 필드 추가 + data = data ? { + ...data, + discharge_pipe_spec: `직경: ${data.pipe_diameter || '-'} / 높이: ${data.pipe_hg || '-'} / 길이: ${data.pipe_lt || '-'}` + } : {}; + + // 수량/수질측정 정보 데이터 호출 + const additionalResponse = await getData(`v1/onsemiro/swgRain/${data.adst_dstr_cd}/${data.manage_no}/mntrngs`); + setMntrngsData(additionalResponse.content); break; case '우수토실': setButtonChk(true) @@ -88,25 +105,29 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, } fetchData(); }, [featureId]); + // 저장 시 모달 띄우기 const handleSaveClick = () => { setIsModalOpen(true); // 모달 열기 }; + const handleModalSave = () => { setIsModalOpen(false); // 모달 닫기 btnClickEvent(); // 저장 이벤트 실행 }; + const handleModalClose = () => { setIsModalOpen(false); // 모달 닫기 }; - const btnClickEvent = async() => { + + const btnClickEvent = async () => { const bodyData: Record<string, any> = {} if (buttonChk && contentRef.current) { const inputElements = contentRef.current.querySelectorAll('input'); const selectElements = contentRef.current.querySelectorAll('select'); - // 카멜케이스 변환 함수 - const toCamelCase = (str: string) => - str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); + // 카멜케이스 변환 함수 + const toCamelCase = (str: string) => + str.replace(/_([a-z])/g, (_, char) => char.toUpperCase()); // select 태그의 값 가져오기 selectElements.forEach((select) => { const camelCaseId = toCamelCase(select.id); @@ -121,19 +142,19 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, else if (layerNm === '우수토실') bodyData['lyrTypeNm'] = 'tb_ms_stofcbr' else if (layerNm === '오수펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn' else if (layerNm === '빗물펌프') bodyData['lyrTypeNm'] = 'tb_ms_swage_pmstn' - else if(layerNm === '하수맨홀')bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl' + else if (layerNm === '하수맨홀') bodyData['lyrTypeNm'] = 'tb_ms_swage_mnhl' bodyData['fid'] = featureId const data = await putData(`v1/fclts/${featureId}`, bodyData); - if(data.status==200){ - alert('저장되었습니다') - setTableData(data) - setfetchData(data) + if (data.status == 200) { + alert('저장되었습니다') + setTableData(data) + setfetchData(data) } } } const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null; return ( - <div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[200]"> + <div id="popup" className="absolute top-0 right-0 w-[350px] h-[calc(100vh-10rem)] bg-white border-r border-gray-300 flex flex-col overflow-hidden transition-all duration-300 z-[50]"> <div className="card-header bg-primary flex items-center justify-between p-4"> <div className="flex mx-3 flex-col"> <h3 className="text-2xl font-medium text-white text-label">{layerNm} 정보</h3> @@ -148,16 +169,40 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, </button> </div> <div className="overflow-y-auto overflow-x-hidden flex-1" ref={contentRef}> - <VerticalTable headName='LX 연계정보' tableData={[tableData]} headlist={headlist} pageSize={20} total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { - throw new Error("Function not implemented."); - }}></VerticalTable> + <VerticalTable + headName='LX 연계정보' + tableName1={layerNm === '중점우수토실' ? "측정지점" : undefined} + tableData={[tableData]} + headlist={headlist} + pageSize={20} + total={0} + children={undefined} + cellClick={undefined} + onPageChange={function (page: number): void { + throw new Error("Function not implemented."); + }} + // 두 번째 테이블 조건부 props + showSecondTable={layerNm === '중점우수토실' ? true : false} + tableName2={layerNm === '중점우수토실' ? "시설현황" : undefined} + tableData2={layerNm === '중점우수토실' && tableData ? [tableData] : []} + headlist2={layerNm === '중점우수토실' ? headlist2 : undefined} + pageSize2={20} + total2={0} + isEditable2={buttonChk} + /> <div className="w-full mx-5 flex items-center justify-end space-x-2"> {buttonChk ? <Button onClick={handleSaveClick} className="px-4 mr-5 py-2 bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700"> 저장 </Button> : null} </div> <Modal isModalOpen={isModalOpen} title="자체입력정보 저장" setIsModalOpen={setIsModalOpen} closeModal={handleModalClose} saveModal={handleModalSave}><div className="text-lg">정보를 저장하시겠습니까?</div></Modal> - {ContentComponent && <ContentComponent featureData={fetchData} chkMainSsc={chkMainSsc} />} + {ContentComponent && + <ContentComponent + featureData={fetchData} + chkMainSsc={chkMainSsc} + additionalData={mntrngsData} + /> + } </div> </div> ) From 94359782917b3b6274e16ff4565342892afed471 Mon Sep 17 00:00:00 2001 From: swcho <swcho@mind-one.co.kr> Date: Thu, 5 Jun 2025 17:24:07 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EC=A4=91=EC=A0=90=EC=9A=B0?= =?UTF-8?q?=EC=88=98=ED=86=A0=EC=8B=A4=20'=EA=B0=95=EC=9A=B0=EC=A0=95?= =?UTF-8?q?=EB=B3=B4',=20'=EC=B8=A1=EC=A0=95=EC=A0=95=EB=B3=B4'=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=88=A8=EA=B9=80=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sn/app/map/components/popupComponent/Ssc.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sn/app/map/components/popupComponent/Ssc.tsx b/apps/sn/app/map/components/popupComponent/Ssc.tsx index aa20261ab..2ca7c174d 100644 --- a/apps/sn/app/map/components/popupComponent/Ssc.tsx +++ b/apps/sn/app/map/components/popupComponent/Ssc.tsx @@ -33,7 +33,7 @@ const Ssc = ({ featureData, chkMainSsc, additionalData }: { tableData={preprocessedData} headlist={untrtSwgRainCol3} pageSize={10} - total={preprocessedData.length} + total={0} children={undefined} cellClick={undefined} onPageChange={function (page: number): void { @@ -47,7 +47,7 @@ const Ssc = ({ featureData, chkMainSsc, additionalData }: { tableData={preprocessedData} headlist={untrtSwgRainCol4} pageSize={10} - total={preprocessedData.length} + total={0} cellClick={undefined} onPageChange={function (page: number): void { console.log('Measure data page change:', page);