From cc3c5c5d68a4ec84ae4bdf8966601bc16da293c8 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Wed, 25 Feb 2026 14:39:40 +0900 Subject: [PATCH] =?UTF-8?q?st:=20feat:=20=ED=95=98=EC=88=98=EB=8F=84?= =?UTF-8?q?=EB=B3=B4=EA=B8=89=EB=A5=A0=20=EC=9E=85=EB=A0=A5=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5,=20=EC=B2=B4=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=91=EC=97=85,=20=EB=8F=84-=EC=8B=9C=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=8B=9C=20=EB=B2=84=ED=8A=BC=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/st/src/components/module/DataGrid.tsx | 75 ++++-- .../input/SeweragePenetrationRateInp.tsx | 239 +++++++++++++----- .../statistics/input/SubmitDailog.tsx | 167 ++++++++++++ apps/st/src/components/type/DataGrid.type.ts | 6 + .../components/util/column/input/DoList.ts | 23 ++ .../input/SeweragePenetrationRateInpColumn.ts | 152 +++++------ .../api/statistics-input-service.ts | 9 + .../model/statistics-input-interface.ts | 228 ++++++++--------- .../statistics-input-submit-info-schema.ts | 17 ++ 9 files changed, 628 insertions(+), 288 deletions(-) create mode 100644 apps/st/src/components/statistics/input/SubmitDailog.tsx create mode 100644 apps/st/src/components/util/column/input/DoList.ts create mode 100644 apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index 5eebe138e..e9d4a9620 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -10,6 +10,7 @@ import type { FooterItemWithCallback, ColumnLayoutItem, CellStyleCallback, + CellTooltipCallback, } from "../type/DataGrid.type"; import { getComputedValueCallback } from "@/components/util/grid/computedFieldCallbacks"; import JSZip from "jszip"; @@ -311,6 +312,7 @@ const DataGrid = ({ setRowData, getGridDataRef, cellStyleCallback, + cellTooltipCallback, onRowClick, statusInfo, }: DataGridProps) => { @@ -321,8 +323,10 @@ const DataGrid = ({ const originalRowsRef = useRef[]>>({}); const rowDataRef = useRef[]>(rowData); const cellStyleCallbackRef = useRef(cellStyleCallback); + const cellTooltipCallbackRef = useRef(cellTooltipCallback); rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; + cellTooltipCallbackRef.current = cellTooltipCallback; useLayoutEffect(() => { if (!cellStyleCallback || !gridRef.current?.gridView) return; @@ -980,38 +984,38 @@ const DataGrid = ({ getGridDataRef, ]); - // 행 클릭 이벤트 설정 + // 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식) useEffect(() => { - if (!gridRef.current?.gridView || !onRowClick) return; + const gridView = gridRef.current?.gridView; + if (!gridView || (!onRowClick && !cellTooltipCallback)) return; - const gridView = gridRef.current.gridView; - - // onCellClicked 이벤트 핸들러 설정 gridView.onCellClicked = (grid, clickData) => { - // 데이터 셀만 처리 (헤더, 푸터 등 제외) - if ( - clickData.cellType === "data" && - clickData.itemIndex !== undefined - ) { - try { - // 행 전체 데이터 가져오기 - const row = rowData[clickData.itemIndex]; - - // 부모 컴포넌트로 데이터 전달 - onRowClick(row ?? {}); - } catch (error) { - console.error("행 데이터 가져오기 오류:", error); + if (clickData.cellType !== "data" || clickData.itemIndex === undefined) return; + try { + const row = rowDataRef.current[clickData.itemIndex]; + const cellIndex = (grid as { getCurrent?: () => { itemIndex: number; column?: string; fieldName?: string } }).getCurrent?.(); + // cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex) + if (cellTooltipCallbackRef.current && row && cellIndex) { + const fieldName = cellIndex.fieldName ?? cellIndex.column ?? ""; + const msg = cellTooltipCallbackRef.current({ row: row as Record, fieldName }); + if (msg) { + const colName = columns.find((c) => c.fieldName === fieldName || c.name === cellIndex.column)?.name ?? cellIndex.column ?? fieldName; + (gridView as { showMessage?: (opts: { message: string; hideDuration?: number }, idx: { itemIndex: number; column: string }) => void }).showMessage?.( + { message: msg, hideDuration: 5000 }, + { itemIndex: cellIndex.itemIndex, column: colName } + ); + } } + if (onRowClick) onRowClick((row ?? {}) as Record); + } catch (error) { + console.error("행 데이터 가져오기 오류:", error); } }; - // cleanup: 컴포넌트 언마운트 시 이벤트 핸들러 제거 return () => { - if (gridView.onCellClicked) { - gridView.onCellClicked = undefined; - } + if (gridView.onCellClicked) gridView.onCellClicked = undefined; }; - }, [onRowClick]); + }, [onRowClick, cellTooltipCallback, columns]); return (
@@ -1034,7 +1038,11 @@ const DataGrid = ({ statusInfo.locgStatus === '3' ? '승인' : '반려' } - 제출일: {statusInfo.locgDt ? statusInfo.locgDt : '미제출'} + 제출일: {statusInfo.locgDt + ? (statusInfo.locgDt.length === 8 && /^\d{8}$/.test(statusInfo.locgDt) + ? `${statusInfo.locgDt.slice(0, 4)}-${statusInfo.locgDt.slice(4, 6)}-${statusInfo.locgDt.slice(6, 8)}` + : statusInfo.locgDt) + : '미제출'} 담당자: {statusInfo.locgCharger ? statusInfo.locgCharger : '-'} 연락처: {statusInfo.locgCttpc ? statusInfo.locgCttpc : '-'}
@@ -1083,10 +1091,26 @@ const DataGrid = ({ setTimeout(() => { const gv = gridRef.current?.gridView; if (!gv?.columnByName) return; + // 공식 문서: 데이터 영역 툴팁 = renderer.showTooltip + onShowTooltip (displayOptions는 Ellipsis용이라 사용 안 함) + if (cellTooltipCallbackRef.current) { + (gv as { onShowTooltip?: (grid: unknown, index: { itemIndex: number; column: string }, value: string) => string | undefined }).onShowTooltip = (_grid, index, value) => { + const row = rowDataRef.current[index?.itemIndex]; + if (row == null) return value; + const column = index.column; + const col = columns.find((c) => c.name === column); + const fieldName = col?.fieldName ?? column; + const custom = cellTooltipCallbackRef.current?.({ row: row as Record, fieldName }); + // verificationResult 등 메시지가 있을 때만 툴팁 표시 (없으면 undefined 반환 → 툴팁 안 띄움) + return custom && custom.length > 0 ? custom : undefined; + }; + } columns.forEach((column) => { const baseStyleName = column.styleName; - const gridCol = gv.columnByName(column.name) as { styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string } | null; + const gridCol = gv.columnByName(column.name) as { styleCallback?: (g: unknown, dataCell: { itemIndex?: number; index?: { itemIndex?: number }; dataField?: string; fieldName?: string }) => string; renderer?: { showTooltip?: boolean } } | null; if (gridCol) { + if (cellTooltipCallbackRef.current) { + (gv as { setColumnProperty?: (col: string, key: string, value: unknown) => void }).setColumnProperty?.(column.name, 'renderer', { showTooltip: true }); + } gridCol.styleCallback = (_g, dataCell) => { const itemIndex = dataCell?.index?.itemIndex ?? dataCell?.itemIndex ?? -1; const fieldName = dataCell?.dataField ?? dataCell?.fieldName ?? column.fieldName; @@ -1117,6 +1141,7 @@ const DataGrid = ({ editable={column.editable} {...(baseStyleName != null && { styleName: baseStyleName })} {...(column.visible === false && { visible: false })} + {...(cellTooltipCallback && { renderer: { showTooltip: true } })} footer={column.footer && !Array.isArray(column.footer) ? { text: column.footer.text, expression: column.footer.expression, diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 6880f9ce7..93353d927 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -9,6 +9,7 @@ import LastYearDataButton from "@/components/button/LastYearDataButton"; import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; import { LoadingWithAnimation } from "@common/components"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import SubmitDailog from "./SubmitDailog"; import { useAlertStore } from "@/shared/lib"; import { statisticsInputAPi } from "@/entities/statistics/api/statistics-input-service"; @@ -21,11 +22,13 @@ import type { SewerageSupplyRateSaveRequest, SewerageSupplyRateStatusInfo, } from "@/entities/statistics/model/statistics-input-interface"; +import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { getFooterSummary } from "@/components/util/grid/footerSummary"; import { getUserInfo } from "../../../../../../packages/common/components/src/api"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { doList } from "@/components/util/column/input/DoList"; type GridRowWithVerification = Record & { verificationResult?: SewerageSupplyRateVerificationResult; @@ -36,10 +39,10 @@ function getSewerageSupplyRateParams( overrides?: { yr?: string; demoYr?: string; mode?: string } ): SewerageSupplyRateRequestParams { return { - sido: values.sido, - gugun: values.gugun === 'ALL' ? '' : values.gugun, - bsnmCd: values.bsnmCd, - yr: overrides?.yr ?? values.year, + ctpvNm: values.sido, + sggNm: values.gugun === 'ALL' ? '' : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: overrides?.yr ?? values.year, ...(overrides?.mode && { mode: overrides.mode }), }; } @@ -52,13 +55,15 @@ const SeweragePenetrationRateInp = () => { const [searchValues, setSearchValues] = useState(null); const [isEnterable, setIsEnterable] = useState(true); const [lastSearchedYear, setLastSearchedYear] = useState(null); + const [lastSearchedValues, setLastSearchedValues] = useState(null); const [statusInfo, setStatusInfo] = useState(null); + const [showDailog, setShowDailog] = useState(false); const { confirm } = useConfirm(); - const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number } | null>(null); + const [footerFromApi, setFooterFromApi] = useState<{ totalPopl?: number; poplSm?: number; frgnrPoplSm?: number; refArea?: number } | null>(null); const getGridDataRef = useRef<(() => Record[]) | null>(null); - const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm'] as const; + const footerFieldNames = ['totalPopl', 'poplSm', 'frgnrPoplSm', 'refArea'] as const; const footerWithApiDefault = (fieldName: (typeof footerFieldNames)[number], numberFormat: string) => ({ expression: 'sum' as const, numberFormat, @@ -76,6 +81,7 @@ const SeweragePenetrationRateInp = () => { if (col.fieldName === 'totalPopl') return { ...col, footer: footerWithApiDefault('totalPopl', '#,##0') }; if (col.fieldName === 'poplSm') return { ...col, footer: footerWithApiDefault('poplSm', '#,##0') }; if (col.fieldName === 'frgnrPoplSm') return { ...col, footer: footerWithApiDefault('frgnrPoplSm', '#,##0') }; + if (col.fieldName === 'refArea') return { ...col, footer: footerWithApiDefault('refArea', '#,##0.000') }; return col; }), }; @@ -85,20 +91,22 @@ const SeweragePenetrationRateInp = () => { setRowData([]); setStatusInfo(null); setLastSearchedYear(null); + setLastSearchedValues(null); setFooterFromApi(null); }; - const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams) => { + const fetchSewerageSupplyRate = async (params: SewerageSupplyRateRequestParams, isEnterable: boolean) => { try { setIsLoading(true); const response = await statisticsInputAPi.getSewerageSupplyRateList(params); if (response.status === 'success' && response.data) { const commonInfo = response.data.commonInfo; + const statusInfo = response.data.statusInfo; const convertedData: SewerageSupplyRateItem[] = response.data.sewerageSupplyRateList.map((d) => ({ ...d, - sido: commonInfo.sido, - speclCntrplnAr: d.speclCntrplnAr === '' || d.speclCntrplnAr === null ? '해당없음' : d.speclCntrplnAr, - piplSupprt: commonInfo.piplSupprt, + ctpvNm: commonInfo.ctpvNm, + spcCtpnRgnCn: d.spcCtpnRgnCn === '' || d.spcCtpnRgnCn === null ? '해당없음' : d.spcCtpnRgnCn, + swagePplPsplyRt: commonInfo.swagePplPsplyRt, })); setRowData( convertedData.map((d) => { @@ -109,16 +117,27 @@ const SeweragePenetrationRateInp = () => { ); setStatusInfo(response.data.statusInfo); - setFooterFromApi( commonInfo ? { - totalPopl: commonInfo.bsnmTotalPopl, - poplSm: commonInfo.bsnmPoplSm, - frgnrPoplSm: commonInfo.bsnmFrgnrPoplSm, + totalPopl: commonInfo.sewerBzmnTotalPopl, + poplSm: commonInfo.sewerBzmnPoplSm, + frgnrPoplSm: commonInfo.sewerBzmnFrgnrPoplSm, + refArea: commonInfo.sewerBzmnArea, } : null ); + + if (statusInfo && statusInfo.locgStatus) { + if (['2', '3'].includes(statusInfo.locgStatus)) { + setIsEnterable(false); + } else { + setIsEnterable(isEnterable); + } + } else { + setIsEnterable(isEnterable); + } + } else { alert(response.message); } @@ -142,10 +161,11 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputAPi.checkInputPeriod(values.year || ''); if (response.status === 'success') { - setIsEnterable(response.data); + const checkEnterable = response.data; + // setIsEnterable(response.data); setLastSearchedYear(values.year || null); - - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values)); + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values), checkEnterable); + setLastSearchedValues(values); } else { alert(response.message); } @@ -170,16 +190,101 @@ const SeweragePenetrationRateInp = () => { return; } - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' })); + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(values, { mode: 'PRE' }), true); }; - const submitFunc = () => { + const submitFunc = async () => { const isError = rowData.filter((d) => d.verificationResult); - if (isError.length > 0) { alert('검증 오류 데이터가 있습니다.'); return; } + if (await confirm('제출 하시겠습니까?')) { + setShowDailog(true); + } + }; + + const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { + try { + setIsLoading(true); + const userInfo = await getUserInfo(); + const currentData = getGridDataRef.current?.(); + if (!currentData || currentData.length === 0) { + alert('제출할 데이터가 없습니다.'); + return; + } + const columnFormatMap = new Map(); + SeweragePenetrationRateInpColumn.columns.forEach((col) => { + if (col.fieldName && col.dataFormat) { + columnFormatMap.set(col.fieldName, col.dataFormat); + } + }); + const formatNumber = (value: string | number | null | undefined, fieldName: string): number => { + if (value === null || value === undefined || value === '') return 0; + const num = typeof value === 'string' ? parseFloat(value.replace(/,/g, '')) : value; + if (isNaN(num)) return 0; + const format = columnFormatMap.get(fieldName); + if (!format) return num; + const decimalPlaces = format.match(/\.(\d+)/)?.[1]?.length ?? 0; + return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); + }; + const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ + sggNm: (d.sggNm as string) || null, + admdstCd: d.admdstCd as string, + admdstNm: d.admdstNm as string, + trbtrNm: d.trbtrNm as string, + spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null), + ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), + ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), + ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), + ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'), + ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'), + ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'), + yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'), + staaArea: formatNumber(d.staaArea, 'staaArea'), + unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'), + drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), + spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), + staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), + rmrk: (d.rmrk as string) ?? '', + wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), + wholArea: formatNumber(d.wholArea, 'wholArea'), + wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), + wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'), + wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'), + wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'), + staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'), + altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'), + sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'), + })); + const requestData: SewerageSupplyRateSaveRequest = { + sewerBzmnCd: searchValues?.bsnmCd ?? '', + ctpvNm: searchValues?.sido ?? '', + statsYr: searchValues?.year ?? '', + userId: userInfo.userUnqId, + submitList: convertedData, + sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm, + locgCharger: submitInfo.locgCharger, + locgJob: submitInfo.locgJob, + locgDept: submitInfo.locgDept, + locgCttpc: submitInfo.locgCttpc, + locgCnfirmNm: submitInfo.locgCnfirmNm, + locgCnfirmJob: submitInfo.locgCnfirmJob, + locgCnfirmDept: submitInfo.locgCnfirmDept, + locgCnfirmPh: submitInfo.locgCnfirmPh, + }; + const response = await statisticsInputAPi.submitSewerageSupplyRate(requestData); + if (response.status === 'success') { + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); + alert('제출되었습니다.'); + } else { + alert('데이터 제출 중 오류가 발생했습니다.'); + } + } catch { + alert('데이터 제출 중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } }; const saveFunc = async () => { @@ -218,47 +323,54 @@ const SeweragePenetrationRateInp = () => { }; const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ - gugun: (d.gugun as string) || null, - adstCd: d.adstCd as string, - adstDong: d.adstDong as string, - bstrm: d.bstrm as string, - speclCntrplnAr: d.speclCntrplnAr === '해당없음' ? null : (d.speclCntrplnAr as string | null), - pblswrgPrmTrt: formatNumber(d.pblswrgPrmTrt, 'pblswrgPrmTrt'), - pblswrgScdTrt: formatNumber(d.pblswrgScdTrt, 'pblswrgScdTrt'), - pblswrgTertTrt: formatNumber(d.pblswrgTertTrt, 'pblswrgTertTrt'), - wstrPrmTrt: formatNumber(d.wstrPrmTrt, 'wstrPrmTrt'), - wstrScdTrt: formatNumber(d.wstrScdTrt, 'wstrScdTrt'), - wstrTertTrt: formatNumber(d.wstrTertTrt, 'wstrTertTrt'), - notConectPopltn: formatNumber(d.notConectPopltn, 'notConectPopltn'), - swgeProcZoneInnerAr: formatNumber(d.swgeProcZoneInnerAr, 'swgeProcZoneInnerAr'), - townUnprocPopltn: formatNumber(d.townUnprocPopltn, 'townUnprocPopltn'), - townSwttPopltn: formatNumber(d.townSwttPopltn, 'townSwttPopltn'), - townWrrtnPopltn: formatNumber(d.townWrrtnPopltn, 'townWrrtnPopltn'), - swgeProcZoneOuterAr: formatNumber(d.swgeProcZoneOuterAr, 'swgeProcZoneOuterAr'), - rmk: (d.rmk as string) ?? '', - popltnSm: formatNumber(d.popltnSm, 'popltnSm'), - arSm: formatNumber(d.arSm, 'arSm'), - swgeProcZoneInnerSm: formatNumber(d.swgeProcZoneInnerSm, 'swgeProcZoneInnerSm'), - pblswrgPopltnSm: formatNumber(d.pblswrgPopltnSm, 'pblswrgPopltnSm'), - wstrPopltnSm: formatNumber(d.wstrPopltnSm, 'wstrPopltnSm'), - swgeProcZoneOuterSm: formatNumber(d.swgeProcZoneOuterSm, 'swgeProcZoneOuterSm'), - pblswrgPopltnSuprt: formatNumber(d.pblswrgPopltnSuprt, 'pblswrgPopltnSuprt'), - adtrtPopltnSuprt: formatNumber(d.adtrtPopltnSuprt, 'adtrtPopltnSuprt'), - swrgInstlrt: formatNumber(d.swrgInstlrt, 'swrgInstlrt'), + // 행정구역 + sggNm: (d.sggNm as string) || null, // 시군구 + admdstCd: d.admdstCd as string, // 행정코드 + admdstNm: d.admdstNm as string, // 행정구역명 + trbtrNm: d.trbtrNm as string, // 지류 + spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null : (d.spcCtpnRgnCn as string | null), // 특별대책지역 + // 공공하수처리인구(명) - 물리적/생물학적/고도처리 + ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리 + ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리 + ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), // 고도처리 + // 폐수처리인구(명) - 물리적/생물학적/고도처리 + ts1WwtPrcsPpltnCnt: formatNumber(d.ts1WwtPrcsPpltnCnt, 'ts1WwtPrcsPpltnCnt'), + ts2WwtPrcsPpltnCnt: formatNumber(d.ts2WwtPrcsPpltnCnt, 'ts2WwtPrcsPpltnCnt'), + ts3WwtPrcsPpltnCnt: formatNumber(d.ts3WwtPrcsPpltnCnt, 'ts3WwtPrcsPpltnCnt'), + yetCntnPpltnCnt: formatNumber(d.yetCntnPpltnCnt, 'yetCntnPpltnCnt'), // 미접속인구 + staaArea: formatNumber(d.staaArea, 'staaArea'), // 하수처리구역 내 면적(㎢) + // 하수처리구역 외 + unprcsPpltnCnt: formatNumber(d.unprcsPpltnCnt, 'unprcsPpltnCnt'), // 미처리인구 + drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구 + spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구 + staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢) + rmrk: (d.rmrk as string) ?? '', // 비고 + // 합계·소계(계산필드 저장용) + wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명) + wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢) + wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), // 하수처리구역 내 합계 + wholPblSwtrmtPpltnCnt: formatNumber(d.wholPblSwtrmtPpltnCnt, 'wholPblSwtrmtPpltnCnt'), // 공공하수처리인구 소계 + wholWwtPrcsPpltnCnt: formatNumber(d.wholWwtPrcsPpltnCnt, 'wholWwtPrcsPpltnCnt'), // 폐수처리인구 소계 + wholStaaExcpPpltnCnt: formatNumber(d.wholStaaExcpPpltnCnt, 'wholStaaExcpPpltnCnt'), // 하수처리구역 외 합계 + // 보급률(%) + staaPsplyRt: formatNumber(d.staaPsplyRt, 'staaPsplyRt'), // 공공하수처리구역 인구보급률 + altdPrcsPsplyRt: formatNumber(d.altdPrcsPsplyRt, 'altdPrcsPsplyRt'), // 고도처리 인구보급률 + sewerInstlRt: formatNumber(d.sewerInstlRt, 'sewerInstlRt'), // 하수도 설치율 })); const requestData: SewerageSupplyRateSaveRequest = { - bsnmCd: searchValues?.bsnmCd ?? '', - sido: searchValues?.sido ?? '', - yr: searchValues?.year ?? '', - userId: userInfo.userName, - saveList: convertedData, - } + sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드 + ctpvNm: searchValues?.sido ?? '', // 시도 + statsYr: searchValues?.year ?? '', // 통계 연도 + userId: userInfo.userUnqId, // 사용자 + saveList: convertedData, // 저장 데이터 + sewerBzmnFrgnrPoplSm: footerFromApi?.frgnrPoplSm // 행정참고자료 외국인 합계 + }; const response = await statisticsInputAPi.saveSewerageSupplyRate(requestData); if (response.status === 'success') { - fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {})); + fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); alert('저장되었습니다.'); } else { alert('데이터 저장 중 오류가 발생했습니다.'); @@ -277,12 +389,12 @@ const SeweragePenetrationRateInp = () => { <>
- {rowData.length > 0 && + {rowData.length > 0 && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true) &&
- {isEnterable && } - {isEnterable && getLastYearDataFunc(searchValues ?? {})} />} - {isEnterable && } + + getLastYearDataFunc(searchValues ?? {})} /> + { // 최고 관리자일땐 저장 가능하게 @@ -298,7 +410,7 @@ const SeweragePenetrationRateInp = () => { 0) && isEnterable} + isExcelImport={(rowData.length > 0) && isEnterable && (doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true)} rowStartNum={3} fixedColumns={1} rowData={rowData} @@ -313,9 +425,18 @@ const SeweragePenetrationRateInp = () => { return 'rg-cell-verify-error'; } }} + cellTooltipCallback={({ row, fieldName }) => { + const verification = (row as GridRowWithVerification).verificationResult; + if (!verification) return; + const errors = verification[fieldName]; + if (Array.isArray(errors) && errors.length > 0) { + return errors.map((e) => e.rsnExpln).filter(Boolean).join('\n'); + } + }} />
+ ); }; diff --git a/apps/st/src/components/statistics/input/SubmitDailog.tsx b/apps/st/src/components/statistics/input/SubmitDailog.tsx new file mode 100644 index 000000000..14cfa06b0 --- /dev/null +++ b/apps/st/src/components/statistics/input/SubmitDailog.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Input, + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from "@common/components"; +import { useEffect } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { statisticsInputSubmitInfoSchema, type StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; +import { type SewerageSupplyRateStatusInfo } from "@/entities/statistics/model/statistics-input-interface"; + +/** 숫자만 추출 후 하이픈 포맷 (010-1234-5678, 02-123-4567 등) */ +function formatPhoneWithHyphen(value: string): string { + const digits = value.replace(/\D/g, ""); + if (digits.length <= 3) return digits; + if (digits.startsWith("010")) { + if (digits.length <= 7) return `010-${digits.slice(3)}`; + return `010-${digits.slice(3, 7)}-${digits.slice(7, 11)}`; + } + if (digits.startsWith("02")) { + if (digits.length <= 5) return `02-${digits.slice(2)}`; + if (digits.length <= 9) return `02-${digits.slice(2, 5)}-${digits.slice(5, 9)}`; + return `02-${digits.slice(2, 6)}-${digits.slice(6, 10)}`; + } + if (digits.length <= 6) return `${digits.slice(0, 3)}-${digits.slice(3)}`; + return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6, 10)}`; +} + +const PHONE_FIELDS: (keyof StatisticsInputSubmitInfoFormData)[] = ["locgCttpc", "locgCnfirmPh"]; + +interface SubmitDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + statusInfo: SewerageSupplyRateStatusInfo | null; + /** 다이얼로그에서 저장 클릭 시 호출. 제출 정보를 넘기면 부모에서 requestData에 포함해 제출 API 호출 */ + onConfirm?: (data: StatisticsInputSubmitInfoFormData) => void | Promise; +} + +const defaultValues: StatisticsInputSubmitInfoFormData = { + locgCharger: "", + locgJob: "", + locgDept: "", + locgCttpc: "", + locgCnfirmNm: "", + locgCnfirmJob: "", + locgCnfirmDept: "", + locgCnfirmPh: "", +}; + +function statusInfoToFormData(info: SewerageSupplyRateStatusInfo): StatisticsInputSubmitInfoFormData { + return { + locgCharger: info.locgCharger ?? "", + locgJob: info.locgJob ?? "", + locgDept: info.locgDept ?? "", + locgCttpc: info.locgCttpc ?? "", + locgCnfirmNm: info.locgCnfirmNm ?? "", + locgCnfirmJob: info.locgCnfirmJob ?? "", + locgCnfirmDept: info.locgCnfirmDept ?? "", + locgCnfirmPh: info.locgCnfirmPh ?? "", + }; +} + +const SubmitDailog = ({ open, onOpenChange, statusInfo, onConfirm }: SubmitDialogProps) => { + const { control, handleSubmit, reset, formState: { errors } } = useForm({ + resolver: zodResolver(statisticsInputSubmitInfoSchema), + defaultValues, + }); + + useEffect(() => { + if (open) { + reset(statusInfo ? statusInfoToFormData(statusInfo) : defaultValues); + } + }, [open, statusInfo, reset]); + + async function onSubmit(data: StatisticsInputSubmitInfoFormData) { + await onConfirm?.(data); + onOpenChange(false); + reset(defaultValues); + } + + function handleCancel() { + onOpenChange(false); + reset(defaultValues); + } + + const rows: { name: keyof StatisticsInputSubmitInfoFormData; label: string; placeholder: string }[] = [ + { name: "locgCharger", label: "작성자 명", placeholder: "작성자 명 입력" }, + { name: "locgJob", label: "작성자 직제", placeholder: "작성자 직제 입력" }, + { name: "locgDept", label: "작성자 부서", placeholder: "작성자 부서 입력" }, + { name: "locgCttpc", label: "작성자 연락처", placeholder: "작성자 연락처 입력" }, + { name: "locgCnfirmNm", label: "승인자명", placeholder: "승인자명 입력" }, + { name: "locgCnfirmJob", label: "승인자 직제", placeholder: "승인자 직제 입력" }, + { name: "locgCnfirmDept", label: "승인자 부서", placeholder: "승인자 부서 입력" }, + { name: "locgCnfirmPh", label: "승인자 연락처", placeholder: "승인자 연락처" }, + ]; + + return ( + + + + 제출 정보 + +
+ + + {rows.map(({ name, label, placeholder }) => ( + + + {label} + + + { + const isPhone = PHONE_FIELDS.includes(name); + return ( +
+ { + const v = e.target.value; + field.onChange(isPhone ? formatPhoneWithHyphen(v) : v); + }} + /> + {errors[name]?.message && ( +

+ {errors[name]?.message} +

+ )} +
+ ); + }} + /> +
+
+ ))} +
+
+ + + + +
+
+
+ ); +}; + +export default SubmitDailog; \ No newline at end of file diff --git a/apps/st/src/components/type/DataGrid.type.ts b/apps/st/src/components/type/DataGrid.type.ts index eea0126c9..5635b7f8a 100644 --- a/apps/st/src/components/type/DataGrid.type.ts +++ b/apps/st/src/components/type/DataGrid.type.ts @@ -212,6 +212,10 @@ export type CellStyleCallbackParams = { export type CellStyleCallback = (params: CellStyleCallbackParams) => string | undefined; +/** 셀 hover 시 툴팁 문구 (반환 시 해당 문구 표시, 검증 오류 메시지 등) */ +export type CellTooltipCallbackParams = { row: Record; fieldName: string }; +export type CellTooltipCallback = (params: CellTooltipCallbackParams) => string | undefined; + export type DataGridProps = { columnInfo: ColumnInfo; rowStartNum?: number; // 헤더 행 개수 (1부터 시작, 예: 2면 1행과 2행까지 헤더로 간주) @@ -226,6 +230,8 @@ export type DataGridProps = { >; /** 셀 단위로 styleName을 동적으로 결정 (예: 검증 오류 셀) */ cellStyleCallback?: CellStyleCallback; + /** 셀 hover 시 툴팁 문구 (예: verificationResult의 rsnExpln) */ + cellTooltipCallback?: CellTooltipCallback; /** 저장 등 시점에 그리드 현재 데이터를 가져올 수 있도록 할당 (편집 반영된 값) */ getGridDataRef?: { current: (() => Record[]) | null; diff --git a/apps/st/src/components/util/column/input/DoList.ts b/apps/st/src/components/util/column/input/DoList.ts new file mode 100644 index 000000000..2b58bd57b --- /dev/null +++ b/apps/st/src/components/util/column/input/DoList.ts @@ -0,0 +1,23 @@ +export const doList = [ + "4100000000", + "4200000000", + "4300000000", + "4400000000", + "4500000000", + "4600000000", + "4700000000", + "4800000000", +]; + +export const needBsnmFrgnrPoplSmSidoList = [ + "3600000000", + "4100000000", + "4200000000", + "4300000000", + "4400000000", + "4500000000", + "4600000000", + "4700000000", + "4800000000", + "5000000000", +]; \ No newline at end of file diff --git a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts index 41a9c5700..6cd283565 100644 --- a/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts +++ b/apps/st/src/components/util/column/input/SeweragePenetrationRateInpColumn.ts @@ -7,13 +7,13 @@ import { ValueType } from "realgrid"; export const SeweragePenetrationRateInpColumn = { columns: [ // 행정구역 - { name: 'sido', fieldName: 'sido', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'gugun', fieldName: 'gugun', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'adstDong', fieldName: 'adstDong', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'ctpvNm', fieldName: 'ctpvNm', header: { text: '시도' }, footer: { text: '합계' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'sggNm', fieldName: 'sggNm', header: { text: '시군구' }, editable: false, styleName: 'rg-cell-readonly' }, + { name: 'admdstNm', fieldName: 'admdstNm', header: { text: '행정구역명' }, editable: false, styleName: 'rg-cell-readonly' }, // 수계 및 권역 { name: 'rvbsn', fieldName: 'rvbsn', header: { text: '수계' }, editable: false, styleName: 'rg-cell-readonly' }, - { name: 'bstrm', fieldName: 'bstrm', header: { text: '지류' } }, + { name: 'trbtrNm', fieldName: 'trbtrNm', header: { text: '지류' } }, // 수계영향권별분류 { name: 'mdbsn', fieldName: 'mdbsn', header: { text: '세부단위유역' }, editable: false, styleName: 'rg-cell-readonly' }, @@ -21,7 +21,7 @@ export const SeweragePenetrationRateInpColumn = { { name: 'mdmbsn', fieldName: 'mdmbsn', header: { text: '소권역' }, editable: false, styleName: 'rg-cell-readonly' }, // 특별대책지역 - { name: 'speclCntrplnAr', fieldName: 'speclCntrplnAr', header: { text: '특별대책지역' } }, + { name: 'spcCtpnRgnCn', fieldName: 'spcCtpnRgnCn', header: { text: '특별대책지역' } }, // 행정참고자료인구 { name: 'totalPopl', fieldName: 'totalPopl', header: { text: '총인구' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, @@ -29,113 +29,113 @@ export const SeweragePenetrationRateInpColumn = { { name: 'frgnrPoplSm', fieldName: 'frgnrPoplSm', header: { text: '외국인' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', styleName: 'rg-cell-readonly' }, // 행정참고자료면적 - { name: 'area', fieldName: 'area', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, + { name: 'refArea', fieldName: 'refArea', header: { text: '행정참고자료면적(㎢)' }, footer: footerDecimal3, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', styleName: 'rg-cell-readonly' }, // 총인구 { - name: 'popltnSm', - fieldName: 'popltnSm', + name: 'wholPpltnCnt', + fieldName: 'wholPpltnCnt', header: { text: '총인구(명)' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const }, + computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, // 총면적 { - name: 'arSm', - fieldName: 'arSm', + name: 'wholArea', + fieldName: 'wholArea', header: { text: '총면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000', - computed: { fields: ['swgeProcZoneInnerAr', 'swgeProcZoneOuterAr'], type: 'sum' as const }, + computed: { fields: ['staaArea', 'staaExcpArea'], type: 'sum' as const }, styleName: 'rg-cell-total', }, // 하수처리구역 내 > 합계 { - name: 'swgeProcZoneInnerSm', - fieldName: 'swgeProcZoneInnerSm', + name: 'wholSwtrmtPpltnCnt', + fieldName: 'wholSwtrmtPpltnCnt', header: { text: '합계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], type: 'sum' as const }, + computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, // 하수처리구역 내 > 공공하수처리인구 { - name: 'pblswrgPopltnSm', - fieldName: 'pblswrgPopltnSm', + name: 'wholPblSwtrmtPpltnCnt', + fieldName: 'wholPblSwtrmtPpltnCnt', header: { text: '소계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], type: 'sum' as const }, + computed: { fields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, - { name: 'pblswrgPrmTrt', fieldName: 'pblswrgPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'pblswrgScdTrt', fieldName: 'pblswrgScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'pblswrgTertTrt', fieldName: 'pblswrgTertTrt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts1SwtrmtPpltnCnt', fieldName: 'ts1SwtrmtPpltnCnt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts2SwtrmtPpltnCnt', fieldName: 'ts2SwtrmtPpltnCnt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts3SwtrmtPpltnCnt', fieldName: 'ts3SwtrmtPpltnCnt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 폐수처리인구 { - name: 'wstrPopltnSm', - fieldName: 'wstrPopltnSm', + name: 'wholWwtPrcsPpltnCnt', + fieldName: 'wholWwtPrcsPpltnCnt', header: { text: '소계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], type: 'sum' as const }, + computed: { fields: ['ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, - { name: 'wstrPrmTrt', fieldName: 'wstrPrmTrt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'wstrScdTrt', fieldName: 'wstrScdTrt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'wstrTertTrt', fieldName: 'wstrTertTrt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts1WwtPrcsPpltnCnt', fieldName: 'ts1WwtPrcsPpltnCnt', header: { text: '물리적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts2WwtPrcsPpltnCnt', fieldName: 'ts2WwtPrcsPpltnCnt', header: { text: '생물학적처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'ts3WwtPrcsPpltnCnt', fieldName: 'ts3WwtPrcsPpltnCnt', header: { text: '고도처리' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 미접속인구 - { name: 'notConectPopltn', fieldName: 'notConectPopltn', header: { text: '미접속인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'yetCntnPpltnCnt', fieldName: 'yetCntnPpltnCnt', header: { text: '미접속인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, // 하수처리구역 내 > 면적 - { name: 'swgeProcZoneInnerAr', fieldName: 'swgeProcZoneInnerAr', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'staaArea', fieldName: 'staaArea', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 하수처리구역 외 { - name: 'swgeProcZoneOuterSm', - fieldName: 'swgeProcZoneOuterSm', + name: 'wholStaaExcpPpltnCnt', + fieldName: 'wholStaaExcpPpltnCnt', header: { text: '합계' }, footer: footerInt, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0', - computed: { fields: ['townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], type: 'sum' as const }, + computed: { fields: ['unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'sum' as const }, styleName: 'rg-cell-total', }, - { name: 'townUnprocPopltn', fieldName: 'townUnprocPopltn', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'townSwttPopltn', fieldName: 'townSwttPopltn', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'townWrrtnPopltn', fieldName: 'townWrrtnPopltn', header: { text: '정화조인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, - { name: 'swgeProcZoneOuterAr', fieldName: 'swgeProcZoneOuterAr', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, + { name: 'unprcsPpltnCnt', fieldName: 'unprcsPpltnCnt', header: { text: '미처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'drtwPrcsPpltnCnt', fieldName: 'drtwPrcsPpltnCnt', header: { text: '오수처리인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'spctnkPpltnCnt', fieldName: 'spctnkPpltnCnt', header: { text: '정화조인구' }, footer: footerInt, valueType: ValueType.NUMBER, dataFormat: '#,##0' }, + { name: 'staaExcpArea', fieldName: 'staaExcpArea', header: { text: '면적(㎢)' }, footer: footerDecimal3, valueType: ValueType.NUMBER, dataFormat: '#,##0.000' }, // 공공하수처리구역 인구보급률 (하수처리구역 내 인구 / 전체 인구 * 100) { - name: 'pblswrgPopltnSuprt', - fieldName: 'pblswrgPopltnSuprt', + name: 'staaPsplyRt', + fieldName: 'staaPsplyRt', header: { text: '공공하수처리구역인구보급률(%)' }, footer: footerAvgDecimal, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', computed: { - numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], - denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], + numeratorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], + denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'pblswrgPopltnSuprtRate' as const }, styleName: 'rg-cell-total', @@ -143,16 +143,16 @@ export const SeweragePenetrationRateInpColumn = { // 고도처리 인구보급률 (고도처리 인구 / 전체 인구 * 100) { - name: 'adtrtPopltnSuprt', - fieldName: 'adtrtPopltnSuprt', + name: 'altdPrcsPsplyRt', + fieldName: 'altdPrcsPsplyRt', header: { text: '고도처리인구보급률(%)' }, footer: footerAvgDecimal, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', computed: { - numeratorFields: ['pblswrgTertTrt', 'wstrTertTrt'], - denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'townUnprocPopltn', 'townSwttPopltn', 'townWrrtnPopltn'], + numeratorFields: ['ts3SwtrmtPpltnCnt', 'ts3WwtPrcsPpltnCnt'], + denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'unprcsPpltnCnt', 'drtwPrcsPpltnCnt', 'spctnkPpltnCnt'], type: 'adtrtPopltnSuprtRate' as const }, styleName: 'rg-cell-total', @@ -160,27 +160,27 @@ export const SeweragePenetrationRateInpColumn = { // 하수도 설치율 { - name: 'swrgInstlrt', - fieldName: 'swrgInstlrt', + name: 'sewerInstlRt', + fieldName: 'sewerInstlRt', header: { text: '하수도설치율(%)' }, footer: footerAvgDecimal, editable: false, valueType: ValueType.NUMBER, dataFormat: '#,##0.0', computed: { - numeratorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn', 'swgeProcZoneOuterSm'], - denominatorFields: ['pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt', 'notConectPopltn'], - thirdFields: ['piplSupprt'], + numeratorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt', 'wholStaaExcpPpltnCnt'], + denominatorFields: ['ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt', 'yetCntnPpltnCnt'], + thirdFields: ['swagePplPsplyRt'], type: 'swrgInstlrtRate' as const }, styleName: 'rg-cell-total', }, // 비고 - { name: 'rmk', fieldName: 'rmk', header: { text: '비고' } }, + { name: 'rmrk', fieldName: 'rmrk', header: { text: '비고' } }, - { name: 'piplSupprt', fieldName: 'piplSupprt', header: { text: '하수관로보급률(%)' }, visible: false }, - { name: 'adstCd', fieldName: 'adstCd', header: { text: '행정코드' }, visible: false }, + { name: 'swagePplPsplyRt', fieldName: 'swagePplPsplyRt', header: { text: '하수관로보급률(%)' }, visible: false }, + { name: 'admdstCd', fieldName: 'admdstCd', header: { text: '행정코드' }, visible: false }, ], columnLayout: [ @@ -189,49 +189,49 @@ export const SeweragePenetrationRateInpColumn = { hideChildHeaders: false, header: { text: '행정구역' }, items: [ - { column: 'sido', footerUserSpans: [{ colspan: 3 }] }, - 'gugun', - 'adstDong', + { column: 'ctpvNm', footerUserSpans: [{ colspan: 3 }] }, + 'sggNm', + 'admdstNm', ], }, 'rvbsn', - 'bstrm', + 'trbtrNm', { name: 'groupB', hideChildHeaders: false, header: { text: '수계영향권별분류' }, items: ['mdbsn', 'lgbsn', 'mdmbsn'], }, - 'speclCntrplnAr', + 'spcCtpnRgnCn', { name: 'groupC', hideChildHeaders: false, header: { text: '행정참고자료인구' }, items: ['totalPopl', 'poplSm', 'frgnrPoplSm'], }, - 'area', - 'popltnSm', - 'arSm', + 'refArea', + 'wholPpltnCnt', + 'wholArea', { name: 'groupD', hideChildHeaders: false, header: { text: '하수처리구역내' }, items: [ - 'swgeProcZoneInnerSm', + 'wholSwtrmtPpltnCnt', { name: 'pblswrgGroup', hideChildHeaders: false, header: { text: '공공하수처리인구(명)' }, - items: ['pblswrgPopltnSm', 'pblswrgPrmTrt', 'pblswrgScdTrt', 'pblswrgTertTrt'], + items: ['wholPblSwtrmtPpltnCnt', 'ts1SwtrmtPpltnCnt', 'ts2SwtrmtPpltnCnt', 'ts3SwtrmtPpltnCnt'], }, { name: 'wstrGroup', hideChildHeaders: false, header: { text: '폐수처리인구(명)' }, - items: ['wstrPopltnSm', 'wstrPrmTrt', 'wstrScdTrt', 'wstrTertTrt'], + items: ['wholWwtPrcsPpltnCnt', 'ts1WwtPrcsPpltnCnt', 'ts2WwtPrcsPpltnCnt', 'ts3WwtPrcsPpltnCnt'], }, - 'notConectPopltn', - 'swgeProcZoneInnerAr' + 'yetCntnPpltnCnt', + 'staaArea' ], }, { @@ -239,18 +239,18 @@ export const SeweragePenetrationRateInpColumn = { hideChildHeaders: false, header: { text: '하수처리구역외' }, items: [ - 'swgeProcZoneOuterSm', - 'townUnprocPopltn', - 'townSwttPopltn', - 'townWrrtnPopltn', - 'swgeProcZoneOuterAr' + 'wholStaaExcpPpltnCnt', + 'unprcsPpltnCnt', + 'drtwPrcsPpltnCnt', + 'spctnkPpltnCnt', + 'staaExcpArea' ], }, - 'pblswrgPopltnSuprt', - 'adtrtPopltnSuprt', - 'swrgInstlrt', - 'rmk', - 'piplSupprt', - 'adstCd', + 'staaPsplyRt', + 'altdPrcsPsplyRt', + 'sewerInstlRt', + 'rmrk', + 'swagePplPsplyRt', + 'admdstCd', ] }; \ No newline at end of file diff --git a/apps/st/src/entities/statistics/api/statistics-input-service.ts b/apps/st/src/entities/statistics/api/statistics-input-service.ts index f8fdc0850..a45251549 100644 --- a/apps/st/src/entities/statistics/api/statistics-input-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-input-service.ts @@ -38,6 +38,15 @@ export class StatisticsInputAPiService implements StatisticsInputService { return response.data; } + + public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { + const response = await axios.post ( + `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`, + params + ); + + return response.data; + } } export const statisticsInputAPi = new StatisticsInputAPiService(); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-input-interface.ts b/apps/st/src/entities/statistics/model/statistics-input-interface.ts index 3fd0ba0c8..72b519acc 100644 --- a/apps/st/src/entities/statistics/model/statistics-input-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-input-interface.ts @@ -2,120 +2,83 @@ import type { ApiResponse } from "./statistics-interface"; // 하수도 보급률 데이터 항목 export interface SewerageSupplyRateStatusInfo { - locgStatus: string; - locgDt: string; - locgCharger: string; - locgCttpc: string; - prvnStatus: string; - dcsnStatus: string; + locgStatus: string | null; + locgDt: string | null; + locgCharger: string | null; + locgCttpc: string | null; + locgJob: string | null; + prvnStatus: string | null; + dcsnStatus: string | null; wrterOpinionSm: string | null; chckerOpinionSm: string | null; returnOpinionSm: string | null; + locgCnfirmDept: string | null; + locgCnfirmJob: string | null; + locgCnfirmNm: string | null; + locgCnfirmPh: string | null; + locgDept: string | null; } export interface SewerageSupplyRateCommonInfo { - bsnmArea: number; - bsnmCd: string; - bsnmFrgnrPoplSm: number; - bsnmPoplSm: number; - bsnmTotalPopl: number; + sewerBzmnArea: number; + sewerBzmnCd: string; + sewerBzmnFrgnrPoplSm: number; + sewerBzmnPoplSm: number; + sewerBzmnTotalPopl: number; piplFcltyExtn: number; piplPlanExtn: number; - piplSupprt: number; - sido: string; - yr: string; + swagePplPsplyRt: number; + ctpvNm: string; + statsYr: string; } /** 검증 결과 한 건 (필드별 메시지) */ export interface VerificationResultItem { - type: string; - rsn: string; + vrfcCd: string; + rsnExpln: string; } /** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */ export type SewerageSupplyRateVerificationResult = Record; +/** 하수도 보급률 목록 API 응답 한 행 (실제 응답 필드만) */ export interface SewerageSupplyRateItem { - sido: string; - gugun: string; - adstDong: string; - rgnNm: string; - adstCd: string; - yr: string; - bsnmCd: string; - rvbsn: string; - lgbsn: string; + admdstCd: string; + sggNm: string; + admdstNm: string; + rvbsn: string | null; + trbtrNm: string; mdbsn: string; + lgbsn: string; mdmbsn: string; + spcCtpnRgnCn: string | null; totalPopl: number; poplSm: number; frgnrPoplSm: number; - area: number; - bsnmTotalPopl: number; - bsnmPoplSm: number; - bsnmFrgnrPoplSm: number; - bsnmArea: number; - piplPlanExtn: number; - piplFcltyExtn: number; - piplSupprt: number; - wrssm: string | null; - bstrm: string; - speclCntrplnAr: string | null; - popltnSm: number; - arSm: number; - swgeProcZoneInnerSm: number; - pblswrgPopltnSm: number; - pblswrgPrmTrt: number; - pblswrgScdTrt: number; - pblswrgTertTrt: number; - wstrPopltnSm: number; - wstrPrmTrt: number; - wstrScdTrt: number; - wstrTertTrt: number; - swgeProcZoneInnerAr: number; - swgeProcZoneOuterSm: number; - townUnprocPopltn: number; - townSwttPopltn: number; - townWrrtnPopltn: number; - uptownUnprPopltn: number; - uptownSwttPopltn: number; - uptownWrrtnPopltn: number; - swgeProcZoneOuterAr: number; - pblswrgPopltnSuprt: number; - adtrtPopltnSuprt: number; - swrgInstlrt: number; - wrterOpinion: string | null; - chckerOpinion: string | null; - notConectPopltn: number; - rmk: string; - chkWrssm: string; - chkBstrm: string; - chkSpeclCntrplnAr: string; - chkPopltnSm: string; - chkArSm: string; - chkSwgeProcZoneInnerSm: string; - chkPblswrgPopltnSm: string; - chkPblswrgPrmTrt: string; - chkPblswrgScdTrt: string; - chkPblswrgTertTrt: string; - chkWstrPopltnSm: string; - chkWstrPrmTrt: string; - chkWstrScdTrt: string; - chkWstrTertTrt: string; - chkSwgeProcZoneInnerAr: string; - chkSwgeProcZoneOuterSm: string; - chkTownUnprocPopltn: string; - chkTownSwttPopltn: string; - chkTownWrrtnPopltn: string; - chkUptownUnprPopltn: string; - chkUptownSwttPopltn: string; - chkUptownWrrtnPopltn: string; - chkSwgeProcZoneOuterAr: string; - chkPblswrgPopltnSuprt: string; - chkAdtrtPopltnSuprt: string; - chkSwrgInstlrt: string; - chkNotConectPopltn: string; - verificationResult?: SewerageSupplyRateVerificationResult; + refArea: number; + wholPpltnCnt: number; + wholArea: number; + wholSwtrmtPpltnCnt: number; + wholPblSwtrmtPpltnCnt: number; + ts1SwtrmtPpltnCnt: number; + ts2SwtrmtPpltnCnt: number; + ts3SwtrmtPpltnCnt: number; + wholWwtPrcsPpltnCnt: number; + ts1WwtPrcsPpltnCnt: number; + ts2WwtPrcsPpltnCnt: number; + ts3WwtPrcsPpltnCnt: number; + yetCntnPpltnCnt: number; + staaArea: number; + wholStaaExcpPpltnCnt: number; + unprcsPpltnCnt: number; + drtwPrcsPpltnCnt: number; + spctnkPpltnCnt: number; + staaExcpArea: number; + staaPsplyRt: number; + altdPrcsPsplyRt: number; + sewerInstlRt: number; + rmrk: string | null; + verificationResult?: SewerageSupplyRateVerificationResult | null; } export interface SewerageSupplyRateResponseData { @@ -129,55 +92,63 @@ export type SewerageSupplyRateApiResponse = ApiResponse; checkInputPeriod(params: string): Promise; saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; + submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts b/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts new file mode 100644 index 000000000..fb3380b6b --- /dev/null +++ b/apps/st/src/entities/statistics/model/statistics-input-submit-info-schema.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +export const statisticsInputSubmitInfoSchema = z.object({ + // 작성자 명, 작성자 직제, 작성자 부서, 작성자 연락처, 승인자명, 승인자 직제, 승인자 부서, 승인자 연락처 (전부 필수) + locgCharger: z.string().min(1, "작성자 명을 입력하세요"), + locgJob: z.string().min(1, "작성자 직제를 입력하세요"), + locgDept: z.string().min(1, "작성자 부서를 입력하세요"), + locgCttpc: z.string().min(1, "작성자 연락처를 입력하세요"), + locgCnfirmNm: z.string().min(1, "승인자명을 입력하세요"), + locgCnfirmJob: z.string().min(1, "승인자 직제를 입력하세요"), + locgCnfirmDept: z.string().min(1, "승인자 부서를 입력하세요"), + locgCnfirmPh: z.string().min(1, "승인자 연락처를 입력하세요"), +}); + +export type StatisticsInputSubmitInfoFormData = z.infer< + typeof statisticsInputSubmitInfoSchema +>; \ No newline at end of file