This commit is contained in:
2025-08-06 12:41:35 +09:00
23 changed files with 581 additions and 11740 deletions
@@ -18,8 +18,8 @@ export interface Link {
next_link_excn_dt: string;
last_link_err_cd: string;
mdfcn_dt: string;
mdfr: string;
link_batch_time: string;
mdfr_id_id: string;
link_cycle_mi: string;
}
export interface LinkLog {
@@ -48,8 +48,8 @@ export interface LinkInfo extends LinkFormData {
next_link_excn_dt: string;
last_link_err_cd: string;
mdfcn_dt: string;
mdfr: string;
link_batch_time: number;
mdfr_id: string;
link_cycle_mi: number;
}
export interface LinkStats {
@@ -65,7 +65,7 @@ export interface LinkVrbl {
link_vrbl_cd: string;
link_vrbl_expln: string;
mdfcn_dt: string;
mdfr: string;
mdfr_id: string;
}
export interface LinkTable {
@@ -94,7 +94,7 @@ export interface LinkTableColumns {
pk_yn: string;
col_seq: number;
mdfcn_dt: string;
mdfr: string;
mdfr_id: string;
}
export const linkFormSchema = z.object({
@@ -118,8 +118,8 @@ export const linkFormSchema = z.object({
next_link_excn_dt: z.string().max(40).nullable().optional(),
last_link_err_cd: z.string().max(10).nullable().optional(),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
link_batch_time: z.coerce.number().nullable().optional().refine((val) => {
mdfr_id: z.string().nullable().optional(),
link_cycle_mi: z.coerce.number().nullable().optional().refine((val) => {
if (val == null) return true;
return val >= 5;
}, {
@@ -135,7 +135,7 @@ export const linkVrblFormSchema = z.object({
link_vrbl_cd: z.string().max(10, { message: '변수코드는 10자 이하로 입력해 주세요.' }).nullable().optional(),
link_vrbl_vl: z.string().max(1000, { message: '변수값은 1000자 이하로 입력해 주세요.' }).nonempty({ message: '변수값은 필수입니다.' }),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
mdfr_id: z.string().nullable().optional(),
});
export const LinkTableColumnsSchema = z.object({
@@ -168,7 +168,7 @@ export const LinkTableColumnsSchema = z.object({
message: "컬럼 매칭 순서는 0 이상이어야 합니다."
}),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
mdfr_id: z.string().nullable().optional(),
});
export const LinkTableSchema = z.object({
@@ -182,7 +182,7 @@ export const LinkTableSchema = z.object({
trsf_tbl_korn_nm: z.string().max(40, { message: '40자 이하로 입력해 주세요.' }).nonempty({ message: '변환테이블 한글명은 필수입니다.' }),
trsf_tbl_expln: z.string().max(2000, { message: '설명은 2000자 이하로 입력해 주세요.' }).nullable().optional(),
mdfcn_dt: z.string().nullable().optional(),
mdfr: z.string().nullable().optional(),
mdfr_id: z.string().nullable().optional(),
});
interface LinkStatsListObject {
@@ -53,8 +53,8 @@ const columns = [
header: '연계서버 IP',
cell: (info) => (info.getValue() ? info.getValue() : '-'),
}),
columnHelper.accessor('link_batch_time', {
id: 'link_batch_time',
columnHelper.accessor('link_cycle_mi', {
id: 'link_cycle_mi',
header: '연계 주기',
cell: (info) => convertMinutesToPeriod(Number(info.getValue())),
}),
+4 -4
View File
@@ -336,7 +336,7 @@ const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink,
<TableCell>
<FormField
control={form.control}
name="link_batch_time"
name="link_cycle_mi"
render={({ field }) => {
const timeParts = convertMinutesToPeriod(Number(field.value));
return (
@@ -434,9 +434,9 @@ const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink,
</div>
</div>
</FormControl>
{form.formState.errors.link_batch_time && (
{form.formState.errors.link_cycle_mi && (
<p className="text-red-500 text-base pt-2">
{form.formState.errors.link_batch_time.message}
{form.formState.errors.link_cycle_mi.message}
</p>
)}
</FormItem>
@@ -520,7 +520,7 @@ const LinkForm = ({ form, linkSn, updateLinkInfo, handleInsertLink, deleteLink,
<TableCell>
<FormField
control={form.control}
name="mdfr"
name="mdfr_id"
render={({ field }) => (
<FormItem>
<FormControl>
+3 -3
View File
@@ -35,7 +35,7 @@ function ModifyLinkPage({ linkSn }: { linkSn: string }) {
mutationFn: (data: LinkFormData) => LinkApi.updateLinkInfo({
...data,
link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null,
mdfr: userUniqId
mdfr_id: userUniqId
}),
onSuccess: () => {
alert('수정되었습니다.');
@@ -73,11 +73,11 @@ function ModifyLinkPage({ linkSn }: { linkSn: string }) {
if (linkInfo) {
let newData = { ...linkInfo };
if (linkInfo.mdfr) {
if (linkInfo.mdfr_id) {
const userName = await fetchUserInfo();
newData = {
...linkInfo,
mdfr: userName,
mdfr_id: userName,
};
}
+3 -3
View File
@@ -27,7 +27,7 @@ function RegistLinkPage() {
link_id: '',
link_pswd: '',
srvc_usg: '',
link_batch_time: 5,
link_cycle_mi: 5,
link_clsf_cd: 'none',
},
});
@@ -44,8 +44,8 @@ function RegistLinkPage() {
const insertData = {
...data,
link_clsf_cd: data.link_clsf_cd !== 'none' ? data.link_clsf_cd : null,
next_link_excn_dt: dayjs().add(Number(data.link_batch_time), 'minute').format('YYYY-MM-DD HH:mm:ss'),
mdfr: userUniqId,
next_link_excn_dt: dayjs().add(Number(data.link_cycle_mi), 'minute').format('YYYY-MM-DD HH:mm:ss'),
mdfr_id: userUniqId,
};
insertLinkInfo(insertData);
@@ -292,7 +292,7 @@ function LinkParamsDialog({ selectedLink, form, insertLinkVrbl, dialogOpen, onOp
<TableCell>
<FormField
control={form.control}
name="mdfr"
name="mdfr_id"
render={({ field }) => (
<FormItem>
<FormControl>
@@ -112,7 +112,7 @@ function LinkParamsPage() {
await LinkApi.insertLinkVrbl({
...data,
link_sn: selectedLink.linkSn,
mdfr: userUniqId,
mdfr_id: userUniqId,
}),
onSuccess: async () => {
// 쿼리 무효화를 통한 최신 데이터 가져오기
@@ -128,7 +128,7 @@ function LinkParamsPage() {
mutationFn: async (data: LinkVrblFormData) => await LinkApi.updateLinkVrblInfo({
...data,
link_sn: selectedLink.linkSn,
mdfr: userUniqId,
mdfr_id: userUniqId,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({
@@ -229,11 +229,11 @@ function LinkParamsPage() {
if (linkVrblInfo) {
let newData = { ...linkVrblInfo };
if (linkVrblInfo.mdfr) {
if (linkVrblInfo.mdfr_id) {
const userName = await fetchUserInfo();
newData = {
...linkVrblInfo,
mdfr: userName,
mdfr_id: userName,
};
}
@@ -101,7 +101,7 @@ function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLink
col_seq: null,
pk_yn: 'N',
mdfcn_dt: '',
mdfr: '',
mdfr_id: '',
});
}
@@ -444,7 +444,7 @@ function LinkTableDialog({ dialogOpen, onOpenChange, form, tableInfo, insertLink
<TableCell>
<FormField
control={form.control}
name="mdfr"
name="mdfr_id"
render={({ field }) => (
<FormItem>
<FormControl>
@@ -78,7 +78,7 @@ function LinkTablePage() {
const { mutateAsync: insertLinkTableCol } = useMutation({
mutationFn: async (data: LinkTableColData) => await LinkApi.insertLinkTableCol({
...data,
mdfr: userUniqId,
mdfr_id: userUniqId,
}),
onSuccess: async () => {
if (selectedTableInfo) {
@@ -104,7 +104,7 @@ function LinkTablePage() {
const { mutateAsync: updateLinkTableCol } = useMutation({
mutationFn: async (data: LinkTableColData) => await LinkApi.updateLinkTableCol({
...data,
mdfr: userUniqId,
mdfr_id: userUniqId,
}),
onSuccess: async () => {
if (selectedTableInfo) {
@@ -280,11 +280,11 @@ function LinkTablePage() {
if (colInfo) {
let newData = { ...colInfo };
if (colInfo.mdfr) {
if (colInfo.mdfr_id) {
const userName = await fetchUserInfo();
newData = {
...colInfo,
mdfr: userName,
mdfr_id: userName,
};
}
@@ -319,7 +319,7 @@ function LinkTablePage() {
if (await confirm(confirmMsg)) {
insertLinkTableInfo({
...data,
mdfr: userUniqId,
mdfr_id: userUniqId,
tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "",
});
}
@@ -330,7 +330,7 @@ function LinkTablePage() {
updateLinkTableInfo({
...data,
tbl_sn: (regTblInfo.count > 0) ? regTblInfo.tbl_sn : "",
mdfr: userUniqId,
mdfr_id: userUniqId,
});
}
});
@@ -1,88 +1,62 @@
'use client'
import React, { useState,useEffect } from 'react'
import Modal from '@/components/ui/modal';
import React, { useState } from 'react'
import { VerticalTable } from '@/components/ui/verticalTable';
import { UiTable } from '@common/business_components/ui';
import { UiModal } from '@common/business_components/ui';
import { Button } from '@common/components/ui';
import { useDcTree, flattenFolders, getFileDownload, addFileName, type FlattenedFolder} from '../../searchModal/searchModalReportApi';
import ReportTab from '../../searchModal/reportTab';
// 하수처리시설
const headList = [
{ id: "title", title: "하수도정비 기본계획보고서" },
];
const modalHeadList = [
{ id: "no", title: "번호" },
{ id: "year", title: "승인연도" },
{ id: "cad", title: '평면도' },
{ id: "fileNm", title: "파일명" },
{ id: "date", title: "입력시간" },
]
const SwtrmtFclt = ({ gisObject }: { gisObject: any }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalTitle, setModalTitle] = useState<string>('');
const [dcDataOne, setDcDataOne] = useState<FlattenedFolder|undefined>({});
const openModal = (title: string) => {
setIsModalOpen(true);
setModalTitle(title)
};
const closeModal = () => {
setIsModalOpen(false);
};
const ctpvNm = gisObject?.ctpv_nm;
//const reportOriginData = useDcTree(ctpvNm);
const reportOriginData = useDcTree("강원특별자치도");
useEffect(() => {
const fetch = async () => {
if (!reportOriginData || reportOriginData.length === 0) {
return;
}
const reportData = flattenFolders(reportOriginData);
let filterReportData = [...reportData];
if (gisObject && gisObject.sgg_nm !== undefined && gisObject.sgg_nm !== "") { // 시군구검색
//const sggNm = gisObject?.sgg_nm;
const sggNm = "강릉시";
filterReportData = filterReportData.filter(item => item.sigungu === sggNm);
}
addFileName(filterReportData).then(data => {
filterReportData = data.filter(item => item.filePath !== undefined && item.filePath !== "");
if (filterReportData !== undefined && filterReportData.length > 0) {
setDcDataOne(filterReportData[0]);
}
});
console.log("filterReportData", filterReportData);
};
fetch();
}, [reportOriginData]);
// 하수도정비기본계획 filedownload 핸들러
const handleFileDownloadClick = () => {
if (dcDataOne && dcDataOne !== undefined && dcDataOne.fileSn !== undefined && dcDataOne.fileSn !== ""
&& dcDataOne.filePath !== undefined && dcDataOne.filePath !== "" && dcDataOne.fileName !== undefined && dcDataOne.fileName !== "" ) {
console.log("dcDataOne", dcDataOne);
getFileDownload(String(dcDataOne.fileSn), String(dcDataOne.fileName), String(dcDataOne.filePath));
}
};
//cad: <Button onClick={() => openModal('하수도대장 평면도')}>평면도</Button>,
const ctpvNm = gisObject?.ctpv_nm;
const sggNm = gisObject?.sgg_nm;
return (
<div>
<VerticalTable
children={{
title: <Button className="gray" onClick={() => handleFileDownloadClick()}>다운로드</Button>
}} headName='디지털 자료관 연계정보' tableData={[]} headlist={headList} pageSize={20} total={0} cellClick={undefined} isEditable={true} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}></VerticalTable>
{/* <Modal isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} closeModal={closeModal}>
<div className='grid grid-col-2'>
<UiTable headName={modalTitle} tableData={[]} headlist={modalHeadList} pageSize={20} total={0} cellClick={undefined} onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}
}></UiTable>
</div>
</Modal> */}
</div>
<VerticalTable
children={{
title: <Button className="gray" onClick={() => setIsModalOpen(true)}>조회</Button>
}}
headName='디지털 자료관 연계정보'
tableData={[]}
headlist={headList}
pageSize={20}
total={0}
cellClick={undefined}
isEditable={true}
onPageChange={function (page: number): void {
throw new Error("Function not implemented.");
}}
/>
<UiModal
open={isModalOpen}
onOpenChange={setIsModalOpen}
headerTitle="하수도정비 기본계획보고서"
headerTitleClass="text-3xl font-semibold text-gray-700"
bodyClass={`
h-[100vh]
w-[60vw]
max-w-[60vw]
max-h-[100vh]
overflow-hidden
z-[101]
`}
body={
<ReportTab
open={isModalOpen}
isSearch={false}
ctpvNm={ctpvNm}
sggNm={sggNm}
/>
}
useDirectRender={true}
/>
</div >
)
}
@@ -4,7 +4,7 @@ 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, useCallback, useMemo } from "react";
import { useEffect, useRef, useState, useMemo } from "react";
import { getLayerContentComponent } from "../common";
import { SwtrmtFcltCol, SwtrmtFcltCol2, SwtrmtPrcsZoneCol, SwtrmtPrcsZoneCol2, untrtSwgRainCol1, untrtSwgRainCol2, MsSwagePplCol, MsSwagePplCol2, ExclnSewagePumpCol, ExclnSewagePumpCol2, DrtwSewagePumpCol, DrtwSewagePumpCol2, MsSwageMhlCol, MsSwageMhlCol2, stofcbrCol, stofcbrCol2, MS_SWAGE_MHL_CODES, DRTW_SEWAGE_PUMP_USE_CODES } from "../common/table";
import InfilChart from './chart/infilChart';
@@ -61,9 +61,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
let featureLayer: { get: (key: string) => any }; // 피처가 속한 레이어를 찾기 위한 변수
layers.some((layer: any) => {
const source = layer.getSource();
// console.log('실행됨3 source', source);
// console.log('실행됨3 featureData', featureData);
// console.log('실행됨3 featureData.fid', featureData.fid);
// if (source && featureData && featureData.fid) {
// featureLayer = layer; // 피처가 속한 레이어 찾기
// }
@@ -131,8 +128,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
setIsClicked(false);
console.log('SearchModal data', featureData);
const lyrTyNm = featureData.lyrTypeNm;
console.log('실행됨4 SearchModal 클릭 이벤트', lyrTyNm);
console.log('실행됨4 SearchModal 클릭 이벤트 featureData', featureData);
if (featureData.lyrTypeNm === 'tb_ms_swage_ppl') {
result = '하수관로';
setHeadlist(MsSwagePplCol2)
@@ -187,7 +182,6 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
const selectedLayerName = getSelectedLayers(); // 레이어 이름 가져오기
setLayerNm(selectedLayerName); // 상태 업데이트
// console.log('gis data', featureData.values_);
let data;
if (featureData['ol_uid']) data = featureData.values_; // Gis 레이어 클릭 이벤트일 때
else data = featureData; // SearchModal 클릭 이벤트일 때
@@ -271,11 +265,12 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
// 자체입력정보 API 엔드포인트 선택
let apiEndpoint = '';
const getFeatureId = featureId.toString().split('.').pop() || null;
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${getFeatureId}`;
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${getFeatureId}`;
else if (selectedLayerName === '오수펌프' || selectedLayerName === '빗물펌프') apiEndpoint = `v1/fcltpump/${getFeatureId}`;
else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${getFeatureId}`;
const fcltMngNo = featureData?.values_?.fclt_mng_no || null;
const mngInstCd = featureData?.values_?.mng_inst_cd || null;
if (selectedLayerName === '하수관로') apiEndpoint = `v1/fcltppl/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === '우수토실') apiEndpoint = `v1/fcltstofcbr/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === '하수펌프장') apiEndpoint = `v1/fcltpump/${fcltMngNo}/${mngInstCd}`;
else if (selectedLayerName === '하수맨홀') apiEndpoint = `v1/fcltmhl/${fcltMngNo}/${mngInstCd}`;
// API에서 상세 데이터 가져오기
if (apiEndpoint) {
@@ -354,6 +349,7 @@ const FeatureClickEventPopup = ({ onClose, featureData }: { onClose: () => void,
else if (layerNm === '하수맨홀') apiEndpoint = 'v1/fcltmhl';
bodyData['fcltMngNo'] = featureData.values_.fclt_mng_no.toString() || null;
bodyData['mngInstCd'] = featureData.values_.mng_inst_cd.toString() || null;
// 빈 키 제거
if (bodyData[""] !== undefined) delete bodyData[""];
@@ -1,15 +1,15 @@
import { VerticalTable } from '@/components/ui/verticalTable';
import React, { useState, useEffect } from 'react'
// 빗물펌프 자체 입력 정보
// 빗물펌프장 자체 입력 정보
const exclnInputColumn = [
{ id: "swtrmtZoneCd", title: "배수구역", type: "input" },
{ id: "mltvDtdvNm", title: "배수분구", type: "input" },
{ id: "dsgnFrqnc", title: "설계빈도(년)", type: "input" },
{ id: "designFrqYr", title: "설계빈도(년)", type: "input" },
]
// 오수펌프 자체 입력 정보
// 오수펌프장 자체 입력 정보
const drtwInputColumn = [
{ id: "swtrmt_zone_cd", title: "처리구역", type: "input" },
{ id: "mltv_dtdv_nm", title: "처리분구", type: "input" },
{ id: "swtrmtZoneCd", title: "처리구역", type: "input" },
{ id: "mltvDtdvNm", title: "처리분구", type: "input" },
]
// 하수펌프장
+25 -4
View File
@@ -7,7 +7,7 @@ import GisApp from "@common/business_components/ui/map/lib/gis.core";
import SearchModal from "./searchModal/searchModal";
import FeatureClickEventPopup from "./components/popupComponent/featureClickEventPopup";
import { SearchBar } from "./searchBar/searchBar";
import { WaterDepth } from "./waterDepth/waterDepth";
import { WaterDepth, WATER_DEPTH_LAYER_TYPES, ORDERED_LAYERS } from "./waterDepth/waterDepth";
// 타입 정의 추가
export type LyrStngProps = {
@@ -40,6 +40,8 @@ const Map = () => {
const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
const [featureData, setFeatureData] = useState(null); // 선택된 피쳐 데이터
const [showWaterDepth, setShowWaterDepth] = useState(false); // WaterDepth 표시 여부
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
@@ -118,6 +120,21 @@ const Map = () => {
setIsSearchModalOpen(false);
};
// WaterDepth 표시 여부를 결정하는 함수
const handleWaterDepthVisibility = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
const waterDepthLayers = checkedNodesData.filter(node =>
WATER_DEPTH_LAYER_TYPES.includes(node.data?.lyrTyNm)
);
const hasWaterDepthLayer = waterDepthLayers.length > 0;
setShowWaterDepth(hasWaterDepthLayer);
}, []);
// 체크된 레이어 변경 처리 함수
const handleCheckedLayersChange = useCallback((checkedIds: string[], checkedNodesData: any[]) => {
handleWaterDepthVisibility(checkedIds, checkedNodesData);
}, [handleWaterDepthVisibility]);
return (
<div className="bg-container">
@@ -135,10 +152,14 @@ const Map = () => {
setIsSearchModalOpen={setIsSearchModalOpen}
isOpen={isOpen}
isSearchModalOpen={isSearchModalOpen}
onCheckedLayersChange={undefined}
onCheckedLayersChange={handleCheckedLayersChange}
searchBar={SearchBar}
// waterDepth={WaterDepth}
waterDepth={undefined}
waterDepth={showWaterDepth ? WaterDepth : undefined}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying as any}
targetLayerTypes={WATER_DEPTH_LAYER_TYPES as any}
orderedLayers={ORDERED_LAYERS as any}
checkInterval={2500}
/>
</div>
);
@@ -4,9 +4,9 @@ import { Button, Label } from '@common/components';
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
import { SelectBox, UiTable } from '@common/business_components/ui';
import LoadingOverlay from '@/components/ui/LoadingBar';
import { addFileName, flattenFolders, getFileDownload, useCityCd, useDcTree, useSggCd, reportGubuns } from './searchModalReportApi';
import { type SelectDataType } from '../commonStore';
import { addFileName, flattenFolders, getFileDownload, getDcTree, useDcTree, reportGubuns } from './searchModalReportApi';
import useStore from '../../../store/useStore';
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
// 하수도정비 기본계획 보고서
@@ -15,18 +15,19 @@ export const reportTableHeadlist = [
{ id: "sido", title: "시도" },
{ id: "sigungu", title: "시군구" },
{ id: "approvalYear", title: "승인년도" },
{ id: "plantZoneGugun", title: "구분" },
{ id: "plantZoneGubun", title: "구분" },
{ id: "reportGubun", title: "보고서구분" },
{ id: "fileName", title: "파일명" },
{ id: "regDate", title: "등록일자" },
];
interface BasicTabProps {
interface ReportTabProps {
open?: boolean;
isSearch?: boolean;
ctpvNm?: string;
sggNm?: string;
}
const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sggNm }) => {
const [isLoading, setIsLoading] = useState(false);
const [isResetPage, setIsResetPage] = useState<boolean>(false);
@@ -36,24 +37,20 @@ const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
const [tableCount, setTableCount] = useState(0);
const [curPage, setCurPage] = useState(1);
const [yearFilter, setYearFilter] = useState({ start: "", end: "" });
const [ctpvCd, setCtpvCd] = useState('0'); // 하수정비기본계획 report
const [sigungu, setSigungu] = useState('0'); // 하수정비기본계획 report
const [filteredSiguns, setFilteredSiguns] = useState<SelectDataType[]>([{ val: '0', text: '전체', sido: '0' }]);
const [gubun, setGubun] = useState(""); // 하수정비기본계획 report
const [reportGubun, setReportGubun] = useState("0"); // 하수정비기본계획 report
const [ctpvCd, setCtpvCd] = useState(0);
const [sggCd, setSggCd] = useState(0);
const [reportGubun, setReportGubun] = useState("0");
// const [gubun, setGubun] = useState("");
const { ctpv, sgg } = useStore();
// 하수정비기본계획 report
const sidos = useCityCd();
const siguns = useSggCd();
const reportOriginData = useDcTree(""); //:Folder[]
const dynamicHeadlist: { id: string, title: string }[] = reportTableHeadlist;
useEffect(() => {
// filterParams 초기화
setCtpvCd('0');
setSigungu('0');
setCtpvCd(0);
setSggCd(0);
setReportGubun('0');
setYearFilter({ start: "", end: "" });
setShowTable(false);
@@ -64,27 +61,36 @@ const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
setIsResetSelection(true); // 선택 초기화
}, [open]);
useEffect(() => {
let newCtpvCd = ctpv.find((n) => n.name === ctpvNm)?.id || 0;
let newSggCd = sgg.find((n) => n.name === sggNm)?.id || 0;
setCtpvCd(newCtpvCd);
setSggCd(newSggCd);
}, [ctpvNm, sggNm]);
// ctpvCd, sggCd가 업데이트된 후에 fetchFilteredData 실행
useEffect(() => {
if (!isSearch && ctpvCd && sggCd) {
fetchFilteredData();
}
}, [reportOriginData, ctpvCd, sggCd, isSearch]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
// showTable이 true일 때만 fetch (최초 렌더 방지)
setIsResetPage(false);
setIsResetSelection(true); // 선택 초기화
if (showTable) fetchFilteredData();
}, [curPage]);
const handleCtpvCdChange = (val: string | number) => { // 하수도정비기본계획.시도
const sidoOption = sidos.find((n) => n.val === val);
setCtpvCd(String(val));
if (val !== '0') {
const filterSiguns = siguns.filter(item => item.sido === sidoOption?.text);
filterSiguns.unshift({ val: '0', text: '전체', sido: '0' });
setFilteredSiguns(filterSiguns);
}
setSigungu("0");
setGubun("");
setYearFilter({ start: "", end: "", });
}
setIsResetSelection(true);
if (showTable) fetchFilteredData();
setIsResetSelection(false);
}, [curPage]);
const handleCtpvCdChange = (sidoId: number) => {
setCtpvCd(sidoId);
setSggCd(0);
}
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
const handleSearch = () => {
@@ -96,7 +102,7 @@ const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
};
// 하수도정비기본계획 테이블 cellClick 핸들러
const handleReportCellClick = (index: number | undefined ) => {
const handleReportCellClick = (index: number | undefined) => {
console.log(index);
if (typeof index === "number" && tableData[index]) {
const reportData = tableData[index];
@@ -107,23 +113,30 @@ const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
}
}
};
// 검색 API 호출 함수
const fetchFilteredData = async () => {
setIsLoading(true); // 로딩 시작
setIsResetSelection(true); // 선택 초기화
try {
//const reportOriginData = await getDcTree('경기도');
const reportData = flattenFolders(reportOriginData);
let filterReportData = reportData;
if (ctpvCd !== "0") { // 시도검색
const sidoOption = sidos.find((n) => n.val === ctpvCd);
filterReportData = filterReportData.filter(item => item.sido === sidoOption?.text);
console.log("필터링 전 보고서 데이터:", filterReportData);
console.log('ctpv, sgg, ctpvCd, sggCd', ctpv, sgg, ctpvCd, sggCd);
if (ctpvCd) { // 시도검색
console.log('시도 실행되나');
const sidoOption = ctpv.find((n) => String(n.id) === String(ctpvCd));
console.log('sidoOption', sidoOption)
filterReportData = filterReportData.filter(item => item.sido === sidoOption?.name);
console.log('시도 필터링 후:', filterReportData);
}
if (sigungu !== "0") { // 시군구검색
const sigunguOption = filteredSiguns.find((n) => n.val === sigungu);
filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text);
if (sggCd) { // 시군구검색
console.log('시군구 실행되나');
const sigunguOption = sgg.find((n) => String(n.id) === String(sggCd));
filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.name);
console.log('시군구 필터링 후:', filterReportData);
}
if (reportGubun !== "0") { // 보고서구분검색
const reportGubunOption = reportGubuns.find((n) => n.val === reportGubun);
@@ -139,14 +152,16 @@ const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null;
});
}
if (gubun !== "") { // 구분검색
filterReportData = filterReportData.filter(item => (item.plantZoneGugun) ? String(item.plantZoneGugun).includes(String(gubun)) : null);
}
// if (gubun !== "") { // 구분검색 (추후 사용할 수도 있을 것 같아 남겨둠)
// filterReportData = filterReportData.filter(item => (item.plantZoneGubun) ? String(item.plantZoneGubun).includes(String(gubun)) : null);
// }
console.log('report count:', filterReportData.length);
setTableCount(filterReportData.length || 0);
filterReportData = filterReportData.slice((curPage - 1) * 10, curPage * 10); // paging
filterReportData = filterReportData?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 }));
//const fileList = await getDcFile("CLS20250530000041");
addFileName(filterReportData).then(data => {
filterReportData = data;
setTableData(filterReportData || []);
@@ -159,69 +174,70 @@ const ReportTab: React.FC<BasicTabProps> = ({ open }) => {
setTableData([]);
} finally {
setIsLoading(false); // 로딩 종료
setIsResetSelection(false);
}
};
return (
<>
<div className="relative flex flex-col border rounded-lg p-5 w-full h-[10vh]">
<div className="flex items-center mb-3">
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">시도</Label>
<SelectBox className="w-60 mr-8"
selectClass="rounded-lg bg-white"
label="" selectArray={sidos}
value={ctpvCd} onSelectValue={handleCtpvCdChange}
selectContentClass="z-[101]"
/>
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">시군구</Label>
<SelectBox className="w-48"
selectClass="w-48 rounded-lg bg-white truncate"
label="" selectArray={filteredSiguns}
value={sigungu} onSelectValue={val => setSigungu(String(val))}
selectContentClass="z-[101]"
/>
</div>
<div className="flex items-center mb-6">
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">보고서 구분</Label>
<SelectBox className="w-44 mr-8"
selectClass="rounded-lg bg-white"
label="" selectArray={[...reportGubuns]}
value={reportGubun} onSelectValue={val => setReportGubun(String(val))}
selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2'
/>
{/* 승인년도 필터 */}
<YearRangeSelectBox start={yearFilter.start} end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="승인년도" endLabel=""
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
<div className={`relative flex flex-col border rounded-lg p-5 w-full ${isSearch ? "h-[10vh]" : "h-[6vh]"}`}>
{isSearch && (
<>
<div className="flex items-center mb-3">
<CtpvSggPrcsFcltSelectBox
sido={ctpvCd}
sigungu={sggCd}
onSidoChange={(sidoId: number) => { handleCtpvCdChange(sidoId) }}
onSigunguChange={setSggCd}
setIsLoading={setIsLoading}
/>
</div>
</>
)}
<div className="flex items-center mb-6">
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">보고서 구분</Label>
<SelectBox className="w-44 mr-8"
selectClass="rounded-lg bg-white"
label=""
selectArray={[...reportGubuns]}
value={reportGubun} onSelectValue={val => setReportGubun(String(val))}
selectContentClass="z-[101]" labelClass='whitespace-nowrap text-xl text-gray-600 pr-2'
/>
{/* 승인년도 필터 */}
<YearRangeSelectBox
start={yearFilter.start}
end={yearFilter.end}
onChange={range => setYearFilter({ ...yearFilter, start: range.start, end: range.end })}
startLabel="승인년도" endLabel=""
/>
{/* 조회 버튼 */}
<div className="flex-1 flex justify-end">
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
</div>
</div>
</div>
</div>
<div className="h-[79vh] w-full overflow-hidden rounded-lg border">
{/* 테이블2 영역 (가로/세로 스크롤 가능) */}
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
return (<>조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={tableData}
headlist={dynamicHeadlist}
pageSize={10}
total={tableCount}
cellClick={handleReportCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
/>
<div className="h-[79vh] w-full overflow-hidden rounded-lg border">
{/* 테이블2 영역 (가로/세로 스크롤 가능) */}
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<UiTable
headName={(() => {
return (<>조회 결과 ({tableCount.toLocaleString('ko-KR')})건</>);
})()
}
tableData={tableData}
headlist={reportTableHeadlist}
pageSize={10}
total={tableCount}
cellClick={handleReportCellClick}
onPageChange={setCurPage}
resetPageNo={isResetPage}
resetSelection={isResetSelection}
/>
</div>
</div>
</div>
</>
);
};
@@ -1,39 +1,35 @@
import {useState, useEffect} from 'react';
import { useState, useEffect } from 'react';
import { getDcData } from '@/components/lib/api';
const ADDR_URL = process.env.NEXT_PUBLIC_ADDR_URL;
export type Folder = {
clsfSystmSn: string;
upClsfSystmSn: string;
clsfSystmNm: string;
children: Folder[];
};
clsfSystmSn: string;
upClsfSystmSn: string;
clsfSystmNm: string;
children: Folder[];
};
export type FlattenedFolder = {
[key: string]: string|number|undefined;
};
type SelectDataType = {
text: string;
val: string;
sido?: string; // 시도 코드 (선택 사항)
[key: string]: string | number | undefined;
};
export type SelectDataType = {
text: string;
val: string;
sido?: string; // 시도 코드 (선택 사항)
}
type ReportDataFile = {
no : number;
fileTitle : string;
regDt : string;
fileSn : string;
filePath : string;
no: number;
fileTitle: string;
regDt: string;
fileSn: string;
filePath: string;
}
const isYear = (value: string): boolean => {
return /^[0-9]{4}년$/.test(value);
};
export const reportGubuns = [{ val: '0', text: '전체' }, { val: '1', text: '보고서' }
, { val: '2', text: '요약보고서' }, { val: '3', text: '부록' }, { val: '4', text: '기타' }];
export const reportGubuns = [{val: '0', text: '전체'}, {val: '1', text: '보고서'}
, {val: '2', text: '요약보고서'} , {val: '3', text: '부록'}, {val: '4', text: '기타'}];
/**
* Flatten a hierarchical folder structure into a flat array.
* @param data - The hierarchical folder data to flatten.
@@ -41,18 +37,20 @@ export const reportGubuns = [{val: '0', text: '전체'}, {val: '1', text: '보
*/
export function flattenFolders(data: Folder[]): FlattenedFolder[] {
const result: FlattenedFolder[] = [];
const exceptNodeNm = ["부분","통합","전체"];
const addNodeNm = ["보고서","요약보고서","부록","기타"]; // level5
const columnNms: { [key: number]: string } = { 0: "root", 1:"sido", 2:"sigungu", 3:"approvalYear"
,4:"plantZoneGugun", 5:"reportGubun", 7:"fileName", 8:"regDate" };
const exceptNodeNm = ["부분", "통합", "전체"];
const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5
const columnNms: { [key: number]: string } = {
0: "root", 1: "sido", 2: "sigungu", 3: "approvalYear"
, 4: "plantZoneGubun", 5: "reportGubun", 7: "fileName", 8: "regDate"
};
function traverse(
node: Folder,
path: { [key: string]: string| number },
path: { [key: string]: string | number },
level: number
) {
if (level === 2 && isYear(node.clsfSystmNm)) {
level++;
level++;
}
if (level === 4 && addNodeNm.includes(node.clsfSystmNm)) {
level++; // 레벨을 증가시켜 하위값에 추가되도록 함.
@@ -79,7 +77,7 @@ export function flattenFolders(data: Folder[]): FlattenedFolder[] {
}
}
if (data && data.length > 0 && data[0].clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리
if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리
data.forEach((root) => traverse(root, {}, 0));
} else {
data.forEach((root) => traverse(root, {}, 1));
@@ -87,11 +85,11 @@ export function flattenFolders(data: Folder[]): FlattenedFolder[] {
return result;
}
export const getDcTree = async (clsfSystmNm:string) => {
export const getDcTree = async (clsfSystmNm: string) => {
const result = await getDcData("/api/v1/dc/clsf?clsfSystmNm=" + clsfSystmNm);
return result;
};
export const useDcTree = (clsfSystmNm:string) => {
export const useDcTree = (clsfSystmNm: string) => {
const [data, setData] = useState([]);
useEffect(() => {
@@ -109,31 +107,31 @@ export const useDcTree = (clsfSystmNm:string) => {
return data;
};
export const getDcFile = async (clsfSystmSn:string) => {
const queryString = 'clsfSystmSn='+clsfSystmSn;
export const getDcFile = async (clsfSystmSn: string) => {
const queryString = 'clsfSystmSn=' + clsfSystmSn;
const result = await getDcData(`/api/v1/dc/file${queryString ? `?${queryString}` : ''}`);
return result;
};
export const addFileName = async (data:FlattenedFolder[]) => {
export const addFileName = async (data: FlattenedFolder[]) => {
const fileReportData = await Promise.all(
data.map(async (item) => {
const filenameArr:ReportDataFile[] = await getDcFile(String(item.clsfSystmSn));
const filenameArr: ReportDataFile[] = await getDcFile(String(item.clsfSystmSn));
const f = filenameArr[0];
const result = {...item, fileName:f?.fileTitle, regDate:f?.regDt, fileSn:f?.fileSn, filePath:f?.filePath};
const result = { ...item, fileName: f?.fileTitle, regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath };
return result;
})
);
return fileReportData;
};
export const getDcFileprcs = async (fileSn:string) => {
const queryString = 'fileSn='+fileSn;
export const getDcFileprcs = async (fileSn: string) => {
const queryString = 'fileSn=' + fileSn;
const result = await getDcData(`/api/v1/dc/fileprcs${queryString ? `?${queryString}` : ''}`);
return result;
};
export const getDcFileDownload = async (filePath:string) => {
const queryString = 'filePath='+ encodeURIComponent(filePath);
export const getDcFileDownload = async (filePath: string) => {
const queryString = 'filePath=' + encodeURIComponent(filePath);
const response = await fetch(`/dc-service/api/v1/dc/file/download?${queryString}`, {
method: 'GET',
});
@@ -143,7 +141,7 @@ export const getDcFileDownload = async (filePath:string) => {
const blob = await response.blob(); // PDF, image 등은 blob으로 받아야 함
return blob;
};
export const fileDownload = (data:Blob, fileName:string) => { // Blob데이터 임시link로 연결
export const fileDownload = (data: Blob, fileName: string) => { // Blob데이터 임시link로 연결
const url = window.URL.createObjectURL(data);
const link = document.createElement('a');
link.href = url;
@@ -153,80 +151,9 @@ export const fileDownload = (data:Blob, fileName:string) => { // Blob데이터
link.remove();
window.URL.revokeObjectURL(url);
}
export const getFileDownload = async (fileSn:string, fileName:string, filePath:string) => {
if (filePath === "") alert("filePath 정보 없으므로 다운로드 불가:filesn"+ fileSn);
export const getFileDownload = async (fileSn: string, fileName: string, filePath: string) => {
if (filePath === "") alert("filePath 정보 없으므로 다운로드 불가:filesn" + fileSn);
const blobData = await getDcFileDownload(filePath); // 실제 다운로드 API
fileDownload(blobData, fileName);
}
// 시도 데이터
export const getCityCd = async (): Promise<SelectDataType[]> => {
try {
const response = await fetch(`${ADDR_URL}/api/v1/addr/city`, { method: "GET", headers: { accept: "application/json" } });
if (!response.ok) throw new Error(`시도 데이터 오류: ${response.status}`);
const res = await response.json();
const city = res.data;
let citys:SelectDataType[] = city.map((item: {adstDstrCd:string, sido:string}) => ({
val: item.adstDstrCd,
text: item.sido,
}));
citys = citys.sort((a:SelectDataType, b:SelectDataType) => a.text.localeCompare(b.text));
citys.unshift({ val: '0', text: '전체' });
return citys;
} catch (error) {
console.error('시도 데이터를 가져오는데 실패했습니다:', error);
return [{ val: '0', text: '전체', sido: '0' }];
}
};
export const useSggCd = () => {
const [data, setData] = useState<SelectDataType[]>([]);
useEffect(() => {
const fetch = async () => {
const result = await getSggCd();
if (result) {
setData(result);
}
console.log("useSggCd호출");
};
fetch();
}, []);
return data;
};
// 시군구 데이터
export const getSggCd = async (): Promise<SelectDataType[]> => {
try {
const response = await fetch(`${ADDR_URL}/api/v1/addr/sgg`, { method: "GET", headers: { accept: "application/json" } });
if (!response.ok) throw new Error(`시군구 데이터 오류: ${response.status}`);
const res = await response.json();
const sgg = res.data;
const sggCd = sgg.map((item: {adstDstrCd:string, sido:string, gugun:string}) => ({
val: item.adstDstrCd,
sido: item.sido,
text: item.gugun,
}));
sggCd.unshift({ val: '0', text: '전체' }); // "전체" 옵션 추가
return sggCd;
} catch (error) {
console.error('시군구 데이터를 가져오는데 실패했습니다:', error);
return [{ val: '0', text: '전체', sido: '0' }];
}
};
export const useCityCd = () => {
const [data, setData] = useState<SelectDataType[]>([]);
useEffect(() => {
const fetch = async () => {
const result = await getCityCd();
if (result) {
setData(result);
}
console.log("useCityCd호출");
};
fetch();
}, []);
return data;
};
@@ -3,46 +3,80 @@
import React, { useRef, useEffect, useState } from "react";
import { Label } from "@common/components/ui/label";
export function WaterDepth({ onClose }: { onClose?: () => void }) {
const [isPlaying, setIsPlaying] = useState<boolean>(false);
export const WATER_DEPTH_LAYER_TYPES: string[] = [
'tb_ms_flood_dngr_map',
'tb_ms_sbmrg_dngr_map'
];
// 지정된 순서에 따른 lyr_sn 배열
export const ORDERED_LAYERS: string[] = [
'27', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 30년 빈도
'28', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 50년 빈도
'3', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 80년 빈도
'4', // 침수위험지도(tb_ms_sbmrg_dngr_map) - 100년 빈도
'59', // 도시침수지도(tb_ms_flood_dngr_map) - 30년 빈도
'60', // 도시침수지도(tb_ms_flood_dngr_map) - 50년 빈도
'58' // 도시침수지도(tb_ms_flood_dngr_map) - 80년 빈도
];
interface WaterDepthProps {
onClose?: () => void;
isPlaying: boolean;
setIsPlaying: (isPlaying: boolean) => void;
totalSteps?: number;
currentStep?: number;
}
export function WaterDepth({
onClose,
isPlaying,
setIsPlaying,
totalSteps = 0,
currentStep = 0,
}: WaterDepthProps) {
useEffect(() => {
if (setIsPlaying) setIsPlaying(isPlaying);
}, [isPlaying, setIsPlaying]);
const handlePlayToggle = () => {
setIsPlaying(!isPlaying);
};
// 진행률 계산 (0-100%)
const progressPercentage = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 100;
return (
<>
<div className="fixed top-8 left-8 z-50 bg-white rounded-lg shadow-lg border border-gray-200 w-[160px] px-4 py-3">
<div className="card-header flex items-center justify-between">
<div className="bg-white rounded-lg shadow-lg border border-gray-200 w-[160px] px-4 py-3">
<div className="flex items-center justify-between mx-2 my-4">
<Label className="text-3xl text-gray-900 font-bold">침수심 범례</Label>
<button
type="button"
className="ml-2"
onClick={() => setIsPlaying((prev) => !prev)}
>
<button type="button" className="mt-1 ml-2" onClick={handlePlayToggle}>
{isPlaying ? (
<img
src="/images/map/toolbar/pause_button.png"
alt="pause"
className="w-6 h-6 object-contain"
/>
<img src="/images/map/toolbar/pause_button.png" alt="pause" className="w-6 h-6 object-contain" />
) : (
<img
src="/images/map/toolbar/play_button_arrowhead.png"
alt="play"
className="w-6 h-6 object-contain"
/>
<img src="/images/map/toolbar/play_button_arrowhead.png" alt="play" className="w-6 h-6 object-contain" />
)}
</button>
</div>
<div className="border-b border-gray-300 mb-2" />
<div className="mb-3">
<div className="w-full bg-gray-200 rounded-full h-1">
<div
className="bg-blue-900 h-1 rounded-full transition-all duration-300 ease-in-out"
style={{ width: `${progressPercentage}%` }}
/>
</div>
</div>
<div className="w-[140px] mx-auto flex flex-col items-center">
<div className="bg-blue-900 text-white text-2xl text-center py-1 mb-2 rounded w-full">침 수 심</div>
<div className="space-y-1 w-full">
<LegendItem color="#0000ff" label="0.3~0.5m 미만" />
<LegendItem color="#00e8fe" label="0.5~1.0m 미만" />
<LegendItem color="#00fdb7" label="1.0~1.5m 미만" />
<LegendItem color="#00fe4e" label="1.5~2.0m 미만" />
<LegendItem color="#fce800" label="2.0~3.0m 미만" />
<LegendItem color="#fe0000" label="3.0m 이상" />
<LegendItem color="rgb(255, 255, 127)" label="0.0~0.5m 미만" /> {/* 코드: N330 */}
<LegendItem color="rgb(191, 255, 0)" label="0.5~1.0m 미만" /> {/* 코드: N331 */}
<LegendItem color="rgb(0, 255, 255)" label="1.0~2.0m 미만" /> {/* 코드: N332 */}
<LegendItem color="rgb(191, 127, 255)" label="2.0~5.0m 미만" /> {/* 코드: N333 */}
<LegendItem color="rgb(255, 0, 127)" label="5.0m 이상" /> {/* 코드: N334 */}
</div>
<div className="border-t-4 border-gray-900 mt-2 mx-auto w-full" />
<div className="border-t-2 border-blue-900 mt-1 mx-auto w-full" />
</div>
</div>
</>
@@ -1,5 +1,5 @@
'use client'
import { deleteData, getData } from '@/components/lib/api'
import { deleteData, getData, useCookieAuth } from '@/components/lib/api'
import { UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState, useEffect } from 'react'
@@ -48,6 +48,7 @@ const tableColumn = [
]
const Result = () => {
const AUTH = useCookieAuth();
const form = useForm<ResultFormValues>({
resolver: zodResolver(formSchema),
@@ -123,6 +124,7 @@ const Result = () => {
"ctpvCd": ctpvCd === 0 ? null : ctpvCd,
"sggCd": sggCd === 0 ? null : sggCd,
"swageTmplntCd": processedSwageTmplntCd,
"auth": AUTH,
};
console.log("Search DTO:", inftwtAnlsSearchDto);
@@ -1,5 +1,5 @@
'use client'
import { deleteData, getData } from '@/components/lib/api'
import { deleteData, getData, useCookieAuth } from '@/components/lib/api'
import { UiTable, downloadExcelWithKoreanHeaders } from '@common/business_components/ui'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState, useEffect } from 'react'
@@ -46,6 +46,8 @@ const tableColumn = [
]
const Result = () => {
const AUTH = useCookieAuth();
const form = useForm<ResultFormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -119,6 +121,7 @@ const Result = () => {
"ctpvCd": ctpvCd === 0 ? null : ctpvCd,
"sggCd": sggCd === 0 ? null : sggCd,
"swageTmplntCd": processedSwageTmplntCd,
"auth": AUTH,
};
console.log("Search DTO:", inftwtAnlsSearchDto);
@@ -1,360 +0,0 @@
import React, { useEffect, useState } from 'react';
import { SelectBox } from '@common/business_components/ui/select';
import { Label } from '@radix-ui/react-label';
export type comCodeState = {
code: {
isInit: boolean;
chBuCd: SelectDataType[]
rainFcltCd: SelectDataType[];
bdstUsgCd: SelectDataType[];
cityCd: SelectDataType[];
sggCd: SelectDataType[];
rvbsnCd: SelectDataType[];
mdmbsnCd: SelectDataType[];
lgbsnCd: SelectDataType[];
lclgvrnmntCd: EnvPartSelectDataType[];
orgwtKndCd: SelectDataType[];
yn: SelectDataType[];
buildingType: {
RST: string,
RNB: string,
HTB: string
}
};
};
type SelectDataType = {
text: string;
val: string;
sido?: string; // 시도 코드 (선택 사항)
}
type EnvPartSelectDataType = SelectDataType & {
adstDliStrCd: string,
}
type SearchFormLeftProps = {
isShowPart?: boolean | undefined;
children: React.ReactNode;
selectSidoData?: SelectDataType[] | [];
selectSigunData?: SelectDataType[] | [];
selectRegionData?: SelectDataType[] | [];
selectUnitWaterShedData?: SelectDataType[] | [];
selectMiddleRegionData?: SelectDataType[] | [];
selectEnvPartData?: EnvPartSelectDataType[] | [];
selectEnvPartSidoData?: SelectDataType[] | [];
selectEnvPartSigunData?: SelectDataType[] | [];
setRtnPartValue?: (value: string) => void;
setRtnSidoValue?: (value: string) => void;
setRtnSigunValue?: (value: string) => void;
setRtnRegionValue?: (value: string) => void;
setRtnUnitWaterShedValue?: (value: string) => void;
setRtnMiddleRegionValue?: (value: string) => void;
setRtnEnvPartValue?: (value: string) => void;
setRtnEnvPartSidoValue?: (value: string) => void;
setRtnEnvPartSigunValue?: (value: { val: string, text: string }) => void; // setRtnEnvPartSigunValue prop 타입을 객체로 변경
resetTrigger?: boolean;
}
function GovSidoSigunguSearchForm({ isShowPart = false, children, selectSidoData = [], selectSigunData = [], selectRegionData = [], selectUnitWaterShedData = [], selectMiddleRegionData = [], selectEnvPartData = [], setRtnPartValue, setRtnSidoValue, setRtnSigunValue, setRtnRegionValue, setRtnUnitWaterShedValue, setRtnMiddleRegionValue, setRtnEnvPartValue, setRtnEnvPartSidoValue, setRtnEnvPartSigunValue, resetTrigger = false }: SearchFormLeftProps) {
//환경청별 시도 시군구
const [envPartValue, setEnvPartValue] = useState<string>('0');
const [envPartSidoValue, setEnvPartSidoValue] = useState<string>('0'); // 환경청 선택 시 시/도 값
const [envPartSigunValue, setEnvPartSigunValue] = useState<string>('0'); // 환경청 선택 시 시/군/구 값
const [filteredEnvPartSigunDataSelected, setFilteredEnvPartSigunDataSelected] = useState<SelectDataType[]>([]);
// API로부터 가져온 전체 데이터 목록 (필터링 전)
const [allEnvPartOptions, setAllEnvPartOptions] = useState<EnvPartSelectDataType[]>([]);
const [allSidoOptions, setAllSidoOptions] = useState<SelectDataType[]>([]);
const [allSigunOptions, setAllSigunOptions] = useState<SelectDataType[]>([]);
// 조회된 조건에 맞는 환경청 시도 시군 (SelectBox에 실제 사용될 데이터)
const [filteredEnvPartData, setFilteredEnvPartData] = useState<EnvPartSelectDataType[]>([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }]);
const [filteredEnvPartSidoData, setFilteredEnvPartSidoData] = useState<EnvPartSelectDataType[]>([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }]);
const [filteredEnvPartSigunData, setFilteredEnvPartSigunData] = useState<SelectDataType[]>([{ text: "전체", val: '0', sido: '0' }]);
// 시군구 데이터
const getSggCd = async (): Promise<SelectDataType[]> => {
try {
const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/sgg', { method: "GET", headers: { accept: "application/json" } });
if (!response.ok) throw new Error(`시군구 데이터 오류: ${response.status}`);
const res = await response.json();
const sgg = res.data;
const sggCd = sgg.map((item: any) => ({
val: item.adstDstrCd,
sido: item.sido,
text: item.gugun,
}));
sggCd.unshift({ val: '0', text: '전체' }); // "전체" 옵션 추가
return sggCd;
} catch (error) {
console.error('시군구 데이터를 가져오는데 실패했습니다:', error);
return [{ val: '0', text: '전체', sido: '0' }];
}
};
// 시도 데이터
const getCityCd = async (): Promise<SelectDataType[]> => {
try {
const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/city', { method: "GET", headers: { accept: "application/json" } });
if (!response.ok) throw new Error(`시도 데이터 오류: ${response.status}`);
const res = await response.json();
const city = res.data;
const cityCd = city.map((item: any) => ({
val: item.adstDstrCd,
text: item.sido,
}));
cityCd.unshift({ val: '0', text: '전체' });
return cityCd;
} catch (error) {
console.error('시도 데이터를 가져오는데 실패했습니다:', error);
return [{ val: '0', text: '전체', sido: '0' }];
}
};
// 환경청 데이터
const getEnvPartCd = async (): Promise<EnvPartSelectDataType[]> => {
try {
const response = await fetch('http://con.mind-one.co.kr:11082/api/v1/addr/govArea', { method: "GET", headers: { accept: "application/json" } });
if (!response.ok) throw new Error(`환경청 데이터 오류: ${response.status}`);
const res = await response.json();
const lclgvrnmnt = res.data.filter((item: { lclgvrnmntNm: string; }) => {
// item.lclgvrnmntNm이 null이나 undefined가 아닌지 확인 후 endsWith 사용
return item.lclgvrnmntNm && item.lclgvrnmntNm.endsWith("환경청");
});
const lclgvrnmntCd = lclgvrnmnt.map((item: any) => ({
val: item.lclgvrnmntCd,
text: item.lclgvrnmntNm,
adstDliStrCd: item.adstDliStrCd || ''
}));
lclgvrnmntCd.unshift({ val: '0', text: '전체', adstDliStrCd: '' });
return lclgvrnmntCd;
} catch (error) {
console.error('환경청 데이터를 가져오는데 실패했습니다:', error);
return [{ val: '0', text: '전체', adstDliStrCd: '', sido: '0' }];
}
};
useEffect(() => {
const loadInitialData = async () => {
const [envParts, sidos, siguns] = await Promise.all([
getEnvPartCd(),
getCityCd(),
getSggCd()
]);
setAllEnvPartOptions(envParts.filter(item => item.val !== '0'));
setFilteredEnvPartData(envParts);
setAllSidoOptions(sidos.filter(item => item.val !== '0'));
// 초기 filteredEnvPartSidoData는 모든 시/도를 포함 (환경청 "전체" 선택 시)
setFilteredEnvPartSidoData(sidos.map(s => ({ ...s, adstDliStrCd: '' })));
setAllSigunOptions(siguns.filter(item => item.val !== '0'));
// 초기 filteredEnvPartSigunData는 모든 시/군/구를 포함 (환경청 "전체", 시/도 "전체" 선택 시)
setFilteredEnvPartSigunData(siguns);
// 만약 isShowPart가 true이고, partValue가 'G'(환경청별)로 기본 설정되어 있다면,
// 초기 envPartValue ('0')에 대한 시도/시군구 목록을 설정합니다.
initializeCityDataByEnvPartSido(envPartValue); // envPartValue 기본값 '0'
};
if (isShowPart) {
loadInitialData();
}
}, [isShowPart]); // isShowPart가 변경될 때 데이터를 다시 로드
// 환경청 세팅 - 시도
const initializeCityDataByEnvPartSido = (envPartCd: string) => {
const foundEnvPart = allEnvPartOptions.find((item: EnvPartSelectDataType) => item.val === envPartCd);
console.log('foundEnvPart:', foundEnvPart);
let relevantSidos: SelectDataType[] = []; // 기본값을 빈 배열로 설정
let relevantSigunsForSidoDerivation: SelectDataType[] = []; // 기본값을 빈 배열로 설정
if (envPartCd === '0') { // "전체" 환경청 선택 시
relevantSidos = [...allSidoOptions];
relevantSigunsForSidoDerivation = [...allSigunOptions];
} else if (foundEnvPart && foundEnvPart.adstDliStrCd) { // 특정 환경청 선택 & 해당 환경청에 adstDliStrCd 정보가 있는 경우
// adstDliStrCd가 시도 코드를 포함한다고 가정 (주석 참고)
const cityCodesInEnvPart = foundEnvPart.adstDliStrCd.toString().split('|').join('').split(',').filter(c => c);
console.log('cityCodesInEnvPart:', cityCodesInEnvPart);
// 1. allSigunOptions에서 val이 cityCodesInEnvPart에 포함된 데이터의 sido만 추출
const relevantSigunSidos = Array.from(
new Set(
allSigunOptions
.filter(sigungu => cityCodesInEnvPart.includes(sigungu.val))
.map(sigungu => sigungu.sido)
)
);
console.log('relevantSigunTexts:', relevantSigunSidos);
// 2. relevantSigunsForSidoDerivation은 해당 sido만을 가진 배열로 반환
relevantSidos = allSidoOptions.filter(sido => relevantSigunSidos.includes(sido.text));
console.log('relevantSidos:', relevantSidos);
// 해당 시도들에 속하는 시군구들 (가나다순 오름차순 정렬)
relevantSigunsForSidoDerivation = allSigunOptions
.filter(sigun => {
const sigunSidoPrefix = sigun.val.substring(0, 2);
return relevantSidos.some(s => s.val === sigunSidoPrefix);
})
.sort((a, b) => a.text.localeCompare(b.text, 'ko'));
}
// 특정 환경청이 선택되었으나, foundEnvPart가 없거나 adstDliStrCd 정보가 없는 경우,
// relevantSidos와 relevantSigunsForSidoDerivation은 초기 빈 배열 상태를 유지합니다.
setFilteredEnvPartSidoData([
{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' },
...relevantSidos.map((item: SelectDataType) => ({ text: item.text, val: item.val, adstDliStrCd: '', sido: '0' }))
]);
setFilteredEnvPartSigunDataSelected(relevantSigunsForSidoDerivation);
// 시/도 선택이 "전체"('0')로 초기화되므로, 해당 시/군/구 목록도 업데이트
initializeCityDataByEnvPartSigun('0');
}
// 환경청 세팅 - 시군구
const initializeCityDataByEnvPartSigun = (selectedSidoInEnvPart: string) => {
let sigunsToList: SelectDataType[];
if (selectedSidoInEnvPart === '0') { // 선택된 시도가 "전체"이면
// filteredEnvPartSigunDataSelected는 이미 해당 환경청에 맞는 시군구들로 필터링 되어있음
sigunsToList = [...filteredEnvPartSigunDataSelected];
} else {
sigunsToList = filteredEnvPartSigunDataSelected.filter((item: SelectDataType) => item.val.substring(0, 2) === selectedSidoInEnvPart);
}
setFilteredEnvPartSigunData([
{ text: "전체", val: '0' },
...sigunsToList
]);
}
const onSelectValue = (val: string, type: string) => {
switch (type) {
// 환경청 세팅
case 'envPart':
setEnvPartValue(val);
setRtnEnvPartValue?.(val);
initializeCityDataByEnvPartSido(val);
setEnvPartSidoValue('0'); // 시도 "전체"로 초기화
setEnvPartSigunValue('0'); // 시군구 "전체"로 초기화
// 부모에게도 초기화된 값 전달 (text 포함)
if (setRtnEnvPartSidoValue) {
const sidoDefault = filteredEnvPartSidoData.find(item => item.val === '0') || { val: '0', text: '전체' };
// setRtnEnvPartSidoValue(sidoDefault.val); // 이전 방식: 값만 전달
// TODO: 만약 시도 값도 객체로 전달해야 한다면 아래와 같이 수정
// setRtnEnvPartSidoValue({ val: sidoDefault.val, text: sidoDefault.text });
}
if (setRtnEnvPartSigunValue) {
const sigunDefault = filteredEnvPartSigunData.find(item => item.val === '0') || { val: '0', text: '전체' };
setRtnEnvPartSigunValue({ val: sigunDefault.val, text: sigunDefault.text });
}
break;
case 'envPartSido': // 환경청별 - 시도 선택
setEnvPartSidoValue(val);
setRtnEnvPartSidoValue?.(val);
initializeCityDataByEnvPartSigun(val);
setEnvPartSigunValue('0'); // 시군구 "전체"로 초기화
if (setRtnEnvPartSigunValue) {
const sigunDefault = filteredEnvPartSigunData.find(item => item.val === '0') || { val: '0', text: '전체' };
setRtnEnvPartSigunValue({ val: sigunDefault.val, text: sigunDefault.text });
}
break;
case 'envPartSigun': // 환경청별 - 시군구 선택
setEnvPartSigunValue(val);
const selectedSigun = filteredEnvPartSigunData.find(item => item.val === val);
if (setRtnEnvPartSigunValue && selectedSigun) {
setRtnEnvPartSigunValue({ val: selectedSigun.val, text: selectedSigun.text });
} else if (setRtnEnvPartSigunValue && val === '0') { // "전체"를 명시적으로 처리 (find 실패 대비)
setRtnEnvPartSigunValue({ val: '0', text: '전체' });
}
break;
}
}
// 리셋 함수
const resetAllValues = () => {
setEnvPartValue('0');
setEnvPartSidoValue('0');
setEnvPartSigunValue('0');
// 필터링된 목록들도 초기 "전체" 또는 전체 목록으로 리셋
setFilteredEnvPartSigunData([{ text: "전체", val: '0', sido: '0' }, ...allSigunOptions]);
setFilteredEnvPartData([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, ...allEnvPartOptions]);
setFilteredEnvPartSidoData([{ text: "전체", val: '0', adstDliStrCd: '', sido: '0' }, ...allSidoOptions.map(s => ({ ...s, adstDliStrCd: '' }))]);
setFilteredEnvPartSigunData([{ text: "전체", val: '0', sido: '0' }, ...allSigunOptions]);
setFilteredEnvPartSigunDataSelected(allSigunOptions);
setRtnPartValue?.('A');
setRtnSidoValue?.('0');
setRtnSigunValue?.('0');
setRtnRegionValue?.('0');
setRtnUnitWaterShedValue?.('0');
setRtnMiddleRegionValue?.('0');
setRtnEnvPartValue?.('0');
setRtnEnvPartSidoValue?.('0');
setRtnEnvPartSigunValue?.({ val: '0', text: '전체' });
};
// 리셋 트리거 변경 감지
useEffect(() => {
if (resetTrigger) {
resetAllValues();
// 리셋 후 트리거를 false로 설정하는 로직은 부모 컴포넌트에서 처리해야 할 수 있습니다.
// 여기서는 resetTrigger props가 true일 때 리셋을 수행합니다.
}
}, [resetTrigger]);
return (
isShowPart ? (
<div className="flex items-center my-4">
<>
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap ml-2 mr-4">환경청</Label>
<SelectBox
className="w-[160px] mr-8"
label={''}
selectArray={filteredEnvPartData}
onSelectValue={(val) => onSelectValue(val.toString(), 'envPart')}
value={envPartValue}
selectClass={'rounded-lg bg-white truncate'}
/>
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-4">시도</Label>
<SelectBox
className="w-[160px] mr-8"
label={''}
selectArray={filteredEnvPartSidoData}
onSelectValue={(val) => onSelectValue(val.toString(), 'envPartSido')}
value={envPartSidoValue}
selectClass={'rounded-lg bg-white truncate'}
/>
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-4">시군구</Label>
<SelectBox
className="w-[160px] mr-8"
label={''}
selectArray={filteredEnvPartSigunData}
onSelectValue={(val) => onSelectValue(val.toString(), 'envPartSigun')}
value={envPartSigunValue}
selectClass={'rounded-lg bg-white truncate'}
/>
</>
{children}
</div>
) : (
<div className="grid grid-cols-12 gap-5 w-11/12 m-4">
{children}
</div>
)
)
}
export { GovSidoSigunguSearchForm };
@@ -9,6 +9,7 @@ type ModalType = {
noHeaderYn?: 'N' | 'Y';
noFooterYn?: 'N' | 'Y';
headerTitle?: string;
headerTitleClass?: string;
footerContent?: React.ReactNode;
useDirectRender?: boolean;
}
@@ -21,6 +22,7 @@ export function UiModal({
noHeaderYn = 'N',
noFooterYn = 'N',
headerTitle,
headerTitleClass,
footerContent,
useDirectRender = false
}: ModalType) {
@@ -31,7 +33,7 @@ export function UiModal({
<DialogContent className={bodyClass}>
{noHeaderYn === 'N' && (
<DialogHeader>
<DialogDescription>
<DialogDescription className={headerTitleClass}>
{headerTitle}
</DialogDescription>
</DialogHeader>
@@ -116,13 +116,20 @@ function Gis({
setIsSearchModalOpen,
onCheckedLayersChange,
searchBar: SearchBar,
waterDepth: WaterDepth,
height:height="h-[calc(100vh-10rem)]",
waterDepth: WaterDepth,
isPlaying = false,
setIsPlaying = () => { },
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
height: height = "h-[calc(100vh-10rem)]",
}) {
const mapRef = useRef();
const [mapObject, setMapObject] = useState({});
const [tabVisible, setTabVisible] = useState(true);
const [layerNm, setLayerNm] = useState("");
const [totalSteps, setTotalSteps] = useState(0);
const [currentStep, setCurrentStep] = useState(0);
// const [isOpen, setIsOpen] = useState(false); // 팝업 열기/닫기 상태
// const [featureData, setFeatureData] = useState(null) // 선택된 피쳐 데이터
// const [isSearchModalOpen, setIsSearchModalOpen] = useState(false); // SearchModal 열림 상태
@@ -275,6 +282,12 @@ function Gis({
[onCheckedLayersChange],
);
// 진행률 변경 핸들러
const handleProgressChange = useCallback((total, current) => {
setTotalSteps(total);
setCurrentStep(current);
}, []);
return (
<>
<GisContext.Provider
@@ -305,8 +318,26 @@ function Gis({
onFeatureSelect(dummyFeature);
}}
imageVisible={imgVisble}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
targetLayerTypes={targetLayerTypes}
orderedLayers={orderedLayers}
checkInterval={checkInterval}
onProgressChange={handleProgressChange}
/>
{typeof WaterDepth === "function" && (
<div className={`absolute bottom-4 z-40 ${tabVisible ? 'left-[260px]' : 'left-4'}`}>
<WaterDepth
onClose={() => setIsOpen(false)}
isPlaying={isPlaying}
setIsPlaying={setIsPlaying}
totalSteps={totalSteps}
currentStep={currentStep}
/>
</div>
)}
{/* DetailView가 존재할 때만 렌더링 isSearchModalOpen이 true면 visible=false로 팝업 숨김 */}
{isOpen && !isSearchModalOpen && (
<DetailView
@@ -335,7 +366,6 @@ function Gis({
<GisToolBar />
<GisMapToggle />
<GisSelect onFeatureSelect={handleFeatureSelect} />
{typeof WaterDepth === "function" && <WaterDepth onClose={() => setIsOpen(false)} />}
</div>
</div>
@@ -343,6 +373,7 @@ function Gis({
<div className="absolute right-8 top-[305px] z-20">
<button
onClick={() => setIsSearchModalOpen(true)}
title="정보 검색"
className="flex h-[40px] w-[40px] items-center justify-center overflow-hidden rounded border border-[#9aa7b8] bg-white p-0 shadow-[0_0_5px_#999]"
>
<Search size={24} className="text-gray-700" />
@@ -5,18 +5,37 @@ import {
DndProvider
} from "@minoru/react-dnd-treeview";
import { ChevronLeft, ChevronRight } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useGisContext } from './Gis';
import GisApp from './lib/gis.core';
export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes = () => { }, visible, setVisible, onCheckedChange, onLayerIconClick,imageVisible=false }) {
export default React.memo(function GisTree({
checkedNodes = [],
setCheckedNodes = () => { },
visible,
setVisible,
onCheckedChange,
onLayerIconClick,
imageVisible = false,
isPlaying = false,
setIsPlaying = () => { },
targetLayerTypes = [],
orderedLayers = [],
checkInterval = 3000,
onProgressChange = () => { },
}) {
const { gisCore, treeData } = useGisContext();
const [checked, setChecked] = useState([]);
const [expanded, setExpanded] = useState([]);
const [nodes, setNodes] = useState([]);
const [activeTab, setActiveTab] = useState('layers');
const currentAnimationIndexRef = useRef(0);
const targetNodesRef = useRef([]); // 대상 노드들을 저장할 ref
const isPausedRef = useRef(false); // 일시정지 상태를 저장할 ref
const timeoutIdRef = useRef(null); // setTimeout ID를 저장할 ref
useEffect(() => {
if (treeData && treeData.length > 0) {
const nodeData = setNodeValues(treeData);
@@ -188,11 +207,182 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
return image;
};
const handleLayerImgClick = (nodeNm, nodeId, layerNm) => {
if(!checked.includes(nodeId)) alert('시설을 선택해주세요')
if (!checked.includes(nodeId)) alert('시설을 선택해주세요')
else onLayerIconClick(nodeNm)
}
// 특정 노드를 체크/언체크하는 함수
const checkSpecificNode = useCallback((nodeId, shouldCheck = true) => {
setChecked((prevChecked) => {
let newChecked = [...prevChecked];
const isCurrentlyChecked = prevChecked.includes(nodeId);
if (shouldCheck && !isCurrentlyChecked) {
// 체크하기
const descendants = getDescendants(nodeId, nodes);
newChecked = Array.from(new Set([...newChecked, nodeId, ...descendants]));
const ancestors = getAncestors(nodeId, nodes);
ancestors.forEach((p) => {
if (isEveryChildChecked(p, nodes, newChecked)) {
newChecked.push(p);
}
});
} else if (!shouldCheck && isCurrentlyChecked) {
// 언체크하기
const descendants = getDescendants(nodeId, nodes);
newChecked = newChecked.filter((cid) => cid !== nodeId && !descendants.includes(cid));
const ancestors = getAncestors(nodeId, nodes);
ancestors.forEach((p) => {
if (newChecked.includes(p)) {
newChecked = newChecked.filter((cid) => cid !== p);
}
});
}
// 지도에 레이어 표시/숨김 적용
const allIds = nodes.map((n) => n.id);
const unchecked = allIds.filter((nid) => !newChecked.includes(nid));
gisCore.setLayerOnMap(newChecked, true);
gisCore.setLayerOnMap(unchecked, false);
// 상위로 변경 사항 전달
const checkedNodesData = nodes.filter(node => newChecked.includes(node.id));
onCheckedChange?.(newChecked, checkedNodesData);
return newChecked;
});
}, [nodes]);
// 침수위험지도 실행 함수
const handleAutoCheckAnimation = useCallback((isEnabled, layerTypes = [], interval = 3000) => {
// 기존 타임아웃 정리
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
if (!isEnabled || layerTypes.length === 0) {
// 애니메이션 비활성화 시 상태 초기화
isPausedRef.current = false;
targetNodesRef.current = [];
currentAnimationIndexRef.current = 0;
onProgressChange(0, 0);
return;
}
// 대상 노드들을 찾아서 특정 순서로 정렬 후 배열로 반환
const getTargetNodesInOrder = () => {
const orderedNodes = [];
orderedLayers.forEach(lyrSn => {
const node = nodes.find(n => n.id === lyrSn && layerTypes.includes(n.data?.lyrTyNm));
if (node) {
orderedNodes.push(node);
}
});
return orderedNodes;
};
const targetNodes = getTargetNodesInOrder();
if (targetNodes.length === 0) {
console.warn('대상 레이어를 찾을 수 없습니다:', layerTypes);
return;
}
// 처음 시작하는 경우에만 초기화
if (targetNodesRef.current.length === 0) {
targetNodesRef.current = targetNodes;
currentAnimationIndexRef.current = 0;
console.log('자동 체크 대상 노드들 (순서대로):', targetNodes.map(n => ({ id: n.id, name: n.text, type: n.data?.lyrTyNm })));
// 진행률 초기화 (전체 단계 수 설정)
onProgressChange(targetNodes.length, 0);
// 모든 대상 레이어를 먼저 언체크
targetNodes.forEach(node => {
checkSpecificNode(node.id, false);
});
}
// 애니메이션 재개/시작
isPausedRef.current = false;
const processNextLayer = () => {
// 일시정지 상태이거나 애니메이션이 비활성화되면 중단
if (isPausedRef.current || !isPlaying) {
return;
}
const currentTargetNodes = targetNodesRef.current;
const currentIndex = currentAnimationIndexRef.current;
if (currentIndex < currentTargetNodes.length) {
checkSpecificNode(currentTargetNodes[currentIndex].id, true);
if (currentIndex > 0) {
checkSpecificNode(currentTargetNodes[currentIndex - 1].id, false);
}
currentAnimationIndexRef.current++;
// 진행률 업데이트
onProgressChange(currentTargetNodes.length, currentAnimationIndexRef.current);
// 다음 레이어 처리를 위한 타이머 설정
timeoutIdRef.current = setTimeout(processNextLayer, interval);
} else {
// 애니메이션 완료 - 상태 초기화 및 완료 신호 전송
console.log('애니메이션 완료');
isPausedRef.current = false;
targetNodesRef.current = [];
currentAnimationIndexRef.current = 0;
timeoutIdRef.current = null;
// 진행률 100% 완료
onProgressChange(currentTargetNodes.length, currentTargetNodes.length);
// 잠시 후 진행률 초기화
setTimeout(() => {
onProgressChange(0, 0);
}, 1000);
setIsPlaying?.(false);
}
};
processNextLayer();
}, [nodes, checkSpecificNode, setIsPlaying, isPlaying, onProgressChange]);
// 애니메이션 일시정지 함수
const pauseAnimation = useCallback(() => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
timeoutIdRef.current = null;
}
isPausedRef.current = true;
}, []);
// 외부에서 전달받은 props로 자동 체크 제어
useEffect(() => {
if (isPlaying) handleAutoCheckAnimation(true, targetLayerTypes, checkInterval); // 재생 시작/재개
else pauseAnimation(); // 일시정지
}, [isPlaying, handleAutoCheckAnimation, targetLayerTypes, checkInterval, pauseAnimation]);
// 컴포넌트 언마운트 시 정리
useEffect(() => {
return () => {
if (timeoutIdRef.current) {
clearTimeout(timeoutIdRef.current);
}
};
}, []);
if (!treeData || treeData.length === 0) return null;
return (
<>
<div
@@ -223,7 +413,7 @@ export default React.memo(function GisTree({ checkedNodes = [], setCheckedNodes
<input type="checkbox" checked={checked.includes(node.id)} onChange={() => handleCheck(node.id)} className="cursor-pointer" />
{node.icon && <span>{node.icon}</span>}
<span className="text-[1.3rem] font-medium text-label">{node.text}</span>
{imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={()=>{handleLayerImgClick(node.text, node.id, node.data.lyrTyNm)}} alt={node.text}></img></span> : null}
{imageVisible ? depth > 0 && <span><img className="cursor-pointer w-5 h-5" src='/images/map/layericon/Layer_Select_BTN.png' onClick={() => { handleLayerImgClick(node.text, node.id, node.data.lyrTyNm) }} alt={node.text}></img></span> : null}
</div>
)}
sort={(a, b) => {
-10995
View File
File diff suppressed because it is too large Load Diff