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

293 lines
8.2 KiB
TypeScript

/**
* 침수예방 엑셀 데이터 테이블 팝업
*
* 첨부 엑셀의 관리SHEET와 운영관리현황 점검결과를 읽어 공통 표준
* 테이블로 표시한다. 원본 엑셀의 열 순서와 값은 그대로 유지한다.
*/
"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";
const TABS = [
{ val: "management", text: "관리SHEET" },
{ val: "inspection-results", text: "운영관리현황 점검결과" },
];
const PAGE_SIZE = 10;
type ExcelTableHeader = {
id: string;
title: string;
};
type ExcelTableRow = 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: ExcelTableHeader[],
): ColumnDef<ExcelTableRow>[] {
return headers.map((header) => ({
id: header.id,
accessorFn: (row) => row[header.id],
header: header.title,
cell: ({ getValue }) => String(getValue() ?? "-"),
size: 140,
}));
}
function filterEmptyRows(data: ExcelTableRow[]) {
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 &&
value !== "" &&
value !== "-" &&
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 filePickedSelectedSheets(
file: File,
headerNum: number = 0,
sheetNames: string[],
): Promise<Record<string, ExcelTableRow[]>> {
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, ExcelTableRow[]> = {};
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<ExcelTableRow>(
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 ?? "management");
const [managementHeaders, setManagementHeaders] = useState<
ExcelTableHeader[]
>([]);
const [managementData, setManagementData] = useState<ExcelTableRow[]>([]);
const [resultHeaders, setResultHeaders] = useState<ExcelTableHeader[]>([]);
const [resultData, setResultData] = useState<ExcelTableRow[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const loadFile = async () => {
setIsLoading(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, [
"관리SHEET",
"운영관리현황 점검결과",
]);
const managementHeaderRow = jsonDataBySheet["관리SHEET"]?.[7] ?? {};
setManagementHeaders(
Object.entries(managementHeaderRow)
.filter(([key]) => {
const match = key.match(/^__EMPTY_(\d+)$/);
if (!match) return false;
const index = parseInt(match[1] ?? "", 10);
return (index > 0 && index < 47) || index > 73;
})
.map(([key, value]) => ({
id: key,
title: normalizeHeaderTitle(value),
})),
);
setManagementData(
filterEmptyRows((jsonDataBySheet["관리SHEET"] ?? []).slice(8)),
);
const resultHeaderRow =
jsonDataBySheet["운영관리현황 점검결과"]?.[2] ?? {};
const nextResultHeaders = Object.entries(resultHeaderRow).map(
([key, value]) => ({
id: key,
title: normalizeHeaderTitle(value),
}),
);
nextResultHeaders.splice(1, 0, { id: "__EMPTY_2", title: "" });
setResultHeaders(nextResultHeaders);
setResultData(
filterEmptyRows(
(jsonDataBySheet["운영관리현황 점검결과"] ?? []).slice(3),
),
);
} catch (error) {
console.error("FloodPre attachment fetch details:", {
atflId,
fileNm,
error,
});
alert("파일 처리 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
};
void loadFile();
}, [alert, atflId, fileNm]);
const managementColumns = useMemo(
() => createColumns(managementHeaders),
[managementHeaders],
);
const resultColumns = useMemo(
() => createColumns(resultHeaders),
[resultHeaders],
);
const managementTable = useReactTable({
data: managementData,
columns: managementColumns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: { pagination: { pageIndex: 0, pageSize: PAGE_SIZE } },
});
const resultTable = useReactTable({
data: resultData,
columns: resultColumns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
initialState: { pagination: { pageIndex: 0, pageSize: PAGE_SIZE } },
});
const isManagementTab = activeTab === "management";
const activeHeaderCount = isManagementTab
? managementHeaders.length
: resultHeaders.length;
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}
/>
{isManagementTab ? (
<StandardDataTable
table={managementTable}
title="관리SHEET 목록"
loading={isLoading}
scrollMode="horizontal"
minTableWidth={`${Math.max(activeHeaderCount * 9, 80)}rem`}
/>
) : (
<StandardDataTable
table={resultTable}
title="운영관리현황 점검결과 목록"
loading={isLoading}
scrollMode="horizontal"
minTableWidth={`${Math.max(activeHeaderCount * 9, 80)}rem`}
/>
)}
</div>
);
};
export default TableModal;