Files
frontend/apps/sn/app/(with-base-layout)/oper/floodPre/page.tsx
T
2026-08-25 15:29:55 +09:00

362 lines
11 KiB
TypeScript

/**
* ➡️ 경로 : 유지관리 정보 - 도시침수예방사업
*
* 📄 FloodPre Page
* 침수예방 사업 정보 등록 및 조회 페이지 컴포넌트
*
* - 사업 정보 등록 현황 조회 및 테이블 표시
* - 페이징 및 검색 기능 (등록기간/등록자/파일명 기반)
* - 파일 업로드 및 등록 기능
* - 파일 다운로드 및 삭제 기능
* - 엑셀 데이터 테이블 모달 표시
* - 로딩 오버레이 및 에러 핸들링 지원
*
* 🔗 사용 API
* - 도시침수예방사업 목록 조회: GET /api/v1/sbmrgPrevnt (sewerage-service)
* - 도시침수예방사업 등록: POST /api/v1/sbmrgPrevnt (sewerage-service)
* - 첨부파일 삭제및 Soft Delete 삭제: DELETE /api/v1/atfl/attachments/{atflId} (attach-service)
*/
"use client";
import { deleteData, useCookieAuth } from "@/components/lib/api";
import { searchData } from "@/components/lib/common/apiHelpers";
import { formatDate, handleDownload } from "@/components/lib/utils";
import LoadingOverlay from "@/components/ui/LoadingBar";
import {
StandardDataTable,
getStandardRowNumber,
} from "@common/business_components";
import { Button, Input, Label } from "@common/components";
import { useEffect, useMemo, useState } from "react";
import {
getCoreRowModel,
useReactTable,
type ColumnDef,
type PaginationState,
} from "@tanstack/react-table";
import { openWindowPopup } from "@/features/windowPopup/lib/openWindowPopup";
import { WINDOW_POPUP_MESSAGE_TYPE } from "@/features/windowPopup/model/types";
import { SearchDatePicker } from "../components/SearchDatePicker";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useAlertStore } from "@/shared/lib";
const PAGE_SIZE = 10;
type FloodPreRow = Record<string, unknown> & {
rowNumber: number;
sbmrgPrevntBizSn: string;
sbmrgPrevntBizNm?: string;
atflId?: string;
};
type FloodPreSearch = {
fromDate: Date;
toDate: Date;
projectName: string;
};
const FLOOD_PRE_COLUMNS: Array<{
id: keyof FloodPreRow;
title: string;
width: number;
}> = [
{ id: "rowNumber", title: "순번", width: 70 },
{ id: "sbmrgPrevntBizNm", title: "파일명", width: 260 },
{ id: "mngYr", title: "년도", width: 90 },
{ id: "mngQu", title: "분기", width: 90 },
{ id: "regDt", title: "등록 일자", width: 120 },
{ id: "rgtrNm", title: "등록자", width: 120 },
];
const Page = () => {
const AUTH = useCookieAuth();
const { confirm } = useConfirm();
const alert = useAlertStore((state) => state.setMessage);
// 조회조건
const [fromDate, setFromDate] = useState(() => {
const date = new Date();
date.setDate(date.getDate() - 7);
return date;
});
const [toDate, setToDate] = useState(new Date());
const [usr, setUsr] = useState("");
const [pjNm, setpjNm] = useState("");
// 파일업로드 관련 상태
const [fileNm, setFileNm] = useState(""); //파일명
const [atflId, setAtflId] = useState(""); //파일 ID
const [sbmrgPrevntBizSn, setSbmrgPrevntBizSn] = useState(""); //파일 일련번호
const [tableData, setTableData] = useState<FloodPreRow[]>([]); //테이블 데이터
const [curPage, setCurPage] = useState(1); //현재 페이지
const [totalData, setTotalData] = useState(0); //총 데이터 수
const [isLoading, setIsLoading] = useState(true); //로딩 상태
const columns = useMemo<ColumnDef<FloodPreRow>[]>(
() =>
FLOOD_PRE_COLUMNS.map((column) => ({
id: String(column.id),
accessorFn: (row) => row[column.id],
header: column.title,
cell: ({ getValue }) => String(getValue() ?? ""),
meta: { width: column.width, align: "center" },
})),
[],
);
const pagination: PaginationState = {
pageIndex: curPage - 1,
pageSize: PAGE_SIZE,
};
const table = useReactTable({
data: tableData,
columns,
state: { pagination },
rowCount: totalData,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
getRowId: (row) => String(row.sbmrgPrevntBizSn),
onPaginationChange: (updater) => {
const next =
typeof updater === "function" ? updater(pagination) : updater;
setCurPage(next.pageIndex + 1);
},
});
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
if (event.origin !== window.location.origin) return;
if (
event.data?.type !==
WINDOW_POPUP_MESSAGE_TYPE.FLOOD_PRE_UPLOAD_COMPLETED
)
return;
handleSearch();
};
window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage);
}, [fromDate, toDate, pjNm, curPage]);
// 페이지 변경 시마다 데이터 fetch
useEffect(() => {
fetchData();
}, [curPage]);
// 조회 버튼 클릭 시 1페이지로 이동 및 데이터 초기화
const handleSearch: () => Promise<void> = async () => {
if (curPage === 1) {
await fetchData(1);
return;
}
setCurPage(1); // 페이지 변경 effect에서 한 번만 조회
};
// 검색조건을 초기값으로 되돌리고 첫 페이지부터 다시 조회한다.
const handleReset = async () => {
const nextFromDate = new Date();
nextFromDate.setDate(nextFromDate.getDate() - 7);
const nextToDate = new Date();
setFromDate(nextFromDate);
setToDate(nextToDate);
setUsr("");
setpjNm("");
if (curPage !== 1) {
setCurPage(1);
return;
}
await fetchData(1, {
fromDate: nextFromDate,
toDate: nextToDate,
projectName: "",
});
};
// 조회
const fetchData = async (
page = curPage,
condition: FloodPreSearch = {
fromDate,
toDate,
projectName: pjNm,
},
) => {
setIsLoading(true);
// searchCmmnCd('SITEKNDCD')
try {
const { data, total } = await searchData("v1/sbmrgPrevnt", {
fromRegDt: formatDate(condition.fromDate),
toRegDt: formatDate(condition.toDate),
sbmrgPrevntBizNm: condition.projectName,
auth: AUTH,
page: page - 1,
size: String(PAGE_SIZE),
sort: "sbmrgPrevntBizSn,desc",
});
// 일련번호를 추가한 데이터로 변환 (전체 데이터 기준으로 일련번호 계산)
const dataWithRowNumber = (data || []).map(
(item: any, index: number) => ({
...item,
rowNumber: getStandardRowNumber({
total,
rowIndex: index,
pageIndex: page - 1,
pageSize: PAGE_SIZE,
}),
}),
);
setTableData(dataWithRowNumber);
setTotalData(total);
} catch (error) {
console.error("Error fetching data:", error);
} finally {
setFileNm("");
setAtflId("");
setSbmrgPrevntBizSn("");
setIsLoading(false);
}
};
// 파일 다운로드
const isChckFile = () => {
if (fileNm === "" || fileNm === undefined) {
alert("파일을 선택해주세요");
return false;
}
handleDownload(`/attach-service/api/v1/download/${atflId}`, fileNm);
};
// 선택 파일 삭제
const isDeleteFile = async () => {
if (sbmrgPrevntBizSn === "" || sbmrgPrevntBizSn === undefined) {
alert("삭제할 파일을 선택해주세요");
return false;
}
if (!(await confirm("삭제하시겠습니까?"))) return false;
const data = await deleteData(
`/attach-service/api/v1/atfl/attachments/${atflId}`,
);
if (data) {
const delData = await deleteData(`v1/sbmrgPrevnt/${sbmrgPrevntBizSn}`);
if (delData.status === 200) {
alert("삭제되었습니다.", () => void fetchData());
}
} else {
alert("삭제에 실패했습니다.");
}
};
// 등록 버튼 클릭 핸들러
const handleRegisterClick = () => {
const popupWindow = openWindowPopup("floodPreUpload");
if (!popupWindow) {
alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.");
}
};
const handleTablePopupOpen = (fileName: string, id: string) => {
const popupWindow = openWindowPopup("floodPreTable", {
fileNm: fileName,
atflId: id,
sourcePath: window.location.pathname,
});
if (!popupWindow) {
alert("팝업이 차단되었습니다. 브라우저 팝업 허용 설정을 확인해주세요.");
}
};
return (
<>
<LoadingOverlay isLoading={isLoading ?? false} />
<div className="card card-border !mt-6">
<form
className="flex flex-wrap items-center gap-2 p-2"
onSubmit={(event) => {
event.preventDefault();
void handleSearch();
}}
>
<div className="flex items-center gap-2">
<Label className="mr-2 whitespace-nowrap text-label">
등록기간
</Label>
<SearchDatePicker
selected={fromDate}
onSelect={(date: Date | undefined) => {
if (date) setFromDate(date);
}}
disabled={{ after: new Date() }}
/>
<span>~</span>
<SearchDatePicker
selected={toDate}
onSelect={(date: Date | undefined) => {
if (date) setToDate(date);
}}
disabled={{ after: new Date() }}
/>
</div>
<div className="flex items-center gap-2">
<Label className="mr-2 whitespace-nowrap text-label">등록자</Label>
<Input
className="w-[12rem] !text-[1.3rem]"
value={usr}
onChange={(event) => setUsr(event.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Label className="mr-2 whitespace-nowrap text-label">파일명</Label>
<Input
className="w-[18rem] !text-[1.3rem]"
value={pjNm}
onChange={(event) => setpjNm(event.target.value)}
/>
</div>
<Button type="submit" className="w-[7.4rem]">
조회
</Button>
<Button
type="button"
color="white"
className="w-[8.8rem]"
onClick={() => void handleReset()}
>
초기화
</Button>
</form>
</div>
<div className="mt-4 w-full min-w-0 max-w-full overflow-hidden">
<StandardDataTable
table={table}
title="도시침수예방사업 목록"
onRowClick={(row) => {
const fileName = String(row.sbmrgPrevntBizNm ?? "");
const id = String(row.atflId ?? "");
setFileNm(fileName);
setAtflId(id);
setSbmrgPrevntBizSn(String(row.sbmrgPrevntBizSn ?? ""));
handleTablePopupOpen(fileName, id);
}}
actions={
<>
<Button type="button" onClick={isChckFile}>
엑셀다운로드
</Button>
<Button type="button" onClick={handleRegisterClick}>
등록
</Button>
<Button
type="button"
color="red"
disabled={isLoading || !sbmrgPrevntBizSn}
onClick={isDeleteFile}
>
삭제
</Button>
</>
}
/>
</div>
</>
);
};
export default Page;