From 549debf8bf14e468e9402545e5e7986101557b78 Mon Sep 17 00:00:00 2001 From: ljpark Date: Wed, 23 Sep 2026 09:07:56 +0900 Subject: [PATCH] =?UTF-8?q?sn:=20=EB=85=B8=ED=9B=84=ED=95=98=EC=88=98?= =?UTF-8?q?=EA=B4=80=EB=A1=9C=EC=A0=95=EB=B9=84=EC=82=AC=EC=97=85=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../001-fe-SN-노후하수관로정비사업-통합구현기록.md | 18 ++++++ .../features/obsltPpl/lib/obslt-ppl-excel.ts | 28 +++++----- .../obsltPpl/model/obslt-ppl-types.ts | 1 - .../features/obsltPpl/ui/ObsltPplFormPage.tsx | 2 - .../features/obsltPpl/ui/ObsltPplListPage.tsx | 56 ++++++++++++++++++- 5 files changed, 86 insertions(+), 19 deletions(-) diff --git a/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md b/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md index 0bf92de3e..3bda67dc1 100644 --- a/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md +++ b/apps/sn/docs/작업내용/SN-노후하수관로정비사업/001-fe-SN-노후하수관로정비사업-통합구현기록.md @@ -148,3 +148,21 @@ - 목록은 CMS 코드값 또는 코드명과 정확히 일치하는 값만 코드명으로 표시하고 미등록 기존 값은 그대로 표시한다. - 영문 `lwpoCd` 저장 전환과 기존 데이터 이관은 이번 범위에서 수행하지 않았다. - 백엔드 API, Excel 입출력과 DB는 변경하지 않았다. + +## 2026-09-22 목록 사업코드·추진현황 검색조건 추가 + +- 사용자 유지보수 요청에 따라 목록 검색조건에 사업코드와 추진현황을 추가했다. +- 검색조건은 `사업구분 → 시도 → 시군구 → 사업코드 → 추진현황 → 사업명` 순서로 배치했다. +- 사업코드는 자유 입력으로 부분일치 조회하고, 추진현황은 기존 자유문자를 직접 입력하면서 `BIZPRGRSSTTS` 공통코드 후보를 datalist로 선택할 수 있게 했다. +- 두 조건을 목록 검색 상태에 포함해 조회 URL, 페이지 이동, 정렬, 상세·등록 후 목록 복귀와 초기화에서 동일하게 유지한다. +- Excel 다운로드도 적용된 검색 상태를 재사용하므로 현재 화면과 같은 사업코드·추진현황 조건의 전체 결과를 내려받는다. +- SN 운영 빌드와 별도 TypeScript 검사를 통과했다. 최초 빌드는 Windows 파일 핸들 한도 오류로 중단됐으나 단독 재시도에서 성공했다. + +## 2026-09-22 공단의견 목록·폼·Excel 제거 + +- 사용자 유지보수 요청에 따라 목록의 공단의견 열과 등록·상세·수정 화면의 공단 의견 입력란을 제거했다. +- FE 저장 요청 타입에서 `pbcpOpnnCn`을 제거해 등록·수정 API 요청에 공단의견을 포함하지 않는다. +- 업로드 헤더 검증과 행 파싱에서 V열 공단의견을 제거하고 B~U의 20개 업무 열만 저장 요청으로 변환한다. +- 기존 B~V 업로드 파일의 V열이 남아 있어도 이를 읽거나 저장하지 않으며, 나머지 B~U 열은 같은 위치로 처리한다. +- FE Excel 다운로드의 V열 공단의견, 열 폭, 스타일 범위를 제거하고 B~U 양식으로 생성한다. +- DB 컬럼과 기존 값은 유지하며 화면·API·Excel에서만 제외한다. diff --git a/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts b/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts index 6fef56b6d..8de99efef 100644 --- a/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts +++ b/apps/sn/src/features/obsltPpl/lib/obslt-ppl-excel.ts @@ -33,7 +33,7 @@ const SHEET_CONTRACTS: SheetContract[] = [ const DATA_START_ROW_INDEX = 7; const FIRST_DATA_COLUMN_INDEX = 1; -const DATA_COLUMN_COUNT = 21; +const DATA_COLUMN_COUNT = 20; const DOWNLOAD_FILENAME = "노후하수관로 정밀조사 및 정비사업 현황.xlsx"; /** API 문자열에 남은 Excel 개행 이스케이프를 실제 줄바꿈으로 정리한다. */ @@ -63,7 +63,7 @@ const validateHeaders = (sheet: XLSX.WorkSheet, contract: SheetContract) => { [6, 14, "증빙서류제출여부"], [5, 15, contract.progressHeader], [6, 15, contract.startDateHeader], [6, 16, "준공(예정)일자(yyyy.mm.dd)"], [5, 17, "사업추진부진사유"], [5, 18, "사업담당자(지자체)"], - [6, 18, "이름"], [6, 19, "연락처"], [5, 20, "비고"], [6, 21, "공단의견"], + [6, 18, "이름"], [6, 19, "연락처"], [5, 20, "비고"], ]; for (const [rowIndex, columnIndex, expected] of expectedHeaders) { @@ -116,7 +116,7 @@ const dateText = (value: string): string | null => { return (digits || normalized).slice(0, 8); }; -/** B~V가 모두 비어 있는 행은 데이터가 아닌 양식 여백으로 판단한다. */ +/** B~U가 모두 비어 있는 행은 데이터가 아닌 양식 여백으로 판단한다. */ const isEmptyDataRow = (sheet: XLSX.WorkSheet, rowIndex: number) => Array.from({ length: DATA_COLUMN_COUNT }, (_, offset) => cellText(sheet, rowIndex, FIRST_DATA_COLUMN_INDEX + offset), @@ -159,7 +159,6 @@ const parseSheet = (sheet: XLSX.WorkSheet, contract: SheetContract): ObsltPplBus bizPicNm: optionalText(valueAt(17)), bizPicCnpl: optionalText(valueAt(18)), rmrkCn: optionalText(valueAt(19)), - pbcpOpnnCn: optionalText(valueAt(20)), }); } return result; @@ -183,27 +182,27 @@ export const parseObsltPplExcel = async (file: File): Promise { - const rows = Array.from({ length: DATA_START_ROW_INDEX }, () => Array(22).fill("")); + const rows = Array.from({ length: DATA_START_ROW_INDEX }, () => Array(21).fill("")); const topHeaders = [ "순번", "정밀\n조사\n차수", contract.businessCodeHeader.replace("사업코드", "\n사업코드"), "시도", "시군", "환경청", "사업명", "추진현황", "착수\n년도", "준공(예정)\n년도", contract.extensionHeader, "", "", "", contract.progressHeader.replace("추진현황", " 추진현황"), - "", "사업추진 부진 사유", "사업 담당자(지자체)", "", "비고", "", + "", "사업추진 부진 사유", "사업 담당자(지자체)", "", "비고", ]; const bottomHeaders = [ "", "", "", "", "", "", "", "", "", "", "계획연장(km)", "상반기(예상)연장(km)", "잔여연장(km)", "증빙서류\n제출여부", contract.startDateHeader.replace("(yyyy.mm.dd)", "\n(yyyy.mm.dd)"), "준공(예정)일자\n(yyyy.mm.dd)", "", "이름", "연락처", - "(필요시 사업 담당자 추가 작성)", "공단의견", + "(필요시 사업 담당자 추가 작성)", ]; topHeaders.forEach((value, index) => { rows[5]![index + FIRST_DATA_COLUMN_INDEX] = value; }); bottomHeaders.forEach((value, index) => { rows[6]![index + FIRST_DATA_COLUMN_INDEX] = value; }); return rows; }; -/** DB에 저장하지 않는 B열 순번을 시트 출력 순서로 만들고 C~V에 업무값을 배치한다. */ +/** DB에 저장하지 않는 B열 순번을 시트 출력 순서로 만들고 C~U에 업무값을 배치한다. */ const downloadDataRow = (row: ObsltPplBusiness, index: number): Array => [ "", index + 1, @@ -226,7 +225,6 @@ const downloadDataRow = (row: ObsltPplBusiness, index: number): Array ({ wch })); XLSX.utils.book_append_sheet(workbook, worksheet, contract.sheetName); @@ -304,7 +302,7 @@ export const downloadObsltPplExcel = async ( fill: "FFF2CC", border: true, }); - styledSheet.range("B6:V7").style({ + styledSheet.range("B6:U7").style({ bold: true, fontFamily: "맑은 고딕", fontSize: 10, @@ -315,7 +313,7 @@ export const downloadObsltPplExcel = async ( }); styledSheet.range("B6:H7").style("fill", "B4C6E7"); styledSheet.range("I6:Q7").style("fill", "FFF2CC"); - styledSheet.range("R6:V7").style("fill", "FCE4D6"); + styledSheet.range("R6:U7").style("fill", "FCE4D6"); styledSheet.row(2).height(26); styledSheet.row(3).height(26); styledSheet.row(5).height(22); @@ -325,7 +323,7 @@ export const downloadObsltPplExcel = async ( if (rowCount > 0) { const dataStartRow = DATA_START_ROW_INDEX + 1; const dataEndRow = DATA_START_ROW_INDEX + rowCount; - styledSheet.range(`B${dataStartRow}:V${dataEndRow}`).style({ + styledSheet.range(`B${dataStartRow}:U${dataEndRow}`).style({ fontFamily: "맑은 고딕", fontSize: 10, horizontalAlignment: "center", @@ -336,7 +334,7 @@ export const downloadObsltPplExcel = async ( styledSheet .range(`L${dataStartRow}:N${dataEndRow}`) .style("numberFormat", "#,##0.###"); - ["H", "R", "U", "V"].forEach((column) => + ["H", "R", "U"].forEach((column) => styledSheet .range(`${column}${dataStartRow}:${column}${dataEndRow}`) .style({ horizontalAlignment: "left", wrapText: true }), diff --git a/apps/sn/src/features/obsltPpl/model/obslt-ppl-types.ts b/apps/sn/src/features/obsltPpl/model/obslt-ppl-types.ts index dd991d1bf..dfd7adf90 100644 --- a/apps/sn/src/features/obsltPpl/model/obslt-ppl-types.ts +++ b/apps/sn/src/features/obsltPpl/model/obslt-ppl-types.ts @@ -22,7 +22,6 @@ export interface ObsltPplBusiness { bizPicNm?: string | null; bizPicCnpl?: string | null; rmrkCn?: string | null; - pbcpOpnnCn?: string | null; regDt?: string | null; mdfcnDt?: string | null; } diff --git a/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx b/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx index e69c24d5d..5ba43063e 100644 --- a/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx +++ b/apps/sn/src/features/obsltPpl/ui/ObsltPplFormPage.tsx @@ -79,7 +79,6 @@ const FIELDS: Array<{ { key: "bizPicNm", label: "담당자 이름" }, { key: "bizPicCnpl", label: "담당자 연락처" }, { key: "rmrkCn", label: "비고", type: "textarea" }, - { key: "pbcpOpnnCn", label: "공단 의견", type: "textarea" }, ]; const NUMBER_KEYS = new Set([ @@ -530,7 +529,6 @@ export function ObsltPplFormPage({ id }: { id?: string }) { {renderField("bizPicCnpl", 2)} {renderField("bizPrplDlyRsnCn", 3)} {renderField("rmrkCn", 3)} - {renderField("pbcpOpnnCn", 3)}
diff --git a/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx b/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx index ee64be2d9..f15c0554a 100644 --- a/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx +++ b/apps/sn/src/features/obsltPpl/ui/ObsltPplListPage.tsx @@ -65,8 +65,10 @@ type ObsltPplTableColumn = { }; type ObsltPplSearch = { bizSe: string; + bizCd: string; ctpvNm: string; sigunNm: string; + prplPrst: string; bizNm: string; }; @@ -120,7 +122,6 @@ const TABLE_COLUMNS: ObsltPplTableColumn[] = [ { id: "bizPicNm", title: "담당자 이름", width: 90 }, { id: "bizPicCnpl", title: "담당자 연락처", width: 120 }, { id: "rmrkCn", title: "비고", width: 160, align: "left" }, - { id: "pbcpOpnnCn", title: "공단\n의견", width: 90, align: "left" }, ]; const TABLE_MIN_WIDTH = "280rem"; @@ -154,6 +155,7 @@ export function ObsltPplListPage() { // 검색값·원본 행은 유지하고 사업구분·추진·증빙 표시명만 CMS 기준으로 변환한다. const { obsltBusinessTypeOptions, + businessProgressOptions, evidenceStatusLabels, businessProgressLabels, } = useOperBusinessCommonCodes(); @@ -162,8 +164,10 @@ export function ObsltPplListPage() { const initialSearch = useMemo( () => ({ bizSe: searchParams.get("bizSe") ?? "정밀조사", + bizCd: searchParams.get("bizCd") ?? "", ctpvNm: searchParams.get("ctpvNm") ?? "", sigunNm: searchParams.get("sigunNm") ?? "", + prplPrst: searchParams.get("prplPrst") ?? "", bizNm: searchParams.get("bizNm") ?? "", }), [searchParams], @@ -454,8 +458,10 @@ export function ObsltPplListPage() { const handleReset = () => { const nextSearch: ObsltPplSearch = { bizSe: "정밀조사", + bizCd: "", ctpvNm: "", sigunNm: "", + prplPrst: "", bizNm: "", }; setSearch(nextSearch); @@ -599,6 +605,54 @@ export function ObsltPplListPage() {
+
+ + + setSearch((current) => ({ + ...current, + bizCd: event.target.value, + })) + } + /> +
+ +
+ + + setSearch((current) => ({ + ...current, + prplPrst: event.target.value, + })) + } + /> + + {businessProgressOptions.map((option) => ( + + ))} + +
+