sn: 도시침수예방사업 다운로드 header 수정
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import * as XLSX from "xlsx";
|
||||
import XlsxPopulate from "xlsx-populate/browser/xlsx-populate";
|
||||
|
||||
import type {
|
||||
CtyFldngDetail,
|
||||
@@ -369,8 +370,238 @@ const writeDetail = (
|
||||
}
|
||||
};
|
||||
|
||||
/** 현재 연도까지 업로드와 같은 열 위치를 가진 한 시트 xlsx를 생성한다. */
|
||||
export const downloadCtyFldngPrvtnExcel = (
|
||||
/** 헤더 배열의 지정 위치에 값을 기록한다. */
|
||||
const putHeader = (
|
||||
aoa: unknown[][],
|
||||
row: number,
|
||||
col: number,
|
||||
value: string,
|
||||
) => {
|
||||
aoa[row]![col] = value;
|
||||
};
|
||||
|
||||
/** 0기준 좌표를 SheetJS 병합 범위로 추가한다. */
|
||||
const addMerge = (
|
||||
merges: XLSX.Range[],
|
||||
startRow: number,
|
||||
startCol: number,
|
||||
endRow: number,
|
||||
endCol: number,
|
||||
) => {
|
||||
merges.push({ s: { r: startRow, c: startCol }, e: { r: endRow, c: endCol } });
|
||||
};
|
||||
|
||||
/** 공통 3열 우수관로 헤더와 세로 병합을 구성한다. */
|
||||
const writePipeHeader = (
|
||||
aoa: unknown[][],
|
||||
merges: XLSX.Range[],
|
||||
start: number,
|
||||
) => {
|
||||
putHeader(aoa, 9, start, "우수관로(km)");
|
||||
addMerge(merges, 9, start, 9, start + 2);
|
||||
["합계", "신설", "개량"].forEach((label, index) => {
|
||||
putHeader(aoa, 10, start + index, label);
|
||||
addMerge(merges, 10, start + index, 11, start + index);
|
||||
});
|
||||
};
|
||||
|
||||
/** 펌프장·유수지 세부번호 헤더를 병합 구조로 구성한다. */
|
||||
const writePumpHeader = (
|
||||
aoa: unknown[][],
|
||||
merges: XLSX.Range[],
|
||||
start: number,
|
||||
pumpCount: number,
|
||||
) => {
|
||||
const pumpWidth = pumpCount + 1;
|
||||
putHeader(aoa, 9, start, "빗물펌프장(㎥/분, ㎥)");
|
||||
addMerge(merges, 9, start, 9, start + pumpWidth + 3);
|
||||
putHeader(aoa, 10, start, "펌프장");
|
||||
addMerge(merges, 10, start, 10, start + pumpWidth - 1);
|
||||
putHeader(aoa, 10, start + pumpWidth, "유수지");
|
||||
addMerge(merges, 10, start + pumpWidth, 10, start + pumpWidth + 3);
|
||||
[
|
||||
"합계",
|
||||
...Array.from({ length: pumpCount }, (_, index) => `#${index + 1}`),
|
||||
].forEach((label, index) => putHeader(aoa, 11, start + index, label));
|
||||
["합계", "#1", "#2", "#3"].forEach((label, index) =>
|
||||
putHeader(aoa, 11, start + pumpWidth + index, label),
|
||||
);
|
||||
};
|
||||
|
||||
/** 저류시설 합계·번호 헤더를 2개 행 병합 구조로 구성한다. */
|
||||
const writeStorageHeader = (
|
||||
aoa: unknown[][],
|
||||
merges: XLSX.Range[],
|
||||
start: number,
|
||||
itemCount: number,
|
||||
) => {
|
||||
putHeader(aoa, 9, start, "저류시설(㎥)");
|
||||
addMerge(merges, 9, start, 10, start + itemCount);
|
||||
[
|
||||
"합계",
|
||||
...Array.from({ length: itemCount }, (_, index) => `#${index + 1}`),
|
||||
].forEach((label, index) => putHeader(aoa, 11, start + index, label));
|
||||
};
|
||||
|
||||
/** 대심도터널 4개 항목 헤더를 구성한다. */
|
||||
const writeTunnelHeader = (
|
||||
aoa: unknown[][],
|
||||
merges: XLSX.Range[],
|
||||
start: number,
|
||||
) => {
|
||||
putHeader(aoa, 9, start, "대심도터널");
|
||||
addMerge(merges, 9, start, 9, start + 3);
|
||||
["직경(m)", "길이(km)", "저수용량(㎥)", "굴착심도(m)"].forEach(
|
||||
(label, index) => {
|
||||
putHeader(aoa, 10, start + index, label);
|
||||
addMerge(merges, 10, start + index, 11, start + index);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/** EM/MP/DD/CW 구역별 5단 병합 헤더를 원본 위치 계약에 맞게 구성한다. */
|
||||
const writeDetailSectionHeader = (
|
||||
aoa: unknown[][],
|
||||
merges: XLSX.Range[],
|
||||
start: number,
|
||||
code: (typeof DETAIL_CODES)[number],
|
||||
) => {
|
||||
const end = start + DETAIL_WIDTHS[code] - 1;
|
||||
putHeader(aoa, 7, start, DETAIL_TITLES[code]);
|
||||
addMerge(merges, 7, start, 7, end);
|
||||
putHeader(aoa, 8, start, "면적\n(ha)");
|
||||
addMerge(merges, 8, start, 11, start);
|
||||
putHeader(aoa, 8, start + 1, "설계기준");
|
||||
addMerge(merges, 8, start + 1, 8, start + 3);
|
||||
["설계빈도\n(년)", "강우강도\n(mm/hr)", "방재성능목표\n(mm/hr)"].forEach(
|
||||
(label, index) => {
|
||||
putHeader(aoa, 9, start + 1 + index, label);
|
||||
addMerge(merges, 9, start + 1 + index, 11, start + 1 + index);
|
||||
},
|
||||
);
|
||||
|
||||
const contentEnd =
|
||||
code === "EM"
|
||||
? start + 25
|
||||
: code === "MP"
|
||||
? start + 25
|
||||
: code === "DD"
|
||||
? start + 23
|
||||
: start + 25;
|
||||
putHeader(aoa, 8, start + 4, "사업내용 ※ 개소별 용량 기재");
|
||||
addMerge(merges, 8, start + 4, 8, contentEnd);
|
||||
writePipeHeader(aoa, merges, start + 4);
|
||||
|
||||
const pumpCount = PUMP_COUNTS[code];
|
||||
writePumpHeader(aoa, merges, start + 7, pumpCount);
|
||||
const storageStart = start + 7 + pumpCount + 5;
|
||||
writeStorageHeader(aoa, merges, storageStart, IMPD_COUNTS[code]);
|
||||
const tunnelStart = storageStart + IMPD_COUNTS[code] + 1;
|
||||
writeTunnelHeader(aoa, merges, tunnelStart);
|
||||
|
||||
let next = tunnelStart + 4;
|
||||
if (code !== "EM") {
|
||||
["빗물받이\n(개소)", "맨홀추락방지\n(개소)"].forEach((label) => {
|
||||
putHeader(aoa, 9, next, label);
|
||||
addMerge(merges, 9, next, 11, next);
|
||||
next += 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (code === "EM") {
|
||||
putHeader(aoa, 8, next, "사업비\n(백만원)");
|
||||
addMerge(merges, 8, next, 11, next);
|
||||
return;
|
||||
}
|
||||
|
||||
putHeader(aoa, 8, next, "사업비(백만원)");
|
||||
addMerge(merges, 8, next, 8, next + 2);
|
||||
["총사업비", "국고", "지방비"].forEach((label, index) => {
|
||||
putHeader(aoa, 9, next + index, label);
|
||||
addMerge(merges, 9, next + index, 11, next + index);
|
||||
});
|
||||
next += 3;
|
||||
|
||||
if (code === "CW") {
|
||||
[
|
||||
"착공(예정)일",
|
||||
"준공(예정)일",
|
||||
"비고(출처)\nex. 사업추진현황, 운영관리현황, 승인 등",
|
||||
].forEach((label, index) => {
|
||||
putHeader(aoa, 8, next + index, label);
|
||||
addMerge(merges, 8, next + index, 11, next + index);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
putHeader(aoa, 8, next, code === "MP" ? "수립기간" : "설계기간");
|
||||
addMerge(merges, 8, next, 8, next + 1);
|
||||
["착수", "준공"].forEach((label, index) => {
|
||||
putHeader(aoa, 9, next + index, label);
|
||||
addMerge(merges, 9, next + index, 11, next + index);
|
||||
});
|
||||
next += 2;
|
||||
putHeader(aoa, 8, next, code === "MP" ? "정비대책 승인" : "승인");
|
||||
addMerge(merges, 8, next, 8, end);
|
||||
const approvals =
|
||||
code === "MP" ? ["최초", "1차 변경", "2차 변경"] : ["최초", "변경"];
|
||||
approvals.forEach((label, index) => {
|
||||
putHeader(aoa, 9, next + index, label);
|
||||
addMerge(merges, 9, next + index, 11, next + index);
|
||||
});
|
||||
};
|
||||
|
||||
/** XlsxPopulate로 병합 헤더 색·테두리를 적용하고 브라우저 다운로드를 시작한다. */
|
||||
const saveStyledWorkbook = async (
|
||||
workbook: XLSX.WorkBook,
|
||||
filename: string,
|
||||
starts: Record<(typeof DETAIL_CODES)[number], number>,
|
||||
last: number,
|
||||
) => {
|
||||
const data = XLSX.write(workbook, { type: "array", bookType: "xlsx" });
|
||||
const styledWorkbook = await XlsxPopulate.fromDataAsync(data);
|
||||
const styledSheet = styledWorkbook.sheet(SHEET_NAME);
|
||||
const lastColumn = XLSX.utils.encode_col(last);
|
||||
const commonHeaderStyle = {
|
||||
bold: true,
|
||||
fontFamily: "맑은 고딕",
|
||||
fontSize: 11,
|
||||
horizontalAlignment: "center",
|
||||
verticalAlignment: "center",
|
||||
wrapText: true,
|
||||
border: true,
|
||||
};
|
||||
styledSheet.range(`B8:${lastColumn}12`).style(commonHeaderStyle);
|
||||
styledSheet
|
||||
.range(`B8:${XLSX.utils.encode_col(starts.EM - 1)}12`)
|
||||
.style("fill", "B4C6E7");
|
||||
DETAIL_CODES.forEach((code) => {
|
||||
const end = starts[code] + DETAIL_WIDTHS[code] - 1;
|
||||
styledSheet
|
||||
.range(
|
||||
`${XLSX.utils.encode_col(starts[code])}8:${XLSX.utils.encode_col(end)}12`,
|
||||
)
|
||||
.style("fill", code === "EM" || code === "DD" ? "EDFEE7" : "E7F4FE");
|
||||
});
|
||||
styledSheet
|
||||
.range(
|
||||
`${XLSX.utils.encode_col(starts.CW + DETAIL_WIDTHS.CW)}8:${lastColumn}12`,
|
||||
)
|
||||
.style("fill", "B4C6E7");
|
||||
[8, 9, 10, 11, 12].forEach((row) =>
|
||||
styledSheet.row(row).height(row === 8 ? 30 : 24),
|
||||
);
|
||||
const blob = await styledWorkbook.outputAsync();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 0);
|
||||
};
|
||||
|
||||
/** 현재 연도까지 원본과 같은 병합 헤더 위치·구분색을 가진 한 시트 xlsx를 생성한다. */
|
||||
export const downloadCtyFldngPrvtnExcel = async (
|
||||
businesses: CtyFldngPrvtnBiz[],
|
||||
templateOnly = false,
|
||||
) => {
|
||||
@@ -393,41 +624,58 @@ export const downloadCtyFldngPrvtnExcel = (
|
||||
const aoa: unknown[][] = Array.from({ length: DATA_START_ROW }, () =>
|
||||
Array(last + 1).fill(""),
|
||||
);
|
||||
const merges: XLSX.Range[] = [];
|
||||
aoa[0]![1] = "하수도정비중점관리지역 도시침수대응사업 현황";
|
||||
const basic = [
|
||||
"순번",
|
||||
"단계",
|
||||
"시도",
|
||||
"시군구",
|
||||
"환경청",
|
||||
"지정년도",
|
||||
"지정일",
|
||||
"지정면적(ha)",
|
||||
"유형",
|
||||
"배수구역",
|
||||
"배수분구",
|
||||
"위치",
|
||||
"사업명",
|
||||
"사업코드",
|
||||
"추진현황",
|
||||
"사업추진 현황",
|
||||
"총사업비",
|
||||
"국고",
|
||||
"지방비",
|
||||
const verticalBasicHeaders: Array<[number, string]> = [
|
||||
[1, "순번"],
|
||||
[2, "단계"],
|
||||
[3, "시도"],
|
||||
[4, "시군구"],
|
||||
[5, "환경청"],
|
||||
[6, "지정년도"],
|
||||
[7, "지정일"],
|
||||
[8, "지정면적\n(ha)"],
|
||||
[9, "유형"],
|
||||
[13, "사업명"],
|
||||
[14, "사업코드"],
|
||||
[15, "추진현황\n(수립중/수립완료/설계중/설계완료/\n공사중/완료)"],
|
||||
[16, "사업추진\n현황"],
|
||||
];
|
||||
basic.forEach((v, i) => {
|
||||
aoa[11]![i + 1] = v;
|
||||
verticalBasicHeaders.forEach(([col, label]) => {
|
||||
putHeader(aoa, 7, col, label);
|
||||
addMerge(merges, 7, col, 11, col);
|
||||
});
|
||||
putHeader(aoa, 7, 10, "범위");
|
||||
addMerge(merges, 7, 10, 7, 12);
|
||||
["배수구역", "배수분구", "위치"].forEach((label, index) => {
|
||||
putHeader(aoa, 8, 10 + index, label);
|
||||
addMerge(merges, 8, 10 + index, 11, 10 + index);
|
||||
});
|
||||
putHeader(aoa, 7, 17, "총사업비(백만원)");
|
||||
addMerge(merges, 7, 17, 8, 19);
|
||||
["총사업비", "국고", "지방비"].forEach((label, index) => {
|
||||
putHeader(aoa, 9, 17 + index, label);
|
||||
addMerge(merges, 9, 17 + index, 11, 17 + index);
|
||||
});
|
||||
aoa[7]![20] = "결산 현황(백만원)";
|
||||
addMerge(merges, 7, 20, 8, starts.EM - 1);
|
||||
putHeader(aoa, 9, 20, "총계");
|
||||
addMerge(merges, 9, 20, 11, 20);
|
||||
years.forEach((year, i) => {
|
||||
aoa[11]![21 + i] = year;
|
||||
putHeader(aoa, 9, 21 + i, `${year.slice(2)}년도`);
|
||||
addMerge(merges, 9, 21 + i, 11, 21 + i);
|
||||
});
|
||||
DETAIL_CODES.forEach((code) => {
|
||||
aoa[7]![starts[code]] = DETAIL_TITLES[code];
|
||||
writeDetailSectionHeader(aoa, merges, starts[code], code);
|
||||
});
|
||||
aoa[11]![last - 2] = "담당자 이름";
|
||||
aoa[11]![last - 1] = "담당자 연락처";
|
||||
aoa[11]![last] = "비고(특이사항, 사업 지연 사유 등)";
|
||||
putHeader(aoa, 7, last - 2, "담당자");
|
||||
addMerge(merges, 7, last - 2, 8, last - 1);
|
||||
["이름", "연락처"].forEach((label, index) => {
|
||||
putHeader(aoa, 9, last - 2 + index, label);
|
||||
addMerge(merges, 9, last - 2 + index, 11, last - 2 + index);
|
||||
});
|
||||
putHeader(aoa, 7, last, "비고\n(특이사항, 사업 지연 사유 등)");
|
||||
addMerge(merges, 7, last, 11, last);
|
||||
if (!templateOnly)
|
||||
businesses.forEach((b, index) => {
|
||||
const row = Array(last + 1).fill("");
|
||||
@@ -472,11 +720,14 @@ export const downloadCtyFldngPrvtnExcel = (
|
||||
aoa.push(row);
|
||||
});
|
||||
const sheet = XLSX.utils.aoa_to_sheet(aoa);
|
||||
sheet["!merges"] = merges;
|
||||
sheet["!cols"] = Array.from({ length: last + 1 }, () => ({ wch: 14 }));
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, sheet, SHEET_NAME);
|
||||
XLSX.writeFile(
|
||||
await saveStyledWorkbook(
|
||||
workbook,
|
||||
templateOnly ? "도시침수예방사업_업로드양식.xlsx" : DOWNLOAD_FILENAME,
|
||||
starts,
|
||||
last,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
declare module "xlsx-populate/browser/xlsx-populate" {
|
||||
type Range = {
|
||||
style(styles: Record<string, unknown>): Range;
|
||||
style(name: string, value: unknown): Range;
|
||||
};
|
||||
|
||||
type Row = {
|
||||
height(value: number): Row;
|
||||
};
|
||||
|
||||
type Sheet = {
|
||||
range(address: string): Range;
|
||||
row(index: number): Row;
|
||||
};
|
||||
|
||||
type Workbook = {
|
||||
sheet(name: string): Sheet;
|
||||
outputAsync(): Promise<Blob>;
|
||||
};
|
||||
|
||||
const XlsxPopulate: {
|
||||
fromDataAsync(data: ArrayBuffer | Uint8Array): Promise<Workbook>;
|
||||
};
|
||||
|
||||
export default XlsxPopulate;
|
||||
}
|
||||
@@ -353,7 +353,9 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
Object.entries(applied).forEach(([k, v]) => {
|
||||
if (v.trim()) query.set(k, v.trim());
|
||||
});
|
||||
downloadCtyFldngPrvtnExcel(await getAllCtyFldngPrvtnBusinesses(query));
|
||||
await downloadCtyFldngPrvtnExcel(
|
||||
await getAllCtyFldngPrvtnBusinesses(query),
|
||||
);
|
||||
} catch (e) {
|
||||
setNotice(e instanceof Error ? e.message : "엑셀을 내려받지 못했습니다.");
|
||||
} finally {
|
||||
@@ -486,7 +488,7 @@ export function CtyFldngPrvtnBizListPage() {
|
||||
<Button
|
||||
className={ACTION}
|
||||
size="sm"
|
||||
onClick={() => downloadCtyFldngPrvtnExcel([], true)}
|
||||
onClick={() => void downloadCtyFldngPrvtnExcel([], true)}
|
||||
>
|
||||
양식받기
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user