293 lines
8.2 KiB
TypeScript
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;
|