sn: 노후하수관로정비사업 검색조건 추가
This commit is contained in:
@@ -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에서만 제외한다.
|
||||
|
||||
@@ -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<ObsltPplBusinessSa
|
||||
return parseObsltPplWorkbook(await file.arrayBuffer());
|
||||
};
|
||||
|
||||
/** 업로드 검증과 정확히 같은 B6:V7 위치에 시트별 헤더를 배치한다. */
|
||||
/** 업로드 검증과 정확히 같은 B6:U7 위치에 시트별 헤더를 배치한다. */
|
||||
const downloadHeaderRows = (contract: SheetContract) => {
|
||||
const rows = Array.from({ length: DATA_START_ROW_INDEX }, () => Array<string | number>(22).fill(""));
|
||||
const rows = Array.from({ length: DATA_START_ROW_INDEX }, () => Array<string | number>(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<string | number> => [
|
||||
"",
|
||||
index + 1,
|
||||
@@ -226,7 +225,6 @@ const downloadDataRow = (row: ObsltPplBusiness, index: number): Array<string | n
|
||||
excelTextValue(row.bizPicNm),
|
||||
excelTextValue(row.bizPicCnpl),
|
||||
excelTextValue(row.rmrkCn),
|
||||
excelTextValue(row.pbcpOpnnCn),
|
||||
];
|
||||
|
||||
/** 조회 결과를 정밀조사·정비사업 두 시트의 재업로드 가능한 위치 구조로 생성한다. */
|
||||
@@ -266,7 +264,7 @@ export const buildObsltPplWorkbook = (businesses: ObsltPplBusiness[]): XLSX.Work
|
||||
];
|
||||
const widths = [
|
||||
3, 7, 9, 16, 12, 12, 16, 34, 10, 9, 11, 13, 16, 13, 11, 16, 16,
|
||||
34, 11, 16, 34, 24,
|
||||
34, 11, 16, 34,
|
||||
];
|
||||
worksheet["!cols"] = widths.map((wch) => ({ 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 }),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<FormKey>([
|
||||
@@ -530,7 +529,6 @@ export function ObsltPplFormPage({ id }: { id?: string }) {
|
||||
{renderField("bizPicCnpl", 2)}
|
||||
{renderField("bizPrplDlyRsnCn", 3)}
|
||||
{renderField("rmrkCn", 3)}
|
||||
{renderField("pbcpOpnnCn", 3)}
|
||||
</StandardFormSection>
|
||||
|
||||
<div className="flex justify-center gap-4 pb-8 pt-6">
|
||||
|
||||
@@ -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<ObsltPplSearch>(
|
||||
() => ({
|
||||
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() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="obslt-ppl-biz-code"
|
||||
className="mr-2 whitespace-nowrap text-label"
|
||||
>
|
||||
사업코드
|
||||
</Label>
|
||||
<Input
|
||||
id="obslt-ppl-biz-code"
|
||||
className="w-[14rem] rounded-sm !text-[1.3rem]"
|
||||
value={search.bizCd}
|
||||
onChange={(event) =>
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
bizCd: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="obslt-ppl-progress"
|
||||
className="mr-2 whitespace-nowrap text-label"
|
||||
>
|
||||
추진현황
|
||||
</Label>
|
||||
<Input
|
||||
id="obslt-ppl-progress"
|
||||
className="w-[15rem] rounded-sm !text-[1.3rem]"
|
||||
list="obslt-ppl-progress-suggestions"
|
||||
value={search.prplPrst}
|
||||
onChange={(event) =>
|
||||
setSearch((current) => ({
|
||||
...current,
|
||||
prplPrst: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<datalist id="obslt-ppl-progress-suggestions">
|
||||
{businessProgressOptions.map((option) => (
|
||||
<option key={option.code} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="obslt-ppl-biz-name"
|
||||
|
||||
Reference in New Issue
Block a user