Merge remote-tracking branch 'origin/iipbs/sn/dev' into iipbs-sn-map

This commit is contained in:
yschoi
2025-06-05 18:03:48 +09:00
11 changed files with 606 additions and 214 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ export const putData = async (url: string, data: object): Promise<any> => {
return resData;
};
export const getData = async (url: string): Promise<any> => {
export const getData = async (url: string, data?: object): Promise<any> => {
const res = await fetch(url.startsWith('/') ? `${url}` : `${BASE_URL}/${url}`, {
method: "GET",
});
@@ -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, {
+2 -1
View File
@@ -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 (
<MainDiv>
<Title title={title} />
+78 -1
View File
@@ -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}
+117 -72
View File
@@ -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>
)
}
+1 -1
View File
@@ -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 (오수관망도)
+151 -59
View File
@@ -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>
)
}
+6 -4
View File
@@ -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, }, // 중점우수토실 여부 설정
+57 -2
View File
@@ -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)' }
];
+123 -50
View File
@@ -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={0}
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={0}
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>
)
}
+68 -23
View File
@@ -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>
)