정보검색 기본계획보고서 탭 조회 UX 개선
This commit is contained in:
@@ -1,18 +1,19 @@
|
||||
/**
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창 내 하수도정비기본계획보고서 탭
|
||||
*
|
||||
* 📄 Report Tab
|
||||
* Next.js App Router (app/) 기반 검색 모달 보고서 탭 컴포넌트
|
||||
*
|
||||
* - 하수도정비 기본계획 보고서 목록 표시 및 필터링
|
||||
* - 시도/시군구/보고서 구분/승인년도 기반 검색 기능
|
||||
* - 시도/시군구/구분/보고서 구분/승인년도 기반 검색 기능
|
||||
* - 테이블 데이터 표시 및 페이지네이션 지원
|
||||
* - 보고서 클릭 시 파일 목록 모달 열기
|
||||
* - 파일명 클릭 시 대표 파일 바로 다운로드
|
||||
* - 탭 진입 시 기본 조건 자동 조회 지원
|
||||
* - Zustand를 통한 모달 상태 및 데이터 관리
|
||||
* - 로딩 오버레이 및 선택 초기화 기능
|
||||
*/
|
||||
'use client';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Button, Label } from '@common/components';
|
||||
import { YearRangeSelectBox } from '@/components/ui/YearRangeSelectBox';
|
||||
import { SelectBox, UiTable } from '@common/business_components/ui';
|
||||
@@ -22,19 +23,17 @@ import useStore from '@/store/useStore';
|
||||
import { CtpvSggPrcsFcltSelectBox } from '@/components/ui/ctpvSggPrcsFcltSelectBox';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { useReportTabStore } from '@/store/useReportTabStore';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
|
||||
// 하수도정비 기본계획 보고서
|
||||
export const reportTableHeadlist = [
|
||||
{ id: "no", title: "NO" },
|
||||
{ id: "sido", title: "시도" },
|
||||
{ id: "sigungu", title: "시군구" },
|
||||
{ id: "approvalYear", title: "승인년도" },
|
||||
{ id: "plantZoneGubun", title: "구분" },
|
||||
{ id: "reportGubun", title: "보고서구분" },
|
||||
{ id: "fileName", title: "파일명" },
|
||||
{ id: "regDate", title: "등록일자" },
|
||||
{ id: 'no', title: 'NO' },
|
||||
{ id: 'sido', title: '시도' },
|
||||
{ id: 'sigungu', title: '시군구' },
|
||||
{ id: 'approvalYear', title: '승인년도' },
|
||||
{ id: 'plantZoneGubun', title: '구분' },
|
||||
{ id: 'reportGubun', title: '보고서구분' },
|
||||
{ id: 'fileName', title: '파일명' },
|
||||
{ id: 'regDate', title: '등록일자' },
|
||||
];
|
||||
interface ReportTabProps {
|
||||
open?: boolean;
|
||||
@@ -43,19 +42,26 @@ interface ReportTabProps {
|
||||
sggNm?: string;
|
||||
}
|
||||
|
||||
const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sggNm }) => {
|
||||
const normalizeYear = (value: string) => {
|
||||
if (!value || value === '0' || value === '9999') return '';
|
||||
return value;
|
||||
};
|
||||
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const getApprovalYearNumber = (value: string | number | undefined) => {
|
||||
const normalized = String(value ?? '').replace('년', '').trim();
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
};
|
||||
|
||||
const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sggNm }) => {
|
||||
const [isResetPage, setIsResetPage] = useState<boolean>(false);
|
||||
const [isResetSelection, setIsResetSelection] = useState<boolean>(false);
|
||||
const [showTable, setShowTable] = useState(false);
|
||||
// const [tableData, setTableData] = useState<any[]>([]);
|
||||
// const [tableCount, setTableCount] = useState(0);
|
||||
// const [curPage, setCurPage] = useState(1);
|
||||
// const [yearFilter, setYearFilter] = useState({ start: "", end: "" });
|
||||
const [ctpvCd, setCtpvCd] = useState('0');
|
||||
const [sggCd, setSggCd] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState("0");
|
||||
const [plantZoneGubun, setPlantZoneGubun] = useState('0');
|
||||
const [reportGubun, setReportGubun] = useState('0');
|
||||
const hasAutoFetchedRef = useRef(false);
|
||||
|
||||
const {
|
||||
tableData,
|
||||
@@ -73,26 +79,43 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
const {
|
||||
dcTree: reportOriginData,
|
||||
fetchDcTree
|
||||
} = useReportTabStore(); // 모달 상태 관리 스토어
|
||||
const { REPORT_TYPES, flattenFolders, addFileName } = useReportTab();
|
||||
} = useReportTabStore();
|
||||
const { REPORT_TYPES, flattenFolders, addFileName, downloadReportFile } = useReportTab();
|
||||
|
||||
const flattenedReportData = useMemo(() => flattenFolders(reportOriginData), [reportOriginData]);
|
||||
const plantZoneOptions = useMemo(() => {
|
||||
const uniqueOptions = Array.from(
|
||||
new Set(
|
||||
flattenedReportData
|
||||
.map((item) => String(item.plantZoneGubun ?? '').trim())
|
||||
.filter((item) => item.length > 0)
|
||||
)
|
||||
).sort((a, b) => a.localeCompare(b, 'ko'));
|
||||
|
||||
return [
|
||||
{ val: '0', text: '전체' },
|
||||
...uniqueOptions.map((item) => ({ val: item, text: item })),
|
||||
];
|
||||
}, [flattenedReportData]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchDcTree("기본계획"); // 하수정비기본계획 report
|
||||
}, []);
|
||||
void fetchDcTree('기본계획');
|
||||
}, [fetchDcTree]);
|
||||
|
||||
useEffect(() => {
|
||||
// filterParams 초기화
|
||||
hasAutoFetchedRef.current = false;
|
||||
setCtpvCd('0');
|
||||
setSggCd('0');
|
||||
setPlantZoneGubun('0');
|
||||
setReportGubun('0');
|
||||
setYearFilter({ start: "", end: "" });
|
||||
setYearFilter({ start: '', end: '' });
|
||||
setShowTable(false);
|
||||
setTableData([]);
|
||||
setTableCount(0);
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
}, [open]);
|
||||
setCurPage(1);
|
||||
setIsResetPage(true);
|
||||
setIsResetSelection(true);
|
||||
}, [open, setCurPage, setTableCount, setTableData, setYearFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const newCtpvCd = ctpv.find((n) => n.text === ctpvNm)?.val || '0';
|
||||
@@ -100,174 +123,189 @@ const ReportTab: React.FC<ReportTabProps> = ({ open, isSearch = true, ctpvNm, sg
|
||||
|
||||
setCtpvCd(newCtpvCd);
|
||||
setSggCd(newSggCd);
|
||||
}, [ctpvNm, sggNm]);
|
||||
}, [ctpv, ctpvNm, sgg, sggNm]);
|
||||
|
||||
// ctpvCd, sggCd가 업데이트된 후에 fetchFilteredData 실행
|
||||
useEffect(() => {
|
||||
if (!isSearch && ctpvCd && sggCd) {
|
||||
fetchFilteredData();
|
||||
}
|
||||
}, [reportOriginData, ctpvCd, sggCd, isSearch]);
|
||||
if (!open || flattenedReportData.length === 0 || hasAutoFetchedRef.current) return;
|
||||
|
||||
hasAutoFetchedRef.current = true;
|
||||
void fetchFilteredData(1);
|
||||
}, [flattenedReportData.length, open]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
// showTable이 true일 때만 fetch (최초 렌더 방지)
|
||||
setIsResetPage(false);
|
||||
|
||||
if (showTable) fetchFilteredData();
|
||||
if (showTable) {
|
||||
void fetchFilteredData();
|
||||
}
|
||||
}, [curPage]);
|
||||
|
||||
const handleCtpvCdChange = (sidoId: string) => {
|
||||
setCtpvCd(sidoId);
|
||||
setSggCd('0');
|
||||
}
|
||||
};
|
||||
|
||||
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
|
||||
const handleSearch = () => {
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
fetchFilteredData();
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetSelection(false); // 선택 초기화
|
||||
hasAutoFetchedRef.current = true;
|
||||
setCurPage(1);
|
||||
void fetchFilteredData(1);
|
||||
setIsResetPage(true);
|
||||
setIsResetSelection(false);
|
||||
setShowTable(true);
|
||||
};
|
||||
|
||||
// 하수도정비기본계획 테이블 cellClick 핸들러
|
||||
const handleReportCellClick = (index: number | undefined) => {
|
||||
if (typeof index === "number" && tableData[index]) {
|
||||
const reportData = tableData[index];
|
||||
if (reportData.fileSn !== "") {
|
||||
const popupWindow = openWindowPopup("reportFiles", {
|
||||
clsfSystmSn: reportData.clsfSystmSn,
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
const handleReportCellClick = (index: number | undefined, headId: string) => {
|
||||
if (headId !== 'fileName') return;
|
||||
if (typeof index !== 'number' || !tableData[index]) return;
|
||||
|
||||
if (!popupWindow) {
|
||||
alert('팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.');
|
||||
}
|
||||
const reportData = tableData[index];
|
||||
if (!reportData.fileSn) return;
|
||||
|
||||
void (async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await downloadReportFile(reportData);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
};
|
||||
|
||||
// 검색 API 호출 함수
|
||||
const fetchFilteredData = async () => {
|
||||
setIsLoading(true); // 로딩 시작
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
const fetchFilteredData = async (page = curPage) => {
|
||||
setIsLoading(true);
|
||||
setIsResetSelection(true);
|
||||
|
||||
try {
|
||||
const reportData = flattenFolders(reportOriginData);
|
||||
let filterReportData = reportData;
|
||||
let filterReportData = flattenedReportData;
|
||||
|
||||
if (ctpvCd !== '0') { // 시도검색
|
||||
const sidoOption = ctpv.find((n) => n.val.trim() === ctpvCd.trim());
|
||||
filterReportData = filterReportData.filter(item => item.sido === sidoOption?.text);
|
||||
if (ctpvCd !== '0') {
|
||||
const sidoOption = ctpv.find((n) => String(n.val).trim() === ctpvCd.trim());
|
||||
filterReportData = filterReportData.filter((item) => item.sido === sidoOption?.text);
|
||||
}
|
||||
if (sggCd !== '0') { // 시군구검색
|
||||
if (sggCd !== '0') {
|
||||
const sigunguOption = sgg.find((n) => String(n.val) === String(sggCd));
|
||||
filterReportData = filterReportData.filter(item => item.sigungu === sigunguOption?.text);
|
||||
filterReportData = filterReportData.filter((item) => item.sigungu === sigunguOption?.text);
|
||||
}
|
||||
if (reportGubun !== "0") { // 보고서구분검색
|
||||
if (plantZoneGubun !== '0') {
|
||||
filterReportData = filterReportData.filter((item) => String(item.plantZoneGubun ?? '') === plantZoneGubun);
|
||||
}
|
||||
if (reportGubun !== '0') {
|
||||
const reportGubunOption = REPORT_TYPES.find((n) => n.val === reportGubun);
|
||||
filterReportData = filterReportData.filter(item => item.reportGubun === reportGubunOption?.text);
|
||||
filterReportData = filterReportData.filter((item) => item.reportGubun === reportGubunOption?.text);
|
||||
}
|
||||
|
||||
const { start, end } = yearFilter;
|
||||
if (start && !end) //승인년도검색
|
||||
filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" : null);
|
||||
else if (!start && end)
|
||||
filterReportData = filterReportData.filter(item => (item.approvalYear) ? item.approvalYear <= yearFilter.end + "년" : null);
|
||||
else if (start && end) {
|
||||
filterReportData = filterReportData.filter(item => {
|
||||
return (item.approvalYear) ? item.approvalYear >= yearFilter.start + "년" && item.approvalYear <= yearFilter.end + "년" : null;
|
||||
const normalizedStartYear = normalizeYear(yearFilter.start);
|
||||
const normalizedEndYear = normalizeYear(yearFilter.end);
|
||||
if (normalizedStartYear || normalizedEndYear) {
|
||||
filterReportData = filterReportData.filter((item) => {
|
||||
const approvalYear = getApprovalYearNumber(item.approvalYear);
|
||||
if (approvalYear === null) return false;
|
||||
if (normalizedStartYear && approvalYear < Number(normalizedStartYear)) return false;
|
||||
if (normalizedEndYear && approvalYear > Number(normalizedEndYear)) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
// if (gubun !== "") { // 구분검색 (추후 사용할 수도 있을 것 같아 남겨둠)
|
||||
// filterReportData = filterReportData.filter(item => (item.plantZoneGubun) ? String(item.plantZoneGubun).includes(String(gubun)) : null);
|
||||
// }
|
||||
|
||||
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 pagedReportData = filterReportData
|
||||
.slice((page - 1) * 10, page * 10)
|
||||
.map((item, index) => ({ ...item, no: (page - 1) * 10 + index + 1 }));
|
||||
|
||||
addFileName(filterReportData).then(data => {
|
||||
filterReportData = data;
|
||||
setTableData(filterReportData || []);
|
||||
setIsResetSelection(false); // 데이터 설정 후 선택 초기화 해제
|
||||
});
|
||||
|
||||
setTableData(filterReportData || []);
|
||||
const reportRows = await addFileName(pagedReportData);
|
||||
setTableData(reportRows || []);
|
||||
setShowTable(true);
|
||||
setIsResetSelection(false);
|
||||
} catch (err) {
|
||||
console.error('API ERROR:', err);
|
||||
setTableData([]);
|
||||
setIsResetSelection(false); // 에러 시에도 선택 초기화 해제
|
||||
setIsResetSelection(false);
|
||||
} finally {
|
||||
setIsLoading(false); // 로딩 종료
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`relative flex flex-col border rounded-lg pt-5 px-5 w-full ${isSearch ? "min-h-[10vh]" : "min-h-[6vh]"}`}>
|
||||
{isSearch && (
|
||||
<>
|
||||
<div className="flex items-center mb-3">
|
||||
<div className="relative mt-4 rounded-lg border px-5 pt-5 pb-3">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="flex min-w-max items-center gap-4 whitespace-nowrap">
|
||||
{isSearch && (
|
||||
<CtpvSggPrcsFcltSelectBox
|
||||
className="shrink-0"
|
||||
sido={ctpvCd}
|
||||
sigungu={sggCd}
|
||||
onSidoChange={(sidoId: string) => { handleCtpvCdChange(sidoId) }}
|
||||
onSidoChange={(sidoId: string) => { handleCtpvCdChange(sidoId); }}
|
||||
onSigunguChange={setSggCd}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
)}
|
||||
<YearRangeSelectBox
|
||||
className="shrink-0 !mr-1"
|
||||
selectClass="rounded-lg bg-white flex-shrink-0 w-[78px]"
|
||||
start={yearFilter.start}
|
||||
end={yearFilter.end}
|
||||
onChange={(range) => setYearFilter({
|
||||
start: normalizeYear(range.start),
|
||||
end: normalizeYear(range.end),
|
||||
})}
|
||||
startLabel="승인년도"
|
||||
endLabel=""
|
||||
/>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">구분</Label>
|
||||
<SelectBox
|
||||
className="w-36 shrink-0"
|
||||
selectClass="rounded-lg bg-white"
|
||||
label=""
|
||||
selectArray={plantZoneOptions}
|
||||
value={plantZoneGubun}
|
||||
onSelectValue={(val) => setPlantZoneGubun(String(val))}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
<Label className="text-xl text-gray-600 font-medium whitespace-nowrap mr-2">보고서 구분</Label>
|
||||
<SelectBox
|
||||
className="w-36 shrink-0"
|
||||
selectClass="rounded-lg bg-white"
|
||||
label=""
|
||||
selectArray={[...REPORT_TYPES]}
|
||||
value={reportGubun}
|
||||
onSelectValue={(val) => setReportGubun(String(val))}
|
||||
selectContentClass="z-[101]"
|
||||
/>
|
||||
<div className="ml-auto shrink-0 pl-4">
|
||||
<Button className="text-base py-2" onClick={handleSearch}>조회</Button>
|
||||
</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={[...REPORT_TYPES]}
|
||||
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 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 (<>조회 결과</>);
|
||||
})()
|
||||
}
|
||||
tableData={tableData}
|
||||
headlist={reportTableHeadlist}
|
||||
pageSize={10}
|
||||
total={tableCount}
|
||||
cellClick={handleReportCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
className="!m-0"
|
||||
clickableHeadIds={["fileName"]}
|
||||
headName={<>조회 결과</>}
|
||||
tableData={tableData}
|
||||
headlist={reportTableHeadlist}
|
||||
pageSize={10}
|
||||
total={tableCount}
|
||||
cellClick={handleReportCellClick}
|
||||
cellClassName={(row, headId) => {
|
||||
if (headId !== 'fileName') return '';
|
||||
if (!row.fileSn) return '!text-gray-400';
|
||||
return '!text-left !text-blue-600 underline underline-offset-4 hover:!text-blue-800';
|
||||
}}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ReportTab);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* ➡️ 경로 : 하수관망도 메인 페이지 (오른쪽 돋보기 모양 클릭) - 검색 모달 창
|
||||
*
|
||||
* 📄 Search Modal
|
||||
@@ -16,22 +16,23 @@ import LoadingOverlay from '@/components/ui/LoadingBar';
|
||||
import { UITab, UiTable } from '@common/business_components/ui';
|
||||
import { Button } from '@common/components/ui';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import useSearchModalStore from '@/store/useSearchModalStore';
|
||||
import { ExclnSewagePumpCol2, MsSwageMhlCol2, MsSwagePplCol2, stofcbrCol2, SwtrmtFcltCol2, SwtrmtPrcsZoneCol2 } from '../components/common/table';
|
||||
import BasicTab, { BASIC_TYPES } from './basicTab';
|
||||
import OperationTab from './operationTab';
|
||||
import ReportTab from './reportTab';
|
||||
import useSearchModal from './useSearchModal';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
import { openWindowPopup } from '@/features/windowPopup/lib/openWindowPopup';
|
||||
|
||||
type SearchModalProps = {
|
||||
renderMode?: "launcher" | "popup";
|
||||
renderMode?: 'launcher' | 'popup';
|
||||
autoSearchReady?: boolean;
|
||||
};
|
||||
|
||||
const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", autoSearchReady = false }) => {
|
||||
const SearchModal: React.FC<SearchModalProps> = ({ renderMode = 'launcher', autoSearchReady = false }) => {
|
||||
const {
|
||||
showTable,
|
||||
isSearchModalOpen, setIsSearchModalOpen,
|
||||
@@ -65,40 +66,38 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", auto
|
||||
operationResultExcelDownload,
|
||||
handleOperationPageNation
|
||||
} = useOperationTab();
|
||||
const hasAutoOpenedReportRef = useRef(false);
|
||||
const hasAutoSearchedRef = useRef(false);
|
||||
|
||||
const hasInitializedTabAutoSearchRef = useRef(false);
|
||||
|
||||
const dynamicHeadlist: { id: string, title: string }[] = useMemo(() => {
|
||||
if (activeTab === 'basic') { // 기본정보
|
||||
if (basicType === 'tb_ms_swage_ppl') return MsSwagePplCol2;// 하수관로
|
||||
if (basicType === 'tb_il_swage_prcs_fclt') return SwtrmtFcltCol2;// 하수처리시설
|
||||
if (basicType === 'tb_ms_swage_prcs_zone') return SwtrmtPrcsZoneCol2; // 하수처리구역
|
||||
if (basicType === 'tb_ms_swage_mhl') return MsSwageMhlCol2; // 하수맨홀
|
||||
if (basicType === 'tb_ms_swage_pmstn') return ExclnSewagePumpCol2; // 하수펌프장
|
||||
if (basicType === 'tb_ms_stofcbr') return stofcbrCol2; // 우수토실
|
||||
|
||||
if (activeTab === 'basic') {
|
||||
if (basicType === 'tb_ms_swage_ppl') return MsSwagePplCol2;
|
||||
if (basicType === 'tb_il_swage_prcs_fclt') return SwtrmtFcltCol2;
|
||||
if (basicType === 'tb_ms_swage_prcs_zone') return SwtrmtPrcsZoneCol2;
|
||||
if (basicType === 'tb_ms_swage_mhl') return MsSwageMhlCol2;
|
||||
if (basicType === 'tb_ms_swage_pmstn') return ExclnSewagePumpCol2;
|
||||
if (basicType === 'tb_ms_stofcbr') return stofcbrCol2;
|
||||
return [];
|
||||
}
|
||||
if (activeTab === 'operation') return OPERATION_TABLE_HEADLIST; // 유지관리 정보
|
||||
if (activeTab === 'operation') return OPERATION_TABLE_HEADLIST;
|
||||
|
||||
return [];
|
||||
}, [activeTab, basicType, workType]);
|
||||
}, [activeTab, basicType, workType, OPERATION_TABLE_HEADLIST]);
|
||||
|
||||
const dynamicHeadlist2: { id: string, title: string }[] = useMemo(() => {
|
||||
if (activeTab === 'operation') {
|
||||
if (workType === WORK_TYPES[0]?.val) return MAINTENANACE_TABLE_HEADLIST; // 보수 및 갱신
|
||||
if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST; // 점검 및 진단
|
||||
if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST; // 청소 및 준설
|
||||
if (workType === WORK_TYPES[0]?.val) return MAINTENANACE_TABLE_HEADLIST;
|
||||
if (workType === WORK_TYPES[1]?.val) return CLEANING_TABLE_HEADLIST;
|
||||
if (workType === WORK_TYPES[2]?.val) return INSPECTION_TABLE_HEADLIST;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [activeTab, workType]);
|
||||
}, [activeTab, workType, WORK_TYPES, MAINTENANACE_TABLE_HEADLIST, CLEANING_TABLE_HEADLIST, INSPECTION_TABLE_HEADLIST]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderMode !== "launcher" || !isSearchModalOpen) return;
|
||||
if (renderMode !== 'launcher' || !isSearchModalOpen) return;
|
||||
|
||||
const popupWindow = openWindowPopup("mapSearch", {
|
||||
const popupWindow = openWindowPopup('mapSearch', {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
@@ -107,59 +106,49 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", auto
|
||||
}
|
||||
|
||||
setIsSearchModalOpen(false);
|
||||
}, [renderMode, isSearchModalOpen]);
|
||||
|
||||
const openReportTabPopup = () => {
|
||||
const popupWindow = openWindowPopup("reportTab", {
|
||||
sourcePath: window.location.pathname,
|
||||
});
|
||||
|
||||
if (!popupWindow) {
|
||||
alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'report') {
|
||||
hasAutoOpenedReportRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasAutoOpenedReportRef.current) return;
|
||||
|
||||
openReportTabPopup();
|
||||
hasAutoOpenedReportRef.current = true;
|
||||
}, [activeTab]);
|
||||
}, [isSearchModalOpen, renderMode, setIsSearchModalOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
resetFilters();
|
||||
}, [activeTab, basicType, workType]);
|
||||
}, [activeTab, basicType, workType, resetFilters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renderMode !== "popup" || !autoSearchReady || hasAutoSearchedRef.current) return;
|
||||
if (renderMode !== 'popup' || !autoSearchReady || hasAutoSearchedRef.current) return;
|
||||
|
||||
hasAutoSearchedRef.current = true;
|
||||
hasInitializedTabAutoSearchRef.current = true;
|
||||
void fetchFilteredData();
|
||||
}, [autoSearchReady, fetchFilteredData, renderMode]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
if (renderMode !== 'popup' || !autoSearchReady) return;
|
||||
if (!hasInitializedTabAutoSearchRef.current) return;
|
||||
if (activeTab === 'report') return;
|
||||
|
||||
setCurPage(1);
|
||||
setIsResetPage(true);
|
||||
setIsResetDetailPage(true);
|
||||
void fetchFilteredData();
|
||||
}, [activeTab, basicType, workType]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsResetPage(false);
|
||||
setIsResetDetailPage(true);
|
||||
setDetailBizId(0);
|
||||
if (showTable) fetchFilteredData();
|
||||
if (showTable) {
|
||||
void fetchFilteredData();
|
||||
}
|
||||
}, [curPage]);
|
||||
|
||||
// 페이지 변경 시마다 데이터 fetch
|
||||
useEffect(() => {
|
||||
setIsResetDetailSelection(false);
|
||||
if (showTable) handleOperationPageNation(Number(detailBizId));
|
||||
if (showTable) {
|
||||
void handleOperationPageNation(Number(detailBizId));
|
||||
}
|
||||
}, [detailCurPage]);
|
||||
|
||||
|
||||
const renderBody = () => (
|
||||
<>
|
||||
{/* 탭 영역 */}
|
||||
<UITab
|
||||
defaultValue={activeTab}
|
||||
tabList={TABS.map((tab: any) => ({
|
||||
@@ -168,69 +157,47 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", auto
|
||||
}))}
|
||||
onValueChange={(value: string) => setActiveTab(value)}
|
||||
/>
|
||||
{activeTab === 'basic' && (<BasicTab />)} {/* 기본정보 탭 */}
|
||||
{activeTab === 'operation' && (<OperationTab />)} {/* 유지관리 정보 탭 */}
|
||||
{activeTab === 'report' && (
|
||||
<div className="mt-4 flex h-[79dvh] w-full items-center justify-center rounded-lg border bg-white p-6">
|
||||
<div className="flex max-w-[520px] flex-col items-center text-center">
|
||||
<h2 className="mb-3 text-2xl font-semibold text-gray-900">
|
||||
하수도정비 기본계획보고서
|
||||
</h2>
|
||||
<p className="mb-5 text-lg leading-8 text-gray-600">
|
||||
보고서 상세는 모달 내부 탭 대신
|
||||
<br />
|
||||
전용 윈도우 팝업으로 열리도록 전환되었습니다.
|
||||
</p>
|
||||
<Button className="px-6 py-2 text-white" onClick={openReportTabPopup}>
|
||||
전용 팝업 열기
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)} {/* 하수도정비기본계획보고서 탭 */}
|
||||
{activeTab === 'basic' && (<BasicTab />)}
|
||||
{activeTab === 'operation' && (<OperationTab />)}
|
||||
{activeTab === 'report' && (<ReportTab open={activeTab === 'report'} />)}
|
||||
|
||||
|
||||
{/* 📋 결과 리스트 */}
|
||||
{/* 기본정보 탭 */}
|
||||
{activeTab === 'basic' && (
|
||||
<div className="mt-4 h-[79dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable className='overflow-x-auto [&_table]:min-w-full [&_table]:w-max'
|
||||
headName={(() => {
|
||||
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text
|
||||
return (<>{label} 조회 결과</>);
|
||||
})()
|
||||
}
|
||||
tableData={tableData}
|
||||
headlist={dynamicHeadlist}
|
||||
pageSize={10}
|
||||
total={tableCount}
|
||||
cellClick={handleBasicCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
curPage={curPage}
|
||||
headButton={
|
||||
<div className='flex'>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<UiTable
|
||||
className='overflow-x-auto [&_table]:min-w-full [&_table]:w-max'
|
||||
headName={(() => {
|
||||
const label = BASIC_TYPES.find((type) => type.val === basicType)?.text;
|
||||
return (<>{label} 조회 결과</>);
|
||||
})()}
|
||||
tableData={tableData}
|
||||
headlist={dynamicHeadlist}
|
||||
pageSize={10}
|
||||
total={tableCount}
|
||||
cellClick={handleBasicCellClick}
|
||||
onPageChange={setCurPage}
|
||||
resetPageNo={isResetPage}
|
||||
resetSelection={isResetSelection}
|
||||
curPage={curPage}
|
||||
headButton={
|
||||
<div className='flex'>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium primary" size="sm" onClick={viewMultipleFeature}>지도에서 보기</Button>
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-lg font-medium green" size="sm" onClick={basicExcelDownload}>파일 다운로드</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 유지관리 정보 탭 */}
|
||||
|
||||
{activeTab === 'operation' && (
|
||||
<>
|
||||
<div className="h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border mt-4">
|
||||
{/* <div className="flex-1 px-2 pb-2"> */}
|
||||
<div className="mt-4 h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
|
||||
<LoadingOverlay isLoading={isLoading} />
|
||||
<UiTable
|
||||
headName={
|
||||
(() => {
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text
|
||||
return (<>{label} 사업 현황</>);
|
||||
})()
|
||||
}
|
||||
headName={(() => {
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text;
|
||||
return (<>{label} 사업 현황</>);
|
||||
})()}
|
||||
tableData={tableData}
|
||||
headlist={dynamicHeadlist}
|
||||
pageSize={OPERATION_TABLE_SIZE}
|
||||
@@ -244,20 +211,15 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", auto
|
||||
<Button className="px-4 py-2 !mr-[1.2rem] text-white rounded-sm text-sm font-medium green" size="sm" onClick={operationExcelDownload}>파일 다운로드</Button>
|
||||
}
|
||||
/>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
|
||||
<div className="h-[39dvh] w-full overflow-x-hidden overflow-y-auto rounded-lg border">
|
||||
{/* 테이블2 영역 */}
|
||||
{/* <div className="flex-1 px-2 pb-2"> */}
|
||||
<LoadingOverlay isLoading={isDetailLoading} />
|
||||
<UiTable
|
||||
headName={
|
||||
(() => {
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text
|
||||
return (<>{label} 결과표</>);
|
||||
})()
|
||||
}
|
||||
headName={(() => {
|
||||
const label = WORK_TYPES.find((type) => type.val === workType)?.text;
|
||||
return (<>{label} 결과표</>);
|
||||
})()}
|
||||
tableData={detailTableData}
|
||||
headlist={dynamicHeadlist2}
|
||||
pageSize={OPERATION_TABLE_SIZE}
|
||||
@@ -274,14 +236,13 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", auto
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{/* </div> */}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (renderMode === "launcher") {
|
||||
if (renderMode === 'launcher') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -291,9 +252,10 @@ const SearchModal: React.FC<SearchModalProps> = ({ renderMode = "launcher", auto
|
||||
<div className="h-[calc(100dvh-5.6rem)] w-full overflow-y-hidden rounded-lg border bg-white p-4">
|
||||
{renderBody()}
|
||||
</div>
|
||||
<Toaster position="top-center"></Toaster>
|
||||
<Toaster position="top-center" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(SearchModal);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 📄 useReportTab Hook
|
||||
* 검색 모달 보고서 탭 훅
|
||||
*
|
||||
@@ -40,6 +40,15 @@ export type ReportDataFile = {
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const getStringValue = (value: string | number | undefined) => String(value ?? '');
|
||||
|
||||
const buildDisplayFileName = (files: ReportDataFile[]) => {
|
||||
if (files.length === 0) return '-';
|
||||
if (files.length === 1) return files[0]?.fileTitle ?? '-';
|
||||
|
||||
return `${files[0]?.fileTitle ?? '대표 파일'} 외 ${files.length - 1}건`;
|
||||
};
|
||||
|
||||
export default function useReportTab() {
|
||||
const REPORT_TYPES = [
|
||||
{ val: '0', text: '전체' },
|
||||
@@ -59,11 +68,11 @@ export default function useReportTab() {
|
||||
*/
|
||||
const flattenFolders = (data: Folder[]): FlattenedFolder[] => {
|
||||
const result: FlattenedFolder[] = [];
|
||||
const exceptNodeNm = ["부분", "통합", "전체"];
|
||||
const addNodeNm = ["보고서", "요약보고서", "부록", "기타"]; // level5
|
||||
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"
|
||||
0: 'root', 1: 'sido', 2: 'sigungu', 3: 'approvalYear',
|
||||
4: 'plantZoneGubun', 5: 'reportGubun', 7: 'fileName', 8: 'regDate'
|
||||
};
|
||||
|
||||
const traverse = (
|
||||
@@ -92,28 +101,26 @@ export default function useReportTab() {
|
||||
newPath.clsfSystmSn = node.clsfSystmSn; // 파일 조회를 위한 고유번호
|
||||
result.push(newPath);
|
||||
} else {
|
||||
if (level === 4) {
|
||||
if (exceptNodeNm.includes(node.clsfSystmNm)) {
|
||||
level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함.
|
||||
}
|
||||
if (level === 4 && exceptNodeNm.includes(node.clsfSystmNm)) {
|
||||
level--; // 제외할 노드 이름인 경우, 현재의 레벨에 하위값이 써지도록 함.
|
||||
}
|
||||
node.children.forEach((child) => {
|
||||
traverse(child, newPath, level + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (data && data.length > 0 && data[0]?.clsfSystmNm == "기본계획") { // data가 비어있지 않고 children이 있는 경우에만 처리
|
||||
if (data && data.length > 0 && data[0]?.clsfSystmNm == '기본계획') {
|
||||
data.forEach((root) => traverse(root, {}, 0));
|
||||
} else {
|
||||
data.forEach((root) => traverse(root, {}, 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const getDcFiles = async (clsfSystmSn: string): Promise<ReportDataFile[]> => {
|
||||
if (!clsfSystmSn) {
|
||||
console.error("clsfSystmSn is required to get files.");
|
||||
console.error('clsfSystmSn is required to get files.');
|
||||
return [];
|
||||
}
|
||||
const queryString = 'clsfSystmSn=' + clsfSystmSn;
|
||||
@@ -126,29 +133,54 @@ export default function useReportTab() {
|
||||
const fileReportData = await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const fileArr: ReportDataFile[] = await getDcFiles(String(item.clsfSystmSn));
|
||||
let result: FlattenedFolder;
|
||||
if (fileArr.length > 0) {
|
||||
const f = fileArr[0];
|
||||
const fCount = fileArr.length;
|
||||
result = { ...item, fileName: f?.fileTitle + " 외 " + fCount + "건", regDate: f?.regDt, fileSn: f?.fileSn, filePath: f?.filePath };
|
||||
} else {
|
||||
result = { ...item, fileName: "", regDate: "", fileSn: "", filePath: "" };
|
||||
if (fileArr.length === 0) {
|
||||
return {
|
||||
...item,
|
||||
fileName: '-',
|
||||
regDate: '',
|
||||
fileSn: '',
|
||||
filePath: '',
|
||||
fileCount: 0,
|
||||
downloadFileTitle: '',
|
||||
};
|
||||
}
|
||||
return result;
|
||||
|
||||
const primaryFile = fileArr[0];
|
||||
return {
|
||||
...item,
|
||||
fileName: buildDisplayFileName(fileArr),
|
||||
regDate: primaryFile?.regDt ?? '',
|
||||
fileSn: primaryFile?.fileSn ?? '',
|
||||
filePath: primaryFile?.filePath ?? '',
|
||||
fileCount: fileArr.length,
|
||||
downloadFileTitle: primaryFile?.fileTitle ?? '',
|
||||
};
|
||||
})
|
||||
);
|
||||
return fileReportData;
|
||||
};
|
||||
|
||||
const getDcFileDownload = async (fileSn: string, fileName: string, filePath: string) => {
|
||||
if (filePath === "") {
|
||||
if (filePath === '') {
|
||||
alert(`filePath 정보가 없으므로 다운로드가 불가합니다.\nfilesn: ${fileSn}`);
|
||||
return;
|
||||
}
|
||||
const queryString = 'filePath=' + encodeURIComponent(filePath);
|
||||
const responseBlob = await getDcData(`/api/v1/fileDownload${queryString ? `?${queryString}` : ''}`, "blob");
|
||||
const responseBlob = await getDcData(`/api/v1/fileDownload${queryString ? `?${queryString}` : ''}`, 'blob');
|
||||
fileDownload(responseBlob, fileName);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadReportFile = async (item: FlattenedFolder) => {
|
||||
const fileSn = getStringValue(item.fileSn);
|
||||
const filePath = getStringValue(item.filePath);
|
||||
const downloadFileTitle = getStringValue(item.downloadFileTitle || item.fileName);
|
||||
|
||||
if (!fileSn || !filePath || !downloadFileTitle || downloadFileTitle === '-') {
|
||||
return;
|
||||
}
|
||||
|
||||
await getDcFileDownload(fileSn, downloadFileTitle, filePath);
|
||||
};
|
||||
|
||||
// 파일 목록 조회
|
||||
const getFiles = async () => {
|
||||
@@ -159,7 +191,7 @@ export default function useReportTab() {
|
||||
data = data?.map((item, index) => ({ ...item, no: (curPage - 1) * 10 + index + 1 }));
|
||||
setFiles(data);
|
||||
} catch (error) {
|
||||
console.error("파일 목록 조회 에러:", error);
|
||||
console.error('파일 목록 조회 에러:', error);
|
||||
} finally {
|
||||
setIsLoading(false); // 로딩 종료
|
||||
}
|
||||
@@ -167,10 +199,10 @@ export default function useReportTab() {
|
||||
|
||||
// cellClick 핸들러
|
||||
const handleCellClick = (index: number | undefined) => {
|
||||
if (typeof index === "number" && files[index]) {
|
||||
if (typeof index === 'number' && files[index]) {
|
||||
const fileName = files[index].fileTitle;
|
||||
const filePath = files[index].filePath;
|
||||
if (files[index].fileSn !== "") {
|
||||
if (files[index].fileSn !== '') {
|
||||
getDcFileDownload(files[index].fileSn, fileName, filePath);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +214,8 @@ export default function useReportTab() {
|
||||
addFileName,
|
||||
getDcFiles,
|
||||
getDcFileDownload,
|
||||
downloadReportFile,
|
||||
getFiles,
|
||||
handleCellClick
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 📄 useSearchModal Hook
|
||||
* 검색 모달 훅
|
||||
*
|
||||
@@ -12,10 +12,10 @@
|
||||
* 🔗 사용 API
|
||||
* - 유지관리 정보 목록 조회: GET /api/v1/bizInfo (sewerage-service)
|
||||
*/
|
||||
import { getData } from "@/components/lib/api";
|
||||
import { getData } from '@/components/lib/api';
|
||||
import { formatDisplayData } from '@/components/lib/utils/formatDisplayValue';
|
||||
import useStore from '../../../store/useStore';
|
||||
import GisApp from "@common/business_components/ui/map/lib/gis.core";
|
||||
import GisApp from '@common/business_components/ui/map/lib/gis.core';
|
||||
import useSearchModalStore from '../../../store/useSearchModalStore';
|
||||
import useBasicTab from './useBasicTab';
|
||||
import useOperationTab from './useOperationTab';
|
||||
@@ -23,61 +23,78 @@ import { postPopupMessageAndClose } from '@/features/windowPopup/lib/popupMessag
|
||||
import { WINDOW_POPUP_MESSAGE_TYPE } from '@/features/windowPopup/model/types';
|
||||
|
||||
const isPopupWindow = () =>
|
||||
typeof window !== "undefined" &&
|
||||
typeof window !== 'undefined' &&
|
||||
!!window.opener &&
|
||||
window.location.pathname.startsWith("/popup/");
|
||||
window.location.pathname.startsWith('/popup/');
|
||||
|
||||
export default function useSearchModal() {
|
||||
const {
|
||||
activeTab, workType,
|
||||
tableData, detailTableData,
|
||||
curPage,
|
||||
var1, var2,
|
||||
yearFilter,
|
||||
todayYear, todayMonth, todayDate,
|
||||
activeTab,
|
||||
tableData,
|
||||
detailTableData,
|
||||
setIsSearchModalOpen,
|
||||
setShowTable,
|
||||
setTableData, setDetailTableData,
|
||||
setTableCount, setDetailTableCount,
|
||||
setTableData,
|
||||
setCurPage,
|
||||
setIsLoading,
|
||||
setIsResetPage, setIsResetDetailPage,
|
||||
setIsResetSelection, setIsResetDetailSelection,
|
||||
getDateString
|
||||
setIsResetPage,
|
||||
setIsResetDetailPage,
|
||||
setIsResetSelection,
|
||||
setIsResetDetailSelection,
|
||||
getDateString,
|
||||
} = useSearchModalStore();
|
||||
const { fetchBasicFilteredData } = useBasicTab();
|
||||
const { OPERATION_TABLE_SIZE } = useOperationTab();
|
||||
const { BIZ_TY_CD } = useStore();
|
||||
|
||||
const TABS = [
|
||||
{ val: "basic", text: "기본 정보" },
|
||||
{ val: "operation", text: "유지관리 정보" },
|
||||
{ val: "report", text: "하수도 정비 기본계획 보고서" },
|
||||
{ val: 'basic', text: '기본 정보' },
|
||||
{ val: 'operation', text: '유지관리 정보' },
|
||||
{ val: 'report', text: '하수도 정비 기본계획 보고서' },
|
||||
];
|
||||
|
||||
// 검색 API 호출 함수
|
||||
const fetchFilteredData = async () => {
|
||||
setIsLoading(true); // 로딩 시작
|
||||
setIsResetSelection(true); // 선택 초기화
|
||||
setIsResetDetailSelection(true); // 선택 초기화
|
||||
const {
|
||||
activeTab: currentTab,
|
||||
curPage,
|
||||
var1,
|
||||
var2,
|
||||
yearFilter,
|
||||
workType,
|
||||
todayYear,
|
||||
todayMonth,
|
||||
todayDate,
|
||||
setTableData,
|
||||
setDetailTableData,
|
||||
setTableCount,
|
||||
setDetailTableCount,
|
||||
setShowTable,
|
||||
setIsLoading,
|
||||
setIsResetSelection,
|
||||
setIsResetDetailSelection,
|
||||
} = useSearchModalStore.getState();
|
||||
|
||||
setIsLoading(true);
|
||||
setIsResetSelection(true);
|
||||
setIsResetDetailSelection(true);
|
||||
|
||||
try {
|
||||
if (activeTab === 'basic') {
|
||||
fetchBasicFilteredData();
|
||||
|
||||
} else if (activeTab === 'operation') {
|
||||
const RequestDTO: Record<string, string> = {
|
||||
if (currentTab === 'basic') {
|
||||
await fetchBasicFilteredData();
|
||||
} else if (currentTab === 'operation') {
|
||||
const requestDto: Record<string, string> = {
|
||||
page: `${curPage - 1}`,
|
||||
size: `${OPERATION_TABLE_SIZE}`,
|
||||
};
|
||||
if (var1) RequestDTO.bizInfoNm = var1;
|
||||
if (var2 != '0' && var2) RequestDTO.bizTypeCd = var2;
|
||||
// 수행기간 범위
|
||||
if (var1) requestDto.bizInfoNm = var1;
|
||||
if (var2 !== '0' && var2) requestDto.bizTypeCd = var2;
|
||||
|
||||
const startDate = getDateString(
|
||||
yearFilter.start,
|
||||
'',
|
||||
'',
|
||||
"19000101",
|
||||
'19000101',
|
||||
);
|
||||
const endDate = getDateString(
|
||||
yearFilter.end,
|
||||
@@ -85,22 +102,19 @@ export default function useSearchModal() {
|
||||
'',
|
||||
`${todayYear}${todayMonth}${todayDate}`,
|
||||
);
|
||||
RequestDTO.fromflfmtEndYmd = startDate;
|
||||
RequestDTO.toflfmtEndYmd = endDate;
|
||||
|
||||
// JSON.parse 사용으로 문자열 -> 배열 변환
|
||||
RequestDTO.workTypes = JSON.parse(workType.replace(/'/g, '"'));
|
||||
|
||||
const params = new URLSearchParams(RequestDTO).toString();
|
||||
requestDto.fromflfmtEndYmd = startDate;
|
||||
requestDto.toflfmtEndYmd = endDate;
|
||||
requestDto.workTypes = JSON.parse(workType.replace(/'/g, '"'));
|
||||
|
||||
const params = new URLSearchParams(requestDto).toString();
|
||||
const res = await getData(`v1/bizInfo?${params}`);
|
||||
|
||||
const mappedContent = (res.content || []).map((item: any, index: number) => {
|
||||
const formattedItem = formatDisplayData(item);
|
||||
const matched = BIZ_TY_CD.find(code => code.lwpoCd === item.bizTypeCd);
|
||||
const matched = BIZ_TY_CD.find((code) => code.lwpoCd === item.bizTypeCd);
|
||||
return {
|
||||
...formattedItem,
|
||||
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1, // 1부터 시작하는 일련번호 추가
|
||||
rowNumber: (curPage - 1) * OPERATION_TABLE_SIZE + index + 1,
|
||||
bizTypeCd: matched ? matched.lwpoCdNm : item.bizTypeCd,
|
||||
};
|
||||
});
|
||||
@@ -118,20 +132,19 @@ export default function useSearchModal() {
|
||||
setTableData([]);
|
||||
setIsResetSelection(false);
|
||||
} finally {
|
||||
setIsLoading(false); // 로딩 종료
|
||||
setIsLoading(false);
|
||||
setIsResetSelection(false);
|
||||
// setIsResetDetailSelection(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
|
||||
const handleSearch = () => {
|
||||
setCurPage(1); // 페이지를 1로 초기화
|
||||
fetchFilteredData();
|
||||
setIsResetPage(true); // 페이지를 초기화
|
||||
setIsResetDetailPage(true); // 디테일 페이지를 초기화
|
||||
setIsResetSelection(false); // 선택 초기화
|
||||
setIsResetDetailSelection(false); // 선택 초기화
|
||||
setCurPage(1);
|
||||
void fetchFilteredData();
|
||||
setIsResetPage(true);
|
||||
setIsResetDetailPage(true);
|
||||
setIsResetSelection(false);
|
||||
setIsResetDetailSelection(false);
|
||||
setShowTable(true);
|
||||
};
|
||||
|
||||
@@ -186,7 +199,7 @@ export default function useSearchModal() {
|
||||
setIsSearchModalOpen(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { TABS, getDateString, handleSearch, viewMultipleFeature, fetchFilteredData };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# 정보검색-하수도정비기본계획보고서 통합검증계획
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
- 분석도구 `정보검색` 내 `하수도 정비 기본계획 보고서` 유지보수 검증 계획을 기록한다.
|
||||
|
||||
## 2. 검증 범위
|
||||
|
||||
- report 탭 추가 팝업 제거 여부
|
||||
- 조회조건 순서 및 필터 동작
|
||||
- 탭 전환 시 자동조회 동작
|
||||
- 파일명 셀 직접 다운로드 동작
|
||||
- 보고서 행 클릭 제한 동작
|
||||
- 공통 select 포커스 표시 잘림 여부
|
||||
- 기존 `dc-service` 호출 유지 여부
|
||||
|
||||
## 3. 검증 기준
|
||||
|
||||
1. `정보검색` 팝업에서 `report` 탭 선택 시 추가 윈도우 팝업이 열리지 않아야 한다.
|
||||
2. 조회조건이 `시도 > 시군구 > 승인년도 > 구분 > 보고서 구분` 순서로 보여야 한다.
|
||||
3. 조회조건이 한 줄 우선 정렬로 보이고 불필요한 하단 공백이 없어야 한다.
|
||||
4. `기본 정보`, `유지관리 정보`, `하수도 정비 기본계획 보고서` 탭 전환 시 각 탭의 기본조회가 자동 실행되어야 한다.
|
||||
5. 파일이 있는 행의 `파일명`만 링크 스타일을 가져야 한다.
|
||||
6. 파일명 클릭 시 별도 파일목록 팝업 없이 다운로드가 시작되어야 한다.
|
||||
7. 파일명 외 다른 셀 클릭 시 추가 동작이 없어야 한다.
|
||||
8. `시도`, `시군구`, `승인년도`, `구분`, `보고서 구분` select 포커스 표시가 잘리지 않아야 한다.
|
||||
9. Network에서 `dc-service`의 `clsf`, `file`, `fileDownload` 호출이 유지되어야 한다.
|
||||
|
||||
## 4. 라운드별 검증 계획
|
||||
|
||||
### 4.1 정적 검증
|
||||
|
||||
- 수정 파일 import/상태 흐름 재검토
|
||||
- 기존 popup 호출 제거 여부 검색
|
||||
- `UiTable` 옵션 추가가 다른 화면 기본동작을 깨지 않는지 코드 기준 확인
|
||||
|
||||
### 4.2 명령 검증
|
||||
|
||||
- `pnpm --filter sn lint`
|
||||
- 목적: 타입/구문/기본 lint 문제 확인
|
||||
|
||||
### 4.3 수동 브라우저 검증
|
||||
|
||||
- 분석도구 `정보검색` 클릭
|
||||
- `기본 정보`, `유지관리 정보`, `하수도 정비 기본계획 보고서` 순서로 탭 전환
|
||||
- 각 탭 진입 시 버튼 없이 기본조회 자동 실행 확인
|
||||
- 보고서 탭 조건 변경 후 조회
|
||||
- 파일명 클릭 다운로드 확인
|
||||
- 파일명 외 셀 클릭 무반응 확인
|
||||
- 기본정보/유지관리/보고서 탭의 select 포커스 잘림 여부 확인
|
||||
- 하수처리시설 상세의 `조회` 버튼으로 여는 `reportTab` 팝업 회귀 확인
|
||||
|
||||
## 5. 차단 사유 분류 기준
|
||||
|
||||
- 코드 문제: 타입 오류, 런타임 오류, 잘못된 자동조회/필터/다운로드 동작
|
||||
- 환경 문제: pnpm 설치 정책, lockfile 설정, TTY 제약, 개발 서버 충돌
|
||||
@@ -0,0 +1,37 @@
|
||||
# 정보검색-하수도정비기본계획보고서 통합검증결과
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
- 분석도구 `정보검색` 내 `하수도 정비 기본계획 보고서` 유지보수 검증 수행 결과를 기록한다.
|
||||
|
||||
## 2. 수행한 검증
|
||||
|
||||
### 2.1 정적 확인
|
||||
|
||||
- `report` 탭 자동 팝업 오픈 제거 반영 여부 확인
|
||||
- `reportFiles` 팝업 호출 제거 반영 여부 확인
|
||||
- 파일명 직접 다운로드 로직 연결 여부 확인
|
||||
- 탭 전환 시 자동조회 흐름 추가 여부 확인
|
||||
- `UiTable`의 `clickableHeadIds` 옵션이 미지정 화면에는 영향이 없는 구조인지 확인
|
||||
- 공통 `SelectBox` 포커스 보정이 style 변경 수준인지 확인
|
||||
|
||||
### 2.2 명령 실행
|
||||
|
||||
- 2026-06-30: `pnpm --filter sn lint`
|
||||
- 1차 결과: 샌드박스 임시 파일 권한 오류로 실패
|
||||
- 2차 결과: `ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY`로 실패
|
||||
- 3차 결과: `CI=true` 재시도 후 `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`로 실패
|
||||
|
||||
## 3. 현재 검증 상태
|
||||
|
||||
- 코드 반영: 완료
|
||||
- 정적 구조 확인: 완료
|
||||
- 자동 lint 검증: 환경 문제로 미완료
|
||||
- 브라우저 수동 검증: 미실행
|
||||
|
||||
## 4. 보류/리스크
|
||||
|
||||
1. 현재 워크스페이스 pnpm 환경이 lockfile 설정 불일치로 자동 lint를 진행하지 못한다.
|
||||
2. 브라우저 수동 검증이 남아 있어 탭별 자동조회 체감, select 포커스 표시, 여백 보정 체감은 아직 확인하지 못했다.
|
||||
3. 다건 파일 대표 다운로드 기준은 현재 첫 번째 파일 기준이다.
|
||||
4. `reportFiles` 팝업 관련 미사용 코드 정리는 후속 정리 대상으로 남긴다.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 정보검색-하수도정비기본계획보고서 통합구현기록
|
||||
|
||||
## 1. 문서 목적
|
||||
|
||||
- 분석도구 `정보검색` 내 `하수도 정비 기본계획 보고서` 유지보수 구현 내용을 기록한다.
|
||||
|
||||
## 2. 입력 근거
|
||||
|
||||
- 공통 설계: `IIPBS/docs/작업계획/SN-정보검색-하수도정비기본계획보고서-유지보수/001-de-정보검색-하수도정비기본계획보고서-설계.md`
|
||||
- 사용자 요구사항
|
||||
- report 탭 조회결과를 기존 영역에 표시
|
||||
- 파일명 클릭 즉시 다운로드
|
||||
- 조회조건 순서 변경
|
||||
- 조회조건 한 줄 정렬 및 여백 보정
|
||||
- 탭 전환 시 기본조회
|
||||
- select 포커스 잘림 보정
|
||||
|
||||
## 3. 공통 계약
|
||||
|
||||
- 기존 `dc-service` 프록시 계약 `clsf`, `file`, `fileDownload`를 그대로 사용한다.
|
||||
- `sewerage-service` 신규 API와 DB 변경은 이번 범위에서 수행하지 않는다.
|
||||
- 공통 컴포넌트 수정은 `sn` 요구사항을 위한 옵션 추가 또는 스타일 보정으로만 한정한다.
|
||||
|
||||
## 4. 구현 라운드 요약
|
||||
|
||||
### 4.1 report 탭 인라인 복구
|
||||
|
||||
- `SearchModal`에서 `report` 탭 선택 시 전용 팝업을 자동으로 열던 흐름을 제거했다.
|
||||
- 안내 카드 대신 `ReportTab`을 같은 본문 영역에 직접 렌더링하도록 바꿨다.
|
||||
|
||||
### 4.2 파일명 직접 다운로드 전환
|
||||
|
||||
- `ReportTab`의 셀 클릭을 `파일명` 컬럼으로 한정했다.
|
||||
- 파일이 있는 행의 `파일명` 셀에 파란 글씨, 밑줄, 포인터 스타일을 적용했다.
|
||||
- 파일 클릭 시 `dc-service` 파일 다운로드를 바로 호출하도록 연결했다.
|
||||
- 다건 파일은 현재 구조상 대표 파일 정의가 필요해서 첫 번째 파일을 대표 다운로드 대상으로 사용했다.
|
||||
- 표시 문자열은 `1건이면 실제 파일명`, `2건 이상이면 첫 파일명 외 N건`으로 보정했다.
|
||||
|
||||
### 4.3 조회조건 재배치 및 필터 보강
|
||||
|
||||
- 조회조건 표시 순서를 `시도 > 시군구 > 승인년도 > 구분 > 보고서 구분`으로 맞췄다.
|
||||
- 기존에 조회 UI에 없던 `구분(plantZoneGubun)` 필터를 추가했다.
|
||||
- 승인년도는 `0`, `9999` 같은 placeholder 값을 실제 필터에서 제외하도록 정리했다.
|
||||
|
||||
### 4.4 탭 기본조회 보강
|
||||
|
||||
- 팝업 최초 진입 시 1회 자동조회만 하던 흐름을 탭 전환 기준으로 보강했다.
|
||||
- `기본 정보`, `유지관리 정보`는 탭 또는 대표 조건이 바뀔 때 기본조건 조회가 다시 돌도록 `SearchModal/useSearchModal` 공통 흐름을 정리했다.
|
||||
- `하수도 정비 기본계획 보고서`는 `ReportTab` 내부에서 탭 진입 시 1회 자동조회되도록 유지했다.
|
||||
|
||||
### 4.5 조회조건 레이아웃 및 결과 영역 정리
|
||||
|
||||
- 보고서 탭 조회조건은 한 줄 우선 정렬로 재배치하고, 화면이 좁을 때는 줄바꿈 대신 가로 스크롤이 나도록 바꿨다.
|
||||
- 조회조건 박스의 과한 최소 높이와 하단 여백을 줄여 빈 줄처럼 보이던 공간을 제거했다.
|
||||
- 결과 영역은 고정 높이 wrapper를 제거하고 `UiTable` 자체 레이아웃을 사용하게 정리해 상하 공백을 줄였다.
|
||||
- 최종 보정으로 조회조건 각 요소 간 간격을 조금 더 벌려 보이도록 조정했다.
|
||||
|
||||
### 4.6 행 클릭 제한 및 공통 select 포커스 보정
|
||||
|
||||
- 보고서 표는 이제 행 전체가 아니라 `파일명` 컬럼만 클릭되도록 `UiTable`에 `clickableHeadIds` 옵션을 추가했다.
|
||||
- 이 옵션은 기본 미지정 시 기존 동작을 유지하므로 다른 화면에 영향이 없다.
|
||||
- 공통 `SelectBox`는 포커스 outline이 위에서 잘리지 않도록 root에 `overflow-visible`과 세로 여유를 주고, trigger 높이를 조금 늘렸다.
|
||||
- 추가로 포커스 ring을 바깥이 아닌 안쪽(`ring-inset`)으로 바꿔 기본정보/유지관리/보고서 탭 모두에서 잘림 없이 보이도록 보정했다.
|
||||
|
||||
## 5. 수정 파일
|
||||
|
||||
- `frontend/apps/sn/app/(map-layout)/map/searchModal/searchModal.tsx`
|
||||
- `frontend/apps/sn/app/(map-layout)/map/searchModal/reportTab.tsx`
|
||||
- `frontend/apps/sn/app/(map-layout)/map/searchModal/useReportTab.ts`
|
||||
- `frontend/apps/sn/app/(map-layout)/map/searchModal/useSearchModal.ts`
|
||||
- `frontend/apps/sn/src/features/windowPopup/ui/SearchModalPopupContent.tsx`
|
||||
- `frontend/apps/sn/src/features/windowPopup/ui/ReportTabPopupContent.tsx`
|
||||
- `frontend/packages/common/business_components/src/ui/table/index.tsx`
|
||||
- `frontend/packages/common/business_components/src/ui/select/select.css`
|
||||
- `frontend/packages/common/business_components/src/ui/select/lib/selectBox.tsx`
|
||||
|
||||
## 6. 남은 리스크
|
||||
|
||||
- 다건 파일 대표 다운로드 기준은 현재 첫 번째 파일 기준이다.
|
||||
- `reportFiles` 윈도우 팝업 코드는 현재 미호출 상태로 남아 있다.
|
||||
- lint/build 자동 검증은 워크스페이스 pnpm 환경 문제로 완료하지 못했다.
|
||||
- 브라우저에서 최신 select 포커스 스타일과 자동조회 체감은 추가 수동 확인이 필요하다.
|
||||
@@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import ReportTab from "@/app/(map-layout)/map/searchModal/reportTab";
|
||||
import { useEffect, useState } from "react";
|
||||
import { windowPopupDefinitions } from "../model/popupDefinitions";
|
||||
import type { WindowPopupContentProps } from "../model/types";
|
||||
import { getPopupParamValue } from "./popupContentUtils";
|
||||
@@ -6,9 +9,18 @@ import { getPopupParamValue } from "./popupContentUtils";
|
||||
export default function ReportTabPopupContent({
|
||||
searchParams,
|
||||
}: WindowPopupContentProps) {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const ctpvNm = getPopupParamValue(searchParams.ctpvNm);
|
||||
const sggNm = getPopupParamValue(searchParams.sggNm);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!isMounted) {
|
||||
return <div className="h-dvh bg-white" suppressHydrationWarning />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-white p-4">
|
||||
<h1 className="mb-3 shrink-0 text-2xl font-semibold">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import SearchModal from "@/app/(map-layout)/map/searchModal/searchModal";
|
||||
import useStore from "@/store/useStore";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function SearchModalPopupContent() {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const {
|
||||
ctpv,
|
||||
sgg,
|
||||
@@ -17,15 +18,27 @@ export default function SearchModalPopupContent() {
|
||||
} = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
|
||||
// Popup windows do not run map-layout initialization, so load location codes here.
|
||||
if (ctpv.length === 0) void fetchCtpv();
|
||||
if (sgg.length === 0) void fetchSgg();
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, sgg.length]);
|
||||
}, [ctpv.length, fetchCtpv, fetchSgg, isMounted, sgg.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isMounted) return;
|
||||
|
||||
if (BIZ_SE_CD.length === 0) void fetchBIZ_SE_CD();
|
||||
if (BIZ_TY_CD.length === 0) void fetchBIZ_TY_CD();
|
||||
}, [BIZ_SE_CD.length, BIZ_TY_CD.length, fetchBIZ_SE_CD, fetchBIZ_TY_CD]);
|
||||
}, [BIZ_SE_CD.length, BIZ_TY_CD.length, fetchBIZ_SE_CD, fetchBIZ_TY_CD, isMounted]);
|
||||
|
||||
if (!isMounted) {
|
||||
return <div className="h-dvh bg-white" suppressHydrationWarning />;
|
||||
}
|
||||
|
||||
return <SearchModal renderMode="popup" autoSearchReady={ctpv.length > 0 && sgg.length > 0} />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
@@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-13 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xl ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-14 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-xl ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-inset focus:ring-ring focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -148,3 +148,5 @@ export {
|
||||
Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@tailwind components;
|
||||
@tailwind components;
|
||||
|
||||
@layer components {
|
||||
.selectbox_root {
|
||||
@apply flex items-center;
|
||||
@apply flex items-center overflow-visible py-1;
|
||||
}
|
||||
|
||||
.selectbox_label {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"use client";
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
@@ -37,6 +37,7 @@ type UiTableProps<T extends object> = {
|
||||
headButton?: React.ReactNode | undefined;
|
||||
widthFixed?: boolean;
|
||||
isHideCount?: boolean; // 총 건수 default false
|
||||
clickableHeadIds?: string[];
|
||||
};
|
||||
|
||||
type UiHeadProps = {
|
||||
@@ -69,7 +70,8 @@ export function UiTable<T extends object = Record<string, unknown>>({
|
||||
curPage = 1,
|
||||
headButton = undefined,
|
||||
widthFixed = false,
|
||||
isHideCount = false, // 총 건수
|
||||
isHideCount = false,
|
||||
clickableHeadIds,
|
||||
}: UiTableProps<T>) {
|
||||
const [cur, setCur] = useState<number>(curPage);
|
||||
const [idx, setIdx] = useState<number>(index);
|
||||
@@ -97,32 +99,36 @@ export function UiTable<T extends object = Record<string, unknown>>({
|
||||
|
||||
useEffect(() => {
|
||||
setIdx(index);
|
||||
}, [index])
|
||||
}, [index]);
|
||||
|
||||
useEffect(() => {
|
||||
setCur(curPage);
|
||||
}, [curPage])
|
||||
}, [curPage]);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCur(page);
|
||||
onPageChange(page);
|
||||
if (index == 0)
|
||||
setIdx(0)
|
||||
if (index == 0) setIdx(0);
|
||||
};
|
||||
|
||||
const isHeadClickable = (headId: string) => {
|
||||
if (!cellClick) return false;
|
||||
if (!clickableHeadIds || clickableHeadIds.length === 0) return true;
|
||||
return clickableHeadIds.includes(headId);
|
||||
};
|
||||
|
||||
const handleCellClick = (index: number, headId: string) => {
|
||||
if (cellClick) {
|
||||
cellClick(index, headId);
|
||||
setIdx(index);
|
||||
}
|
||||
if (!isHeadClickable(headId) || !cellClick) return;
|
||||
|
||||
cellClick(index, headId);
|
||||
setIdx(index);
|
||||
};
|
||||
|
||||
const getLeafCols = (list: UiHeadProps[]) => {
|
||||
const maxRow = list?.reduce((max, head) => Math.max(max, head.row || 0), 0);
|
||||
return list
|
||||
.filter(h => h.visible !== false)
|
||||
.filter(h => ((h.row ?? 0) + (h.rowSpan ?? 1) - 1) >= maxRow)
|
||||
;
|
||||
.filter((h) => h.visible !== false)
|
||||
.filter((h) => ((h.row ?? 0) + (h.rowSpan ?? 1) - 1) >= maxRow);
|
||||
};
|
||||
|
||||
const leafCols = getLeafCols(headlist);
|
||||
@@ -153,12 +159,15 @@ export function UiTable<T extends object = Record<string, unknown>>({
|
||||
)}
|
||||
<div className={`card !p-0r !m-[1.2rem] ${className}`}>
|
||||
<Table variant="secondary" className={widthFixed ? "table-fixed" : ""}>
|
||||
{widthFixed ?
|
||||
{widthFixed ? (
|
||||
<colgroup>
|
||||
{leafCols.map(col => (
|
||||
{leafCols.map((col) => (
|
||||
<col key={col.id} style={{ width: col.widthPercent ? `${col.widthPercent}%` : undefined }} />
|
||||
))}
|
||||
</colgroup> : ""}
|
||||
</colgroup>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<TableHeader ref={headerRef} className="font-bold">
|
||||
{children ? children : <HeadMakeColSpan upHeadList={headlist} />}
|
||||
</TableHeader>
|
||||
@@ -174,9 +183,9 @@ export function UiTable<T extends object = Record<string, unknown>>({
|
||||
head.id.length > 1 &&
|
||||
(boldArray.includes(index) ? (
|
||||
<TableCell
|
||||
className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${cellClick && idx === index
|
||||
className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${isHeadClickable(head.id) && idx === index
|
||||
? "cursor-pointer !font-[1000]"
|
||||
: cellClick
|
||||
: isHeadClickable(head.id)
|
||||
? "cursor-pointer"
|
||||
: ""
|
||||
} ${cellClassName ? cellClassName(el, head.id, index) : ""} sticky top-20 z-40 bg-secondary !font-bold whitespace-pre-line`}
|
||||
@@ -190,55 +199,38 @@ export function UiTable<T extends object = Record<string, unknown>>({
|
||||
? String(cellValue ?? "")
|
||||
: String(cellValue).includes(".")
|
||||
? String(cellValue)
|
||||
.split(".")
|
||||
.map((part, index) =>
|
||||
index === 0
|
||||
? part.replace(
|
||||
/\B(?=(\d{3})+(?!\d))/g,
|
||||
",",
|
||||
)
|
||||
: part,
|
||||
)
|
||||
.join(".")
|
||||
: String(cellValue ?? "").replace(
|
||||
/\B(?=(\d{3})+(?!\d))/g,
|
||||
",",
|
||||
);
|
||||
.split(".")
|
||||
.map((part, index) =>
|
||||
index === 0
|
||||
? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
: part,
|
||||
)
|
||||
.join(".")
|
||||
: String(cellValue ?? "").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
})()}
|
||||
</TableCell>
|
||||
) : (
|
||||
<TableCell
|
||||
className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${cellClick && idx === index
|
||||
className={`${head.align === "right" ? "text-right" : head.align === "center" ? "text-center" : "text-center"} ${isHeadClickable(head.id) && idx === index
|
||||
? "cursor-pointer !font-[1000]"
|
||||
: cellClick
|
||||
: isHeadClickable(head.id)
|
||||
? "cursor-pointer"
|
||||
: ""
|
||||
} ${cellClassName ? cellClassName(el, head.id, index) : ""} mt-[${boldArray.length * 40}px] whitespace-pre-line`}
|
||||
onClick={() => handleCellClick(index, head.id)}
|
||||
key={`cell-${head.id}-${index}`}
|
||||
>
|
||||
{
|
||||
//체크박스 유형애서 readOnly가 필요한 경우를 위해서 ChkClass 를 사용해서 readOnly 처리함.
|
||||
(() => {
|
||||
const cellValue = (el as Record<string, unknown>)[head.id];
|
||||
return typeof cellValue === "object" &&
|
||||
cellValue !== null &&
|
||||
"chk" in (cellValue as object) &&
|
||||
"readonly" in (cellValue as object) ? (
|
||||
{(() => {
|
||||
const cellValue = (el as Record<string, unknown>)[head.id];
|
||||
return typeof cellValue === "object" &&
|
||||
cellValue !== null &&
|
||||
"chk" in (cellValue as object) &&
|
||||
"readonly" in (cellValue as object) ? (
|
||||
<Checkbox
|
||||
checked={
|
||||
(cellValue as ChkClass)?.chk === true
|
||||
}
|
||||
checked={(cellValue as ChkClass)?.chk === true}
|
||||
onCheckedChange={(checked) => {
|
||||
{
|
||||
console.log(cellValue);
|
||||
}
|
||||
if (
|
||||
(cellValue as ChkClass).readonly == true
|
||||
)
|
||||
return;
|
||||
(cellValue as ChkClass).chk =
|
||||
Boolean(checked);
|
||||
if ((cellValue as ChkClass).readonly == true) return;
|
||||
(cellValue as ChkClass).chk = Boolean(checked);
|
||||
}}
|
||||
>
|
||||
<Check className="h-6 w-6 text-primary" />
|
||||
@@ -258,22 +250,13 @@ export function UiTable<T extends object = Record<string, unknown>>({
|
||||
String(cellValue)
|
||||
.split(".")
|
||||
.map((part, index) =>
|
||||
index === 0
|
||||
? part.replace(
|
||||
/\B(?=(\d{3})+(?!\d))/g,
|
||||
",",
|
||||
)
|
||||
: part,
|
||||
index === 0 ? part.replace(/\B(?=(\d{3})+(?!\d))/g, ",") : part,
|
||||
)
|
||||
.join(".")
|
||||
) : (
|
||||
String(cellValue ?? "").replace(
|
||||
/\B(?=(\d{3})+(?!\d))/g,
|
||||
",",
|
||||
)
|
||||
String(cellValue ?? "").replace(/\B(?=(\d{3})+(?!\d))/g, ",")
|
||||
);
|
||||
})()
|
||||
}
|
||||
})()}
|
||||
</TableCell>
|
||||
)),
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user