Files
frontend/apps/sn/app/(with-base-layout)/oper/oldPp/tableModal.tsx
T
2025-12-26 15:42:59 +09:00

479 lines
16 KiB
TypeScript

/**
* 📄 Table Modal
* 노후 하수관로 엑셀 데이터 테이블 모달 컴포넌트
*
* - 엑셀 파일 다운로드 및 특정 시트 파싱 기능
* - 탭 기반 테이블 표시 (raw data, 정밀조사, 정비사업 등)
* - 데이터 필터링 및 빈 행 제거 처리
* - 로딩 오버레이 및 에러 핸들링 지원
* - XLSX 라이브러리를 통한 엑셀 데이터 변환
* - 테이블 헤더 및 데이터 동적 생성
*
* 🔗 사용 API
* - 첨부파일 다운로드(파일ID): GET /attach-service/api/v1/download/{atflId} (attach-service)
*/
'use client'
import { useState, useEffect } from 'react'
import LoadingOverlay from '@/components/ui/LoadingBar'
import { UITab, UiTable } from '@common/business_components/ui'
import * as XLSX from 'xlsx';
import { HeadMakeColSpan } from '@common/business_components/ui/table/HeadMakeColSpan';
import { oldPpTable2HeadList, oldPpTable2NodeList, oldPpTable6HeadList, oldPpTable6NodeList } 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.정비사업최종" },
];
interface TableModalProps {
fileNm: string;
atflId: string;
}
const TableModal: React.FC<TableModalProps> = ({ fileNm, atflId }) => {
const [activeTab, setActiveTab] = useState(TABS[0]?.val ?? "1");
const [tableHeader1, setTableHeader1] = useState<{ id: string; title: string }[]>([])
const [tableHeader2, setTableHeader2] = useState<{ id: string; title: string }[]>([])
const [tableHeader4, setTableHeader4] = useState<{ id: string; title: string }[]>([])
const [tableHeader5, setTableHeader5] = useState<{ id: string; title: string }[]>([])
const [tableData1, setTableData1] = useState<any>([{}])
const [tableData2, setTableData2] = useState<any>([{}])
const [tableData3, setTableData3] = useState<any>([{}])
const [tableData4, setTableData4] = useState<any>([{}])
const [tableData5, setTableData5] = useState<any>([{}])
const [tableData6, setTableData6] = useState<any>([{}])
const [isLoading, setIsLoading] = useState(false)
useEffect(() => {
isChckFile();
}, []);
// 파일 다운로드
const isChckFile = async () => {
setIsLoading(true);
try {
if (fileNm === '' || fileNm === undefined) {
alert('파일을 선택해주세요');
setIsLoading(false);
return false;
}
const response = await fetch(`/attach-service/api/v1/download/${atflId}`);
if (!response.ok) {
throw new Error('파일을 가져오는 중 오류가 발생했습니다.');
}
const blob = await response.blob();
const file = new File([blob], fileNm, {
type: blob.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const targetSheets = ['raw data', '1.정밀조사', '2.정밀조사_정비사업연계', ' 3.정비사업', '4.정비사업요약', '4.정비사업최종'];
const jsonDataBySheet = await filePickedSelectedSheets(file, 0, targetSheets);
const header1 = jsonDataBySheet['raw data']?.[1] || [];
const header2 = jsonDataBySheet['1.정밀조사']?.[1] || [];
const header4 = jsonDataBySheet[' 3.정비사업']?.[1] || [];
const header5 = jsonDataBySheet['4.정비사업요약']?.[2] || [];
// raw data
const allEntries1 = Object.entries(header1);
const filteredHeader1 = [
allEntries1[0],
allEntries1[1],
allEntries1[2],
allEntries1[3],
allEntries1[5],
allEntries1[6],
allEntries1[7],
allEntries1[8],
allEntries1[9],
allEntries1[10],
allEntries1[11],
allEntries1[12],
allEntries1[13],
allEntries1[14],
allEntries1[15]
].filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
}));
filteredHeader1.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader1(filteredHeader1);
const data1 = Array.isArray(jsonDataBySheet['row data'])
? jsonDataBySheet['row data'].slice(2)
: [];
const filteredData1 = filterEmptyRows(data1).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
__EMPTY_1: item.__EMPTY_1 || '-'
}));
setTableData1(filteredData1);
// 1.정밀조사
const allEntries2 = Object.entries(header2);
let filteredHeader2 = [
allEntries2[0],
allEntries2[1],
allEntries2[2],
allEntries2[3],
allEntries2[5],
allEntries2[6],
allEntries2[7],
allEntries2[8],
allEntries2[9],
allEntries2[10],
allEntries2[11],
allEntries2[12],
allEntries2[13],
allEntries2[14],
allEntries2[15]
].filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
}));
filteredHeader2.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader2(filteredHeader2);
const data2 = Array.isArray(jsonDataBySheet['1.정밀조사'])
? jsonDataBySheet['1.정밀조사'].slice(2)
: [];
const filteredData2 = filterEmptyRows(data2).map((item: any, index: number) => ({
...item,
rowNumber: index + 1
}));
setTableData2(filteredData2);
// 2.정밀조사_정비사업연계 (헤더는 import)
const data3 = Array.isArray(jsonDataBySheet['2.정밀조사_정비사업연계'])
? jsonDataBySheet['2.정밀조사_정비사업연계'].slice(5)
: [];
const filteredData3 = filterEmptyRows(data3).map((item: any, index: number) => {
const newItem = { ...item };
// 매핑 에러로 인해 특정 키를 "__EMPTY_0"으로 변경
const targetKey = "*1차 출처: 공단자료\r\n*2차 출처: 정산서, 정밀조사보고서 등.\r\n (파랑) 2024년말 기준 실적 제출 자료. 준공 후 재확인 필요\r\n (노랑셀) 준공(또는 정산) 후에 재확인 필요";
if (newItem[targetKey] !== undefined) {
newItem["__EMPTY_0"] = newItem[targetKey];
delete newItem[targetKey];
}
return {
...newItem,
__EMPTY_4: newItem?.__EMPTY_4 || '-',
rowNumber: index + 1
};
});
setTableData3(filteredData3);
// 3.정비사업
const allEntries4 = Object.entries(header4);
const filteredHeader4 = allEntries4.filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n')
}));
filteredHeader4.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader4(filteredHeader4);
const data4 = Array.isArray(jsonDataBySheet[' 3.정비사업'])
? jsonDataBySheet[' 3.정비사업'].slice(2)
: [];
const filteredData4 = filterEmptyRows(data4).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
__EMPTY_2: item?.__EMPTY_2 || '-',
__EMPTY_11: item?.__EMPTY_11 || '-',
}));
setTableData4(filteredData4);
// 4.정비사업요약
const allEntries5 = Object.entries(header5);
const excludeKeys5 = [
": \"정비사업\"sheet에서 사업코드 ctrl+c, Ctrl+v 한 다음 중복된 데이터 제거실시",
"__EMPTY",
"__EMPTY_8",
"__EMPTY_15"
];
const filteredHeader5 = allEntries5
.filter(([key]) => !excludeKeys5.includes(key))
.filter((e): e is [string, any] => Boolean(e))
.map(([key, value]) => ({
id: key,
title: String(value).replace(/\r\n/g, '\n'),
}));
filteredHeader5.splice(0, 0, {
id: "rowNumber",
title: '일련번호'
});
setTableHeader5(filteredHeader5);
const data5 = Array.isArray(jsonDataBySheet['4.정비사업요약'])
? jsonDataBySheet['4.정비사업요약'].slice(3)
: [];
const filteredData5 =
filterEmptyRows(data5).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
}));
setTableData5(filteredData5);
// 4.정비사업최종 (헤더는 import)
const data6 = Array.isArray(jsonDataBySheet['4.정비사업최종'])
? jsonDataBySheet['4.정비사업최종'].slice(6)
: [];
const filteredData6 =
filterEmptyRows(data6).map((item: any, index: number) => ({
...item,
rowNumber: index + 1,
}));
setTableData6(filteredData6);
} catch (error) {
console.error('파일 처리 중 오류 발생:', error);
alert('파일 처리 중 오류가 발생했습니다.');
} finally {
setIsLoading(false);
}
}
function filterEmptyRows(data: any[]) {
return data
.filter(row => {
return 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() !== '-' &&
String(value).trim() !== '';
});
})
.map((row) => {
const processedRow = { ...row };
const keys = Object.keys(processedRow);
keys.forEach((key, keyIndex) => {
// 마지막 속성(사업 근거)이 아닌 경우에만 빈 값을 '-'으로 변경
if (keyIndex !== keys.length - 1) {
const value = processedRow[key];
if (value === undefined ||
value === null ||
value === '' ||
String(value).trim() === '') {
processedRow[key] = '-';
}
}
});
return processedRow;
});
};
// 특정 시트만 선택적으로 JSON으로 변환하는 함수
function filePickedSelectedSheets(file: File, headerNum: number = 0, sheetNames: string[]): Promise<Record<string, any[]>> {
const reader = new FileReader();
return new Promise((resolve, reject) => {
reader.onload = function (e) {
const data = e.target?.result;
if (!data) {
console.error("Failed to read file: e.target.result is null");
reject(new Error("Failed to read file"));
return;
}
// Excel 파일 읽기
const wb = XLSX.read(data, { type: 'binary', cellDates: true });
const result: Record<string, any[]> = {};
// 지정된 시트 이름만 찾아서 처리
sheetNames.forEach(targetSheetName => {
// 대소문자 구분 없이 시트 이름 찾기
const sheetName = wb.SheetNames.find(name =>
name.toLowerCase() === targetSheetName.toLowerCase()
);
if (sheetName) {
const sheet = wb.Sheets[sheetName];
if (sheet) {
const jsonData = XLSX.utils.sheet_to_json(sheet, { range: headerNum, raw: false });
result[targetSheetName] = jsonData;
}
} else {
console.warn(`Sheet "${targetSheetName}" not found in workbook`);
result[targetSheetName] = [];
}
});
resolve(result);
};
reader.onerror = function () {
console.error("Error reading file");
reject(new Error("Error reading file"));
};
// 파일 읽기 시작
reader.readAsBinaryString(file);
});
};
return (
<>
{/* 탭 영역 */}
<UITab
defaultValue={activeTab}
tabList={TABS.map((tab) => ({
value: tab.val,
label: tab.text
}))}
onValueChange={(value: string) => setActiveTab(value)}
/>
{/* row data 탭 */}
{activeTab === '1' && (
<div className="h-[79vh] rounded-lg border flex flex-col">
<div className="flex-1 min-w-[50vw] max-w-[100vw] overflow-auto">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-max">
<UiTable
headName={'row data'}
tableData={tableData1}
headlist={tableHeader1}
pageSize={10}
total={0}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 1.정밀조사 탭 */}
{activeTab === '2' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[150%]">
<UiTable
headName={'1.정밀조사'}
tableData={tableData2}
headlist={tableHeader2}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 2.정밀조사_정비사업연계 탭 */}
{activeTab === '3' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[550%]">
<UiTable
headName={'2.정밀조사_정비사업연계'}
tableData={tableData3}
headlist={oldPpTable2HeadList}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
>
<HeadMakeColSpan upHeadList={oldPpTable2NodeList} />
</UiTable>
</div>
</div>
</div>
)}
{/* 3.정비사업 탭 */}
{activeTab === '4' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[160%]">
<UiTable
headName={'3.정비사업'}
tableData={tableData4}
headlist={tableHeader4}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 4.정비사업요약 탭 */}
{activeTab === '5' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[130%]">
<UiTable
headName={'4.정비사업요약'}
tableData={tableData5}
headlist={tableHeader5}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
/>
</div>
</div>
</div>
)}
{/* 4.정비사업최종 탭 */}
{activeTab === '6' && (
<div className="h-[79vh] w-full overflow-auto rounded-lg border">
<div className="flex-1 px-2 pb-2">
<LoadingOverlay isLoading={isLoading} />
<div className="min-w-[200%]">
<UiTable
headName={'4.정비사업최종'}
tableData={tableData6}
headlist={oldPpTable6HeadList}
pageSize={10}
total={0}
className={"overflow-x-auto overflow-y-auto"}
cellClick={undefined}
onPageChange={() => { }}
>
<HeadMakeColSpan upHeadList={oldPpTable6NodeList} />
</UiTable>
</div>
</div>
</div>
)}
</>
)
};
export default TableModal;