diff --git a/apps/sn/app/components/lib/api/index.ts b/apps/sn/app/components/lib/api/index.ts new file mode 100644 index 000000000..a171844a1 --- /dev/null +++ b/apps/sn/app/components/lib/api/index.ts @@ -0,0 +1,61 @@ +const BASE_URL = process.env.NEXT_PUBLIC_SEWERAGE_API_URL || ''; + +export const postData = async (url: string, data: object): Promise => { + const options: RequestInit = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }; + const res = await fetch(`${BASE_URL}/${url}`, options); + const resData = await res.json(); + return resData; +}; + +export const postMutiPart = async (url: string, data: FormData): Promise => { + const options: RequestInit = { + method: "POST", + body: data, + }; + const res = await fetch(`${BASE_URL}/${url}`, options); + const resData = await res.json(); + return resData; +}; + +export const putData = async (url: string, data: object): Promise => { + const options: RequestInit = { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }; + const res = await fetch(`${BASE_URL}/${url}`, options); + const resData = await res.json(); + return resData; +}; + +export const getData = async (url: string): Promise => { + const res = await fetch(`${BASE_URL}/${url}`, { + method: "GET", + }); + if (res.ok) { + const data = await res.json(); + return data; + } + throw new Error('Failed to fetch data'); +}; + +export const deleteData = async (url: string, data?: object): Promise => { + const options: RequestInit = { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: data ? JSON.stringify(data) : undefined, + }; + const res = await fetch(`${BASE_URL}/${url}`, options); + const resData = await res.json(); + return resData; +}; \ No newline at end of file diff --git a/apps/sn/app/components/ui/modal.tsx b/apps/sn/app/components/ui/modal.tsx new file mode 100644 index 000000000..bc8085f71 --- /dev/null +++ b/apps/sn/app/components/ui/modal.tsx @@ -0,0 +1,31 @@ +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, DialogOverlay } from '@common/components/ui' +import React from 'react' + +interface ModalProps { + isModalOpen: boolean; + setIsModalOpen: (open: boolean) => void; + title?: string; + children: React.ReactNode; + closeModal: () => void; +} + +const Modal: React.FC = ({ isModalOpen, setIsModalOpen, title, children, closeModal }) => { + return ( + + + + {title ? + {title} + : null} + {children} + + + + + + + + ) +} + +export default Modal \ No newline at end of file diff --git a/apps/sn/app/components/verticalTable.tsx b/apps/sn/app/components/ui/verticalTable.tsx similarity index 99% rename from apps/sn/app/components/verticalTable.tsx rename to apps/sn/app/components/ui/verticalTable.tsx index b792f3426..36451bfbd 100644 --- a/apps/sn/app/components/verticalTable.tsx +++ b/apps/sn/app/components/ui/verticalTable.tsx @@ -47,7 +47,6 @@ export function VerticalTable({ }, [onPageChange, resetPageNo]); useEffect(() => { - console.log('data',tableData) if (tableData.length > 0 && tableData[0]) { setFormData(tableData[0] as { [key: string]: string | number | boolean }); } diff --git a/apps/sn/app/map/components/InputSection.tsx b/apps/sn/app/map/components/InputSection.tsx new file mode 100644 index 000000000..6c411e2cf --- /dev/null +++ b/apps/sn/app/map/components/InputSection.tsx @@ -0,0 +1,50 @@ +import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@common/components/ui"; +import React from "react"; + +interface InputSectionProps { + title: string; + data: { year: string; type: string }[]; + sectionKey: string; + onInputChange: (section: string, index: number, field: string, value: string) => void; +} + +const InputSection: React.FC = ({ title, data, sectionKey, onInputChange }) => { + return ( +
+ {/* 섹션 제목 */} +
+

{title}

+
+ {/* 테이블 */} +
+ + + + 년도 + 구분 + + + + {data.map((row, index) => ( + + + onInputChange(sectionKey, index, "year", e.target.value)} + className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal" + /> + + + {row.type} + + + ))} + +
+
+
+ ); +}; + +export default InputSection; \ No newline at end of file diff --git a/apps/sn/app/map/components/Ssc.tsx b/apps/sn/app/map/components/Ssc.tsx new file mode 100644 index 000000000..a6e783ac9 --- /dev/null +++ b/apps/sn/app/map/components/Ssc.tsx @@ -0,0 +1,66 @@ +// 우수토실 +import Modal from '@/components/ui/modal'; +import { VerticalTable } from '@/components/ui/verticalTable'; +import { UiTable } from '@common/business_components/ui'; +import { Button, TableHead, TableRow } from '@common/components/ui'; +import React from 'react' +const inputColumn = [ + { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, + { 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 = () => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + const openModal = () => { + setIsModalOpen(true); + }; + const closeModal = () => { + setIsModalOpen(false); + }; + return ( +
+
+
+

중점우수토실 운영정보

+
+ + +
+ {/* 커스텀 헤더 */} + + + 수량 + + + 수질 + + + + 측정시작일시 + 측정종료일시 + 측정간격(분) + 측정기기설치위치 + 유량계 종류 및 모델명 + 채수시작일시 + 채수종료일시 + 채수횟수 + +
+
+
+
+ ) +} + +export default Ssc \ No newline at end of file diff --git a/apps/sn/app/map/components/SwtrmtFclt.tsx b/apps/sn/app/map/components/SwtrmtFclt.tsx new file mode 100644 index 000000000..bcf2e08d6 --- /dev/null +++ b/apps/sn/app/map/components/SwtrmtFclt.tsx @@ -0,0 +1,45 @@ +import { VerticalTable } from '@/components/ui/verticalTable'; +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 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 }, + }; + 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/common/index.ts b/apps/sn/app/map/components/common/index.ts new file mode 100644 index 000000000..9228c09ba --- /dev/null +++ b/apps/sn/app/map/components/common/index.ts @@ -0,0 +1,45 @@ +import smh from "../smh"; +import Ssc from "../Ssc"; +import SwtrmtFclt from "../SwtrmtFclt"; + +// 레이어 이름과 컴포넌트를 매핑 +const layerContentMap: { [key: string]: { component: React.FC; props?: any; } } = { + // TODO: gis데이터 추가되면 추가 작업 필요 + "하수처리시설": { + component: SwtrmtFclt, + }, + "하수처리구역": { component: Ssc, }, + "중점우수토실":{component: Ssc,}, + "하수관로":{component: SwtrmtFclt,}, + "하수맨홀":{component: smh,}, +}; + +// 동적 컴포넌트 반환 함수 +export const getLayerContentComponent = (layerNm: string): React.FC => { + const component = layerContentMap[layerNm]; + if (!component) { + throw new Error(`Component not found for layer name: ${layerNm}`); + } + return component.component; +}; +// 사용자 입력 필드 (보수 및 갱신, 청소 및 준설, 점검 및 진단) +export const handleInputChange = (section: string, + index: number, + field: string, + value: string, + dataMap: { + [key: string]: { + data: { year: string; type: string }[]; + setData: React.Dispatch>; + }; + } +) => { + const target = dataMap[section]; + if (!target) return; + + const updated = [...target.data]; + if (updated[index]) { + updated[index][field as keyof typeof updated[number]] = value; + } + target.setData(updated); +}; \ No newline at end of file diff --git a/apps/sn/app/map/components/smh.tsx b/apps/sn/app/map/components/smh.tsx new file mode 100644 index 000000000..e693a28ce --- /dev/null +++ b/apps/sn/app/map/components/smh.tsx @@ -0,0 +1,21 @@ +// 하수맨홀 +import { VerticalTable } from '@/components/ui/verticalTable'; +import React from 'react' +const inputColumn = [ + { id: "guyeok", title: "처리구역", type: "select", options: ["구역1", "구역2"] }, + { id: "basu", title: "배수구역", type: "input" }, + { id: "bungu", title: "배수분구", type: "input" }, + { id: "place", title: "매설위치", type: "select", options: ["도로", "하상"] }, + {id:'spec', title:'규격', type:'input'}, +]; +const Smh = () => { + return ( +
+ +
+ ) +} + +export default Smh; \ No newline at end of file diff --git a/apps/sn/app/map/featureClickEventPopup.tsx b/apps/sn/app/map/featureClickEventPopup.tsx index 545bc1697..07064e8e7 100644 --- a/apps/sn/app/map/featureClickEventPopup.tsx +++ b/apps/sn/app/map/featureClickEventPopup.tsx @@ -1,102 +1,59 @@ 'use client' -import { VerticalTable } from "@/components/verticalTable"; +import { VerticalTable } from "@/components/ui/verticalTable"; import { Button, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components/ui"; -import { useState } from "react"; -import { CommonTableColumn } from "./table"; -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 FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData:any }) => { - // TODO: 런타임 에러 해결 필요 - 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 handleInputChange = (section: string, index: number, field: string, value: string) => { - const updateData = (data: { year: string; type: string }[], setData: React.Dispatch>) => { - const updated = [...data]; - if (updated[index]) { - updated[index][field as keyof typeof updated[number]] = value; +import { use, useEffect, useState } from "react"; +import { SwtrmtFcltCol, SwtrmtPrcsZoneCol } from "./table"; +import { useGisContext } from "@common/business_components/ui/map/Gis"; +import { getData } from "@/components/lib/api"; +import InputSection from "./components/InputSection"; +import { getLayerContentComponent } from "./components/common"; +// TODO: +const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void, featureData: any }) => { + const { gisMap } = useGisContext() as unknown as { checkedLayerIds: React.MutableRefObject; treeData: { lyrSn: number }[] | null; gisCore: any; gisMap: any; }; + const featureId = featureData.getId(); // 피처의 id 가져오기 + const [layerNm, setLayerNm] = useState(""); // 레이어 이름 상태 + const [tableData, setTableData] = useState(null); // 피처 데이터 상태 + const [headlist, setHeadlist] = useState<{ id: string; title: string; }[]>([]); // 컬럼리스트 변경상태 + // 피쳐데이터의 현재 레이어 이름 가져오는 함수 + const getSelectedLayers = () => { + const layers = gisMap.getLayers().getArray(); // gisMap에서 모든 레이어 가져오기 + let featureLayer: { get: (key: string) => any } = { get: (key: string) => null }; // 피처가 속한 레이어를 찾기 위한 변수 + layers.forEach((layer: any) => { + const source = layer.getSource(); + if (source && source.hasFeature && source.hasFeature(featureData)) { + featureLayer = layer; // 피처가 속한 레이어 찾기 } - setData(updated); - }; - - if (section === "repair") { - updateData(repairData, setRepairData); - } else if (section === "cleaning") { - updateData(cleaningData, setCleaningData); - } else if (section === "inspection") { - updateData(inspectionData, setInspectionData); - } + }); + return featureLayer.get('lyrNm'); // 레이어 이름 출력 }; - function transformFeatureDataToArray(featureData: any, keys: string[]) { - // 필요한 키만 추출하여 새로운 객체 생성 - const transformedData = keys.reduce((acc, key) => { - acc[key] = featureData[key] !== undefined ? featureData[key] : "N/A"; // 값이 없으면 "N/A"로 처리 - return acc; - }, {} as Record); - - // 배열로 감싸서 반환 - return [transformedData]; - } - const keys = CommonTableColumn.map((column) => column.id); - const transformedFeatureData = transformFeatureDataToArray(featureData, keys); - const renderSection = (title: string, data: { year: string; type: string }[], sectionKey: string) => ( -
- {/* 섹션 제목 */} -
-

{title}

-
- {/* 테이블 */} -
- - - - 년도 - 구분 - - - - {data.map((row, index) => ( - - - handleInputChange(sectionKey, index, "year", e.target.value)} - className="w-full text-xl border-radius-0 px-3 py-2 text-base text-center leading-normal" - /> - - {row.type} - - ))} - -
-
-
- ); + useEffect(() => { + const fetchData = async () => { + if (featureId) { + const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기 + setLayerNm(selectedLayerName); // 상태 업데이트 + + const data = await getData(`v1/map/features/${featureId}`); + switch (selectedLayerName) { + case '하수처리시설': + setHeadlist(SwtrmtFcltCol); + break; + case '하수처리구역': + setHeadlist(SwtrmtPrcsZoneCol) + break; + default: + break; + } + setTableData(data); + }; + } + fetchData(); + }, [featureId]); + const ContentComponent = layerNm ? getLayerContentComponent(layerNm) : null; return ( -