Files
frontend/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx
T

366 lines
11 KiB
TypeScript

/** 노후 하수관로 정비사업 첨부 엑셀 상세 팝업 테이블 */
"use client";
import { useEffect, useMemo, useState } from "react";
import {
getCoreRowModel,
getPaginationRowModel,
useReactTable,
type ColumnDef,
} from "@tanstack/react-table";
import * as XLSX from "xlsx";
import { getBlobData } from "@/components/lib/api";
import { useAlertStore } from "@/shared/lib";
import { StandardDataTable } from "@common/business_components";
import { UITab } from "@common/business_components/ui";
import { oldPpTable2HeadList, oldPpTable6HeadList } from "./headList";
const TABS = [
{ val: "1", text: "raw data" },
{ val: "2", text: "1.정밀조사" },
{ val: "3", text: "2.정밀조사_정비사업연계" },
{ val: "4", text: "3.정비사업" },
{ val: "5", text: "4.정비사업요약" },
{ val: "6", text: "4.정비사업최종" },
];
const PAGE_SIZE = 10;
type ExcelHeader = {
id: string;
title: string;
};
type ExcelRow = Record<string, unknown>;
interface TableModalProps {
fileNm: string;
atflId: string;
}
function normalizeHeaderTitle(value: unknown) {
const title = String(value).replace(/\r\n/g, "\n");
return /^(no|일련번호)$/i.test(title.trim()) ? "순번" : title;
}
function createColumns(headers: ExcelHeader[]): ColumnDef<ExcelRow>[] {
return headers.map((header) => ({
id: header.id,
accessorFn: (row) => row[header.id],
header: () => <span className="whitespace-pre-line">{header.title}</span>,
cell: ({ getValue }) => String(getValue() ?? "-"),
size: 140,
meta: { align: "center" },
}));
}
function useExcelTable(data: ExcelRow[], headers: ExcelHeader[]) {
const columns = useMemo(() => createColumns(headers), [headers]);
return useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: { pagination: { pageIndex: 0, pageSize: PAGE_SIZE } },
});
}
function filterEmptyRows(data: ExcelRow[]) {
return data
.filter((row) =>
Object.entries(row).some(([key, value]) => {
const match = key.match(/^__EMPTY_(\d+)$/);
if (!match || parseInt(match[1] ?? "", 10) < 2) return false;
return (
value !== undefined &&
value !== null &&
String(value).trim() !== "" &&
String(value).trim() !== "-"
);
}),
)
.map((row) => {
const processedRow = { ...row };
const keys = Object.keys(processedRow);
keys.forEach((key, keyIndex) => {
if (keyIndex === keys.length - 1) return;
const value = processedRow[key];
if (
value === undefined ||
value === null ||
value === "" ||
String(value).trim() === ""
) {
processedRow[key] = "-";
}
});
return processedRow;
});
}
function withDescendingRowNumber(rows: ExcelRow[]) {
return rows.map((row, index) => ({
...row,
rowNumber: rows.length - index,
}));
}
function filePickedSelectedSheets(
file: File,
headerNum: number = 0,
sheetNames: string[],
): Promise<Record<string, ExcelRow[]>> {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = (event) => {
const data = event.target?.result;
if (!data) {
reject(new Error("Failed to read file"));
return;
}
const workbook = XLSX.read(data, { type: "binary", cellDates: true });
const result: Record<string, ExcelRow[]> = {};
sheetNames.forEach((targetSheetName) => {
const sheetName = workbook.SheetNames.find(
(name) => name.toLowerCase() === targetSheetName.toLowerCase(),
);
const sheet = sheetName ? workbook.Sheets[sheetName] : undefined;
if (!sheet) {
console.warn(`Sheet "${targetSheetName}" not found in workbook`);
result[targetSheetName] = [];
return;
}
result[targetSheetName] = XLSX.utils.sheet_to_json<ExcelRow>(sheet, {
range: headerNum,
raw: false,
});
});
resolve(result);
};
reader.onerror = () => reject(new Error("Error reading file"));
reader.readAsBinaryString(file);
});
}
const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
const alert = useAlertStore((state) => state.setMessage);
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "1");
const [headers1, setHeaders1] = useState<ExcelHeader[]>([]);
const [headers2, setHeaders2] = useState<ExcelHeader[]>([]);
const [headers4, setHeaders4] = useState<ExcelHeader[]>([]);
const [headers5, setHeaders5] = useState<ExcelHeader[]>([]);
const [data1, setData1] = useState<ExcelRow[]>([]);
const [data2, setData2] = useState<ExcelRow[]>([]);
const [data3, setData3] = useState<ExcelRow[]>([]);
const [data4, setData4] = useState<ExcelRow[]>([]);
const [data5, setData5] = useState<ExcelRow[]>([]);
const [data6, setData6] = useState<ExcelRow[]>([]);
const [loading, setLoading] = useState(false);
const headers3 = useMemo<ExcelHeader[]>(
() =>
oldPpTable2HeadList.map((header) => ({
id: header.id,
title: normalizeHeaderTitle(header.title),
})),
[],
);
const headers6 = useMemo<ExcelHeader[]>(
() =>
oldPpTable6HeadList.map((header) => ({
id: header.id,
title: normalizeHeaderTitle(header.title),
})),
[],
);
const table1 = useExcelTable(data1, headers1);
const table2 = useExcelTable(data2, headers2);
const table3 = useExcelTable(data3, headers3);
const table4 = useExcelTable(data4, headers4);
const table5 = useExcelTable(data5, headers5);
const table6 = useExcelTable(data6, headers6);
useEffect(() => {
const loadFile = async () => {
setLoading(true);
try {
if (!fileNm) {
alert("파일을 선택해주세요");
return;
}
if (!atflId) {
alert("첨부파일 ID가 없어 파일을 불러올 수 없습니다.");
return;
}
const blob = await getBlobData(
`/attach-service/api/v1/download/${atflId}`,
);
const file = new File([blob], fileNm, {
type:
blob.type ||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const jsonDataBySheet = await filePickedSelectedSheets(file, 0, [
"raw data",
"1.정밀조사",
"2.정밀조사_정비사업연계",
" 3.정비사업",
"4.정비사업요약",
"4.정비사업최종",
]);
const header1Entries = Object.entries(
jsonDataBySheet["raw data"]?.[1] ?? {},
);
const header2Entries = Object.entries(
jsonDataBySheet["1.정밀조사"]?.[1] ?? {},
);
const selectedIndexes = [
0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
];
const selectHeaders = (entries: [string, unknown][]) => [
{ id: "rowNumber", title: "순번" },
...selectedIndexes
.map((index) => entries[index])
.filter((entry): entry is [string, unknown] => Boolean(entry))
.map(([id, title]) => ({
id,
title: normalizeHeaderTitle(title),
})),
];
setHeaders1(selectHeaders(header1Entries));
setHeaders2(selectHeaders(header2Entries));
const nextData1 = filterEmptyRows(
(jsonDataBySheet["raw data"] ?? []).slice(2),
).map((item) => ({
...item,
__EMPTY_1: item.__EMPTY_1 || "-",
}));
setData1(withDescendingRowNumber(nextData1));
setData2(
withDescendingRowNumber(
filterEmptyRows((jsonDataBySheet["1.정밀조사"] ?? []).slice(2)),
),
);
const sourceDescriptionKey =
"*1차 출처: 공단자료\r\n*2차 출처: 정산서, 정밀조사보고서 등.\r\n (파랑) 2024년말 기준 실적 제출 자료. 준공 후 재확인 필요\r\n (노랑셀) 준공(또는 정산) 후에 재확인 필요";
const nextData3 = filterEmptyRows(
(jsonDataBySheet["2.정밀조사_정비사업연계"] ?? []).slice(5),
).map((item) => {
const next = { ...item };
if (next[sourceDescriptionKey] !== undefined) {
next.__EMPTY_0 = next[sourceDescriptionKey];
delete next[sourceDescriptionKey];
}
return { ...next, __EMPTY_4: next.__EMPTY_4 || "-" };
});
setData3(withDescendingRowNumber(nextData3));
const header4Entries = Object.entries(
jsonDataBySheet[" 3.정비사업"]?.[1] ?? {},
);
setHeaders4([
{ id: "rowNumber", title: "순번" },
...header4Entries.map(([id, title]) => ({
id,
title: normalizeHeaderTitle(title),
})),
]);
const nextData4 = filterEmptyRows(
(jsonDataBySheet[" 3.정비사업"] ?? []).slice(2),
).map((item) => ({
...item,
__EMPTY_2: item.__EMPTY_2 || "-",
__EMPTY_11: item.__EMPTY_11 || "-",
}));
setData4(withDescendingRowNumber(nextData4));
const excludedSummaryKeys = [
': "정비사업"sheet에서 사업코드 ctrl+c, Ctrl+v 한 다음 중복된 데이터 제거실시',
"__EMPTY",
"__EMPTY_8",
"__EMPTY_15",
];
const header5Entries = Object.entries(
jsonDataBySheet["4.정비사업요약"]?.[2] ?? {},
).filter(([key]) => !excludedSummaryKeys.includes(key));
setHeaders5([
{ id: "rowNumber", title: "순번" },
...header5Entries.map(([id, title]) => ({
id,
title: normalizeHeaderTitle(title),
})),
]);
setData5(
withDescendingRowNumber(
filterEmptyRows((jsonDataBySheet["4.정비사업요약"] ?? []).slice(3)),
),
);
setData6(
withDescendingRowNumber(
filterEmptyRows((jsonDataBySheet["4.정비사업최종"] ?? []).slice(6)),
),
);
} catch (error) {
console.error("OldPp attachment fetch details:", {
atflId,
fileNm,
error,
});
alert("파일 처리 중 오류가 발생했습니다.");
} finally {
setLoading(false);
}
};
void loadFile();
}, [alert, atflId, fileNm]);
const activeConfig =
activeTab === "1"
? { table: table1, headers: headers1 }
: activeTab === "2"
? { table: table2, headers: headers2 }
: activeTab === "3"
? { table: table3, headers: headers3 }
: activeTab === "4"
? { table: table4, headers: headers4 }
: activeTab === "5"
? { table: table5, headers: headers5 }
: { table: table6, headers: headers6 };
const activeTitle =
TABS.find((tab) => tab.val === activeTab)?.text ?? "첨부 데이터";
return (
<div className="flex min-h-0 flex-1 flex-col gap-4">
<UITab
defaultValue={activeTab}
tabList={TABS.map((tab) => ({ value: tab.val, label: tab.text }))}
onValueChange={setActiveTab}
/>
<StandardDataTable
table={activeConfig.table}
title={`${activeTitle} 목록`}
loading={loading}
scrollMode="horizontal"
minTableWidth={`${Math.max(activeConfig.headers.length * 9, 80)}rem`}
/>
</div>
);
};
export default TableModal;