This commit is contained in:
2025-06-13 18:09:20 +09:00
40 changed files with 2371 additions and 504 deletions
@@ -16,6 +16,7 @@ export const ENDPOINT = {
ATTACHMENTS: "/attach-service/api/v1/atfl/attachments",
DOWNLOAD: "/attach-service/api/v1/atflcd/download",
STREAM_VIDEO: "/attach-service/api/v1/stream/video",
DELETE_FILE_BY_ATFLCD: "/attach-service/api/v1/atfl/attachments/atflCd",
},
};
+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",
});
@@ -0,0 +1,109 @@
import { getCookie } from "@utils";
import { ENDPOINT } from "../api/attachment/attachment-service";
const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id";
const CLAIM_NAME = process.env.NEXT_PUBLIC_CLAIM_NAME || "Authorization";
type DeleteOptions = {
handleDeleteStart?: () => void;
handleDeleteSuccess?: (response: Response) => void;
handleDeleteError?: (error: Error) => void;
};
export const handleDelete = async (
atflCd: string,
options?: DeleteOptions
): Promise<void> => {
if (!atflCd) {
const error = new Error("첨부파일 코드가 필요합니다.");
options?.handleDeleteError?.(error);
throw error;
}
try {
options?.handleDeleteStart?.();
const response = await fetch(
`${ENDPOINT.ATTACH_SERVICE.DELETE_FILE_BY_ATFLCD}/${atflCd}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
[CLAIM_NAME]: getCookie(CLAIM_NAME) ?? "",
[AUTH_USER_ID]: getCookie(AUTH_USER_ID) ?? "",
},
}
);
if (!response.ok) {
throw new Error(`파일 삭제 실패: ${response.status} ${response.statusText}`);
}
options?.handleDeleteSuccess?.(response);
console.log("파일 삭제 완료:", atflCd);
} catch (error) {
const deleteError = error instanceof Error
? error
: new Error("파일 삭제 중 알 수 없는 오류가 발생했습니다.");
console.error("파일 삭제 오류:", deleteError);
options?.handleDeleteError?.(deleteError);
throw deleteError;
}
};
// 여러 파일을 삭제하는 함수
// export const handleMultipleDelete = async (
// atflCds: string[],
// options?: DeleteOptions & {
// handleProgress?: (completed: number, total: number) => void;
// }
// ): Promise<void> => {
// if (!atflCds || atflCds.length === 0) {
// const error = new Error("삭제할 파일이 없습니다.");
// options?.handleDeleteError?.(error);
// throw error;
// }
// try {
// options?.handleDeleteStart?.();
// const deletePromises = atflCds.map(async (atflCd, index) => {
// try {
// await handleDelete(atflCd);
// options?.handleProgress?.(index + 1, atflCds.length);
// return { success: true, atflCd };
// } catch (error) {
// return { success: false, atflCd, error };
// }
// });
// const results = await Promise.allSettled(deletePromises);
// const failedDeletes = results
// .filter((result): result is PromiseFulfilledResult<{ success: false; atflCd: string; error: any }> =>
// result.status === 'fulfilled' && !result.value.success
// )
// .map(result => result.value);
// if (failedDeletes.length > 0) {
// const error = new Error(
// `일부 파일 삭제 실패: ${failedDeletes.map(f => f.atflCd).join(', ')}`
// );
// options?.handleDeleteError?.(error);
// throw error;
// }
// options?.handleDeleteSuccess?.(new Response());
// console.log("모든 파일 삭제 완료:", atflCds);
// } catch (error) {
// const deleteError = error instanceof Error
// ? error
// : new Error("파일 삭제 중 알 수 없는 오류가 발생했습니다.");
// console.error("파일 삭제 오류:", deleteError);
// options?.handleDeleteError?.(deleteError);
// throw deleteError;
// }
// };
@@ -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, {
@@ -13,7 +15,7 @@ export const handleDownload = async (api:string, fileNm:string) => {
// 다운로드를 트리거할 가짜 <a> 태그 생성
const link = document.createElement('a');
link.href = url;
link.download = fileNm; // 원하는 파일명 지정
link.download = fileNm.normalize('NFC'); // 원하는 파일명 지정
document.body.appendChild(link);
link.click();
+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}
+119 -68
View File
@@ -2,10 +2,9 @@
import { getData } 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'
@@ -17,7 +16,8 @@ type TitleName = {
type SelectBox = {
labelNm?: Label
searchChk?: boolean
title?: string // SearchDiv의 제목 (선택 사항)
title?: string;
onSearchClick?: () => Promise<void>;
}
type Label = {
first: string
@@ -29,15 +29,17 @@ type Label = {
interface ApiSelectOption {
text: string;
val: string;
}
}
type BaseLayout = {
children?: ReactNode[]
titleName: TitleName
form: any
onSubmitEvent: Function
graphBtn?: boolean
analysisBtn?: string
firstDivBtn?: boolean
searchBox?: SelectBox
selectBox?: SelectBox
showDatePicker?: boolean;
enoCd?: string | number
setEnoCd?: (enoCd: string) => void
ctpvCd?: string | number
@@ -48,6 +50,14 @@ type BaseLayout = {
onSwageTreatmentPlantChange?: (value: string | number) => void;
selectedSwageProcessingZone?: string | number;
onSwageProcessingZoneChange?: (value: string | number) => void;
aplcnYm?: Date
setAplcnYm?: (aplcnYm: Date | undefined) => void;
isLoading?: boolean;
// Excel 다운로드 관련 props 추가
onExcelDownload?: () => void;
onDeleteFile?: () => void;
onAnalysis?: () => void;
selectedRowData?: any;
}
// TODO: 나중에 이벤트 추가되면 파라미터 추가 필요 ?
/**
@@ -59,23 +69,33 @@ type BaseLayout = {
* @param graphBtn : 하단에 그래프 버튼 유무 처리
* @returns
*/
const BaseLayout = ({
children,
titleName,
form,
onSubmitEvent,
graphBtn = false,
searchBox,
const BaseLayout = ({
children,
titleName,
form,
onSubmitEvent,
graphBtn = false,
analysisBtn,
selectBox,
showDatePicker = false,
enoCd,
setEnoCd,
ctpvCd,
setCtpvCd,
sigungu,
sigungu,
setSigungu,
selectedSwageTreatmentPlant,
onSwageTreatmentPlantChange,
selectedSwageProcessingZone,
onSwageProcessingZoneChange,
aplcnYm,
setAplcnYm,
isLoading,
// Excel 다운로드 관련 props 추가
onExcelDownload,
onDeleteFile,
onAnalysis,
selectedRowData
}: BaseLayout) => {
const defaultOption: ApiSelectOption = { val: '0', text: '선택' };
@@ -83,6 +103,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 +182,94 @@ 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}
// 파일 다운로드
const isChckFile = () => {
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
if (onExcelDownload) {
onExcelDownload();
return;
}
return true
}
// 파일 삭제
const isDeleteFile = async () => {
// Excel 다운로드 함수가 있으면 Excel 다운로드 실행
if (onDeleteFile) {
onDeleteFile();
return;
}
}
</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]}
return (
<Form {...form} className="min-h-screen flex flex-col">
<ContentLayout title={titleName.mainTitle} className="flex-grow flex flex-col overflow-hidden" isLoading={isLoading}>
<ListDiv>
{/* searchBox.title이 있으면 h3 태그로 제목을 표시합니다. */}
{selectBox?.title && (
<div className="card-header">
<h3 className="text-2xl font-medium">{selectBox.title}</h3>
</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 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>
{selectBox?.searchChk ? <SearchFormRight><Button size="sm" onClick={() => selectBox.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 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 !mr-[1.2rem] bg-blue-600 text-white rounded-sm text-sm font-medium hover:bg-blue-700" size="sm" 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]" 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" onClick={isDeleteFile}>삭제</Button> : null}
{/* 분석 버튼 */}
{analysisBtn ? <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" onClick={onAnalysis}>{analysisBtn}</Button> : null}
</div>
</div>
<div className="card !m-[1.2rem]">{children?.[1]}</div>
</ListDiv> : null
}
</ContentLayout >
<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]" 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
}
</ContentLayout >
</Form>
)
}
+58 -22
View File
@@ -3,7 +3,8 @@ import { postData } from '@/components/lib/api'
import FileUploadForm from '@/components/ui/fileUploadForm'
import FormSection from '@/components/ui/formSection'
import { DatePicker, filePicked } from '@common/business_components/ui'
import { Button, Input } from '@common/components/ui'
import ListDiv from '@common/business_components/ui/list_div'
import { Input } from '@common/components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
@@ -67,6 +68,7 @@ const IrSetting = () => {
const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장코드
const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역코드
const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태
const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태
// 폼 초기화 함수
const resetAllForms = () => {
@@ -117,6 +119,7 @@ const IrSetting = () => {
const registFile = async (analysisData: any) => {
setIsLoadingResult(true); // 로딩 상태 시작
console.log("Submitting combined analysis data to backend:", analysisData);
// 추후 API 엔드포인트에 맞게 URL 수정 필요
const data = await postData('v1/inftwtAnls', analysisData);
@@ -125,10 +128,12 @@ const IrSetting = () => {
alert("분석 데이터가 성공적으로 제출되었습니다.");
resetAllForms(); // 성공 시 폼 초기화 함수 호출
} else alert("분석 데이터 제출 중 오류가 발생했습니다.");
setIsLoadingResult(false); // 로딩 상태 시작
};
// 침입수 분석 실행 버튼 클릭 핸들러
const handleAnalysisClick = async () => {
setIsLoadingResult(true); // 로딩 상태 시작
console.log(uploadedFilesData);
const file1 = uploadedFilesData[0]; // 오수관망도
const file2 = uploadedFilesData[1]; // 수용가-맨홀 매칭결과
@@ -141,18 +146,28 @@ const IrSetting = () => {
console.log("유량자료 정보:", file4);
// 모든 필수 파일이 선택되었는지 확인 (UploadedFileInfo 객체와 그 안의 file 객체 모두 확인)
if (!file1?.file || !file2?.file || !file3?.file || !file4?.file) {
// 유량자료는 선택값
if (!file1?.file || !file2?.file || !file3?.file) {
setIsLoadingResult(false);
alert('모든 분석용 파일을 선택해주세요.');
return;
}
const filePickers = [
// filePicked 유틸리티가 File 객체를 직접 받는지, ArrayBuffer 등을 받는지 확인 필요
// 여기서는 File 객체를 받고, 내부적으로 ArrayBuffer로 변환하여 parser에 전달한다고 가정
{ file: file2, parser: excelToMatchingResultData },
{ file: file3, parser: excelToTimePatternData },
{ file: file4, parser: excelToFlowRateData }
];
type MatchingResult = { anlsSn: number; mhlMngNo: string; rcptrNo: string; };
type TimePattern = { hrId: number; rt?: number | null | undefined; };
type FlowRate = { pplMngNo: string; mtrngDt: string; oilq?: number | null | undefined; };
const filePickers: {
file: UploadedFileInfo;
parser: (excelData: any) => MatchingResult[] | TimePattern[] | FlowRate[];
}[] = [
{ file: file2, parser: excelToMatchingResultData },
{ file: file3, parser: excelToTimePatternData }
];
if (file4?.file) {
filePickers.push({ file: file4, parser: excelToFlowRateData });
}
try {
const parsedResults = await Promise.all(
@@ -186,7 +201,9 @@ const IrSetting = () => {
// 각 파싱 결과 접근
const matchingResultData = parsedResults.find(r => r && r.fileName === file2.file?.name)?.parsedData;
const timePatternData = parsedResults.find(r => r && r.fileName === file3.file?.name)?.parsedData;
const flowRateData = parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData;
const flowRateData = file4?.file
? parsedResults.find(r => r && r.fileName === file4.file?.name)?.parsedData
: null;
// 새로운 JSON 데이터 생성
const combinedAnalysisData = {
@@ -196,25 +213,26 @@ 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 (오수관망도)
atflInpNm: file1?.file?.name, // 오수관망도 파일명
atflHrPttnNm: file3?.file?.name, // 오수량 시간패턴 파일명
atflMpngNm: file2?.file?.name, // 수용가-맨홀 매칭결과 파일명
atflOilqNm: file4?.file?.name, // 유량자료 파일명
atflOilqNm: file4?.file?.name || null, // 유량자료 파일명 (없을 수 있음)
tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과
tbSnHrPttn: timePatternData, // 오수량 시간패턴
tbSnOilqData: flowRateData, // 유량자료
tbSnOilqData: flowRateData, // 유량자료 (없을 수 있음)
};
console.log("통합 분석 데이터 (JSON):", combinedAnalysisData);
// 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다.
await registFile(combinedAnalysisData);
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
console.error("파일 파싱 중 오류 발생:", error);
alert("파일 처리 중 오류가 발생했습니다.");
return;
@@ -224,11 +242,12 @@ const IrSetting = () => {
return (
<>
<BaseLayout
titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력', secondCardTitle: '실측자료 등록' }}
titleName={{ mainTitle: '침입수 분석', firstCardTitle: '침입수 분석 자료 입력' }}
form={form}
onSubmitEvent={() => { }}
graphBtn={false}
searchBox={{ title: '분석 대상지역 선택' }}
analysisBtn={'침입수분석 실행'}
selectBox={{ title: '분석 대상지역 선택' }}
enoCd={select1}
setEnoCd={setSelect1}
ctpvCd={select2}
@@ -238,8 +257,11 @@ const IrSetting = () => {
selectedSwageTreatmentPlant={select4}
onSwageTreatmentPlantChange={setSelect4}
selectedSwageProcessingZone={select5}
onSwageProcessingZoneChange={setSelect5}>
<div className="grid gap-4">
onSwageProcessingZoneChange={setSelect5}
isLoading={isLoadingResult}
onAnalysis={handleAnalysisClick}
>
{[<div className="grid gap-4">
<FormSection name='suyeongaPlace' form={form} titleNm='▷ 오수관망도'>
<FileUploadForm
key={`file-upload-0-${formResetKey}`} // 폼 초기화를 위한 key
@@ -320,8 +342,22 @@ const IrSetting = () => {
onChange={(e) => setInput2(e.target.value)}
className="ml-5 h-[3.2rem] text-center text-xl w-[150px]" />
</FormSection>
</div>
<div className="flex items-center gap-4">
<ListDiv>
<div className="card !m-[1.2rem]">
<FormSection name='region' form={form} titleNm='▷ 유량자료 등록'>
<FileUploadForm
key={`file-upload-3-${formResetKey}`} // 폼 초기화를 위한 key
onFileSelect={(file, cd, id) => handleFileSelection(3, file, cd, id)}
showButton={false}
showPreview={true}
accept={".xls, .xlsx, .xlsm, .xltx"}
fileIdx={3}
/>
</FormSection>
</div>
</ListDiv>
</div>]}
{/* <div className="flex items-center gap-4">
<FormSection
name='region' form={form} titleNm='▷ 유량자료 등록'>
<FileUploadForm
@@ -333,9 +369,9 @@ const IrSetting = () => {
fileIdx={3} />
</FormSection>
<div className="flex justify-center mt-1">
<Button type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button>
<Button size="sm" type="submit" onClick={handleAnalysisClick}>침입수분석 실행</Button>
</div>
</div>
</div> */}
</BaseLayout>
</>
)
+48 -41
View File
@@ -4,15 +4,15 @@ import { postData } from '@/components/lib/api'
import FileUploadForm from "@/components/ui/fileUploadForm"
import FormSection from "@/components/ui/formSection"
import BaseLayout from "@/ii/BaseLayout"
import { filePicked, DatePicker } from "@common/business_components/ui"
import { Button, Input } from "@common/components/ui"
import { DatePicker, filePicked } from "@common/business_components/ui"
import { Input } from "@common/components/ui"
import { zodResolver } from "@hookform/resolvers/zod"
import { Label } from '@radix-ui/react-label'
import { useState } from "react"
import { useFieldArray, useForm } from "react-hook-form"
import { z } from "zod"
import { excelToMatchingResultData, excelToRainData } from '../../components/lib/utils/excelToJson'
import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/editableFormTable'
import { EditableFormTable, type EditableTableHeadItem } from '../../components/ui/EditableFormTable'
// zod 스키마 정의
@@ -47,31 +47,31 @@ const formSchema = z.object({
})
// zod 스키마 타입 추론
type FormSchemaType = z.infer<typeof formSchema>;
// 기본 폼 값 정의
// 기본 폼 값 정의 (테스트 편의를 위해 초기값 설정, 추후 삭제 필요)
const defaultFormValues: FormSchemaType = {
enoCd: "",
ctpvCd: "",
sggCd: "",
swageTmplntCd: "",
swtrmtZoneCd: "",
rtkSource: "",
rdiiArea: "", // 문자열로 기본값 설정
rtkSource: "출처",
rdiiArea: "123", // 문자열로 기본값 설정
unitFactors: [
{ response: "Short-Term", R: "", T: "", K: "" }, // 문자열로 기본값 설정
{ response: "Medium-Term", R: "", T: "", K: "" },
{ response: "Long-Term", R: "", T: "", K: "" },
{ response: "Short-Term", R: "0.040", T: "0.5", K: "1" }, // 문자열로 기본값 설정
{ response: "Medium-Term", R: "0.005", T: "1", K: "2" },
{ response: "Long-Term", R: "0.005", T: "2", K: "3" },
],
initialDepths: [
{ response: "Short-Term", Dmax: "", Drec: "", Do: "" }, // 문자열로 기본값 설정
{ response: "Medium-Term", Dmax: "", Drec: "", Do: "" },
{ response: "Long-Term", Dmax: "", Drec: "", Do: "" },
{ response: "Short-Term", Dmax: "0", Drec: "0", Do: "0" }, // 문자열로 기본값 설정
{ response: "Medium-Term", Dmax: "0", Drec: "0", Do: "0" },
{ response: "Long-Term", Dmax: "0", Drec: "0", Do: "0" },
],
// osumap: undefined,
// matchingResultFile: undefined,
aplcnYm: "",
rate: "", // 문자열로 기본값 설정
rate: "0.8", // 문자열로 기본값 설정
// rainFile: undefined,
analysisInfo: "",
analysisInfo: "test 분석정보",
date: new Date(),
};
@@ -92,6 +92,7 @@ interface ApiSelectOption {
export default function RTKForm() {
const [formResetKey, setFormResetKey] = useState(0); // FileUploadForm 리셋을 위한 key 상태
const [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태
// FileUploadForm에서 전달받은 파일 정보를 저장할 상태 배열
// 각 인덱스는 FileUploadForm의 fileIdx와 매칭됨
@@ -107,6 +108,7 @@ export default function RTKForm() {
const [select4, setSelect4] = useState<string | number>('0'); // 하수처리장 (swageTmplntCd)
const [select5, setSelect5] = useState<string | number>('0'); // 하수처리구역 (swtrmtZoneCd)
// form 인스턴스 생성
const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
@@ -160,6 +162,7 @@ export default function RTKForm() {
// 침입수 분석 실행 버튼 클릭 핸들러
const handleAnalysisClick = async () => {
setIsLoadingResult(true); // 로딩 상태 시작
const formData = form.getValues();
console.log(uploadedFilesData);
const file1 = uploadedFilesData[0]; // 오수관망도
@@ -172,6 +175,7 @@ export default function RTKForm() {
// 모든 필수 파일이 선택되었는지 확인
if (!file1?.file || !file2?.file || !file3?.file) {
setIsLoadingResult(false);
alert('모든 분석용 파일을 선택해주세요.');
return;
}
@@ -232,31 +236,31 @@ export default function RTKForm() {
cntbArea: parseFloat(formData.rdiiArea) || null, // RDII 기여면적(ha)
srcNm: formData.rtkSource, // 출처이름 (RTK계수 출처)
shortPrdR: parseFloat(formData.unitFactors[0]?.R !== undefined ? formData.unitFactors[0]?.R : "0") || null, // 단기간R
shortPrdT: parseFloat(formData.unitFactors[0]?.T !== undefined ? formData.unitFactors[0]?.T : "0") || null, // 단기간T
shortPrdK: parseFloat(formData.unitFactors[0]?.K !== undefined ? formData.unitFactors[0]?.K : "0") || null, // 단기간K
shortPrdR: parseFloat(formData.unitFactors[0]?.R || "0"),
shortPrdT: parseFloat(formData.unitFactors[0]?.T || "0"),
shortPrdK: parseFloat(formData.unitFactors[0]?.K || "0"),
mdPrdR: parseFloat(formData.unitFactors[1]?.R !== undefined ? formData.unitFactors[1]?.R : "0") || null, // 중간기간R
mdPrdT: parseFloat(formData.unitFactors[1]?.T !== undefined ? formData.unitFactors[1]?.T : "0") || null, // 중간기간T
mdPrdK: parseFloat(formData.unitFactors[1]?.K !== undefined ? formData.unitFactors[1]?.K : "0") || null, // 중간기간K
mdPrdR: parseFloat(formData.unitFactors[1]?.R || "0"),
mdPrdT: parseFloat(formData.unitFactors[1]?.T || "0"),
mdPrdK: parseFloat(formData.unitFactors[1]?.K || "0"),
longPrdR: parseFloat(formData.unitFactors[2]?.R !== undefined ? formData.unitFactors[2]?.R : "0") || null, // 장기간R
longPrdT: parseFloat(formData.unitFactors[2]?.T !== undefined ? formData.unitFactors[2]?.T : "0") || null, // 장기간T
longPrdK: parseFloat(formData.unitFactors[2]?.K !== undefined ? formData.unitFactors[2]?.K : "0") || null, // 장기간K
longPrdR: parseFloat(formData.unitFactors[2]?.R || "0"),
longPrdT: parseFloat(formData.unitFactors[2]?.T || "0"),
longPrdK: parseFloat(formData.unitFactors[2]?.K || "0"),
shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax !== undefined ? formData.initialDepths[0]?.Dmax : "0") || null, // 단기간최대깊이
shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec !== undefined ? formData.initialDepths[0]?.Drec : "0") || null, // 단기간회복비율
shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do !== undefined ? formData.initialDepths[0]?.Do : "0") || null, // 단기간시작깊이
shortPrdMaxDpth: parseFloat(formData.initialDepths[0]?.Dmax || "0"),
shortPrdRcvryRt: parseFloat(formData.initialDepths[0]?.Drec || "0"),
shortPrdBgngDpth: parseFloat(formData.initialDepths[0]?.Do || "0"),
mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax !== undefined ? formData.initialDepths[1]?.Dmax : "0") || null, // 중간기간최대깊이
mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec !== undefined ? formData.initialDepths[1]?.Drec : "0") || null, // 중간기간회복비율
mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do !== undefined ? formData.initialDepths[1]?.Do : "0") || null, // 중간기간시작깊이
mdPrdMaxDpth: parseFloat(formData.initialDepths[1]?.Dmax || "0"),
mdPrdRcvryRt: parseFloat(formData.initialDepths[1]?.Drec || "0"),
mdPrdBgngDpth: parseFloat(formData.initialDepths[1]?.Do || "0"),
longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax !== undefined ? formData.initialDepths[2]?.Dmax : "0") || null, // 장기간최대깊이
longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec !== undefined ? formData.initialDepths[2]?.Drec : "0") || null, // 장기간회복비율
longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do !== undefined ? formData.initialDepths[2]?.Do : "0") || null, // 장기간시작깊이
longTermMaxDpth: parseFloat(formData.initialDepths[2]?.Dmax || "0"),
longTermRcvryRt: parseFloat(formData.initialDepths[2]?.Drec || "0"),
longTermBgngDpth: parseFloat(formData.initialDepths[2]?.Do || "0"),
dtrwSwtcRt: parseFloat(formData.rate !== undefined ? formData.rate : "0") || null, // 오수전환율
drtwSwtcRt: parseFloat(formData.rate || "0"), // 오수전환율
anlsInfo: formData.analysisInfo, // 분석정보
tbSnRcptrMhlMpng: matchingResultData, // 수용가-맨홀 매칭결과
@@ -267,8 +271,9 @@ export default function RTKForm() {
// 수정된 registFile 함수를 호출하여 combinedAnalysisData를 전송합니다.
await registFile(combinedAnalysisData);
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
console.error("파일 파싱 중 오류 발생:", error);
alert("파일 처리 중 오류가 발생했습니다.");
return;
@@ -313,12 +318,12 @@ export default function RTKForm() {
return (
// <BaseLayout searchBox={{ title: '분석 대상지역 선택' }} titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }} form={form} onSubmitEvent={() => { }} graphBtn={false} >
<BaseLayout
searchBox={{ title: '분석 대상지역 선택' }}
selectBox={{ title: '분석 대상지역 선택' }}
titleName={{ mainTitle: 'RDII 분석', firstCardTitle: 'RDII 분석자료 입력', secondCardTitle: '실측자료 등록' }}
form={form} // react-hook-form 인스턴스 전달
onSubmitEvent={() => { }} // BaseLayout의 form 태그에 연결될 onSubmit
graphBtn={false}
// select 상태 및 핸들러 전달
analysisBtn={'RDII분석 실행'}
enoCd={select1}
setEnoCd={setSelect1}
ctpvCd={select2}
@@ -329,8 +334,10 @@ export default function RTKForm() {
onSwageTreatmentPlantChange={setSelect4}
selectedSwageProcessingZone={select5}
onSwageProcessingZoneChange={setSelect5}
isLoading={isLoadingResult}
onAnalysis={handleAnalysisClick}
>
{[<div className="space-y-[1.2rem] overflow-x-auto" key="form-section">
{[<div className="space-y-[1.2rem] overflow-x-auto pt-4 pb-4 px-2" key="form-section">
<div className="grid grid-cols-2 gap-4">
<FormSection name="rtkSource" form={form} titleNm="○ RTK계수 출처">
<Input {...form.register("rtkSource")} className="h-12 text-center text-xl w-[225px]" />
@@ -414,16 +421,16 @@ export default function RTKForm() {
}}
// className을 직접 사용하거나, DatePicker 내부에서 selectClass를 처리하도록 수정 필요
// 여기서는 DatePicker의 selectClass prop을 사용하고, date-picker.css의 스타일을 적용
selectClass="ml-5 date-picker-trigger"
selectClass="ml-5 date-picker-trigger"
/>
</FormSection>
<FormSection name='rate' form={form} titleNm='○ 분석 정보'>
<Input {...form.register("analysisInfo")} className="h-12 text-center text-xl w-[225px] ml-5" />
</FormSection>
</div>
<div className="flex justify-end mt-1">
<Button type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button>
</div>
{/* <div className="flex justify-end mt-1">
<Button size="sm" type="submit" onClick={handleAnalysisClick}>RDII분석 실행</Button>
</div> */}
</div>
]}
</BaseLayout >
+258
View File
@@ -0,0 +1,258 @@
'use client'
import { deleteData, getData } from '@/components/lib/api'
import { UiTable, downloadExcel } from '@common/business_components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import BaseLayout from '../BaseLayout'
import { handleDelete } from '@/components/lib/utils/fileDeleteUtil'
const formSchema = z.object({
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>
const tableColumn = [
{ id: 'rainInfwtAnlsSn', title: '분석번호' },
{ id: 'swageTmplntNm', title: '하수처리장' },
{ id: 'swtrmtZoneNm', title: '처리구역' },
{ id: 'aplcnYm', title: '상수사용연월' },
{ id: 'drtwSwtcRt', title: '오수 전환율' },
{ id: 'srcNm', title: 'RTX 계수 출처' },
{ id: 'cntbArea', title: 'RDII 기여면적' },
{ id: 'anlsInfo', title: 'RDII 분석정보' },
{ id: 'regDt', title: '분석일자' },
{ id: 'rgtrNm', title: '분석자' }
]
const Result = () => {
const form = useForm<ResultFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
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 [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태
const [selectedRowData, setSelectedRowData] = useState<any>(null);
const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined);
const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => {
setIsLoadingResult(true);
const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기
console.log("formValues", formValues);
let inftwtAnlsSearchDto;
if (customFormValues) {
inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm,
"enoCd": formValues.enoCd,
"ctpvCd": formValues.ctpvCd,
"sggCd": formValues.sggCd,
"swtrmtZoneCd": formValues.swtrmtZoneCd,
};
} else {
inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null,
"enoCd": formValues.enoCd === '0' ? null : formValues.enoCd,
"ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd,
"sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val,
"swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd,
};
}
console.log("Search DTO:", inftwtAnlsSearchDto);
const queryParams = new URLSearchParams({
page: '0',
size: '10',
sort: 'rainInfwtAnlsSn,desc'
});
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/rdiiAnls${queryString ? `?${queryString}` : ''}`);
try {
const data = await getData(`v1/rdiiAnls${queryString ? `?${queryString}` : ''}`);
setTableData(data.content || []);
setTotalData(tableData.length);
console.log("response:", data);
console.log("response data:", data.content || []);
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
console.error("Error fetching analysis result data:", error);
alert("분석결과 조회 중 오류가 발생했습니다.");
setTableData([]);
setTotalData(0);
}
};
// 테이블 셀 클릭 핸들러 추가
const handleCellClick = (index: number | undefined, headId: string) => {
if (index !== undefined && tableData[index]) {
setSelectedRowData(tableData[index]);
setSelectedRowIndex(index);
console.log("Selected row data:", tableData[index]);
}
};
// Excel 다운로드 핸들러 추가
const handleExcelDownload = async () => {
setIsLoadingResult(true);
console.log("Selected row data for download:", selectedRowData);
if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) {
setIsLoadingResult(false);
alert('다운로드할 데이터를 선택해주세요.');
return;
}
try {
const data = await getData(`v1/rdiiAnlsRslt/ppl/${selectedRowData.rainInfwtAnlsSn}`);
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = data.content; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const fileName = `RDII분석결과_${selectedRowData.rainInfwtAnlsSn}_${selectedRowData.aplcnYm}`;
downloadExcel({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: ["RDII 분석결과"],
headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정
});
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
// 파일 삭제 핸들러 추가
const handleFileDelete = async () => {
setIsLoadingResult(true);
console.log("Selected row data for deletion:", selectedRowData);
if (!selectedRowData || selectedRowData.rainInfwtAnlsSn === undefined) {
alert('삭제할 파일을 선택해주세요.');
return;
}
// 삭제 확인 대화상자
const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?');
if (!confirmDelete) {
return;
}
try {
// 삭제 API 호출
const response1 = await deleteData(`v1/rdiiAnlsRslt/${selectedRowData.rainInfwtAnlsSn}`);
const response2 = await deleteData(`v1/rdiiAnls/${selectedRowData.rainInfwtAnlsSn}`);
// 첨부파일(inp파일) 삭제 API 호출
handleDelete(selectedRowData.atflCd, {
handleDeleteStart: () => console.log("파일 삭제 시작"),
handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response),
handleDeleteError: (error) => console.error("파일 삭제 실패:", error),
});
console.log("File delete response1:", response1);
console.log("File delete response2:", response2);
// 삭제 후 테이블 새로고침을 위해 재검색
const customFormValues: Partial<ResultFormValues> = {
aplcnYm: selectedRowData.aplcnYm || '0',
enoCd: selectedRowData.enoCd || '0',
ctpvCd: selectedRowData.ctpvCd || '0',
sggCd: selectedRowData.sggCd || '0',
swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0',
};
await handleSearchButtonClick(customFormValues);
// 선택된 행 초기화
setSelectedRowData(null);
setSelectedRowIndex(undefined);
alert('삭제가 완료되었습니다.');
} catch (error) {
console.error("분석결과 파일 삭제 중 오류 발생:", error);
alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요.");
}
setIsLoadingResult(false);
};
return (
<BaseLayout
selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }}
titleName={{ mainTitle: '분석결과', firstCardTitle: '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))
}
}}
isLoading={isLoadingResult}
// Excel 다운로드 핸들러 추가
onExcelDownload={handleExcelDownload}
onDeleteFile={handleFileDelete}
selectedRowData={selectedRowData}
>
{[<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">○ RDII 분석결과 현황</div>
<div className="flex-grow overflow-auto">
<UiTable
headName=''
headlist={tableColumn}
tableData={tableData}
total={totalData}
pageSize={10}
children={undefined}
cellClick={handleCellClick}
onPageChange={(e) => { }}
/>
</div>
</div>
]}
</BaseLayout>
)
}
export default Result
+239 -71
View File
@@ -1,89 +1,257 @@
'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 { deleteData, getData } from '@/components/lib/api'
import { UiTable, downloadExcel } 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'
import { handleDelete } from '@/components/lib/utils/fileDeleteUtil'
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와 연동)
})
// TODO: 분석구분에 따라서 테이블 컬럼 변경필요
type ResultFormValues = z.infer<typeof formSchema>
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: 'hasu', title: '하수처리장' },
{ id: 'guyeok', title: '처리구역' },
{ id: 'sangsu', title: '상수 사용량' },
{ id: 'rate', title: '오수 전환율' },
{ id: 'rtx', title: 'RTX계수출처' },
{ id: 'rdii', title: 'RDII 기여면적' },
{ id: 'water', title: '순오수량' },
{ id: 'measureWater', title: '강우유출량' },
{ id: 'ii', title: 'RDII(m2/d)' },
{ id: 'date', title: '분석일자' },
{ id: 'usr', title: '분석자' }
]
const tableInfoData = [{ 'no': '1', 'hasu': '양평하수처리장', 'guyeok': '양평', 'sangsu': '1000', 'rate': '0.5', 'water': '500', 'measureWater': '1000', 'ii': '500', 'date': '2023-10-01', 'usr': '홍길동' }]
const Result = () => {
const form = useForm<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 [isLoadingResult, setIsLoadingResult] = useState<boolean>(false); // 로딩 상태
const [selectedRowData, setSelectedRowData] = useState<any>(null);
const [selectedRowIndex, setSelectedRowIndex] = useState<number | undefined>(undefined);
const handleSearchButtonClick = async (customFormValues?: Partial<ResultFormValues>) => {
setIsLoadingResult(true);
const formValues = customFormValues || form.getValues(); // react-hook-form에서 현재 값 가져오기
console.log("formValues", formValues);
let inftwtAnlsSearchDto;
if (customFormValues) {
inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm,
"enoCd": formValues.enoCd,
"ctpvCd": formValues.ctpvCd,
"sggCd": formValues.sggCd,
"swtrmtZoneCd": formValues.swtrmtZoneCd,
};
} else {
inftwtAnlsSearchDto = {
"aplcnYm": formValues.aplcnYm ? formValues.aplcnYm.toISOString().substring(0, 7).replace("-", "") : null,
"enoCd": formValues.enoCd === '0' ? null : formValues.enoCd,
"ctpvCd": formValues.ctpvCd === '0' ? null : formValues.ctpvCd,
"sggCd": formValues.sggCd?.val === '0' ? null : formValues.sggCd?.val,
"swtrmtZoneCd": formValues.swtrmtZoneCd === '0' ? null : formValues.swtrmtZoneCd,
};
}
console.log("Search DTO:", inftwtAnlsSearchDto);
const queryParams = new URLSearchParams({
page: '0',
size: '10',
sort: 'inftwtAnlsSn,desc'
});
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 || []);
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
console.error("Error fetching analysis result data:", error);
alert("분석결과 조회 중 오류가 발생했습니다.");
setTableData([]);
setTotalData(0);
}
};
// 테이블 셀 클릭 핸들러 추가
const handleCellClick = (index: number | undefined, headId: string) => {
if (index !== undefined && tableData[index]) {
setSelectedRowData(tableData[index]);
setSelectedRowIndex(index);
console.log("Selected row data:", tableData[index]);
}
};
// Excel 다운로드 핸들러 추가
const handleExcelDownload = async () => {
setIsLoadingResult(true);
console.log("Selected row data for download:", selectedRowData);
if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) {
setIsLoadingResult(false);
alert('다운로드할 데이터를 선택해주세요.');
return;
}
try {
const data = await getData(`v1/inftwtAnlsRslt/ppl/${selectedRowData.inftwtAnlsSn}`);
// 선택된 행 데이터를 Excel 다운로드 형식으로 변환
const excelData = data.content; // 배열로 감싸서 전달
// 파일명 생성 (분석번호 기반)
const fileName = `침입수분석결과_${selectedRowData.inftwtAnlsSn}_${selectedRowData.aplcnYm}`;
downloadExcel({
data: excelData as JSON[],
fileName: fileName,
sheetNameArray: ["침입수 분석결과"],
headerRange: "A1:D1" // 테이블 컬럼 수에 맞춰 조정
});
setIsLoadingResult(false);
} catch (error) {
setIsLoadingResult(false);
console.error("Excel 다운로드 중 오류 발생:", error);
alert("Excel 다운로드 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
// 파일 삭제 핸들러 추가
const handleFileDelete = async () => {
setIsLoadingResult(true);
console.log("Selected row data for deletion:", selectedRowData);
if (!selectedRowData || selectedRowData.inftwtAnlsSn === undefined) {
setIsLoadingResult(false);
alert('삭제할 파일을 선택해주세요.');
return;
}
// 삭제 확인 대화상자
const confirmDelete = window.confirm('선택된 분석 결과를 삭제하시겠습니까?');
if (!confirmDelete) {
return;
}
try {
// 삭제 API 호출
const response1 = await deleteData(`v1/inftwtAnlsRslt/${selectedRowData.inftwtAnlsSn}`);
const response2 = await deleteData(`v1/inftwtAnls/${selectedRowData.inftwtAnlsSn}`);
// 첨부파일(inp파일) 삭제 API 호출
handleDelete(selectedRowData.atflCd, {
handleDeleteStart: () => console.log("파일 삭제 시작"),
handleDeleteSuccess: (response) => console.log("파일 삭제 성공:", response),
handleDeleteError: (error) => console.error("파일 삭제 실패:", error),
});
console.log("File delete response1:", response1);
console.log("File delete response2:", response2);
// 삭제 후 테이블 새로고침을 위해 재검색
const customFormValues: Partial<ResultFormValues> = {
aplcnYm: selectedRowData.aplcnYm || '0',
enoCd: selectedRowData.enoCd || '0',
ctpvCd: selectedRowData.ctpvCd || '0',
sggCd: selectedRowData.sggCd || '0',
swtrmtZoneCd: selectedRowData.swtrmtZoneCd || '0',
};
await handleSearchButtonClick(customFormValues);
// 선택된 행 초기화
setSelectedRowData(null);
setSelectedRowIndex(undefined);
setIsLoadingResult(false);
alert('삭제가 완료되었습니다.');
} catch (error) {
setIsLoadingResult(false);
console.error("분석결과 파일 삭제 중 오류 발생:", error);
alert("분석결과 파일 삭제 중 오류가 발생했습니다. 다시 시도해주세요.");
}
};
return (
<BaseLayout searchBox={{labelNm: {first: "분석기간",second: "분석구분",third: "하수처리장",fourth: "하수처리구역",},searchChk: true,}} titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력', secondCardTitle:'RDII 산정결과 현황'}} form={form} onSubmitEvent={() => { }} graphBtn={true}>
{[<div key='table1'>
<BaseLayout
selectBox={{ title: '분석 대상지역 선택', searchChk: true, onSearchClick: handleSearchButtonClick }}
titleName={{ mainTitle: '분석결과', firstCardTitle: '분석이력' }}
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))
}
}}
isLoading={isLoadingResult} // 로딩 상태 전달
// Excel 다운로드 핸들러 추가
onExcelDownload={handleExcelDownload}
onDeleteFile={handleFileDelete}
selectedRowData={selectedRowData}
>
{[<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>,
<div key='table2'>
<UiTable
headName=''
headlist={tableColumn2}
tableData={[]}
total={0}
pageSize={10}
children={undefined}
cellClick={(e) => { }}
onPageChange={(e) => { }}
/>
</div>
]}
<div className="flex-grow overflow-auto">
<UiTable
headName=''
headlist={tableColumn}
tableData={tableData}
total={totalData}
pageSize={10}
children={undefined}
cellClick={handleCellClick}
onPageChange={(e) => { }}
/>
</div>
</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>
)
}
+69 -24
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,23 +142,23 @@ 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>
<div className="font-medium text-white text-label">(00구 00리 하수관로)</div>
{/* <div className="font-medium text-white text-label">(00구 00리 하수관로)</div> */}
</div>
<button
onClick={onClose}
@@ -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>
)
+90 -12
View File
@@ -1,18 +1,20 @@
'use client'
"use client";
import Gis from '@common/business_components/ui/map/Gis';
import { SearchDiv, SelectBox, UiTable, MainDiv, Title, ContentsDiv, SearchFormLeft, SearchFormRight } from '@common/business_components';
import ListDiv from '@common/business_components/ui/list_div';
import { Button, Input } from '@common/components';
import { useEffect, useState } from "react";
import Gis, { useGisContext } from "@common/business_components/ui/map/Gis";
import { mapService } from "@service";
import { useCallback, useEffect, useRef, useState } from "react";
import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./SearchModal";
import FeatureClickEventPopup from "./featureClickEventPopup";
const Map = () => {
const [treeData, setTreeData] = useState([]); // 초기값을 객체 또는 null로 설정
const [selectedFeature,setSelectedFeature] = useState(null); // 선택된 피처 상태
const fetchData = async () => {
const [selectedFeature, setSelectedFeature] = useState(null); // 선택된 피처 상태
const gisMaps = useRef<any | null>(null);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const fetchData = async () => {
try {
const response = await mapService.getLyrStngs();
setTreeData(response.data); // API 응답 저장
@@ -26,10 +28,86 @@ const Map = () => {
fetchData();
}, []);
// SearchModal, GisContent 등에서 호출
const onFeatureSelect = (data: any) => {
let map = gisMaps.current;
if (!map) {
console.warn("지도가 초기화되지 않았습니다.");
return;
}
// 항상 같은 select 객체 사용
const select = GisApp.Module.select as any;
if (!select) return;
// 1. 기존 선택 초기화
select.resetSelectedFeature();
// 2. data가 OpenLayers Feature 인스턴스인지 확인
let foundFeature = null;
if (
data &&
typeof data.getGeometry === "function" &&
typeof data.setStyle === "function"
) {
// 이미 Feature 인스턴스라면 그대로 사용
foundFeature = data;
} else {
// 아니면 featureId로 맵에서 Feature 인스턴스를 찾아서 사용
const featureId = data.fid || data.ftrIdn || data.id;
map
.getLayers()
.getArray()
.forEach((layer: any) => {
const source = layer.getSource?.();
if (source?.getFeatureById) {
const feature = source.getFeatureById(String(featureId).trim());
if (feature) foundFeature = feature;
}
});
}
// 3. 피처 선택
if (foundFeature) {
let geometry = foundFeature.getGeometry();
if (geometry) {
let extent = geometry.getExtent();
map.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
maxZoom: 16,
constrainResolution: true,
nearest: true,
});
}
select.selectFeature(foundFeature, true);
} else {
console.warn("OpenLayers Feature를 찾을 수 없습니다:", data);
}
setFeatureData(data);
setIsOpen(true);
setIsSearchModalOpen(false);
};
return (
<div className="bg-container">
<Gis treeData={treeData} isSearch={true} />
</div>
<Gis
treeData={treeData}
isSearch={true}
gisMaps={gisMaps}
isMiniMap={true}
searchView={SearchModal}
detailView={FeatureClickEventPopup}
onFeatureSelect={onFeatureSelect}
setIsOpen={setIsOpen}
setFeatureData={setFeatureData}
featureData={featureData}
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
/>
</div>
);
};
+2 -1
View File
@@ -20,7 +20,8 @@ export default function Home() {
<div className="flex flex-row gap-4 p-4">
<Link href={"/ii/irsetting"}><Button>침입수 분석</Button></Link>
<Link href={"/ii/rdii"}><Button>RDII 분석</Button></Link>
<Link href={"/ii/result"}><Button>분석결과</Button></Link>
<Link href={"/ii/result"}><Button>침입수 분석 결과</Button></Link>
<Link href={"/ii/rdii_result"}><Button>RDII 분석 결과</Button></Link>
</div>
</div>
</div>
+1 -1
View File
@@ -32,6 +32,6 @@ export type LyrStngProps = {
export const mapService = {
//레이어 설정 조회
getLyrStngs: () => {
return axios.get(`${API_URL}/lyrStngs`)
return axios.get(`${API_URL}/lyrStngs/SN_NETWORK`)
}
}
+2 -1
View File
@@ -33,7 +33,8 @@
"@common/network": ["../../../packages/common/network/"],
"@store": ["./store"],
"@service": ["./service"],
"@utils": ["./components/lib/utils"]
"@utils": ["./components/lib/utils"],
"@common/config": ["../../../packages/common/config/dist/"]
}
},
"include": [
@@ -0,0 +1,137 @@
"use client"
import { Bar, BarChart, CartesianGrid, Legend, XAxis, YAxis } from "recharts";
import {
ChartContainer,
ChartTooltip,
type ChartConfig
} from "./lib/chart";
import type { CSSProperties } from "react";
const customStyles = {
legendContainer: {
display: "flex",
justifyContent: "center",
gap: "20px",
fontSize: "9px",
fontWeight : "bold",
marginTop: "10px",
} as CSSProperties,
};
type BarChartProps = {
chartConfig: ChartConfig | undefined;
chartData: any[] | undefined;
chartTitle: string;
chartClassName?: string;
graphSize?: number;
yFieldText?: string;
showLegend?: boolean;
}
// 커스텀 툴팁
const CustomTooltip = ({
active,
payload,
label
}: {
active?: boolean;
payload?: Array<{
name: string;
value: number;
color: string;
}>;
label?: string;
}) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
{/* 색상 박스 추가 (범례 색상과 동일) */}
<div style={{ width: "10px", height: "10px", backgroundColor: item.color, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value}`}</span>
</div>
))}
</div>
);
}
return null;
};
// 커스텀 범례 (글씨 크기 키우기 + 스타일 추가)
const CustomLegend = (props: any) => {
const { payload } = props;
return (
<div style={customStyles.legendContainer}>
{payload.map((entry: unknown, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: (entry as {color: string}).color }}></div>
<span className="text-[12px]">{(entry as {value: string}).value}</span>
</div>
))}
</div>
);
};
export default function DoubleBarChart({chartConfig=undefined, chartData=[], chartTitle='차트', chartClassName='w-full h-full', graphSize=40, yFieldText='', showLegend=false} : BarChartProps) {
const colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEEAD", "#D4A5A5"];
const getMaxValue = (data: any[], keyPrefix: string) => {
return Math.max(...data.map(item =>
Object.keys(item)
.filter(key => key.includes(keyPrefix))
.map(key => item[key])
).flat());
};
console.log(chartData);
const barMaxValue = getMaxValue(chartData, 'barVal');
return (
<>
{chartTitle && chartTitle !== "" && chartTitle !== "undefined" ? (
<div className="text-left text-[13px] font-bold mt-[13px] mb-[10px]">
{chartTitle}
</div>): null}
<div className="flex justify-center">
<ChartContainer config={chartConfig ?? {}} className={chartClassName + " !w-[90%]"}>
<BarChart accessibilityLayer data={chartData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="label"
tickLine={true}
tickMargin={10}
axisLine={true}
fontSize={13}
fontWeight={500}
/>
<YAxis
tickLine={true}
tickMargin={10}
tickCount={6}
axisLine={true}
fontSize={12}
fontWeight={500}
domain={[0, Math.ceil(barMaxValue * 1)]}
label={yFieldText ? { value: yFieldText, className:"text-[13px] font-bold", position: 'insideLeft', angle: -90, offset: 20 } : undefined}
/>
<ChartTooltip cursor={false} content={<CustomTooltip active={undefined} payload={undefined} label={undefined} />}/>
{chartData && chartData.length > 0 && (
Object.keys(chartData[0])
.filter(key => key !== "label")
.map((key, idx) => (
<Bar
dataKey={key}
radius={5}
key={key}
stackId="a"
barSize={graphSize}
fill={colors[idx]}
/>
))
)}
{showLegend && <Legend content={CustomLegend}/>}
</BarChart>
</ChartContainer>
</div>
</>
)
}
@@ -1,27 +1,116 @@
"use client"
import { Pie, PieChart } from "recharts";
import type { CSSProperties } from "react";
import {
Card,
CardContent
} from "./lib/card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig
} from "./lib/chart";
import { ChartContainer, ChartTooltip } from "./lib/chart";
// eslint-disable-next-line react-refresh/only-export-components
export default (props: { chartConfig: ChartConfig; chartData: never[] | undefined; chartTitle:string}) =>{
type ChartDataItem = {
label: string;
val: number;
};
type ChartConfigItem = {
label: string;
fill: string;
};
type ChartConfig = {
[key: string]: ChartConfigItem;
};
type TooltipPayloadItem = {
name: string;
value: number;
payload: {
label: string;
val: number;
fill: string;
};
};
type LegendEntry = {
value: string;
fill: string;
type?: string;
payload: {
label: string;
val: number;
fill: string;
};
};
type LegendProps = {
payload?: LegendEntry[];
};
/** 차트 기본 스타일 */
const customStyles = {
legendContainer: {
display: "flex",
justifyContent: "center",
gap: "20px",
fontSize: "9px",
fontWeight: "bold",
marginTop: "10px",
} as CSSProperties,
};
// 커스텀 툴팁
const CustomTooltip = ({ active, payload, label }: {
active?: boolean;
payload?: TooltipPayloadItem[];
label?: string
}) => {
if (active && payload && payload.length) {
return (
<div className="w-[180px] rounded bg-white p-2.5 shadow-[0px_2px_10px_rgba(0,0,0,0.2)]">
<p className="text-[12px] font-bold m-3">{label}</p>
{payload.map((item, index) => (
<div key={index} style={{ marginTop: "10px", display: "flex", alignItems: "center", gap: "8px" }}>
<div style={{ width: "10px", height: "10px", backgroundColor: item.payload.fill, borderRadius: "50%" }}></div>
<span className="text-[11px] font-bold">{` ${item.name} : ${item.value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")}`}</span>
</div>
))}
</div>
);
}
return null;
};
// 커스텀 범례
const CustomLegend = (props: LegendProps) => {
const { payload } = props;
if (!payload) return null;
return (
<div style={customStyles.legendContainer}>
{payload.map((entry: LegendEntry, index: number) => (
<div key={`legend-${index}`} className="flex items-center gap-2">
<div className="w-[14px] h-[14px] ml-10" style={{ backgroundColor: entry.fill }}></div>
<span className="text-[12px]">{entry.value}</span>
</div>
))}
</div>
);
};
export default function DoughnutChart(props: {
chartConfig: ChartConfig;
chartData: ChartDataItem[] | undefined;
chartTitle: string
}) {
return (
<Card className="flex flex-col">
<CardContent className="flex-1 pb-0">
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
{props.chartTitle}
</div>): null}
{props.chartTitle && props.chartTitle !== "" && props.chartTitle !== "undefined" ? (
<div style={{ textAlign: "left", fontSize: "13px", fontWeight: "bold", marginTop:"13px", marginBottom: "10px" }}>
{props.chartTitle}
</div>
) : null}
<div className="h-[calc(100%-42px)] justify-center">
<ChartContainer
config={props.chartConfig}
@@ -30,18 +119,22 @@ export default (props: { chartConfig: ChartConfig; chartData: never[] | undefine
<PieChart>
<ChartTooltip
cursor={false}
content={<ChartTooltipContent hideLabel />}
content={<CustomTooltip />}
/>
<Pie
data={props.chartData}
data={props.chartData?.map(item => ({
...item,
fill: props.chartConfig[item.label]?.fill || 'blue'
}))}
dataKey="val"
nameKey="label"
innerRadius={60}
/>
<CustomLegend />
</PieChart>
</ChartContainer>
</div>
</CardContent>
</Card>
)
);
}
@@ -92,9 +92,6 @@ export default function LineBarchart({
return <div>데이터가 없습니다.</div>;
}
const barConfig = chartConfig?.barVal;
const lineConfig = chartConfig?.lineVal;
// 데이터의 최대값 계산
const getMaxValue = (data: any[], keyPrefix: string) => {
return Math.max(...data.map(item =>
@@ -5,6 +5,7 @@ import DoughnutChart from './DoughnutChart';
import { type ChartConfig } from './lib/chart';
import LineChart from './LineChart';
import LineBarchart from './LineBarChart';
import DoubleBarChart from './DoubleBar';
/**차트 FORM */
const CHART_FORM = {
@@ -15,7 +16,9 @@ const CHART_FORM = {
/**도넛 차트 형태*/
DN :'daughnut',
/**LINE,BAR차트 */
LINEBAR : 'linebar'
LINEBAR : 'linebar',
/**더블바 차트 형태*/
DOUBLEBAR : 'doublebar'
};
/**
@@ -34,7 +37,7 @@ const CHART_FORM = {
type MainChartProps = {
chartForm: string;
chartConfig: any;
chartConfig: ChartConfig;
chartData: any;
chartTitle: string;
chartClassName?: string;
@@ -47,23 +50,23 @@ type MainChartProps = {
function MainChart(props: MainChartProps) {
if( props.chartForm == CHART_FORM.BAR ){
return (
<BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}>
</BarChart>
<BarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/>
);
} else if( props.chartForm == CHART_FORM.LINE ){
return (
<LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}>
</LineChart>
<LineChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/>
);
} else if( props.chartForm == CHART_FORM.DN ){
return (
<DoughnutChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}>
</DoughnutChart>
<DoughnutChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle}/>
);
} else if (props.chartForm == CHART_FORM.LINEBAR){
return (
<LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}>
</LineBarchart>
<LineBarchart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} yFieldText2={props.yFieldText2} showLegend={props.showLegend}/>
);
} else if (props.chartForm == CHART_FORM.DOUBLEBAR){
return (
<DoubleBarChart chartConfig={props.chartConfig} chartData={props.chartData} chartTitle={props.chartTitle} chartClassName={props.chartClassName} graphSize={props.graphSize} yFieldText={props.yFieldText} showLegend={props.showLegend}/>
);
}
}
@@ -303,9 +303,7 @@ export const FileUploaderItem = forwardRef<
>
<div className="font-medium leading-none tracking-tight flex items-center gap-1.5 h-full !w-4/5 overflow-hidden "
onClick={() =>{
onClick={() =>{onFileClick?.(index)
}}>
{children}
</div>
@@ -1,5 +1,5 @@
import { Checkbox } from "@common/components/ui";
import React, { useCallback, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { FormControl, FormItem, FormLabel, FormMessage } from "../form";
export const BZNM_TOT_NO = '종사업자등록번호';
@@ -17,7 +17,11 @@ interface FormElementProps extends React.HTMLAttributes<HTMLElement> {
const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
({ children, label, onCheckboxChange, isDisabled }, ref) => {
const [isChecked, setIsChecked] = useState(false);
const [isChecked, setIsChecked] = useState(isDisabled);
useEffect(() => {
setIsChecked(isDisabled);
}, [isDisabled]);
const handleCheckboxChange = useCallback((checked: boolean) => {
setIsChecked(checked);
@@ -35,7 +39,7 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
checked={isChecked}
onCheckedChange={handleCheckboxChange}
/>
<label className="w-20 text-xl text-gray-600" htmlFor="check">해당없음</label>
<label className="w-28 text-xl text-gray-600" htmlFor="check">해당없음</label>
</div>
</div>
{children}
@@ -50,7 +54,7 @@ const FormUi = React.forwardRef<HTMLDivElement, FormUiProps>(
<FormControl>
{React.isValidElement(children) && isDisabled
? React.cloneElement(children, {
disabled: true,
disabled: isDisabled,
...(children.props as FormElementProps)
})
: children}
@@ -19,4 +19,4 @@ export * from "./tab";
export * from "./table";
export * from "./table_div";
export * from "./title";
export * from "./sdmInsertForm";
@@ -9,22 +9,25 @@ import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import GisApp from './lib/gis.core';
import GisContent from './GisContent';
import FeatureClickEventPopup from '@/map/featureClickEventPopup';
import GisTree from './GisTree';
import SearchModal from '@/map/SearchModal';
import GisLonLat from './GisLonLat';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import GisMapToggle from './GisMapToggle';
import { Search } from 'lucide-react';
export const GisContext = createContext({ gisCore: null, gisMap: null, treeData: null, checkedLayerIds: null });
export const useGisContext = () => useContext(GisContext);
function Gis({ treeData = [], isSearch = false }) {
function Gis({ isOpen , gisMaps, isSearchModalOpen, treeData = [], isSearch = false, isMiniMap = false, setIsOpen, setFeatureData, featureData, searchView : SearchView, detailView: DetailView, onFeatureSelect, setIsSearchModalOpen }) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
// const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
// const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터
// const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const gisCore = useMemo(() => new GisApp.core({ targetId: mapRef.current }), []);
const mapInstanceRef = useRef(null); // 맵 생성 여부를 추적할 ref
const mapInstanceRef = useRef(null);
const checkedLayerIds = useRef([]);
let gisMap = null;
const closePopup = () => {
@@ -71,21 +74,6 @@ function Gis({ treeData = [], isSearch = false }) {
if (geometry) {
let extent = geometry.getExtent();
// // EPSG:4326 좌표계로 추정되는 경우(값이 위도/경도 범위)
// console.log('before extent', extent);
// const is4326 =
// extent[0] >= -90 && extent[0] <= 90 &&
// extent[1] >= -180 && extent[1] <= 180 &&
// extent[2] >= -90 && extent[2] <= 90 &&
// extent[3] >= -180 && extent[3] <= 180;
// if (is4326) {
// // extent: [위도, 경도, 위도, 경도] → [경도, 위도, 경도, 위도]로 순서 변경
// const reorderedExtent = [extent[1], extent[0], extent[3], extent[2]];
// // EPSG:4326 extent를 EPSG:3857로 변환
// extent = olProj.transformExtent(reorderedExtent, 'EPSG:4326', 'EPSG:3857');
// console.log('after extent', extent);
// }
gisMap.getView().fit(extent, {
duration: 300,
padding: [50, 50, 50, 50],
@@ -143,21 +131,24 @@ function Gis({ treeData = [], isSearch = false }) {
}),
});
gisMaps.current = map
//미니맵
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
view: new View({
projection: GisApp.TO_PROJ,
minZoom: 9,
}),
collapsed: false,
collapseLabel: '\u00BB',
label: '\u00AB',
});
if(isMiniMap){
const overviewMap = new OverviewMap({
layers: [GisApp.Module.core.baseMiniMapLayers['base']],
view: new View({
projection: GisApp.TO_PROJ,
minZoom: 9,
}),
collapsed: false,
collapseLabel: '\u00BB',
label: '\u00AB',
});
map.addControl(overviewMap);
}
map.addControl(overviewMap);
gisCore.__proto__.map = map;
gisCore.scaleControl();
gisCore.changeBaseLayer('base');
setMapObject({ map: map });
@@ -207,25 +198,46 @@ function Gis({ treeData = [], isSearch = false }) {
<>
<GisContext.Provider value={{ gisCore, gisMap: mapInstanceRef.current, treeData, checkedLayerIds }}>
<div className="h-[calc(100vh-10rem)] flex relative">
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
<GisTree visible={tabVisible} setVisible={setTabVisible} />
<GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} />
{/* <GisContent onFeatureSelect={handleFeatureSelect} isSearch={isSearch} isSearchModalOpen={isSearchModalOpen} setIsSearchModalOpen={setIsSearchModalOpen} /> */}
{/* isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && featureData && !isSearchModalOpen && (
<FeatureClickEventPopup
<DetailView
featureData={featureData}
onClose={closePopup} // 닫기 콜백 전달
/>
)}
<SearchModal
<SearchView
open={isSearchModalOpen}
onOpenChange={setIsSearchModalOpen}
onFeatureSelect={handleFeatureSelect}
onFeatureSelect={onFeatureSelect}
/>
<div ref={mapRef} className="gis-map flex-1 relative" style={{ height: '100%', padding: 0 }} />
{/* <GisLonLat/>
<GisLonLat/>
<GisToolBar/>
<MapToggleBtn/> */}
<GisMapToggle/>
<GisSelect onFeatureSelect={onFeatureSelect} />
</div>
{isSearch && (
<div className="absolute right-8 top-[305px] z-20">
<button
type="button"
onClick={() => setIsSearchModalOpen(true)}
className="overflow-hidden w-[40px] h-[40px] bg-white
border border-[#9aa7b8]
rounded
shadow-[0_0_5px_#999]
flex items-center justify-center
p-0
"
>
<Search size={24} className="text-gray-700" />
</button>
</div>
)}
</GisContext.Provider>
</>
);
@@ -5,7 +5,7 @@ import GisLonLat from './GisLonLat';
import GisSelect from './GisSelect';
import GisToolBar from './GisToolBar';
import './lib/css/gis.edit.css';
import MapToggleBtn from './MapToggleBtn';
import GisMapToggle from './GisMapToggle';
import { Search } from 'lucide-react';
export default function GisContent({ edit = false, onFeatureSelect, isSearch, setIsSearchModalOpen }) {
@@ -51,10 +51,10 @@ export default function GisContent({ edit = false, onFeatureSelect, isSearch, se
return (
<>
{/*<GisTree />*/}
<MapToggleBtn />
{/* <GisMapToggle />
<GisLonLat />
<GisToolBar />
<GisSelect onFeatureSelect={onFeatureSelect} />
<GisToolBar /> */}
{/* <GisSelect onFeatureSelect={onFeatureSelect} /> */}
{/* 검색 모달 */}
{isSearch && (
@@ -1,60 +0,0 @@
import React, { useEffect, useRef, useState } from 'react';
import OLCesium from 'olcs/OLCesium';
import olView from 'ol/View.js';
import olSourceImageWMS from 'ol/source/ImageWMS.js';
import olSourceOSM from 'ol/source/OSM.js';
import olLayerImage from 'ol/layer/Image.js';
import olLayerTile from 'ol/layer/Tile.js';
import olMap from 'ol/Map.js';
import { GLOBAL } from '../../js/consts/global';
const Google3DMap = () => {
const mapRef = useRef(null);
const [ol3d, setOl3d] = useState(null);
useEffect(() => {
if (!mapRef.current) return;
const customProjSource = new olSourceImageWMS({
attributions: '© <a href="http://www.geo.admin.ch/internet/geoportal/en/home.html">National parks / geo.admin.ch</a>',
crossOrigin: 'anonymous',
params: { LAYERS: 'ch.bafu.schutzgebiete-paerke_nationaler_bedeutung' },
projection: 'EPSG:21781',
url: 'https://wms.geo.admin.ch/',
});
const { Cesium } = window;
Cesium.Ion.defaultAccessToken = GLOBAL.CESIUM_ION_TOKEN;
const ol2d = new olMap({
layers: [new olLayerTile({ source: new olSourceOSM() }), new olLayerImage({ source: customProjSource })],
target: mapRef.current,
view: new olView({
center: [860434.6266531206, 6029479.0044273855],
zoom: 6,
}),
});
const ol3dInstance = new OLCesium({
map: ol2d,
time: () => Cesium.JulianDate.now(),
});
const scene = ol3dInstance.getCesiumScene();
Cesium.createWorldTerrainAsync().then(tp => {
scene.terrainProvider = tp;
});
ol3dInstance.setEnabled(true);
setOl3d(ol3dInstance);
return () => {
ol3dInstance.setEnabled(false);
ol2d.setTarget(null);
};
}, []);
return <div ref={mapRef} style={{ width: '100%', height: '100vh' }} />;
};
export default Google3DMap;
@@ -13,10 +13,12 @@ import { Fill, Stroke, Style } from 'ol/style';
import CircleStyle from 'ol/style/Circle';
import VectorLayer from 'ol/layer/Vector';
import { Point } from 'ol/geom';
import { boundingExtent } from 'ol/extent';
import { getTopLeft } from 'ol/extent';
import TileWMTS from 'ol/source/WMTS';
import WMTS from 'ol/tilegrid/WMTS';
import { get as getProjection } from 'ol/proj';
import WMTSTileGrid from 'ol/tilegrid/WMTS';
// import { get as getProjection } from 'ol/proj';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import olSourceImageWMS from 'ol/source/ImageWMS';
import { GeoJSON } from 'ol/format';
import GisOlEditLayer from './gis.edit';
@@ -43,8 +45,8 @@ GisApp.GOOGLE_HYBRID_URL = `https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}&k
GisApp.GOOGLE_3D_URL = `https://tile.googleapis.com/v1/3dtiles/root.json?key=${GisApp.GOOGLE_API_KEY}`;
// TODO: NGII 정보
GisApp.NGII_API_KEY = '305CC242F2920F46BB93BCC13A52DD8A'; // NGII에서 발급받은 API 키
GisApp.NGII_WMTS_URL = `http://map.ngii.go.kr/openapi/wmts_ngiiMap.js?apikey=${GisApp.NGII_API_KEY}`;
GisApp.NGII_API_KEY = '04trYP9_xwLAfALjwZ-B8g'; // NGII에서 발급받은 API 키
GisApp.NGII_WMTS_URL = `https://map.ngii.go.kr/openapi/Gettile.do?apikey=${GisApp.NGII_API_KEY}`;
GisApp.EMAP_URL = '/emap/';
GisApp.EMAP_SATELLITE_URL = '/emap/satellite/';
@@ -92,9 +94,9 @@ GisApp.core.prototype = {
target: '',
centerX: 37.2806094,
centerY: 127.0032502,
zoom: 11,
minZoom: 5,
maxZoom: 22,
zoom: 12,
minZoom: 6,
maxZoom: 20,
},
options: {
@@ -121,14 +123,13 @@ GisApp.core.prototype = {
GisApp.Module.layer = new GisOlLayer(this.defaults);
GisApp.Module.edit = new GisOlEditLayer(this.defaults);
// TODO: vworld layer 정보 (getNGIIbaseMap)활성화 하면 ngii base map으로 변경됨
this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base');
this.baseLayers['base'] = this.getBaseMap('base');
this.baseLayers['satellite'] = this.getBaseMap('satellite');
this.baseLayers['3d'] = this.getBaseMap('3d');
// this.baseLayers['3d'] = this.getNGIIBaseMap('3d');
// this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base');
// this.baseLayers['base'] = this.getNGIIBaseMap('base');
// this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite');
// this.baseMiniMapLayers['base'] = this.getBaseMap('minimap-base');
// this.baseLayers['base'] = this.getBaseMap('base');
// this.baseLayers['satellite'] = this.getBaseMap('satellite');
// this.baseLayers['3d'] = this.getBaseMap('3d');
this.baseLayers['base'] = this.getNGIIBaseMap('base','korean_map');
this.baseLayers['satellite'] = this.getNGIIBaseMap('satellite','satellite_map');
this.baseMiniMapLayers['base'] = this.getNGIIBaseMap('minimap-base','korean_map');
},
layerType: {
point: 'POINT',
@@ -142,44 +143,43 @@ GisApp.core.prototype = {
this.setZoom(15);
},
// NGII Base Map
getNGIIBaseMap: function (_id) {
const layers = {
base: 'korean_map', // 일반지도
satellite: 'satellite_map', // 위성지도
night: 'night_map', // 야간지도
};
// 선택된 레이어
const layerName = layers[_id] || layers.base;
// EPSG:5179 좌표계 설정
const projection = getProjection('EPSG:5179');
const extent = [705680.0000, 1349270.0000,1388291.0000, 2581448.0000]; // NGII 좌표계 범위
const resolutions = [
2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255,
];
const tileGrid = new WMTS({
extent: extent,
resolutions: resolutions,
matrixIds: resolutions.map((_, index) => index.toString()), // WMTS matrix IDs
});
getNGIIBaseMap: function (_id, _layer) {
// EPSG:5179 정의 및 등록이 필요함 (아래에 설명 추가)
proj4.defs([['EPSG:5179','+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',],]);
register(proj4);
const projection = olProj.get('EPSG:5179');
projection.setExtent([-200000.0, -28024123.62, 31824123.62, 4000000.0]);
// const resolutions = [2088.96, 1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255];
// const matrixIds = ["L05", "L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"];
const resolutions = [1044.48, 522.24, 261.12, 130.56, 65.28, 32.64, 16.32, 8.16, 4.08, 2.04, 1.02, 0.51, 0.255];
const matrixIds = ["L06", "L07", "L08", "L09", "L10", "L11", "L12", "L13", "L14", "L15", "L16", "L17", "L18"];
// WMTS 소스 생성
const wmtsSource = new TileWMTS({
url: GisApp.NGII_WMTS_URL,
layer: layerName,
layer: _layer,
matrixSet: 'EPSG:5179',
format: 'image/png',
projection: projection,
tileGrid: tileGrid,
style: 'default',
crossOrigin: 'anonymous',
style: 'korean',
tileGrid: new WMTSTileGrid({
origin: getTopLeft(projection.getExtent()),
resolutions: resolutions,
matrixIds: matrixIds,
tileSize: [256, 256]
}),
requestEncoding: 'KVP',
crossOrigin: 'anonymous'
});
// TileLayer 생성
return new TileLayer({
source: wmtsSource,
let baseMap = new TileLayer({
id: _id,
visible: true,
zIndex: 0,
type: 'base',
source: wmtsSource,
zIndex: 0
});
return baseMap;
},
getBaseMap: function (_id) {
let url = GisApp.VWORLD_URL;
@@ -32,7 +32,7 @@ GisOlSelect.prototype = {
selectFeatureFlag: false,
initialize: function (map, options) {
this.map = map;
_.extend(this.defaults, options);
Object.assign(this.defaults, options);
this.setInteraction();
this.pointerMoveEventHandler();
},
@@ -8,8 +8,6 @@ import Text from 'ol/style/Text.js';
import Point from 'ol/geom/Point.js';
import RegularShape from 'ol/style/RegularShape.js';
import Overlay from 'ol/Overlay';
import _ from 'lodash';
//import DOMPurify from 'dompurify';
import { createPortal } from 'react-dom';
import ReactDOM from 'react-dom/client';
import React from 'react';
@@ -96,7 +94,7 @@ GisOlStyle.prototype = {
scale = Math.max(minScale, Math.min(maxScale, scale));
image.setScale(scale);
style.setImage(image);
_.extend(style, options);
Object.assign(style, options);
return style;
},
line: function (layer, options, feature, resolution) {
@@ -125,7 +123,7 @@ GisOlStyle.prototype = {
color,
});
style.setFill(fill);
_.extend(style, options);
Object.assign(style, options);
return style;
},
lineLabelPoligon: function (layer, options, feature, resolution) {
@@ -152,7 +150,7 @@ GisOlStyle.prototype = {
}
style.setFill(fill);
style.setStroke(stroke);
_.extend(style, options);
Object.assign(style, options);
return style;
},
imagePoligon: function (layer, options, feature, resolution) {
@@ -169,8 +167,7 @@ GisOlStyle.prototype = {
image.setScale(1);
style.setGeometry(retGeom);
style.setImage(image);
_.extend(style, options);
Object.assign(style, options);
return style;
},
setOverlay: function (layer, feature, overlayHtml) {
@@ -267,7 +264,7 @@ GisOlStyle.prototype = {
style.setStroke(null);
style.setFill(null);
style.setText(text);
_.extend(style, options);
Object.assign(style, options);
return style;
},
@@ -0,0 +1,377 @@
import { FormField } from "../form";
import { SelectBox } from "../select";
import { Input } from "@common/components";
import { FormUi } from "../form_ui";
import { useCallback, useEffect, useState } from "react";
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
import { SdmSearchForm } from "./sdmNameBox";
import { set } from "date-fns";
// 수요처 정보
export type DemandCompType = {
index : number | '';
dmdInfoPk : string | '';
dmdInfoFk : string | '';
bzmnNo : string | '';
dmdCoNm : string | '';
daddr : string | '';
telno : string | '';
rgtr : string | '';
useUsgCd : string | '';
ctpvCd : string | '';
sggCd : string | '';
dmdAplyPswd : string | '';
wntUntprc : number | '';
dmndTmng : Date | '';
dmndQty : number | '';
grssClfg : number | '';
tn : number | '';
combResdch : number | '';
tph : number | '';
trbt : number | '';
ph : number | '';
blgcOxygDmndQnt : number | '';
clIonQnt : number | '';
smll : number | '';
cdtnv : number | '';
crmty : number | '';
rmrk : string | '';
reqDt : string | '';
}
type SelectDataType = {
text: string;
val: string;
};
interface FormSchema {
ctpvCd: string;
sggCd: string;
dmdCoNm: string;
dmdInfoPk: string;
daddr: string;
telno: string;
bznmTotNo?: string;
rgtr?: string;
dmdAplyPswd?: string;
}
interface SdmRegFormProps {
form: UseFormReturn<FormSchema>;
onCheckboxChange?: (checked: boolean) => void;
selectSidoData: SelectDataType[];
selectSigunData: SelectDataType[];
dataList: DemandCompType[];
rtnValue: (searchText: string, pageCnt: number) => void;
setIsDuplicate: (value: boolean) => void;
updateList?:DemandCompType
}
type FieldComponent = (field: ControllerRenderProps<FormSchema, keyof FormSchema>) => JSX.Element;
export function SdmRegForm({ form, onCheckboxChange, selectSidoData, selectSigunData, dataList, rtnValue, setIsDuplicate ,updateList}: SdmRegFormProps) {
const [sidoValue, setSidoValue] = useState<string>('');
const [sigunValue, setSigunValue] = useState<string>('');
const [filteredSigunData, setFilteredSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0' }]);
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(true);
const [orgBznmNo, setOrgBznmNo] = useState<string>('');
const [updateCoNm, setUpdateNm] = useState<string >('')
const initializeSigunData = useCallback((sidoCd: string) => {
if (sidoCd === '0') {
setSigunValue('0');
setFilteredSigunData([{ text: "전체", val: '0' }]);
} else {
const newFilteredData = [
{ text: "전체", val: '0' },
...selectSigunData.filter((item: SelectDataType) => item.val.substring(0, 2) === sidoCd)
];
setSigunValue('0');
setFilteredSigunData(newFilteredData);
}
}, [selectSigunData]);
const onSelectValue = useCallback((
val: string,
type: string,
field: ControllerRenderProps<FormSchema, keyof FormSchema> | undefined
) => {
if(!updateList?.dmdInfoPk)
switch (type) {
case "ctpvCd":
initializeSigunData(val);
setSidoValue(val);
field?.onChange(val);
form.setValue('dmdInfoPk', '');
form.setValue('bznmTotNo', '');
break;
case "sggCd":
setSigunValue(val);
field?.onChange(val);
form.setValue('dmdInfoPk', '');
form.setValue('bznmTotNo', '');
break;
}
}, [initializeSigunData, form,updateList]);
const handleCheckboxChange = useCallback((checked: boolean) => {
setIsBznmTotNoDisabled(checked);
onCheckboxChange?.(checked);
}, [onCheckboxChange]);
const setRtnValue = useCallback((searchText: string, pageCnt: number) => {
rtnValue(searchText, pageCnt);
}, [rtnValue]);
const setRtnData = useCallback((item: DemandCompType) => {
const rtnCompData = item;
form.setValue('dmdCoNm', rtnCompData?.dmdCoNm || '');
form.setValue('dmdInfoPk', rtnCompData?.dmdInfoPk.substring(0,10) || '');
form.setValue('bznmTotNo', rtnCompData?.dmdInfoPk.substring(10,14)==='0000' ? '' : rtnCompData?.dmdInfoPk.substring(10,14));
// 수요처 불러온 후 사업자번호를 바꿀경우를 대비함
setOrgBznmNo(rtnCompData?.dmdInfoPk);
const sidoCd = rtnCompData?.ctpvCd.trim() || '';
setSidoValue(sidoCd);
form.setValue('ctpvCd', sidoCd);
initializeSigunData(sidoCd);
setTimeout(() => {
const sigunCd = rtnCompData?.sggCd.trim() || '';
setSigunValue(sigunCd);
form.setValue('sggCd', sigunCd);
}, 100);
if(rtnCompData?.dmdInfoPk.substring(10,14) === '0000') {
handleCheckboxChange(true);
} else {
handleCheckboxChange(false);
}
form.setValue('daddr', rtnCompData?.daddr || '');
form.setValue('telno', rtnCompData?.telno || '');
// 선택을 했을 때 중복 상태를 true로 설정
setIsDuplicate(true);
}, [form, handleCheckboxChange, initializeSigunData, setIsDuplicate]);
const setRtnText = useCallback((text: string) => {
form.setValue('dmdCoNm', text);
}, [form]);
const bznNoChange = useCallback((e: React.ChangeEvent<HTMLInputElement>, noType: string, field?: ControllerRenderProps<FormSchema, keyof FormSchema>) => {
const value = e.target.value;
if(noType === 'dmdInfoPk') {
if(orgBznmNo.substring(0,10) === value) {
setIsDuplicate(true);
} else {
setIsDuplicate(false);
}
field?.onChange(value);
} else {
if(orgBznmNo.substring(10,14) === value) {
setIsDuplicate(true);
} else {
setIsDuplicate(false);
}
field?.onChange(value);
}
}, [orgBznmNo, setIsDuplicate]);
useEffect(() => {
if(updateList?.dmdInfoPk){
form.reset(updateList)
if(updateList?.dmdInfoPk.slice(10,14) !== '0000') {
handleCheckboxChange( false);
form.setValue('bznmTotNo', updateList?.dmdInfoPk.slice(10,14));
} else {
handleCheckboxChange( true);
}
form.setValue('dmdInfoPk', updateList?.dmdInfoPk.slice(0,10));
setUpdateNm(updateList?.dmdCoNm)
// setSigunValue(updateList?.sggCd)
}else{
handleCheckboxChange( true);
}
}, [updateList,form]);
useEffect(() => {
},[form.formState.errors])
const formFields: Array<{
name: keyof FormSchema;
label: string;
component: FieldComponent;
}> = [
{
name: "ctpvCd",
label: "시도",
component: (field) => (
updateList?.dmdInfoPk ? (
<Input
placeholder="시도"
type="text"
className="!text-xl text-gray-600"
onChange={(e) =>{field.onChange(e.target.value)} }
value={ selectSidoData.find(item => item.val === field.value?.trim() )?.text??''}
readOnly={!!updateList?.dmdInfoPk}
/>
) : (
<SelectBox
selectClass={undefined}
label=""
selectArray={selectSidoData}
onSelectValue={(val) => onSelectValue(val.toString(), 'ctpvCd', field??'')}
className=""
value={sidoValue}
/>
)
)
},
{
name: "sggCd",
label: "시군구",
component: (field) => (
updateList?.dmdInfoPk ? (
<Input
placeholder="시도"
type="text"
className="!text-xl text-gray-600"
onChange={(e) =>{field.onChange(e.target.value)} }
value={selectSigunData.find(item => item.val === field.value?.trim() )?.text??''}
readOnly={!!updateList?.dmdInfoPk}
/>
) : (
<SelectBox
selectClass={"!text-xl text-gray-600"}
label=""
selectArray={filteredSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), 'sggCd', field)}
className=""
value={sigunValue}
/>
)
)
},
{
name: "dmdCoNm",
label: "회사명",
component: (field) => (
<SdmSearchForm
rtnData={setRtnData}
label="수요처명"
initValue={updateCoNm}
dataList={dataList}
rtnValue={setRtnValue}
rtnText={setRtnText}
{...field}
/>
)
},
{
name: "dmdInfoPk",
label: "사업자등록번호",
component: (field) => (
<Input
placeholder="사업자등록번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
onChange={(e) => bznNoChange(e, 'dmdInfoPk', field)}
value={field.value}
readOnly={!!updateList?.dmdInfoPk}
/>
)
},
{
name: "bznmTotNo",
label: "종사업자등록번호",
component: (field) => (
<FormUi
label="종사업자등록번호"
onCheckboxChange={handleCheckboxChange}
isDisabled={isBznmTotNoDisabled}
>
<Input
placeholder="여러사업장이 있을 경우 입력해주세요"
type="text"
className="!mt-5 !text-xl text-gray-600"
disabled={isBznmTotNoDisabled}
onChange={(e) => bznNoChange(e, 'bznmTotNo', field)}
// {...field}
value={ field.value}
readOnly={!!updateList?.dmdInfoPk}
/>
</FormUi>
)
},
{
name: "daddr",
label: "사업자주소",
component: (field) => (
<Input
placeholder="주소를 입력하세요"
type="text"
className="!text-xl text-gray-600"
{...field}
/>
)
},
{
name: "telno",
label: "전화번호",
component: (field) => (
<Input
placeholder="전화번호를 입력하세요"
type="text"
className="!text-xl text-gray-600"
{...field}
/>
)
},
{
name: "dmdAplyPswd",
label: "패스워드",
component: (field) => (
<Input
placeholder="패스워드를 입력하세요"
type="password"
className="!text-xl text-gray-600"
{...field}
/>
)
}
]
return (
<>
{formFields.filter((item)=>
{if(updateList?.dmdInfoPk){
return item.name !== 'dmdAplyPswd'
}else{
return item
}
})
.map(({ name, label, component }) => (
<FormField
key={String(name)}
control={form.control}
name={name}
render={({ field }) => (
name === 'bznmTotNo' ? component(field) : (
<FormUi label={label}>
{component(field)}
</FormUi>
)
)}
/>
))}
</>
);
}
@@ -0,0 +1,196 @@
"use client";
import { SdmRegForm, type DemandCompType } from "./form";
import { Button } from "@common/components/ui";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useEffect, useState } from "react";
import { FormProvider, useForm, type SubmitHandler } from "react-hook-form";
import { toast, Toaster } from "sonner";
import * as z from "zod";
import type { SelectDataType } from "../search_input_select";
type thisProps = {
selectSidoData: SelectDataType[];
selectSigunData: SelectDataType[];
dataList: DemandCompType[];
rtnValue: (searchText: string, pageCnt: number) => void;
rtnInputData: (data: DemandCompType) => void;
bodyClassNm?: string;
updateList?:DemandCompType
}
const createFormSchema = (isBznmTotNoDisabled: boolean, isDuplicate: boolean ,updateList?:DemandCompType) => z.object({
ctpvCd: z.string().min(1, "시/도를 선택해주세요"),
sggCd: z.string().min(1, "시/군/구를 선택해주세요"),
dmdCoNm: z.string()
.min(1, "회사명을 입력해주세요")
.max(20, "회사명은 20자 이내로 입력해주세요"),
dmdInfoPk: z.string()
.min(1, "사업자등록번호를 입력해주세요")
.regex(/^\d{10}$/, "사업자등록번호는 10자리 숫자만 입력 가능합니다")
.refine(() => isBznmTotNoDisabled ? !isDuplicate : true, "이미 등록된 사업자등록번호입니다"),
bznmTotNo: isBznmTotNoDisabled
? z.string().optional()
: z.string()
.length(4, "종사업자등록번호는 4자리 숫자만 입력 가능합니다")
.refine(() => !isDuplicate, "이미 등록된 사업자등록번호입니다"),
daddr: z.string()
.min(1, "주소를 입력해주세요")
.min(6, "주소는 6자 이상 입력해주세요")
.max(40, "주소는 40자 이내로 입력해주세요"),
telno: z.string()
.min(8, "전화번호는 8자 이상 입력해주세요")
.max(11, "전화번호는 11자 이내로 입력해주세요")
.regex(/^[0-9-]+$/, "전화번호는 숫자와 하이픈(-)만 입력 가능합니다"),
rgtr: z.string().optional(),
dmdAplyPswd : updateList?.dmdInfoPk // 업데이트일경우
? z.string().optional() // 패스워드는 선택 사항이며 길이 제한 없음
: z.string().optional()
.refine((val) => !val || (val.length >= 8 && val.length <= 16), "패스워드는 8자 이상 16자 이내로 입력해주세요"),
});
type FormSchema = z.infer<ReturnType<typeof createFormSchema>>;
const defaultValues: FormSchema = {
ctpvCd: '',
sggCd: '',
dmdCoNm: '',
dmdInfoPk: '',
bznmTotNo: '',
daddr: '',
telno: '',
rgtr: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
dmdAplyPswd: '',
};
/**
* 수요처 등록 폼
* @param selectSidoData 시/도 데이터
* @param selectSigunData 시/군/구 데이터
* @param dataList 수요처 목록
* @param rtnValue 검색 결과(텍스트) 콜백
* @param rtnInputData 수요처 리스트 콜백
* @returns
*/
/**
* 페이지에서 작업
const {Common, initializeCommonData, getDemandCompList, insertSdmData, Sdm} = useDmStore((state) => state);
const { selectSidoData, selectSigunData } = Common;
const { demandCompList } = Sdm;
// 공통데이터 (시도, 시군구 데이터 호출)
useEffect(() => {
initializeCommonData();
}, [initializeCommonData]);
// 컴포넌트에서 텍스트 검색 호출
const callback = useCallback((searchText: string, pageCnt: number) => {
searchText = searchText.trim();
if(searchText.length > 0) {
getDemandCompList(searchText, pageCnt);
}
}, [getDemandCompList]);
// 컴포넌트에서 수요처 등록 호출
const handleInsertSdmData = useCallback((data: DemandCompType) => {
insertSdmData(data);
}, [insertSdmData]);
*/
export default function SdmInsertForm({selectSidoData, selectSigunData, dataList, rtnValue, rtnInputData,bodyClassNm, updateList}: thisProps) {
/* const { insertSdmData } = useDmStore((state) => state);
const router = useRouter(); */
const [isBznmTotNoDisabled, setIsBznmTotNoDisabled] = useState(false);
const [isDuplicate, setIsDuplicate] = useState(false);
const formSchema = createFormSchema(isBznmTotNoDisabled, isDuplicate, updateList );
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
defaultValues,
mode: "onChange",
criteriaMode: "all",
shouldFocusError: false,
shouldUnregister: false
});
// 체크박스 상태가 변경될 때마다 bznmTotNo 필드의 에러를 초기화
useEffect(() => {
if (isBznmTotNoDisabled===true) {
form.clearErrors('bznmTotNo');
form.setValue('bznmTotNo', '');
}
}, [isBznmTotNoDisabled, form]);
const setRtnValue = useCallback((searchText: string, pageCnt: number) => {
rtnValue(searchText, pageCnt);
}, [rtnValue]);
const onSubmit: SubmitHandler<FormSchema> = async (data) => {
try {
// form 에러 체크
const formErrors = form.formState.errors;
if (Object.keys(formErrors).length > 0) {
const errorMessage = Object.values(formErrors)[0]?.message || '입력값을 확인해주세요.';
toast.error(errorMessage);
return;
}
const dmdInfoPk = data.dmdInfoPk + (data.bznmTotNo || '0000');
const formData = {
...data,
dmdInfoPk,
bzmnNo: data.dmdInfoPk,
};
console.log(' formData ', formData)
/* await insertSdmData(formData); */
rtnInputData(formData as DemandCompType);
//router.push(`/pages/sdmMng/sdmInfoInsert?${queryParams.toString()}`);
} catch (error) {
console.error('Error submitting form:', error);
}
};
return (
<>
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 underline">
수요처등록
</h1>
</div>
<div className= {bodyClassNm?bodyClassNm:"!max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"}>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<SdmRegForm
form={form}
onCheckboxChange={setIsBznmTotNoDisabled}
selectSidoData={selectSidoData}
selectSigunData={selectSigunData}
dataList={dataList}
rtnValue={setRtnValue}
setIsDuplicate={setIsDuplicate}
updateList={updateList}
/>
<div className="flex justify-center pt-4">
<Button
type="submit"
className="w-32 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded-md transition-colors duration-200"
>
수요처등록
</Button>
</div>
</form>
</FormProvider>
</div>
<Toaster position="top-center" />
</>
);
}
@@ -0,0 +1,111 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { DemandCompType } from './form';
export type thisProps = {
label: string | undefined;
initValue: string | undefined;
dataList : DemandCompType[];
rtnValue : (searchText: string, pageCnt: number) => void;
rtnData : (item: DemandCompType) => void;
rtnText : (text: string) => void;
}
/**
* 수요처 검색 컴포넌트
* @param label 라벨
* @param initValue 초기값
* @param rtnValue 검색 결과 반환 함수
* @param dataList 검색할 때 넘기는 데이터 리스트
* @param rtnData 검색 결과 반환 함수
*/
export const SdmSearchForm = ({label=undefined, initValue=undefined, rtnValue=()=>{}, dataList=[], rtnData=()=>{}, rtnText=()=>{} } :thisProps) => {
const [isOpen, setIsOpen] = useState(false);
const [textValue, setTextValue] = useState(initValue || '');
const wrapperRef = useRef<HTMLDivElement>(null);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setTextValue(value);
rtnValue(value, 10);
rtnText(value);
if (value) {
setIsOpen(true);
} else {
setIsOpen(false);
}
}, [rtnValue, rtnText]);
const handleSuggestionClick = useCallback((item: DemandCompType) => {
setTextValue(item.dmdCoNm);
rtnData(item);
setIsOpen(false);
}, [rtnData]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
useEffect(() => {
if(initValue&& initValue?.length>0){
setTextValue(initValue)
}
},[initValue])
return (
<div ref={wrapperRef}>
<input
type="text"
value={textValue}
onChange={handleInputChange}
/* onFocus={() => setIsOpen(true)} */
className="w-full h-[3.2rem] !text-xl pl-3 text-gray-600 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={`${label}을(를) 입력하세요`}
/>
{isOpen && (
<div className=" z-1 w-full mt-2 bg-white border border-gray-300 rounded-md shadow-lg h-90 overflow-auto">
<div className="max-h-72">
{dataList && dataList.length > 0 ? dataList.map((item : DemandCompType, index : number) => (
<div
key={index}
className="!text-xl w-full text-left px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto"
onClick={() => handleSuggestionClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSuggestionClick(item);
}
}}
>
{item.dmdInfoPk.substring(10,14) !== '0000' ? (
<>
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}&nbsp;
<span className="text-gray-500">종사업자번호 : </span> {item.dmdInfoPk.substring(10,14)}&nbsp;
</>
) : (
<>
<span className="text-gray-500">사업자번호 : </span> {item.dmdInfoPk.substring(0,10)}&nbsp;
</>
)}
<span className="float-right">{item.dmdCoNm}</span>
</div>
)) : (
<div className="!text-xl w-full px-5 py-3 hover:bg-gray-100 cursor-pointer overflow-auto">
검색 결과가 없습니다.
</div>
)}
</div>
</div>
)}
</div>
)
}
@@ -20,6 +20,7 @@ type SearchBaseType = {
initClick:() => void;
btnClass:string;
ref:React.RefObject<ReturnType>;
isAdmin:boolean;
}
export type ReturnType = {
partValue: string;
@@ -29,9 +30,10 @@ export type ReturnType = {
unitWaterShedValue: string;
middleRegionValue: string;
envPartValue: string;
sidoText: string;
sigunText: string;
}
function SearchBaseForm ({
leftChildren,
rightChildren,
@@ -46,11 +48,14 @@ function SearchBaseForm ({
queryClick,
excelClick,
initClick,
ref
ref,
isAdmin=false
}: SearchBaseType) {
const [partValue, setPartValue] = useState<string>("A");
const [sidoValue, setSidoValue] = useState<string>("0");
const [sigunValue, setSigunValue] = useState<string>("0");
const [sidoText, setSidoText] = useState<string>("");
const [sigunText, setSigunText] = useState<string>("");
const [regionValue, setRegionValue] = useState<string>("0");
const [untitWaterShedValue, setUntitWaterShedValue] = useState<string>("0");
const [middleRegionValue, setMiddleRegionValue] = useState<string>("0");
@@ -66,6 +71,8 @@ function SearchBaseForm ({
unitWaterShedValue: untitWaterShedValue,
middleRegionValue: middleRegionValue,
envPartValue: envPartValue,
sidoText: sidoText,
sigunText: sigunText,
});
}
const initVal = useCallback(() => {
@@ -86,6 +93,8 @@ function SearchBaseForm ({
unitWaterShedValue: untitWaterShedValue,
middleRegionValue: middleRegionValue,
envPartValue: envPartValue,
sidoText: sidoText,
sigunText: sigunText,
})
};
useEffect(() => {
@@ -98,8 +107,10 @@ function SearchBaseForm ({
customRef.unitWaterShedValue = untitWaterShedValue;
customRef.middleRegionValue = middleRegionValue;
customRef.envPartValue = envPartValue;
customRef.sidoText = sidoText;
customRef.sigunText = sigunText;
}
}, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, ref]);
}, [partValue, sidoValue, sigunValue, regionValue, untitWaterShedValue, middleRegionValue, envPartValue, sidoText, sigunText, ref]);
return (
<div className="flex card card-border !mt-6">
<SearchFormLeft isShowPart={true}
@@ -118,7 +129,10 @@ function SearchBaseForm ({
setRtnEnvPartValue={setEnvPartValue}
setRtnEnvPartSidoValue={setSidoValue}
setRtnEnvPartSigunValue={setSigunValue}
setRtnSidoText={setSidoText}
setRtnSigunText={setSigunText}
resetTrigger={resetTrigger}
isAdmin={isAdmin}
>
{leftChildren}
</SearchFormLeft>
@@ -40,7 +40,10 @@ type SearchFormLeftProps = {
setRtnEnvPartValue?: (value: string) => void;
setRtnEnvPartSidoValue?: (value: string) => void;
setRtnEnvPartSigunValue?: (value: string) => void;
setRtnSidoText?: (value: string) => void;
setRtnSigunText?: (value: string) => void;
resetTrigger?: boolean;
isAdmin?: boolean;
}
export type EnvPartSelectDataType = SelectDataType & {
@@ -53,12 +56,14 @@ const selectPartData:SelectDataType[] = [
{ text: "환경청별", val: 'G' },
];
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) {
function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSidoText, setRtnSigunValue, setRtnSigunText, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false, isAdmin = false }: SearchFormLeftProps) {
/******************선택된 데이터******************/
const [partValue, setPartValue] = useState<string>('A');
//행정별 시도 시군구
const [sidoValue, setSidoValue] = useState<string>('0');
const [sigunValue, setSigunValue] = useState<string>('0');
const [sidoText, setSidoText] = useState<string>('');
const [sigunText, setSigunText] = useState<string>('');
//유역별
const [regionValue, setRegionValue] = useState<string>('0');
const [unitWaterShedValue, setUnitWaterShedValue] = useState<string>('0');
@@ -152,6 +157,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
}
const onSelectValue = (val: string, type: string) => {
let selectedSido;
let selectedSigun;
let selectedEnvPartSido;
let selectedEnvPartSigun;
switch(type) {
case 'part':
setPartValue(val);
@@ -180,9 +190,14 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
break;
case 'sido':
setSidoValue(val);
selectedSido = selectSidoData.find(item => item.val === val);
setSidoText(selectedSido?.text || '');
if (setRtnSidoValue) {
setRtnSidoValue(val);
}
if (setRtnSidoText) {
setRtnSidoText(selectedSido?.text || '');
}
initializeSigunData(val);
onSelectValue(selectSigunData[0]?.val || '0', 'sigun');
break;
@@ -191,6 +206,11 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
setRtnSigunValue(val);
}
setSigunValue(val);
selectedSigun = filteredSigunData.find(item => item.val === val);
if (setRtnSigunText) {
setRtnSigunText(selectedSigun?.text || '');
}
setSigunText(selectedSigun?.text || '');
break;
// 유역 세팅
@@ -228,6 +248,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
break;
case 'envPartSido' :
setSidoValue(val);
selectedEnvPartSido = filteredEnvPartSidoData.find(item => item.val === val);
setSidoText(selectedEnvPartSido?.text || '');
if (setRtnEnvPartSidoValue) {
setRtnEnvPartSidoValue(val);
}
@@ -236,6 +258,8 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
break;
case 'envPartSigun' :
setSigunValue(val);
selectedEnvPartSigun = filteredEnvPartSigunData.find(item => item.val === val);
setSigunText(selectedEnvPartSigun?.text || '');
if (setRtnEnvPartSigunValue) {
setRtnEnvPartSigunValue(val);
}
@@ -285,7 +309,7 @@ function SearchFormLeft({ isShowPart = false, children, selectSidoData = [], sel
return (
isShowPart ? (
<div className="grid grid-cols-12 gap-5 w-11/12 m-4">
<SelectBox className="col-span-3" label={'구분'} selectArray={selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/>
<SelectBox className="col-span-3" label={'구분'} selectArray={isAdmin ? selectPartData.slice(0, 1) : selectPartData} onSelectValue={(val) => onSelectValue(val.toString(), 'part')} value={partValue} selectClass={undefined}/>
{partValue === 'R' ? (
<>
<SelectBox className="col-span-3" label={'유역'} selectArray={selectRegionData} onSelectValue={(val) => onSelectValue(val.toString(), 'region')} value={regionValue} selectClass={undefined}/>
@@ -44,11 +44,11 @@ const HeadMakeColSpan = ({ upHeadList }: { upHeadList: TableUpperProps[] }) => {
<TableRow key={rowIndex+ " headerRow"} className="bg-gray-50">
{upHeadList
.filter((item) => item.visible != false)
.map((head: TableUpperProps) => {
.map((head: TableUpperProps, idx) => {
if (head.row === rowIndex) {
return (
<TableHead
key={head.id}
key={head.id?head.id:rowIndex+"_"+idx}
colSpan={head.colSpan || 1}
rowSpan={head.rowSpan || 1}
style={{ whiteSpace: 'pre-line' }}
@@ -29,7 +29,6 @@ type UiHeadProps = {
export function UiTable({ tableData, headlist, pageSize, total, headName, children, cellClick, onPageChange, className = '', resetPageNo = false, boldArray = [] }: UiTableProps) {
const [cur, setCur] = useState<number>(1);
const [idx, setIdx] = useState<number>(-1);
useEffect(() => {
if (resetPageNo) {
setCur(1);