357 lines
11 KiB
TypeScript
357 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" onClick={isDeleteFile}>
|
|
삭제
|
|
</Button>
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Page;
|