diff --git a/apps/as/docs/spec/dashboard/SPEC.md b/apps/as/docs/spec/dashboard/SPEC.md index b547d1ae7..3ce6f9800 100644 --- a/apps/as/docs/spec/dashboard/SPEC.md +++ b/apps/as/docs/spec/dashboard/SPEC.md @@ -28,18 +28,18 @@ ``` ┌────────────────────────────────┬─────────────────────────────────────────────────┐ │ [영역 A] 전국 행정지원 지도 │ [영역 B] 문서별 진행상황 카드 (2×2) │ -│ 타이틀: 전국 행정지원 지도 │ ┌───────────────┐ ┌───────────────┐ │ -│ │ │ 재원협의 │ │ 설치인가 │ │ -│ (대한민국 시도·시군구 경계) │ │ 신청/진행/완료│ │ 신청/진행/완료│ │ -│ 마커: 서울 강남구, │ ├───────────────┤ ├───────────────┤ │ -│ 대전 유성구, │ │ 설계자문 │ │ 기본계획 │ │ -│ 부산 해운대구 │ │ 신청/진행/완료│ │ 신청/진행/완료│ │ -│ │ └───────────────┘ └───────────────┘ │ +│ 타이틀: 전국 행정지원 지도 │ ┌────────────────┐ ┌────────────────┐ │ +│ │ │ 재원협의 │ │ 설치인가 │ │ +│ (대한민국 시도·시군구 경계) │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │ +│ 마커: 서울 강남구, │ ├────────────────┤ ├────────────────┤ │ +│ 대전 유성구, │ │ 설계자문 │ │ 기본계획 │ │ +│ 부산 해운대구 │ │ 신청/진행/완료 │ │ 신청/진행/완료 │ │ +│ │ └────────────────┘ └────────────────┘ │ │ ├─────────────────────────────────────────────────┤ │ │ [영역 C] 알림이력 │ -│ │ 알림이력 업데이트 HH:mm [전체보기]│ +│ │ 알림이력 업데이트 HH:mm [전체보기] │ │ │ [오늘 N건] [지연 N건] [완료대기 N건] │ -│ │ [뱃지] [일자] 제목 [처리하기]│ +│ │ [뱃지] [일자] 제목 [처리하기] │ │ │ … (최대 5건) │ │ ├─────────────────────────────────────────────────┤ │ │ [영역 D] 공지사항 [더보기] │ diff --git a/apps/as/public/images/region_map.svg b/apps/as/public/images/region_map.svg new file mode 100644 index 000000000..f327bb9fc --- /dev/null +++ b/apps/as/public/images/region_map.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/as/public/images/region_map_bg.png b/apps/as/public/images/region_map_bg.png new file mode 100644 index 000000000..6f59f58cc Binary files /dev/null and b/apps/as/public/images/region_map_bg.png differ diff --git a/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts b/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts index ab60a868f..97adb34b6 100644 --- a/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts +++ b/apps/as/src/entities/fnrs-shar-se/model/fnrs-shar-se-interface.ts @@ -6,7 +6,8 @@ export const FNRS_SHAR_SE_SAVE_MODE = { DELETE: "D", } as const; -export type FnrsSharSeSaveMode = (typeof FNRS_SHAR_SE_SAVE_MODE)[keyof typeof FNRS_SHAR_SE_SAVE_MODE]; +export type FnrsSharSeSaveMode = + (typeof FNRS_SHAR_SE_SAVE_MODE)[keyof typeof FNRS_SHAR_SE_SAVE_MODE]; /** 백엔드 FnrsSharSeMngResponseDto */ export interface FnrsSharSeMngResponseDto { @@ -69,6 +70,8 @@ export interface FnrsSharBizCstTreeDto { ajmtAmt?: number; rmrkCn?: string; envoRmrkCn?: string; + chgAmt: number; + chgRt: number; } /** Controller public 메서드명·시그니처와 동일 */ diff --git a/apps/as/src/pages/dashboard/lib/national-as-map.module.css b/apps/as/src/pages/dashboard/lib/national-as-map.module.css new file mode 100644 index 000000000..459855e88 --- /dev/null +++ b/apps/as/src/pages/dashboard/lib/national-as-map.module.css @@ -0,0 +1,153 @@ +.mapArea { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.bgLayer { + position: absolute; + inset: var(--map-inset); + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + width: calc(100% - var(--map-inset) * 2); + height: calc(100% - var(--map-inset) * 2); + background-image: url("/images/region_map_bg.png"); + background-repeat: no-repeat; + background-size: contain; + background-position: center center; + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + image-rendering: pixelated; + opacity: 0.7; + filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.02)); + pointer-events: none; + min-height: 200px; +} + +.svgLayer { + position: absolute; + inset: var(--map-inset); + display: flex; + align-items: center; + justify-content: center; + z-index: 2; + width: calc(100% - var(--map-inset) * 2); + height: calc(100% - var(--map-inset) * 2); +} + +.svgLayer > svg { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + image-rendering: -webkit-optimize-contrast; + image-rendering: crisp-edges; + shape-rendering: geometricPrecision; +} + +.svgContainer :global(svg) :global(path) { + fill: rgba(0, 150, 255, 0.2) !important; + stroke: #0066cc; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} + +.svgContainer :global(svg) :global(.cls-1) { + fill: rgba(0, 150, 255, 0.2) !important; + stroke: #0066cc; + stroke-width: 1; + vector-effect: non-scaling-stroke; +} + +.svgContainer :global(svg) :global(.basin) { + opacity: 0; + pointer-events: all; + transition: opacity 0.15s ease; + cursor: pointer; +} + +.svgContainer :global(svg) :global(.basin.active), +.svgContainer :global(svg) :global(.basin:hover) { + opacity: 1; +} + +.svgContainer :global(svg) :global(.basin) :global(path), +.svgContainer :global(svg) :global(.basin) :global(polygon), +.svgContainer :global(svg) :global(.basin) :global(polyline) { + fill: rgba(0, 209, 209, 0.5); + stroke: #399ac3; + stroke-width: 1.3; + vector-effect: non-scaling-stroke; +} + +.popup { + position: absolute; + width: 300px; + height: 150px; + min-width: 220px; + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 10px; + box-shadow: + 0 12px 24px rgba(0, 0, 0, 0.18), + 0 2px 0 rgba(0, 0, 0, 0.05); + opacity: 0; + visibility: hidden; + transform: translate(-50%, -8px) scale(0.98); + transition: + opacity 0.12s, + visibility 0.12s, + transform 0.12s; + pointer-events: none; + z-index: 10; +} + +.popup.show { + opacity: 1 !important; + visibility: visible !important; + transform: translate(-50%, -12px) scale(1) !important; +} + +.show { + opacity: 1; + visibility: visible; +} + +.labelsLayer { + position: absolute; + inset: var(--map-inset); + pointer-events: none; + z-index: 3; + width: 100%; + height: 100%; +} + +.basinLabel { + position: absolute; + transform: translate(-50%, -50%); + pointer-events: none; + padding: 6px 12px; + border-radius: 6px; + font-weight: 700; + font-size: 16px; + background: #ffffffee; + border: 2px solid #d0d0d0; + white-space: nowrap; + min-width: 60px; + text-align: center; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.basinLabel.active { + background: #006bf8; + color: #fff; + border-color: #96c3ff; + box-shadow: 0 4px 12px rgba(0, 107, 248, 0.3); +} diff --git a/apps/as/src/pages/dashboard/lib/use-national-as-map.ts b/apps/as/src/pages/dashboard/lib/use-national-as-map.ts new file mode 100644 index 000000000..997c0154a --- /dev/null +++ b/apps/as/src/pages/dashboard/lib/use-national-as-map.ts @@ -0,0 +1,310 @@ +import { type CSSProperties, useCallback, useEffect, useRef, useState } from "react"; + +import { + BASIN_DATA, + BASIN_IDS, + type BasinKey, + MANUAL_LABEL_POS, + type ManualLabelPos, +} from "../model/map-data"; + +import styles from "./national-as-map.module.css"; + +const SVG_URL = "/images/region_map.svg"; +const POPUP_EDGE_PADDING_PX = 12; +const LABEL_OFFSET_Y_PX = 18; +const DEFAULT_VIEWBOX = "0 0 100 100"; + +export const PLACEHOLDER_POPUP_ITEMS = [{ header: "현황", data: "-" }] as const; + +export interface MapPopupItem { + header: string; + data: string; +} + +export interface MapPopupContent { + title: string; + items: MapPopupItem[]; +} + +const INITIAL_POPUP_CONTENT: MapPopupContent = { + title: "", + items: [...PLACEHOLDER_POPUP_ITEMS], +}; + +function isBasinKey(key: string | undefined): key is BasinKey { + return Boolean(key && key in BASIN_DATA); +} + +function resolveBasinName(group: SVGGElement) { + const key = group.dataset.key; + if (isBasinKey(key)) return BASIN_DATA[key].name; + return group.dataset.label ?? ""; +} + +function hidePopup(popup: HTMLDivElement | null) { + if (!popup) return; + if (styles.show) popup.classList.remove(styles.show); + popup.style.opacity = "0"; + popup.style.visibility = "hidden"; +} + +function showPopupAtGroup(stage: HTMLDivElement, popup: HTMLDivElement, group: SVGGElement) { + const stageRect = stage.getBoundingClientRect(); + const groupRect = group.getBoundingClientRect(); + const centerX = (groupRect.left + groupRect.right) / 2 - stageRect.left; + const topY = groupRect.top - stageRect.top; + + popup.style.left = `${centerX}px`; + popup.style.top = `${topY + 6}px`; + popup.style.opacity = "1"; + popup.style.visibility = "visible"; + if (styles.show) popup.classList.add(styles.show); + + const popupRect = popup.getBoundingClientRect(); + const clampedX = Math.max( + POPUP_EDGE_PADDING_PX + popupRect.width / 2, + Math.min(centerX, stageRect.width - POPUP_EDGE_PADDING_PX - popupRect.width / 2), + ); + popup.style.left = `${clampedX}px`; +} + +function collectBasinGroups(svg: SVGSVGElement) { + const groups: SVGGElement[] = []; + + BASIN_IDS.forEach((item) => { + let group: SVGGElement | null = null; + for (const id of item.labels) { + group = svg.getElementById(id) as SVGGElement | null; + if (group) break; + } + if (!group) return; + + group.classList.add("basin"); + group.dataset.key = item.key; + group.dataset.label = item.labels[0] ?? ""; + + group.querySelectorAll("path").forEach((path) => { + path.style.fill = "rgba(0, 150, 255, 0.2)"; + path.style.stroke = "#0066cc"; + path.style.strokeWidth = "1"; + path.style.vectorEffect = "non-scaling-stroke"; + }); + + groups.push(group); + }); + + return groups; +} + +function createLabels(labelsLayer: HTMLDivElement, groups: SVGGElement[]) { + labelsLayer.innerHTML = ""; + return groups.map((group) => { + const label = document.createElement("div"); + label.className = styles.basinLabel ?? ""; + label.textContent = group.dataset.label ?? ""; + labelsLayer.appendChild(label); + return label; + }); +} + +function applyViewBox(svg: SVGSVGElement, stage: HTMLDivElement) { + if (!svg.hasAttribute("viewBox")) { + const bbox = svg.getBBox(); + svg.setAttribute("viewBox", [bbox.x, bbox.y, bbox.width, bbox.height].join(" ")); + } + + const viewBox = (svg.getAttribute("viewBox") || "").split(/\s+/).map(Number); + if (viewBox.length === 4 && viewBox[3]! > 0) + stage.style.aspectRatio = String(viewBox[2]! / viewBox[3]!); + + svg.setAttribute("preserveAspectRatio", "xMidYMid meet"); + svg.style.width = "100%"; + svg.style.height = "100%"; +} + +function positionLabels( + stage: HTMLDivElement, + svg: SVGSVGElement, + groups: SVGGElement[], + labels: HTMLDivElement[], +) { + const stageRect = stage.getBoundingClientRect(); + const inset = parseFloat(getComputedStyle(stage).getPropertyValue("--map-inset") || "0") / 100; + const left = stageRect.width * inset; + const top = stageRect.height * inset; + const width = stageRect.width * (1 - inset * 2); + const height = stageRect.height * (1 - inset * 2); + const [viewX, viewY, viewWidth, viewHeight] = (svg.getAttribute("viewBox") || DEFAULT_VIEWBOX) + .split(/\s+/) + .map(Number); + + const svgRect = svg.getBoundingClientRect(); + const offsetX = svgRect.left - stageRect.left - left; + const offsetY = svgRect.top - stageRect.top - top; + + groups.forEach((group, index) => { + const key = group.dataset.key; + const manual = key ? (MANUAL_LABEL_POS as Record)[key] : undefined; + let posX: number; + let posY: number; + + if (manual && (("x" in manual && "y" in manual) || ("px" in manual && "py" in manual))) { + if ("px" in manual) { + posX = left + (manual.px || 0) + offsetX; + posY = top + (manual.py || 0) + offsetY; + } else { + posX = left + width * (manual.x || 0) + offsetX; + posY = top + height * (manual.y || 0) + offsetY; + } + } else { + const bbox = group.getBBox(); + const centerX = bbox.x + bbox.width / 2; + const centerY = bbox.y + bbox.height / 2; + posX = ((centerX - viewX!) / viewWidth!) * width + left + offsetX; + posY = ((centerY - viewY!) / viewHeight!) * height + top + offsetY; + } + + const label = labels[index]; + if (!label) return; + label.style.left = `${posX}px`; + label.style.top = `${posY + LABEL_OFFSET_Y_PX}px`; + }); +} + +export function useNationalAsMap() { + const stageRef = useRef(null); + const svgLayerRef = useRef(null); + const labelsLayerRef = useRef(null); + const popupRef = useRef(null); + const [popupContent, setPopupContent] = useState(INITIAL_POPUP_CONTENT); + + const showFor = useCallback((group: SVGGElement) => { + const stage = stageRef.current; + const popup = popupRef.current; + if (!stage || !popup) return; + + setPopupContent({ + title: resolveBasinName(group), + items: [...PLACEHOLDER_POPUP_ITEMS], + }); + showPopupAtGroup(stage, popup, group); + }, []); + + useEffect(() => { + const abortController = new AbortController(); + let svg: SVGSVGElement | null = null; + let groups: SVGGElement[] = []; + let labels: HTMLDivElement[] = []; + let resizeObserver: ResizeObserver | null = null; + let positionLabelsHandler: (() => void) | null = null; + let clearActive: (() => void) | null = null; + let handleStageClick: (() => void) | null = null; + + const listeners: Array<{ + target: EventTarget; + type: string; + listener: EventListener; + }> = []; + + function addListener(target: EventTarget, type: string, listener: EventListener) { + target.addEventListener(type, listener); + listeners.push({ target, type, listener }); + } + + fetch(SVG_URL, { signal: abortController.signal }) + .then((response) => { + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + return response.text(); + }) + .then((svgText) => { + if (abortController.signal.aborted) return; + if (!svgLayerRef.current || !stageRef.current) return; + + svgLayerRef.current.innerHTML = svgText; + svg = svgLayerRef.current.querySelector("svg"); + if (!svg) throw new Error("SVG element not found in loaded content"); + + applyViewBox(svg, stageRef.current); + groups = collectBasinGroups(svg); + labels = labelsLayerRef.current ? createLabels(labelsLayerRef.current, groups) : []; + + clearActive = () => { + groups.forEach((group) => group.classList.remove("active")); + hidePopup(popupRef.current); + labels.forEach((label) => label.classList.remove("active")); + }; + + const revealGroup = (group: SVGGElement) => { + showFor(group); + const index = groups.indexOf(group); + labels.forEach((label) => label.classList.remove("active")); + if (index >= 0) labels[index]?.classList.add("active"); + }; + + groups.forEach((group) => { + addListener(group, "mouseenter", () => { + clearActive?.(); + group.classList.add("active"); + revealGroup(group); + }); + addListener(group, "mousemove", () => { + if (group.classList.contains("active")) revealGroup(group); + }); + addListener(group, "mouseleave", () => clearActive?.()); + addListener(group, "click", (event) => { + event.stopPropagation(); + clearActive?.(); + group.classList.add("active"); + revealGroup(group); + }); + }); + + handleStageClick = () => clearActive?.(); + addListener(stageRef.current, "mouseleave", () => clearActive?.()); + addListener(stageRef.current, "click", () => handleStageClick?.()); + + positionLabelsHandler = () => { + if (!stageRef.current || !svg) return; + positionLabels(stageRef.current, svg, groups, labels); + }; + positionLabelsHandler(); + window.addEventListener("resize", positionLabelsHandler); + resizeObserver = new ResizeObserver(positionLabelsHandler); + resizeObserver.observe(stageRef.current); + }) + .catch((error: unknown) => { + if (abortController.signal.aborted) return; + const message = error instanceof Error ? error.message : "알 수 없는 오류"; + if (svgLayerRef.current) { + svgLayerRef.current.innerHTML = `
지도를 불러올 수 없습니다. (${message})
`; + } + }); + + return () => { + abortController.abort(); + listeners.forEach(({ target, type, listener }) => { + target.removeEventListener(type, listener); + }); + if (positionLabelsHandler) window.removeEventListener("resize", positionLabelsHandler); + resizeObserver?.disconnect(); + if (svgLayerRef.current) svgLayerRef.current.innerHTML = ""; + if (labelsLayerRef.current) labelsLayerRef.current.innerHTML = ""; + }; + }, [showFor]); + + const popupGridStyle = { + minWidth: 160, + display: "grid", + gridTemplateColumns: `repeat(${Math.max(1, popupContent.items.length)}, 1fr)`, + } satisfies CSSProperties; + + return { + stageRef, + svgLayerRef, + labelsLayerRef, + popupRef, + popupContent, + popupGridStyle, + }; +} diff --git a/apps/as/src/pages/dashboard/model/map-data.ts b/apps/as/src/pages/dashboard/model/map-data.ts new file mode 100644 index 000000000..18f703161 --- /dev/null +++ b/apps/as/src/pages/dashboard/model/map-data.ts @@ -0,0 +1,29 @@ +export const MANUAL_LABEL_POS = { + han: { x: 0.52, y: 0.14 }, + geum: { x: 0.28, y: 0.47 }, + nakdong: { x: 0.64, y: 0.47 }, + yeongsan: { x: 0.18, y: 0.77 }, +} as const; + +export const BASIN_DATA = { + han: { name: "한강" }, + geum: { name: "금강" }, + nakdong: { name: "낙동강" }, + yeongsan: { name: "영산강" }, +} as const; + +export const BASIN_IDS = [ + { key: "han", labels: ["한강", "han"] }, + { key: "geum", labels: ["금강", "geum"] }, + { key: "nakdong", labels: ["낙동강", "nakdong"] }, + { key: "yeongsan", labels: ["영산강", "yeongsan"] }, +] as const; + +export type BasinKey = keyof typeof BASIN_DATA; + +export interface ManualLabelPos { + x?: number; + y?: number; + px?: number; + py?: number; +} diff --git a/apps/as/src/pages/dashboard/ui/dashboard-page.tsx b/apps/as/src/pages/dashboard/ui/dashboard-page.tsx index 5e4226ffd..0ce8e7bca 100644 --- a/apps/as/src/pages/dashboard/ui/dashboard-page.tsx +++ b/apps/as/src/pages/dashboard/ui/dashboard-page.tsx @@ -11,9 +11,11 @@ export function DashboardPage() { const { data, isPending, isError, dataUpdatedAt } = useGetMainDashboard(); return ( -
- -
+
+
+ +
+

전국 행정지원 지도

- +
+
+
+
+
+
+
+ {popupContent.title} +
+ {popupContent.items.map((item, index) => ( +
+ {item.header} +
+ ))} + {popupContent.items.map((item, index) => ( +
+ {item.data} +
+ ))} +
+
+
); diff --git a/apps/as/src/pages/design-cnstn/ui/sections/business-scale-section.tsx b/apps/as/src/pages/design-cnstn/ui/sections/business-scale-section.tsx index c46fc31be..24ab52bfd 100644 --- a/apps/as/src/pages/design-cnstn/ui/sections/business-scale-section.tsx +++ b/apps/as/src/pages/design-cnstn/ui/sections/business-scale-section.tsx @@ -33,10 +33,6 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { ) : ( bizSclList.map((node) => { const pl = indentPx(node.indCnstpSeCdNm); - const bscPlanQty = Number(node.bscPlanQty) || 0; - const budgetReflct = Number(node.bgtRfltQty) || 0; - const tstmDesignQty = Number(node.tstmDesignQty) || 0; - const diff = tstmDesignQty - budgetReflct; return ( @@ -44,17 +40,17 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { {node.cnstpSeCdNm} -
{fmtNumber(bscPlanQty)}
+
{fmtNumber(node.bscPlanQty)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bgtRfltQty)}
-
{fmtNumber(tstmDesignQty)}
+
{fmtNumber(node.tstmDesignQty)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgQty > 0 ? `+${fmtNumber(node.chgQty)}` : fmtNumber(node.chgQty)}
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx index d880811b6..d892187f6 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-cost-section.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { SectionCollapsible } from "@/shared/ui"; import type { BizCstTreeDto } from "@/entities/fnrs-cnsltn"; -import { calcDiffPercent, fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; +import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; import { sortBizCstListByTreePath } from "../../lib/business-cost-utils"; @@ -46,10 +46,6 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { ) : ( sortedBizCstList.map((node) => { const pl = indentPx(node.indAcntgSeCdNm); - const bscPlanAmt = Number(node.bscPlanAmt) || 0; - const budgetReflct = Number(node.bgtRfltAmt) || 0; - const tstmDesignAmt = Number(node.tstmDesignAmt) || 0; - const diff = tstmDesignAmt - budgetReflct; const showSubIcon = node.treeLevel >= 3; return ( @@ -63,21 +59,21 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { )} -
{fmtNumber(bscPlanAmt)}
+
{fmtNumber(node.bgtRfltAmt)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bscPlanAmt)}
-
{fmtNumber(tstmDesignAmt)}
+
{fmtNumber(node.tstmDesignAmt)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgAmt > 0 ? `+${fmtNumber(node.chgAmt)}` : fmtNumber(node.chgAmt)}
-
{calcDiffPercent(diff, budgetReflct)}
+
{fmtNumber(node.chgRt)}%
); diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx index c1f5a28af..46cd6418d 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/business-scale-section.tsx @@ -33,10 +33,6 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { ) : ( bizSclList.map((node) => { const pl = indentPx(node.indCnstpSeCdNm); - const bscPlanQty = Number(node.bscPlanQty) || 0; - const budgetReflct = Number(node.bgtRfltQty) || 0; - const tstmDesignQty = Number(node.tstmDesignQty) || 0; - const diff = tstmDesignQty - budgetReflct; return ( @@ -44,17 +40,17 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { {node.cnstpSeCdNm} -
{fmtNumber(bscPlanQty)}
+
{fmtNumber(node.bscPlanQty)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bgtRfltQty)}
-
{fmtNumber(tstmDesignQty)}
+
{fmtNumber(node.tstmDesignQty)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgQty > 0 ? `+${fmtNumber(node.chgQty)}` : fmtNumber(node.chgQty)}
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx index ad83c53e4..3d4e3aa93 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/fnrs-shar-section.tsx @@ -61,8 +61,14 @@ function FnrsSharSection({ fnrsSharList }: FnrsSharSectionProps) {
{fmtNumber(node.chgRqtAmt)}
- - + +
+ {node.chgAmt > 0 ? `+${fmtNumber(node.chgAmt)}` : fmtNumber(node.chgAmt)} +
+
+ +
{fmtNumber(node.chgRt)}%
+
{node.rmrkCn ?? ""} ); diff --git a/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx index 77b7cdd2e..7de55ee64 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/business-cost-section.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { SectionCollapsible } from "@/shared/ui"; import type { BizCstTreeDto } from "@/entities/instl-sncn"; -import { calcDiffPercent, fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; +import { fmtNumber, indentPx } from "@/widgets/pbadms-sprt"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@common/components"; import { sortBizCstListByTreePath } from "../../lib/business-cost-utils"; @@ -46,10 +46,6 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { ) : ( sortedBizCstList.map((node) => { const pl = indentPx(node.indAcntgSeCdNm); - const bscPlanAmt = Number(node.bscPlanAmt) || 0; - const budgetReflct = Number(node.bgtRfltAmt) || 0; - const tstmDesignAmt = Number(node.tstmDesignAmt) || 0; - const diff = tstmDesignAmt - budgetReflct; const showSubIcon = node.treeLevel >= 3; return ( @@ -63,21 +59,21 @@ function BusinessCostSection({ bizCstList }: BusinessCostSectionProps) { )} -
{fmtNumber(bscPlanAmt)}
+
{fmtNumber(node.bgtRfltAmt)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bscPlanAmt)}
-
{fmtNumber(tstmDesignAmt)}
+
{fmtNumber(node.tstmDesignAmt)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgAmt > 0 ? `+${fmtNumber(node.chgAmt)}` : fmtNumber(node.chgAmt)}
-
{calcDiffPercent(diff, budgetReflct)}
+
{fmtNumber(node.chgRt)}%
); diff --git a/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx b/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx index 82f7785f8..ca78cfec9 100644 --- a/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx +++ b/apps/as/src/pages/instl-sncn/ui/sections/business-scale-section.tsx @@ -33,10 +33,6 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { ) : ( bizSclList.map((node) => { const pl = indentPx(node.indCnstpSeCdNm); - const bscPlanQty = Number(node.bscPlanQty) || 0; - const budgetReflct = Number(node.bgtRfltQty) || 0; - const tstmDesignQty = Number(node.tstmDesignQty) || 0; - const diff = tstmDesignQty - budgetReflct; return ( @@ -44,17 +40,17 @@ function BusinessScaleSection({ bizSclList }: BusinessScaleSectionProps) { {node.cnstpSeCdNm} -
{fmtNumber(bscPlanQty)}
+
{fmtNumber(node.bscPlanQty)}
-
{fmtNumber(budgetReflct)}
+
{fmtNumber(node.bgtRfltQty)}
-
{fmtNumber(tstmDesignQty)}
+
{fmtNumber(node.tstmDesignQty)}
- {diff > 0 ? `+${fmtNumber(diff)}` : fmtNumber(diff)} + {node.chgQty > 0 ? `+${fmtNumber(node.chgQty)}` : fmtNumber(node.chgQty)}
diff --git a/apps/as/src/shared/model/pbadms-sprt.ts b/apps/as/src/shared/model/pbadms-sprt.ts index 272e6d110..10d620b84 100644 --- a/apps/as/src/shared/model/pbadms-sprt.ts +++ b/apps/as/src/shared/model/pbadms-sprt.ts @@ -28,6 +28,10 @@ export interface BizSclTreeDto { bgtRfltQty: number; /** 금회설계수량 */ tstmDesignQty: number; + /** 증감수량(금회설계수량 - 예산반영수량, C-B) */ + chgQty: number; + /** 증감율 % ((증감수량 / 기본계획수량) * 100) */ + chgRt: number; } /** 회계구분 트리와 사업비용 조합 DTO */ @@ -44,12 +48,16 @@ export interface BizCstTreeDto { treeLevel: number; /** 트리 경로 표시용 문자열 */ treePath: string; - /** 기본계획금액 */ - bscPlanAmt: number; - /** 예산반영금액 */ + /** 예산반영금액(A) */ bgtRfltAmt: number; - /** 금회설계금액 */ + /** 표준사업비(B) */ + bscPlanAmt: number; + /** 금회설계금액(C) */ tstmDesignAmt: number; + /** 증감금액(금회설계금액 - 표준사업비, C-B) */ + chgAmt: number; + /** 증감율 % ((증감금액 / 표준사업비) * 100) */ + chgRt: number; } /** 점검표 검토결과 트리 DTO */ diff --git a/apps/as/src/widgets/pbadms-sprt/lib/format-utils.ts b/apps/as/src/widgets/pbadms-sprt/lib/format-utils.ts index b67ad9860..8fa24d3cb 100644 --- a/apps/as/src/widgets/pbadms-sprt/lib/format-utils.ts +++ b/apps/as/src/widgets/pbadms-sprt/lib/format-utils.ts @@ -1,6 +1,14 @@ -export function fmtNumber(value: number | null | undefined): string { - if (typeof value === "number") return value.toLocaleString(); - if (value === null || value === undefined) return ""; +export function fmtNumber(value: number | string | null | undefined): string { + if (typeof value === "number") { + return value.toLocaleString(); + } + if (value === null || value === undefined) { + return ""; + } + const num = Number(value); + if (!isNaN(num)) { + return num.toLocaleString(); + } return String(value); } diff --git a/apps/as/src/widgets/pbadms-sprt/ui/progress-status-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/progress-status-section.tsx index 4ce4d8a0a..151494379 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/progress-status-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/progress-status-section.tsx @@ -57,7 +57,7 @@ function ProgressStatusSection({ return ( - +
{columns.map((column) => ( diff --git a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx index 279f31b42..36846d327 100644 --- a/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/ExcretaTreatmentFacilityInp.tsx @@ -16,6 +16,7 @@ import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGa import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { statisticsInputExcretaTreatmentFacilityApi } from "@/entities/statistics/api/input/statistics-input-excreta-treatment-facility-service"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -193,13 +194,6 @@ function mapExcretaTreatmentFacilityListToGridRows( }); } -function nullableTrimmedString( - value: string | number | null | undefined, -): string | null { - const s = String(value ?? "").trim(); - return s === "" ? null : s; -} - function toGridNumber(value: string | number | null | undefined): number { const n = Number(value); return Number.isFinite(n) ? n : 0; @@ -212,9 +206,9 @@ function toApiYmd(value: string | number | Date | null | undefined): string | nu function resolveExcretaTreatmentFacilityAdmdst( row: Record, sggOptions: Sgg[], -): { admdstCd: string; admdstNm: string } { - const existingCd = String(row.admdstCd ?? "").trim(); - const existingNm = String(row.admdstNm ?? "").trim(); +): { admdstCd: string | null; admdstNm: string | null } { + const existingCd = nullableTrimmedString(row.admdstCd); + const existingNm = nullableTrimmedString(row.admdstNm); if (existingCd && existingNm) { return { admdstCd: existingCd, admdstNm: existingNm }; } @@ -223,7 +217,10 @@ function resolveExcretaTreatmentFacilityAdmdst( if (matched) { return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } - return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + return { + admdstCd: existingCd, + admdstNm: existingNm ?? nullableTrimmedString(sggNm), + }; } /** 그리드 행 → ST00070001 저장/삭제 API 항목 */ @@ -238,18 +235,18 @@ function buildExcretaTreatmentFacilitySaveItem( return { admdstCd, admdstNm, - fcltSn: String(row.fcltSn ?? "").trim(), - fcltNm: String(row.fcltNm ?? ""), - fcltPstnCn: String(row.fcltPstnCn ?? ""), - telNo: String(row.telNo ?? ""), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), + telNo: nullableTrimmedString(row.telNo), cmptnStrtYmd: toApiYmd(row.cmptnStrtYmd), oprtngStrtYmd: toApiYmd(row.oprtngStrtYmd), wholPjtcoAmt: toGridNumber(row.wholPjtcoAmt), prcsFcltCpct: toGridNumber(row.prcsFcltCpct), cpct: toGridNumber(row.cpct), oprtngDayCnt: toGridNumber(row.oprtngDayCnt), - prcsMthdExpln: String(row.prcsMthdExpln ?? ""), - linkTmplntCn: String(row.linkTmplntCn ?? ""), + prcsMthdExpln: nullableTrimmedString(row.prcsMthdExpln), + linkTmplntCn: nullableTrimmedString(row.linkTmplntCn), linkPrcsQnt: toGridNumber(row.linkPrcsQnt), inflwBodWqVl: toGridNumber(row.inflwBodWqVl), inflwTocWqVl: toGridNumber(row.inflwTocWqVl), @@ -263,11 +260,11 @@ function buildExcretaTreatmentFacilitySaveItem( dschgTnWqVl: toGridNumber(row.dschgTnWqVl), dschgTpWqVl: toGridNumber(row.dschgTpWqVl), dschgClfgCnt: toGridNumber(row.dschgClfgCnt), - wtsysNm: String(row.wtsysNm ?? ""), - trbtrNm: String(row.trbtrNm ?? ""), - msirbCn: String(row.msirbCn ?? ""), - ssirbCn: String(row.ssirbCn ?? ""), - operMthdCdCn: String(row.operMthdCdCn ?? ""), + wtsysNm: nullableTrimmedString(row.wtsysNm), + trbtrNm: nullableTrimmedString(row.trbtrNm), + msirbCn: nullableTrimmedString(row.msirbCn), + ssirbCn: nullableTrimmedString(row.ssirbCn), + operMthdCdCn: nullableTrimmedString(row.operMthdCdCn), wholNope: toGridNumber(row.wholNope), sftyAcdntEvntcsCnt: toGridNumber(row.sftyAcdntEvntcsCnt), rmrk: nullableTrimmedString(row.rmrk), diff --git a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx index dccf6f6fd..768a965fc 100644 --- a/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PrivateSewageFacilityInp.tsx @@ -62,6 +62,7 @@ import { patchVerificationFailedByTab, } from "@/components/util/input/verification-failed-flag"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { useAdminStore } from "@/store/admin-store"; import { useTabStore } from "@/store/tab-store"; @@ -169,17 +170,6 @@ function mapRegionalListToGridRows< }); } -function normalizeGridString(value: unknown): string { - if (value === null || value === undefined) return ""; - return typeof value === "string" ? value : String(value); -} - -function normalizeGridRmrk(value: unknown): string | null { - if (value === null || value === undefined) return null; - const s = typeof value === "string" ? value : String(value); - return s.trim() === "" ? null : s; -} - function buildColumnFormatNumber( columns: { fieldName?: string; dataFormat?: string }[], ) { @@ -208,8 +198,8 @@ function buildSewageTreatFactStatusSaveList( const formatNumber = buildColumnFormatNumber(SewageTreatFactStatusInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), step1StaaPrcsQnt: formatNumber(d.step1StaaPrcsQnt, "step1StaaPrcsQnt"), step2StaaPrcsQnt: formatNumber(d.step2StaaPrcsQnt, "step2StaaPrcsQnt"), step3StaaPrcsQnt: formatNumber(d.step3StaaPrcsQnt, "step3StaaPrcsQnt"), @@ -224,7 +214,7 @@ function buildSewageTreatFactStatusSaveList( step5StaaExcpPrcsQnt: formatNumber(d.step5StaaExcpPrcsQnt, "step5StaaExcpPrcsQnt"), step6StaaExcpPrcsQnt: formatNumber(d.step6StaaExcpPrcsQnt, "step6StaaExcpPrcsQnt"), step7StaaExcpPrcsQnt: formatNumber(d.step7StaaExcpPrcsQnt, "step7StaaExcpPrcsQnt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); } @@ -234,8 +224,8 @@ function buildSepticTankStatusSaveList( const formatNumber = buildColumnFormatNumber(SepticTankStatusInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), staaInsdSttkNprsflCnt: formatNumber(d.staaInsdSttkNprsflCnt, "staaInsdSttkNprsflCnt"), step1StaaInsdSttkFrcCnt: formatNumber(d.step1StaaInsdSttkFrcCnt, "step1StaaInsdSttkFrcCnt"), staaInsdSprktpFltrtbCnt: formatNumber(d.staaInsdSprktpFltrtbCnt, "staaInsdSprktpFltrtbCnt"), @@ -264,7 +254,7 @@ function buildSepticTankStatusSaveList( step6StaaExcpNope: formatNumber(d.step6StaaExcpNope, "step6StaaExcpNope"), step7StaaExcpNope: formatNumber(d.step7StaaExcpNope, "step7StaaExcpNope"), step8StaaExcpNope: formatNumber(d.step8StaaExcpNope, "step8StaaExcpNope"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); } @@ -274,8 +264,8 @@ function buildPrivateSewageDesigStatusSaveList( const formatNumber = buildColumnFormatNumber(PrivateSewageDesigStatusInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), indvSwagePbancRgnCnt: formatNumber(d.indvSwagePbancRgnCnt, "indvSwagePbancRgnCnt"), indvSwageDsgnScpLen: formatNumber(d.indvSwageDsgnScpLen, "indvSwageDsgnScpLen"), spctnkGrssFcltCnt: formatNumber(d.spctnkGrssFcltCnt, "spctnkGrssFcltCnt"), @@ -285,7 +275,7 @@ function buildPrivateSewageDesigStatusSaveList( "drtwPrcsFcltGrssFcltCnt", ), drtwPrcsFcltQnt: formatNumber(d.drtwPrcsFcltQnt, "drtwPrcsFcltQnt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); } diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index 133a9cb28..fa5892179 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -52,6 +52,7 @@ import { toGridRows } from '@/components/util/grid/gridFunction'; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; import { patchFooterVerificationByTab,patchVerificationFailedByTab } from '@/components/util/input/verification-failed-flag'; import type { VerificationResultLikeMap } from '@/components/util/input/verification-result-display'; +import { nullableTrimmedString } from '@/components/util/input/nullableString'; import { useAdminStore } from '@/store/admin-store'; import { useTabStore } from '@/store/tab-store'; @@ -277,19 +278,13 @@ const PublicSewageFacilityInp = () => { return Number.isFinite(num) ? num : 0; }; - const toNullableString = (v: unknown): string | null => { - if (v === null || v === undefined) return null; - const s = typeof v === 'string' ? v : String(v); - return s.trim() === '' ? null : s; - }; - return currentData.map((d) => ({ - admdstCd: String(d.admdstCd ?? ''), - admdstNm: String(d.admdstNm ?? ''), - swtrmtFcltCd: String(d.swtrmtFcltCd ?? ''), - swtrmtFcltNm: String(d.swtrmtFcltNm ?? ''), - fcltSn: String(d.fcltSn ?? ''), - fcltPstnCn: d.fcltPstnCn === '' ? null : toNullableString(d.fcltPstnCn), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), + swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm), + fcltSn: nullableTrimmedString(d.fcltSn), + fcltPstnCn: nullableTrimmedString(d.fcltPstnCn), inflwSwagePhysQnt: toNumber(d.inflwSwagePhysQnt), inflwSwageBoilQnt: toNumber(d.inflwSwageBoilQnt), inflwSwageAltdQnt: toNumber(d.inflwSwageAltdQnt), @@ -298,7 +293,7 @@ const PublicSewageFacilityInp = () => { prcsAltdCpct: toNumber(d.prcsAltdCpct), prxyCtrtCn: normalizePrxyCtrtCn(d.prxyCtrtCn), sftyAcdntEvntcsCnt: toNumber(d.sftyAcdntEvntcsCnt), - rmrk: toNullableString(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); }; @@ -528,12 +523,12 @@ const PublicSewageFacilityInp = () => { const saveList: PublicSewageMainFacilityMappingSaveItem[] = rightRows .filter((r) => isCheckedRow(r)) .map((r) => ({ - fcltSn: String(r.fcltSn ?? ''), - admdstCd: String(r.admdstCd ?? ''), - admdstNm: String(r.admdstNm ?? ''), - swtrmtFcltCd: String(r.swtrmtFcltCd ?? ''), - swtrmtFcltNm: String(r.swtrmtFcltNm ?? ''), - isMainYn: String(r.isMainYn ?? ''), + fcltSn: nullableTrimmedString(r.fcltSn), + admdstCd: nullableTrimmedString(r.admdstCd), + admdstNm: nullableTrimmedString(r.admdstNm), + swtrmtFcltCd: nullableTrimmedString(r.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(r.swtrmtFcltNm), + isMainYn: nullableTrimmedString(r.isMainYn), })); const deleteList = rightRows diff --git a/apps/st/src/components/statistics/input/PumpingStationInp.tsx b/apps/st/src/components/statistics/input/PumpingStationInp.tsx index 2d3370191..9f79b0e67 100644 --- a/apps/st/src/components/statistics/input/PumpingStationInp.tsx +++ b/apps/st/src/components/statistics/input/PumpingStationInp.tsx @@ -16,6 +16,7 @@ import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGa import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { statisticsInputPumpingStationApi } from "@/entities/statistics/api/input/statistics-input-pumping-station-service"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -192,13 +193,6 @@ function mapPumpingStationListToGridRows( }); } -function nullableTrimmedString( - value: string | number | null | undefined, -): string | null { - const s = String(value ?? "").trim(); - return s === "" ? null : s; -} - function toGridNumber(value: string | number | null | undefined): number { const n = Number(value); return Number.isFinite(n) ? n : 0; @@ -207,9 +201,9 @@ function toGridNumber(value: string | number | null | undefined): number { function resolvePumpingStationAdmdst( row: Record, sggOptions: Sgg[], -): { admdstCd: string; admdstNm: string } { - const existingCd = String(row.admdstCd ?? "").trim(); - const existingNm = String(row.admdstNm ?? "").trim(); +): { admdstCd: string | null; admdstNm: string | null } { + const existingCd = nullableTrimmedString(row.admdstCd); + const existingNm = nullableTrimmedString(row.admdstNm); if (existingCd && existingNm) { return { admdstCd: existingCd, admdstNm: existingNm }; } @@ -218,7 +212,10 @@ function resolvePumpingStationAdmdst( if (matched) { return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } - return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + return { + admdstCd: existingCd, + admdstNm: existingNm ?? nullableTrimmedString(sggNm), + }; } /** 그리드 행 → ST00050001 저장/삭제 API 항목 */ @@ -227,15 +224,14 @@ function buildPumpingStationSaveItem( sggOptions: Sgg[] = [], ): PumpingStationSaveItem { const { admdstCd, admdstNm } = resolvePumpingStationAdmdst(row, sggOptions); - const fcltSn = String(row.fcltSn ?? "").trim(); return { admdstCd, admdstNm, - fcltSn: fcltSn, - swtrmtFcltNm: String(row.swtrmtFcltNm ?? ""), - fcltNm: String(row.fcltNm ?? ""), - pumpSeNm: String(row.pumpSeNm ?? ""), - fcltPstnCn: String(row.fcltPstnCn ?? ""), + fcltSn: nullableTrimmedString(row.fcltSn), + swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm), + fcltNm: nullableTrimmedString(row.fcltNm), + pumpSeNm: nullableTrimmedString(row.pumpSeNm), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), planMltvArea: toGridNumber(row.planMltvArea), sndyArea: toGridNumber(row.sndyArea), diff --git a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx index c9ddc2e0c..2e1be284d 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaRelatedInp.tsx @@ -9,6 +9,7 @@ import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGa import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewageExcretaRelatedApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-related-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -319,7 +320,7 @@ function buildSggNmColumn( function resolvePublicInstitutionAdmdst( row: Record, sggOptions: Sgg[], -): { admdstCd: string; admdstNm: string } { +): { admdstCd: string | null; admdstNm: string | null } { const sggNm = String(row.sggNm ?? "").trim(); if (sggNm) { const matched = sggOptions.find((s) => s.gugun === sggNm); @@ -327,9 +328,12 @@ function resolvePublicInstitutionAdmdst( return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } } - const existingCd = String(row.admdstCd ?? "").trim(); - const existingNm = String(row.admdstNm ?? "").trim(); - return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + const existingCd = nullableTrimmedString(row.admdstCd); + const existingNm = nullableTrimmedString(row.admdstNm); + return { + admdstCd: existingCd, + admdstNm: existingNm ?? nullableTrimmedString(sggNm), + }; } function mergeGridRowsWithMapRows( @@ -369,13 +373,6 @@ function mergeGridRowsWithArticleMeta( }); } -function nullableTrimmedString( - value: string | number | null | undefined, -): string | null { - const s = String(value ?? "").trim(); - return s === "" ? null : s; -} - function toGridNumber(value: string | number | null | undefined): number { const n = Number(value); return Number.isFinite(n) ? n : 0; @@ -390,8 +387,8 @@ function buildPublicInstitutionIndustrySaveItem( return { admdstCd, admdstNm, - ognzSn: String(row.ognzSn ?? "").trim(), - ognzNm: String(row.ognzNm ?? "").trim(), + ognzSn: nullableTrimmedString(row.ognzSn), + ognzNm: nullableTrimmedString(row.ognzNm), pbadmsNope: toGridNumber(row.pbadmsNope), engrkCnt: toGridNumber(row.engrkCnt), mchnCnt: toGridNumber(row.mchnCnt), @@ -409,8 +406,8 @@ function buildExcretaRelatedIndustrySaveItem( row: Record, ): ExcretaRelatedIndustrySaveItem { return { - admdstCd: String(row.admdstCd ?? "").trim(), - admdstNm: String(row.admdstNm ?? "").trim(), + admdstCd: nullableTrimmedString(row.admdstCd), + admdstNm: nullableTrimmedString(row.admdstNm), bzentyCnt: toGridNumber(row.bzentyCnt), empNope: toGridNumber(row.empNope), n3TonUdrCnt: toGridNumber(row.n3TonUdrCnt), @@ -427,8 +424,8 @@ function buildPrivateSewageTreatRelatedIndustrySaveItem( row: Record, ): PrivateSewageTreatRelatedIndustrySaveItem { return { - admdstCd: String(row.admdstCd ?? "").trim(), - admdstNm: String(row.admdstNm ?? "").trim(), + admdstCd: nullableTrimmedString(row.admdstCd), + admdstNm: nullableTrimmedString(row.admdstNm), designMdsrCnt: toGridNumber(row.designMdsrCnt), designMdsrNope: toGridNumber(row.designMdsrNope), mfbizCnt: toGridNumber(row.mfbizCnt), @@ -1715,8 +1712,8 @@ const SewageExcretaRelatedInp = () => { newRow.__isNewArticle = 1; if (sggNm) { const { admdstCd, admdstNm } = resolvePublicInstitutionAdmdst(newRow, list); - newRow.admdstCd = admdstCd; - newRow.admdstNm = admdstNm; + newRow.admdstCd = admdstCd ?? ""; + newRow.admdstNm = admdstNm ?? ""; } flushSync(() => { diff --git a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx index d22d81c26..84f53ecc5 100644 --- a/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx +++ b/apps/st/src/components/statistics/input/SewageExcretaSlurgeInp.tsx @@ -17,6 +17,7 @@ import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGa import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewageExcretaSlurgeApi } from "@/entities/statistics/api/input/statistics-input-sewage-excreta-slurge-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -441,13 +442,6 @@ function mergeGridRowsWithMapRows( }); } -function nullableTrimmedString( - value: string | number | null | undefined, -): string | null { - const s = String(value ?? "").trim(); - return s === "" ? null : s; -} - function toGridNumber(value: string | number | null | undefined): number { const n = Number(value); return Number.isFinite(n) ? n : 0; @@ -456,7 +450,7 @@ function toGridNumber(value: string | number | null | undefined): number { function resolveSlurgeFacStatusAdmdst( row: Record, sggOptions: Sgg[], -): { admdstCd: string; admdstNm: string } { +): { admdstCd: string | null; admdstNm: string | null } { const sggNm = String(row.sggNm ?? "").trim(); if (sggNm) { const matched = sggOptions.find((s) => s.gugun === sggNm); @@ -464,12 +458,15 @@ function resolveSlurgeFacStatusAdmdst( return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } } - const existingCd = String(row.admdstCd ?? "").trim(); - const existingNm = String(row.admdstNm ?? "").trim(); + const existingCd = nullableTrimmedString(row.admdstCd); + const existingNm = nullableTrimmedString(row.admdstNm); if (existingCd || existingNm) { - return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + return { + admdstCd: existingCd, + admdstNm: existingNm ?? nullableTrimmedString(sggNm), + }; } - return { admdstCd: "", admdstNm: sggNm }; + return { admdstCd: null, admdstNm: nullableTrimmedString(sggNm) }; } /** 그리드 행 → ST00110001 저장/삭제 API 항목 */ @@ -481,12 +478,12 @@ function buildSlurgeProcessingFacStatusSaveItem( return { admdstCd, admdstNm, - swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), - swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), - fcltSn: String(row.fcltSn ?? "").trim(), + swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm), + fcltSn: nullableTrimmedString(row.fcltSn), itslfQnt: toGridNumber(row.itslfQnt), otsdInflwQnt: toGridNumber(row.otsdInflwQnt), - transfTmplntNm: String(row.transfTmplntNm ?? "").trim(), + transfTmplntNm: nullableTrimmedString(row.transfTmplntNm), transfPrcsQnt: toGridNumber(row.transfPrcsQnt), srpFncRt: toGridNumber(row.srpFncRt), itslfTs1RclmQnt: toGridNumber(row.itslfTs1RclmQnt), @@ -495,31 +492,31 @@ function buildSlurgeProcessingFacStatusSaveItem( itslfTs1DryQnt: toGridNumber(row.itslfTs1DryQnt), itslfTs1FuelQnt: toGridNumber(row.itslfTs1FuelQnt), itslfTs2CmpstgQnt: toGridNumber(row.itslfTs2CmpstgQnt), - cmpsPrcsFcltBzentyNm: String(row.cmpsPrcsFcltBzentyNm ?? "").trim(), + cmpsPrcsFcltBzentyNm: nullableTrimmedString(row.cmpsPrcsFcltBzentyNm), incnrPrcsLfvlm: toGridNumber(row.incnrPrcsLfvlm), incnrPrcsPrdctRmtrlQnt: toGridNumber(row.incnrPrcsPrdctRmtrlQnt), incnrPrcsFmlyrQnt: toGridNumber(row.incnrPrcsFmlyrQnt), - incnrPrcsFcltNm: String(row.incnrPrcsFcltNm ?? "").trim(), + incnrPrcsFcltNm: nullableTrimmedString(row.incnrPrcsFcltNm), stbznPrcsLfvlm: toGridNumber(row.stbznPrcsLfvlm), stbznPrcsFmlyrQnt: toGridNumber(row.stbznPrcsFmlyrQnt), - stbznPrcsFcltNm: String(row.stbznPrcsFcltNm ?? "").trim(), + stbznPrcsFcltNm: nullableTrimmedString(row.stbznPrcsFcltNm), dryPrcsLfvlm: toGridNumber(row.dryPrcsLfvlm), dryPrcsIncnrQnt: toGridNumber(row.dryPrcsIncnrQnt), dryPrcsFuelQnt: toGridNumber(row.dryPrcsFuelQnt), dryPrcsPrdctRmtrlQnt: toGridNumber(row.dryPrcsPrdctRmtrlQnt), dryPrcsFmlyrQnt: toGridNumber(row.dryPrcsFmlyrQnt), dryPrcsCmpstgQnt: toGridNumber(row.dryPrcsCmpstgQnt), - dryPrcsFcltNm: String(row.dryPrcsFcltNm ?? "").trim(), + dryPrcsFcltNm: nullableTrimmedString(row.dryPrcsFcltNm), fuelPrcsIncnrQnt: toGridNumber(row.fuelPrcsIncnrQnt), fuelPrcsFuelQnt: toGridNumber(row.fuelPrcsFuelQnt), - fuelPrcsFcltNm: String(row.fuelPrcsFcltNm ?? "").trim(), + fuelPrcsFcltNm: nullableTrimmedString(row.fuelPrcsFcltNm), otsdCnsgnRclmQnt: toGridNumber(row.otsdCnsgnRclmQnt), otsdCnsgnIncnrQnt: toGridNumber(row.otsdCnsgnIncnrQnt), otsdCnsgnFuelPwplQnt: toGridNumber(row.otsdCnsgnFuelPwplQnt), otsdCnsgnPrdctRmtrlQnt: toGridNumber(row.otsdCnsgnPrdctRmtrlQnt), otsdCnsgnFmlyrQnt: toGridNumber(row.otsdCnsgnFmlyrQnt), otsdCnsgnCmpstgQnt: toGridNumber(row.otsdCnsgnCmpstgQnt), - otsdCnsgnFcltNm: String(row.otsdCnsgnFcltNm ?? "").trim(), + otsdCnsgnFcltNm: nullableTrimmedString(row.otsdCnsgnFcltNm), cparTs1Whol: toGridNumber(row.cparTs1Whol), rmrk: nullableTrimmedString(row.rmrk), }; @@ -534,10 +531,10 @@ function buildDigestorOperatingStatusSaveItem( const base = { admdstCd, admdstNm, - swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), - swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), - fcltSn: String(row.fcltSn ?? "").trim(), - digstnEnYn: String(row.digstnEnYn ?? "").trim(), + swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm), + fcltSn: nullableTrimmedString(row.fcltSn), + digstnEnYn: nullableTrimmedString(row.digstnEnYn), }; if (isDigestorOperatingRowDisabled(row)) { @@ -585,9 +582,9 @@ function buildSewageSlurgeProcessingFacStatusSaveItem( return { admdstCd, admdstNm, - fcltSn: String(row.fcltSn ?? "").trim(), - fcltNm: String(row.fcltNm ?? "").trim(), - prcsMthExpln: String(row.prcsMthExpln ?? "").trim(), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + prcsMthExpln: nullableTrimmedString(row.prcsMthExpln), fcltCpct: toGridNumber(row.fcltCpct), anlPrcsQnt: toGridNumber(row.anlPrcsQnt), dayAvgOprtngHr: toGridNumber(row.dayAvgOprtngHr), @@ -605,9 +602,9 @@ function buildExcretaSlurgeProcessingStatusSaveItem( return { admdstCd, admdstNm, - fcltSn: String(row.fcltSn ?? "").trim(), - fcltNm: String(row.fcltNm ?? "").trim(), - fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), rcycQnt: toGridNumber(row.rcycQnt), olandLfvlm: toGridNumber(row.olandLfvlm), incnrQnt: toGridNumber(row.incnrQnt), @@ -615,7 +612,7 @@ function buildExcretaSlurgeProcessingStatusSaveItem( fuelQnt: toGridNumber(row.fuelQnt), etcQnt: toGridNumber(row.etcQnt), nondspsQnt: toGridNumber(row.nondspsQnt), - swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm), rmrk: nullableTrimmedString(row.rmrk), }; } diff --git a/apps/st/src/components/statistics/input/SewagePipeInp.tsx b/apps/st/src/components/statistics/input/SewagePipeInp.tsx index f5e5725a7..9a09d507d 100644 --- a/apps/st/src/components/statistics/input/SewagePipeInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeInp.tsx @@ -75,6 +75,7 @@ import { patchFooterVerificationByTab, patchVerificationFailedByTab, } from "@/components/util/input/verification-failed-flag"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display"; import { useAdminStore } from "@/store/admin-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; @@ -364,10 +365,10 @@ function buildRegionalFactSubmitListFromGrid( return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); }; return currentData.map((d) => ({ - admdstCd: String(d.admdstCd ?? ""), - sggNm: String(d.sggNm ?? ""), - admdstNm: String(d.admdstNm ?? ""), - goalYr: String(d.goalYr ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), + goalYr: nullableTrimmedString(d.goalYr), wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), @@ -403,7 +404,7 @@ function buildRegionalFactSubmitListFromGrid( exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), - rmrk: (d.rmrk as string | null) ?? null, + rmrk: nullableTrimmedString(d.rmrk), })); } @@ -1257,12 +1258,12 @@ const SewagePipeInp = () => { return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); }; const submitList = currentData.map((d) => ({ - admdstCd: String(d.admdstCd ?? ""), - sggNm: String(d.sggNm ?? ""), - admdstNm: String(d.admdstNm ?? ""), - fcltSn: String(d.fcltSn ?? ""), - swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""), - swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), + fcltSn: nullableTrimmedString(d.fcltSn), + swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm), prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"), prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"), cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"), @@ -1284,7 +1285,7 @@ const SewagePipeInp = () => { cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"), exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"), drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"), - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), })); const requestData: SeweragePipeInputFactInstallStatusSubmitRequest = { ...submitBase, @@ -1328,11 +1329,11 @@ const SewagePipeInp = () => { }); } return { - admdstCd: String(d.admdstCd ?? ""), - admdstNm: String(d.admdstNm ?? ""), - pipeSeCd: String(d.pipeSeCd ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), + pipeSeCd: nullableTrimmedString(d.pipeSeCd), yearlyStatList, - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"), dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"), pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"), @@ -1366,7 +1367,7 @@ const SewagePipeInp = () => { return decimalPlaces > 0 ? parseFloat(num.toFixed(decimalPlaces)) : Math.round(num); }; const submitList = currentData.map((d) => ({ - pipeSeCd: String(d.pipeSeCd ?? ""), + pipeSeCd: nullableTrimmedString(d.pipeSeCd), step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"), step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"), step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"), @@ -1374,10 +1375,10 @@ const SewagePipeInp = () => { step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"), sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"), etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"), - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"), - admdstCd: String(d.admdstCd ?? ""), - admdstNm: String(d.admdstNm ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), })); const requestData: SeweragePipeInputPipeDiameterStatusSubmitRequest = { ...submitBase, @@ -1429,11 +1430,11 @@ const SewagePipeInp = () => { }); } return { - emphsMngSeCd: String(d.emphsMngSeCd ?? ""), - admdstCd: String(d.admdstCd ?? ""), - admdstNm: String(d.admdstNm ?? ""), + emphsMngSeCd: nullableTrimmedString(d.emphsMngSeCd), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), yearlyStats, - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), }; }); const requestData: SeweragePipeInputManholeStatusSubmitRequest = { @@ -1653,10 +1654,10 @@ const SewagePipeInp = () => { const userInfo = await getUserInfo(); const saveList = currentData.map((d) => ({ - admdstCd: String(d.admdstCd ?? ""), - sggNm: String(d.sggNm ?? ""), - admdstNm: String(d.admdstNm ?? ""), - goalYr: String(d.goalYr ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), + goalYr: nullableTrimmedString(d.goalYr), wholPplMtncPlanPrlg: formatNumber(d.wholPplMtncPlanPrlg, "wholPplMtncPlanPrlg"), wholPlanPrlg: formatNumber(d.wholPlanPrlg, "wholPlanPrlg"), wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), @@ -1692,7 +1693,7 @@ const SewagePipeInp = () => { exlnOtflCnt: formatNumber(d.exlnOtflCnt, "exlnOtflCnt"), sewerLdgrEdpPrlg: formatNumber(d.sewerLdgrEdpPrlg, "sewerLdgrEdpPrlg"), sewerLdgrEdpRt: formatNumber(d.sewerLdgrEdpRt, "sewerLdgrEdpRt"), - rmrk: (d.rmrk as string | null) ?? null, + rmrk: nullableTrimmedString(d.rmrk), })); const param: SeweragePipeInputRegionalFactStatusSaveRequest = { @@ -1745,12 +1746,12 @@ const SewagePipeInp = () => { const userInfo = await getUserInfo(); const saveList = currentData.map((d) => ({ - admdstCd: String(d.admdstCd ?? ""), - sggNm: String(d.sggNm ?? ""), - admdstNm: String(d.admdstNm ?? ""), - fcltSn: String(d.fcltSn ?? ""), - swtrmtFcltCd: String(d.swtrmtFcltCd ?? ""), - swtrmtFcltNm: String(d.swtrmtFcltNm ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), + fcltSn: nullableTrimmedString(d.fcltSn), + swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm), prcsFcltCpct: formatNumber(d.prcsFcltCpct, "prcsFcltCpct"), prcsZoneArea: formatNumber(d.prcsZoneArea, "prcsZoneArea"), cbsysPrcsZoneArea: formatNumber(d.cbsysPrcsZoneArea, "cbsysPrcsZoneArea"), @@ -1772,7 +1773,7 @@ const SewagePipeInp = () => { cbsysBrchlnPrlg: formatNumber(d.cbsysBrchlnPrlg, "cbsysBrchlnPrlg"), exlnBrchlnPrlg: formatNumber(d.exlnBrchlnPrlg, "exlnBrchlnPrlg"), drtwBrchlnPrlg: formatNumber(d.drtwBrchlnPrlg, "drtwBrchlnPrlg"), - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), })); const param: SeweragePipeInputFactInstallStatusSaveRequest = { @@ -1847,11 +1848,11 @@ const SewagePipeInp = () => { }); } return { - admdstCd: String(d.admdstCd ?? ""), - admdstNm: String(d.admdstNm ?? ""), - pipeSeCd: String(d.pipeSeCd ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), + pipeSeCd: nullableTrimmedString(d.pipeSeCd), yearlyStatList, - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), lyngPrlgWhol: formatNumber(d.lyngPrlg, "lyngPrlg"), dmltnPrlgWhol: formatNumber(d.dmltnPrlg, "dmltnPrlg"), pipeByPrstWhol: formatNumber(d.pipeByPrstWhol, "pipeByPrstWhol"), @@ -1907,7 +1908,7 @@ const SewagePipeInp = () => { }; const saveList = currentData.map((d) => ({ - pipeSeCd: String(d.pipeSeCd ?? ""), + pipeSeCd: nullableTrimmedString(d.pipeSeCd), step1PdmtPrlg: formatNumber(d.step1PdmtPrlg, "step1PdmtPrlg"), step2PdmtPrlg: formatNumber(d.step2PdmtPrlg, "step2PdmtPrlg"), step3PdmtPrlg: formatNumber(d.step3PdmtPrlg, "step3PdmtPrlg"), @@ -1915,10 +1916,10 @@ const SewagePipeInp = () => { step5PdmtPrlg: formatNumber(d.step5PdmtPrlg, "step5PdmtPrlg"), sqrePdmtPrlg: formatNumber(d.sqrePdmtPrlg, "sqrePdmtPrlg"), etcPdmtPrlg: formatNumber(d.etcPdmtPrlg, "etcPdmtPrlg"), - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), pdmtByTsmWhol: formatNumber(d.pdmtByTsmWhol, "pdmtByTsmWhol"), - admdstCd: String(d.admdstCd ?? ""), - admdstNm: String(d.admdstNm ?? ""), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), })); const userInfo = await getUserInfo(); @@ -1993,11 +1994,11 @@ const SewagePipeInp = () => { }); } return { - emphsMngSeCd: String(d.emphsMngSeCd ?? ""), - admdstCd: String(d.admdstCd ?? ""), - admdstNm: String(d.admdstNm ?? ""), + emphsMngSeCd: nullableTrimmedString(d.emphsMngSeCd), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), yearlyStats, - rmrk: String(d.rmrk ?? ""), + rmrk: nullableTrimmedString(d.rmrk), }; }); diff --git a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx index 31b96d06c..f51f46846 100644 --- a/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx +++ b/apps/st/src/components/statistics/input/SewagePipeMaintainInp.tsx @@ -63,6 +63,7 @@ import { patchVerificationFailedByTab, } from "@/components/util/input/verification-failed-flag"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { useAdminStore } from "@/store/admin-store"; import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store"; import { useTabStore } from "@/store/tab-store"; @@ -153,17 +154,6 @@ function tabRowDataExists( return Array.isArray(rows) && rows.length > 0; } -function normalizeGridString(value: unknown): string { - if (value === null || value === undefined) return ""; - return typeof value === "string" ? value : String(value); -} - -function normalizeGridRmrk(value: unknown): string | null { - if (value === null || value === undefined) return null; - const s = typeof value === "string" ? value : String(value); - return s.trim() === "" ? null : s; -} - const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); async function waitForImrJobCompleted( @@ -217,9 +207,9 @@ function buildRepairRowList( const formatNumber = buildColumnFormatNumber(RepairStatusInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - sggNm: normalizeGridString(d.sggNm), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), mendngWhol: formatNumber(d.mendngWhol, "mendngWhol"), cbsysMendngPrlg: formatNumber(d.cbsysMendngPrlg, "cbsysMendngPrlg"), sprsysExlnPrlg: formatNumber(d.sprsysExlnPrlg, "sprsysExlnPrlg"), @@ -235,7 +225,7 @@ function buildRepairRowList( stofcbrCnt: formatNumber(d.stofcbrCnt, "stofcbrCnt"), otflCnt: formatNumber(d.otflCnt, "otflCnt"), cctvExmnPrlg: formatNumber(d.cctvExmnPrlg, "cctvExmnPrlg"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); } @@ -245,9 +235,9 @@ function buildDredgingRowList( const formatNumber = buildColumnFormatNumber(PipeDredgingStatusInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - sggNm: normalizeGridString(d.sggNm), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), wholFcltPrlg: formatNumber(d.wholFcltPrlg, "wholFcltPrlg"), wholSwagePpl: formatNumber(d.wholSwagePpl, "wholSwagePpl"), cbsysPplPrlg: formatNumber(d.cbsysPplPrlg, "cbsysPplPrlg"), @@ -267,7 +257,7 @@ function buildDredgingRowList( oceanEmsnQnt: formatNumber(d.oceanEmsnQnt, "oceanEmsnQnt"), rcycQnt: formatNumber(d.rcycQnt, "rcycQnt"), etcQnt: formatNumber(d.etcQnt, "etcQnt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); } diff --git a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx index 74d956d76..bb45347c8 100644 --- a/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx +++ b/apps/st/src/components/statistics/input/SewageReservoirPumpInp.tsx @@ -17,6 +17,7 @@ import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGa import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputSewageReservoirPumpApi } from "@/entities/statistics/api/input/statistics-input-sewage-reservoir-pump-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -264,13 +265,6 @@ function mapSewageReservoirListToGridRows( }); } -function nullableTrimmedString( - value: string | number | null | undefined, -): string | null { - const s = String(value ?? "").trim(); - return s === "" ? null : s; -} - function toGridNumber(value: string | number | null | undefined): number { const n = Number(value); return Number.isFinite(n) ? n : 0; @@ -290,13 +284,13 @@ function toPumpDmtrExpln(value: string | number | null | undefined): number { function resolveSewageReservoirAdmdst( sggNm: string, sggOptions: Sgg[], -): { admdstCd: string; admdstNm: string } { +): { admdstCd: string | null; admdstNm: string | null } { const trimmed = sggNm.trim(); const matched = sggOptions.find((s) => s.gugun === trimmed); if (matched) { return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } - return { admdstCd: "", admdstNm: trimmed }; + return { admdstCd: null, admdstNm: nullableTrimmedString(trimmed) }; } /** 그리드 행 → ST00040001 저장/삭제 API 항목 */ @@ -308,19 +302,18 @@ function buildSewageReservoirSaveItem( String(row.sggNm ?? ""), sggOptions, ); - const fcltSn = String(row.fcltSn ?? "").trim(); return { admdstCd, admdstNm, // 항목 추가 직후 저장(신규) — 그리드 fcltSn 없음 → API에는 "1" - fcltSn: fcltSn, - fcltNm: String(row.fcltNm ?? ""), - fcltPstnCn: String(row.fcltPstnCn ?? ""), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), - fldngPrvtnYn: normalizeOxYnValue(row.fldngPrvtnYn), - wqPlltnMtgtYn: normalizeOxYnValue(row.wqPlltnMtgtYn), - rtlztnUsgYn: normalizeOxYnValue(row.rtlztnUsgYn), + fldngPrvtnYn: nullableTrimmedString(normalizeOxYnValue(row.fldngPrvtnYn)), + wqPlltnMtgtYn: nullableTrimmedString(normalizeOxYnValue(row.wqPlltnMtgtYn)), + rtlztnUsgYn: nullableTrimmedString(normalizeOxYnValue(row.rtlztnUsgYn)), fcltStrctCn: nullableTrimmedString(row.fcltStrctCn), wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), impdFcltCpct: toGridNumber(row.impdFcltCpct), @@ -337,17 +330,17 @@ function buildSewageReservoirSaveItem( pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), - prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), - ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), - oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === '무' ? 'N' : 'Y') : ""), + prcsgFcltYn: row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null, + ddrztnEqpmYn: row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null, + oslfEtgtYn: row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null, mltvMthCn: nullableTrimmedString(row.mltvMthCn), impdMthCn: nullableTrimmedString(row.impdMthCn), impdPrcsMthdCn: nullableTrimmedString(row.impdPrcsMthdCn), - dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), + dschgWtrsExpln: nullableTrimmedString(row.dschgWtrsExpln), prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), - prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), - ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), + prcptnLastDspsMthdExpln: nullableTrimmedString(row.prcptnLastDspsMthdExpln), + ctmnLastDspsMthdExpln: nullableTrimmedString(row.ctmnLastDspsMthdExpln), oprtngDayCnt: toGridNumber(row.oprtngDayCnt), grssMltvQnt: toGridNumber(row.grssMltvQnt), rmrk: nullableTrimmedString(row.rmrk), @@ -363,18 +356,17 @@ function buildRainfallPumpStationSaveItem( String(row.sggNm ?? ""), sggOptions, ); - const fcltSn = String(row.fcltSn ?? "").trim(); return { admdstCd, admdstNm, - fcltSn: fcltSn, - fcltNm: String(row.fcltNm ?? ""), - fcltPstnCn: String(row.fcltPstnCn ?? ""), - fcltyKndCn: String(row.fcltyKndCn ?? ""), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), + fcltyKndCn: nullableTrimmedString(row.fcltyKndCn), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), planDesignFreqCnt: toGridNumber(row.planDesignFreqCnt), wtrStrgZoneArea: toGridNumber(row.wtrStrgZoneArea), - rsvrYn: String(row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : ""), + rsvrYn: row.rsvrYn ? (row.rsvrYn === "무" ? "N" : "Y") : null, rsvrImpndQnt: toGridNumber(row.rsvrImpndQnt), abstnRgnImpdQnt: toGridNumber(row.abstnRgnImpdQnt), grssPpqty: toGridNumber(row.grssPpqty), @@ -390,14 +382,14 @@ function buildRainfallPumpStationSaveItem( pumpFcltN3PerPpqty: toGridNumber(row.pumpFcltN3PerPpqty), pumpFcltN3PumpCnt: toGridNumber(row.pumpFcltN3PumpCnt), pumpFcltN3RsrvPumpCnt: toGridNumber(row.pumpFcltN3RsrvPumpCnt), - prcsgFcltYn: String(row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), - ddrztnEqpmYn: String(row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), - oslfEtgtYn: String(row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : ""), - dschgWtrsExpln: String(row.dschgWtrsExpln ?? ""), + prcsgFcltYn: row.prcsgFcltYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null, + ddrztnEqpmYn: row.ddrztnEqpmYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null, + oslfEtgtYn: row.oslfEtgtYn ? (row.prcsgFcltYn === "무" ? "N" : "Y") : null, + dschgWtrsExpln: nullableTrimmedString(row.dschgWtrsExpln), prcptnLastDspsQnt: toGridNumber(row.prcptnLastDspsQnt), ctmnLastDspsQnt: toGridNumber(row.ctmnLastDspsQnt), - prcptnLastDspsMthdExpln: String(row.prcptnLastDspsMthdExpln ?? ""), - ctmnLastDspsMthdExpln: String(row.ctmnLastDspsMthdExpln ?? ""), + prcptnLastDspsMthdExpln: nullableTrimmedString(row.prcptnLastDspsMthdExpln), + ctmnLastDspsMthdExpln: nullableTrimmedString(row.ctmnLastDspsMthdExpln), oprtngDayCnt: toGridNumber(row.oprtngDayCnt), grssMltvQnt: toGridNumber(row.grssMltvQnt), rmrk: nullableTrimmedString(row.rmrk), diff --git a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx index e3c4d7ab7..805eaab56 100644 --- a/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageFinanceInp.tsx @@ -67,6 +67,7 @@ import { patchVerificationFailedByTab, } from "@/components/util/input/verification-failed-flag"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { useAdminStore } from "@/store/admin-store"; import { useTabStore } from "@/store/tab-store"; @@ -168,17 +169,6 @@ function mapRegionalListToGridRows< }); } -function normalizeGridString(value: unknown): string { - if (value === null || value === undefined) return ""; - return typeof value === "string" ? value : String(value); -} - -function normalizeGridRmrk(value: unknown): string | null { - if (value === null || value === undefined) return null; - const s = typeof value === "string" ? value : String(value); - return s.trim() === "" ? null : s; -} - function buildColumnFormatNumber(columns: { fieldName?: string; dataFormat?: string }[]) { const columnFormatMap = new Map(); columns.forEach((col) => { @@ -214,9 +204,9 @@ function buildSewerageFinanceSettleSaveList( const formatNumber = buildColumnFormatNumber(SewerageFinanceSettleInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - sggNm: normalizeGridString(d.sggNm), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), cnterGovSprtAmt: formatNumber(d.cnterGovSprtAmt, "cnterGovSprtAmt"), mctpvLcexpAmt: formatNumber(d.mctpvLcexpAmt, "mctpvLcexpAmt"), wareaSggLclAmt: formatNumber(d.wareaSggLclAmt, "wareaSggLclAmt"), @@ -240,7 +230,7 @@ function buildSewerageFinanceSettleSaveList( etcAcdntCrfwAmt: formatNumber(d.etcAcdntCrfwAmt, "etcAcdntCrfwAmt"), etcNrcpAmt: formatNumber(d.etcNrcpAmt, "etcNrcpAmt"), etcAmt: formatNumber(d.etcAmt, "etcAmt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), fnnByWholPjtcoAmt: formatNumber(d.fnnByWholPjtcoAmt, "fnnByWholPjtcoAmt"), usgByWholPjtcoAmt: formatNumber(d.usgByWholPjtcoAmt, "usgByWholPjtcoAmt"), tmplntWholAmt: formatNumber(d.tmplntWholAmt, "tmplntWholAmt"), @@ -256,13 +246,13 @@ function buildSewerageChargeSaveList( const formatNumber = buildColumnFormatNumber(SewerageChargeInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - sggNm: normalizeGridString(d.sggNm), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), anlLevyQnt: formatNumber(d.anlLevyQnt, "anlLevyQnt"), levyAmt: formatNumber(d.levyAmt, "levyAmt"), grssPrmcoAmt: formatNumber(d.grssPrmcoAmt, "grssPrmcoAmt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), avgUntprcAmt: formatNumber(d.avgUntprcAmt, "avgUntprcAmt"), grssUnitPrmcoAmt: formatNumber(d.grssUnitPrmcoAmt, "grssUnitPrmcoAmt"), rlztnRt: formatNumber(d.rlztnRt, "rlztnRt"), @@ -275,9 +265,9 @@ function buildSewerageUsageFeeSaveList( const formatNumber = buildColumnFormatNumber(SewerageUsageFeeInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - sggNm: normalizeGridString(d.sggNm), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), hshusLevyAmt: formatNumber(d.hshusLevyAmt, "hshusLevyAmt"), hshusLevyQnt: formatNumber(d.hshusLevyQnt, "hshusLevyQnt"), hshusUntprcAmt: formatNumber(d.hshusUntprcAmt, "hshusUntprcAmt"), @@ -299,7 +289,7 @@ function buildSewerageUsageFeeSaveList( etcLevyAmt: formatNumber(d.etcLevyAmt, "etcLevyAmt"), etcLevyQnt: formatNumber(d.etcLevyQnt, "etcLevyQnt"), etcLevyUntprcAmt: formatNumber(d.etcLevyUntprcAmt, "etcLevyUntprcAmt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), levyAmt: formatNumber(d.levyAmt, "levyAmt"), levyQnt: formatNumber(d.levyQnt, "levyQnt"), })); @@ -311,16 +301,16 @@ function buildCauserBurdenSaveList( const formatNumber = buildColumnFormatNumber(CauserBurdenInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - sggNm: normalizeGridString(d.sggNm), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + sggNm: nullableTrimmedString(d.sggNm), + admdstNm: nullableTrimmedString(d.admdstNm), levyEvntcsCnt: formatNumber(d.levyEvntcsCnt, "levyEvntcsCnt"), levyAmt: formatNumber(d.levyAmt, "levyAmt"), mltvEqpmEvntcsCnt: formatNumber(d.mltvEqpmEvntcsCnt, "mltvEqpmEvntcsCnt"), mltvEqpmAmt: formatNumber(d.mltvEqpmAmt, "mltvEqpmAmt"), othrCstrnOthrActrCnt: formatNumber(d.othrCstrnOthrActrCnt, "othrCstrnOthrActrCnt"), othrCstrnOthrActrAmt: formatNumber(d.othrCstrnOthrActrAmt, "othrCstrnOthrActrAmt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), trgtByWholClctnAmt: formatNumber(d.trgtByWholClctnAmt, "trgtByWholClctnAmt"), })); } diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index b4b2351f5..aba30eee3 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -50,6 +50,7 @@ import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input import { getFooterSummary } from "@/components/util/grid/footerSummary"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { toExcelExportSearchContextFromSearchForm } from "@/components/util/gridExcelExport"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { isVerificationFailedFromCommonInfo } from "@/components/util/input/verification-failed-flag"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display"; import { useAdminStore } from "@/store/admin-store"; @@ -130,14 +131,6 @@ function getSewerageSupplyRateParams( }; } -function normalizeGridString(value: unknown): string | null { - if (typeof value !== "string") { - return null; - } - - return value.trim() === "" ? null : value; -} - // 하수도 통계 관리 > 01. 하수도 보급률 const SeweragePenetrationRateInp = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -488,12 +481,12 @@ const SeweragePenetrationRateInp = () => { }; const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ - sggNm: normalizeGridString(d.sggNm), - admdstCd: normalizeGridString(d.admdstCd), - admdstNm: normalizeGridString(d.admdstNm), - trbtrNm: normalizeGridString(d.trbtrNm), + sggNm: nullableTrimmedString(d.sggNm), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), + trbtrNm: nullableTrimmedString(d.trbtrNm), spcCtpnRgnCn: - d.spcCtpnRgnCn === '해당없음' ? null : normalizeGridString(d.spcCtpnRgnCn), + d.spcCtpnRgnCn === '해당없음' ? null : nullableTrimmedString(d.spcCtpnRgnCn), ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), ts3SwtrmtPpltnCnt: formatNumber(d.ts3SwtrmtPpltnCnt, 'ts3SwtrmtPpltnCnt'), @@ -506,7 +499,7 @@ const SeweragePenetrationRateInp = () => { drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), - rmrk: normalizeGridString(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), wholArea: formatNumber(d.wholArea, 'wholArea'), wholSwtrmtPpltnCnt: formatNumber(d.wholSwtrmtPpltnCnt, 'wholSwtrmtPpltnCnt'), @@ -671,14 +664,14 @@ const SeweragePenetrationRateInp = () => { const convertedData: SewerageSupplyRateSaveItem[] = currentData.map((d) => ({ // 행정구역 - sggNm: normalizeGridString(d.sggNm), // 시군구 - admdstCd: normalizeGridString(d.admdstCd), // 행정코드 - admdstNm: normalizeGridString(d.admdstNm), // 행정구역명 - trbtrNm: normalizeGridString(d.trbtrNm), // 지류 + sggNm: nullableTrimmedString(d.sggNm), // 시군구 + admdstCd: nullableTrimmedString(d.admdstCd), // 행정코드 + admdstNm: nullableTrimmedString(d.admdstNm), // 행정구역명 + trbtrNm: nullableTrimmedString(d.trbtrNm), // 지류 spcCtpnRgnCn: d.spcCtpnRgnCn === '해당없음' ? null - : normalizeGridString(d.spcCtpnRgnCn), // 특별대책지역 + : nullableTrimmedString(d.spcCtpnRgnCn), // 특별대책지역 // 공공하수처리인구(명) - 물리적/생물학적/고도처리 ts1SwtrmtPpltnCnt: formatNumber(d.ts1SwtrmtPpltnCnt, 'ts1SwtrmtPpltnCnt'), // 물리적처리 ts2SwtrmtPpltnCnt: formatNumber(d.ts2SwtrmtPpltnCnt, 'ts2SwtrmtPpltnCnt'), // 생물학적처리 @@ -694,7 +687,7 @@ const SeweragePenetrationRateInp = () => { drtwPrcsPpltnCnt: formatNumber(d.drtwPrcsPpltnCnt, 'drtwPrcsPpltnCnt'), // 오수처리인구 spctnkPpltnCnt: formatNumber(d.spctnkPpltnCnt, 'spctnkPpltnCnt'), // 정화조인구 staaExcpArea: formatNumber(d.staaExcpArea, 'staaExcpArea'), // 하수처리구역 외 면적(㎢) - rmrk: normalizeGridString(d.rmrk), // 비고 + rmrk: nullableTrimmedString(d.rmrk), // 비고 // 합계·소계(계산필드 저장용) wholPpltnCnt: formatNumber(d.wholPpltnCnt, 'wholPpltnCnt'), // 총인구(명) wholArea: formatNumber(d.wholArea, 'wholArea'), // 총면적(㎢) diff --git a/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx b/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx index 2e7903860..5581c3a21 100644 --- a/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx +++ b/apps/st/src/components/statistics/input/SewerageServiceLevelInp.tsx @@ -56,6 +56,7 @@ import { patchVerificationFailedByTab, } from "@/components/util/input/verification-failed-flag"; import type { VerificationResultLikeMap } from "@/components/util/input/verification-result-display"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { useAdminStore } from "@/store/admin-store"; import { useTabStore } from "@/store/tab-store"; @@ -172,17 +173,6 @@ function mapServiceLevelListToGridRows< }); } -function normalizeGridString(value: unknown): string { - if (value === null || value === undefined) return ""; - return typeof value === "string" ? value : String(value); -} - -function normalizeGridRmrk(value: unknown): string | null { - if (value === null || value === undefined) return null; - const s = typeof value === "string" ? value : String(value); - return s.trim() === "" ? null : s; -} - function buildColumnFormatNumber( columns: { fieldName?: string; dataFormat?: string }[], ) { @@ -211,8 +201,8 @@ function buildComplaintSaveList( const formatNumber = buildColumnFormatNumber(ComplaintInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - admdstNm: normalizeGridString(d.admdstNm), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), swtrmtPpltnCnt: formatNumber(d.swtrmtPpltnCnt, "swtrmtPpltnCnt"), dschgWqCnt: formatNumber(d.dschgWqCnt, "dschgWqCnt"), odorCnt: formatNumber(d.odorCnt, "odorCnt"), @@ -221,7 +211,7 @@ function buildComplaintSaveList( swagePplCnt: formatNumber(d.swagePplCnt, "swagePplCnt"), sewerCrgAmt: formatNumber(d.sewerCrgAmt, "sewerCrgAmt"), etcCnt: formatNumber(d.etcCnt, "etcCnt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), whol: formatNumber(d.whol, "whol"), crtrPpltnCvlcptEvntcsCnt: formatNumber( d.crtrPpltnCvlcptEvntcsCnt, @@ -236,11 +226,11 @@ function buildResidentFriendlyFactSaveList( const formatNumber = buildColumnFormatNumber(ResidentFriendlyFactInpColumn.columns); return currentData.map((d) => ({ - admdstCd: normalizeGridString(d.admdstCd), - admdstNm: normalizeGridString(d.admdstNm), - swtrmtFcltCd: normalizeGridString(d.swtrmtFcltCd), - swtrmtFcltNm: normalizeGridString(d.swtrmtFcltNm), - fcltSn: normalizeGridString(d.fcltSn), + admdstCd: nullableTrimmedString(d.admdstCd), + admdstNm: nullableTrimmedString(d.admdstNm), + swtrmtFcltCd: nullableTrimmedString(d.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(d.swtrmtFcltNm), + fcltSn: nullableTrimmedString(d.fcltSn), tmplntArea: formatNumber(d.tmplntArea, "tmplntArea"), brkFcltArea: formatNumber(d.brkFcltArea, "brkFcltArea"), childFrshpSpacArea: formatNumber(d.childFrshpSpacArea, "childFrshpSpacArea"), @@ -250,7 +240,7 @@ function buildResidentFriendlyFactSaveList( eduMultiFcltArea: formatNumber(d.eduMultiFcltArea, "eduMultiFcltArea"), wtlndMakeArea: formatNumber(d.wtlndMakeArea, "wtlndMakeArea"), etcCnt: formatNumber(d.etcCnt, "etcCnt"), - rmrk: normalizeGridRmrk(d.rmrk), + rmrk: nullableTrimmedString(d.rmrk), })); } diff --git a/apps/st/src/components/statistics/input/WaterReuseInp.tsx b/apps/st/src/components/statistics/input/WaterReuseInp.tsx index ad6e91999..7bc053439 100644 --- a/apps/st/src/components/statistics/input/WaterReuseInp.tsx +++ b/apps/st/src/components/statistics/input/WaterReuseInp.tsx @@ -17,6 +17,7 @@ import { useAlertStore, useGlobalProgressStore, useStatisticsInputToolbarGroupGa import { STATISTICS_INPUT_URL_PATH } from "@/shared/lib/stats-cd-input-navigation"; import { useStatisticsInputPendingNavigation } from "@/shared/lib/use-statistics-input-pending-navigation"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; +import { nullableTrimmedString } from "@/components/util/input/nullableString"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; import { statisticsInputWaterReuseApi } from "@/entities/statistics/api/input/statistics-input-water-reuse-service"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; @@ -235,13 +236,6 @@ function buildSggNmColumn( }; } -function nullableTrimmedString( - value: string | number | null | undefined, -): string | null { - const s = String(value ?? "").trim(); - return s === "" ? null : s; -} - function toGridNumber(value: string | number | null | undefined): number { const n = Number(value); return Number.isFinite(n) ? n : 0; @@ -250,9 +244,9 @@ function toGridNumber(value: string | number | null | undefined): number { function resolveArticleAdmdst( row: Record, sggOptions: Sgg[], -): { admdstCd: string; admdstNm: string } { - const existingCd = String(row.admdstCd ?? "").trim(); - const existingNm = String(row.admdstNm ?? "").trim(); +): { admdstCd: string | null; admdstNm: string | null } { + const existingCd = nullableTrimmedString(row.admdstCd); + const existingNm = nullableTrimmedString(row.admdstNm); if (existingCd && existingNm) { return { admdstCd: existingCd, admdstNm: existingNm }; } @@ -261,7 +255,10 @@ function resolveArticleAdmdst( if (matched) { return { admdstCd: matched.bsnmCd, admdstNm: matched.gugun }; } - return { admdstCd: existingCd, admdstNm: existingNm || sggNm }; + return { + admdstCd: existingCd, + admdstNm: existingNm ?? nullableTrimmedString(sggNm), + }; } function getArticleRowSnField(tabKey: string): "fcltSn" | "bplcSn" | null { @@ -297,11 +294,11 @@ function buildRecycledWaterStatusSaveItem( return { admdstCd, admdstNm, - fcltSn: String(row.fcltSn ?? "").trim(), - fcltNm: String(row.fcltNm ?? "").trim(), - bldgSeCn: String(row.bldgSeCn ?? "").trim(), - fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), - lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + bldgSeCn: nullableTrimmedString(row.bldgSeCn), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), + lglTrgtYn: nullableTrimmedString(row.lglTrgtYn), cmptnStrtYmd: toCompactYmdOrNull(row.cmptnStrtYmd), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), archWholArea: toGridNumber(row.archWholArea), @@ -309,7 +306,7 @@ function buildRecycledWaterStatusSaveItem( wtrUseQnt: toGridNumber(row.wtrUseQnt), ruswtrUtztnQnt: toGridNumber(row.ruswtrUtztnQnt), sbstCntUtztnQnt: toGridNumber(row.sbstCntUtztnQnt), - mnUsgCn: String(row.mnUsgCn ?? "").trim(), + mnUsgCn: nullableTrimmedString(row.mnUsgCn), ruswtrOperAmt: toGridNumber(row.ruswtrOperAmt), rmrk: nullableTrimmedString(row.rmrk), }; @@ -323,18 +320,18 @@ function buildRainFallUtilFactStatusSaveItem( return { admdstCd, admdstNm, - fcltSn: String(row.fcltSn ?? "").trim(), - fcltNm: String(row.fcltNm ?? "").trim(), - bdstTypCn: String(row.bdstTypCn ?? "").trim(), - fcltPstnCn: String(row.fcltPstnCn ?? "").trim(), - lglTrgtYn: String(row.lglTrgtYn ?? "").trim(), + fcltSn: nullableTrimmedString(row.fcltSn), + fcltNm: nullableTrimmedString(row.fcltNm), + bdstTypCn: nullableTrimmedString(row.bdstTypCn), + fcltPstnCn: nullableTrimmedString(row.fcltPstnCn), + lglTrgtYn: nullableTrimmedString(row.lglTrgtYn), cmptYmd: toCompactYmdOrNull(row.cmptYmd), instlAmt: toGridNumber(row.instlAmt), - rcptMynCn: String(row.rcptMynCn ?? "").trim(), - filtrPrcsFcltYnExpln: String(row.filtrPrcsFcltYnExpln ?? "").trim(), + rcptMynCn: nullableTrimmedString(row.rcptMynCn), + filtrPrcsFcltYnExpln: nullableTrimmedString(row.filtrPrcsFcltYnExpln), impdCpct: toGridNumber(row.impdCpct), anlUseQnt: toGridNumber(row.anlUseQnt), - exlnUtlzUsgExpln: String(row.exlnUtlzUsgExpln ?? "").trim(), + exlnUtlzUsgExpln: nullableTrimmedString(row.exlnUtlzUsgExpln), anlOperAmt: toGridNumber(row.anlOperAmt), rmrk: nullableTrimmedString(row.rmrk), }; @@ -348,10 +345,10 @@ function buildTreatWaterReuseDesignConstStatusSaveItem( return { admdstCd, admdstNm, - bplcSn: String(row.bplcSn ?? "").trim(), - bplcNm: String(row.bplcNm ?? "").trim(), - rprsvNm: String(row.rprsvNm ?? "").trim(), - bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), + bplcSn: nullableTrimmedString(row.bplcSn), + bplcNm: nullableTrimmedString(row.bplcNm), + rprsvNm: nullableTrimmedString(row.rprsvNm), + bplcPstnCn: nullableTrimmedString(row.bplcPstnCn), coRegYmd: toCompactYmdOrNull(row.coRegYmd), rmrk: nullableTrimmedString(row.rmrk), }; @@ -365,10 +362,10 @@ function buildTreatWaterReuseBusinessApprStatusSaveItem( return { admdstCd, admdstNm, - bplcSn: String(row.bplcSn ?? "").trim(), - bplcNm: String(row.bplcNm ?? "").trim(), - rprsvNm: String(row.rprsvNm ?? "").trim(), - bplcPstnCn: String(row.bplcPstnCn ?? "").trim(), + bplcSn: nullableTrimmedString(row.bplcSn), + bplcNm: nullableTrimmedString(row.bplcNm), + rprsvNm: nullableTrimmedString(row.rprsvNm), + bplcPstnCn: nullableTrimmedString(row.bplcPstnCn), coRegYmd: toCompactYmdOrNull(row.coRegYmd), rmrk: nullableTrimmedString(row.rmrk), }; @@ -378,18 +375,18 @@ function buildPublicSewageTreatWaterReuseSaveItem( row: Record, ): PublicSewageTreatWaterReuseSaveItem { return { - admdstCd: String(row.admdstCd ?? "").trim(), - admdstNm: String(row.admdstNm ?? row.sggNm ?? "").trim(), - fcltSn: String(row.fcltSn ?? "").trim(), - swtrmtFcltCd: String(row.swtrmtFcltCd ?? "").trim(), - swtrmtFcltNm: String(row.swtrmtFcltNm ?? "").trim(), + admdstCd: nullableTrimmedString(row.admdstCd), + admdstNm: nullableTrimmedString(row.admdstNm ?? row.sggNm), + fcltSn: nullableTrimmedString(row.fcltSn), + swtrmtFcltCd: nullableTrimmedString(row.swtrmtFcltCd), + swtrmtFcltNm: nullableTrimmedString(row.swtrmtFcltNm), wholPrcsQnt: toGridNumber(row.wholPrcsQnt), - lglYn: String(row.lglYn ?? "").trim(), - instlYn: String(row.instlYn ?? "").trim(), + lglYn: nullableTrimmedString(row.lglYn), + instlYn: nullableTrimmedString(row.instlYn), oprtngStrtYmd: toCompactYmdOrNull(row.oprtngStrtYmd), instlAprvYmd: toCompactYmdOrNull(row.instlAprvYmd), rprcsFcltCpct: toGridNumber(row.rprcsFcltCpct), - mtcnsNm: String(row.mtcnsNm ?? "").trim(), + mtcnsNm: nullableTrimmedString(row.mtcnsNm), ruswtPplLen: toGridNumber(row.ruswtPplLen), ruswtPumpLen: toGridNumber(row.ruswtPumpLen), rmrk: nullableTrimmedString(row.rmrk), diff --git a/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx b/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx index f30dc87d7..4ce873a5d 100644 --- a/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx +++ b/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx @@ -14,12 +14,12 @@ import { CodeApi, CodeSelect } from "@/entities/code"; import { type ApproveRequest, type RejectRequest, + type ReportStatusResponseDto, statisticsApprovalApi, type StatisticsVerificationFilterState, statisticsVerificationReportColumnInfo, statisticsVerificationResultColumnInfo, type StatisticsVerificationSubFilterState, - type StatsInputStatusList, useSggList, useSidoList, useStatsVrfcRptData, @@ -72,8 +72,8 @@ const getYearOptions = () => { }; function isSameStatsInputStatusRow( - a: StatsInputStatusList, - b: StatsInputStatusList, + a: ReportStatusResponseDto, + b: ReportStatusResponseDto, ): boolean { return ( a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd @@ -132,11 +132,12 @@ const DEFAULT_FILTERS: StatisticsVerificationFilterState = { statsYr: String(dayjs().year() - 1), sewerBzmnCd: null, statsCd: null, - laeSbmsnSttsCd: "3", + pbcpSbmsnSttsCd: "3", }; const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = { ...DEFAULT_FILTERS, + laeSbmsnSttsCd: "3", prcsYnList: "1,2", rejectCode: "0", pbcpAprvYn: "N", @@ -153,6 +154,13 @@ const REJECT_STATUS_OPTIONS = [ { label: "공단반려", value: "2" }, ]; +const PBCP_SBMSN_STTS_OPTIONS = [ + { label: "미작성", value: "0" }, + { label: "작성중", value: "1" }, + { label: "승인", value: "3" }, + { label: "반려", value: "4" }, +]; + function StatisticsVerificationReport() { const queryClient = useQueryClient(); const userInfo = useUserStore((state) => state.userInfo); @@ -163,7 +171,7 @@ function StatisticsVerificationReport() { const [subFilters, setSubFilters] = useState(DEFAULT_SUB_FILTERS); const [selectedReportRows, setSelectedReportRows] = useState< - StatsInputStatusList[] + ReportStatusResponseDto[] >([]); const [selectedResultRows, setSelectedResultRows] = useState< VerifyResultItem[] @@ -227,9 +235,6 @@ function StatisticsVerificationReport() { const statisticsVerificationReportQuery = useStatsVrfcRptData( statisticsVerificationReportFilters, ); - const statisticsVerificationResultQuery = - useStatsVrfcRsltData(detailParams); - const statisticsVerificationReportData = statisticsVerificationReportQuery.data ?? []; const statisticsVerificationReportGridRows = useMemo( @@ -237,6 +242,8 @@ function StatisticsVerificationReport() { [statisticsVerificationReportData], ); + const statisticsVerificationResultQuery = + useStatsVrfcRsltData(detailParams); const statisticsVerificationResultData = useMemo( () => statisticsVerificationResultQuery.data?.pages.flatMap( @@ -560,7 +567,7 @@ function StatisticsVerificationReport() { function handleReportRefresh() { queryClient.invalidateQueries({ - queryKey: [ENDPOINT.ST_SERVICE.INPUT, "statistics-input-status"], + queryKey: [ENDPOINT.ST_SERVICE.INPUT, "validation-report"], }); } @@ -605,6 +612,12 @@ function StatisticsVerificationReport() { onChange={handleFilterChange("statsYr")} options={getYearOptions()} /> +
{}} showHeaderCheckbox={true} gridHeight={30} - onRowClick={(row: StatsInputStatusList) => { + onRowClick={(row: ReportStatusResponseDto) => { setSubFilters((prev) => ({ ...prev, ctpvNm: row?.ctpvNm ?? "", diff --git a/apps/st/src/components/util/input/nullableString.ts b/apps/st/src/components/util/input/nullableString.ts new file mode 100644 index 000000000..a02f9c84a --- /dev/null +++ b/apps/st/src/components/util/input/nullableString.ts @@ -0,0 +1,8 @@ +/** 그리드/폼 문자열 → API 전송용. 빈값·공백만이면 null */ +export function nullableTrimmedString( + value: string | number | null | undefined | unknown, +): string | null { + if (value === null || value === undefined) return null; + const s = typeof value === "string" ? value.trim() : String(value).trim(); + return s === "" ? null : s; +} diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts index 3d3d164aa..1293af423 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-status-service.ts @@ -2,6 +2,8 @@ import { stClient } from "@/shared/api"; import { ENDPOINT } from "@/shared/config"; import type { + ReportSearchRequestDto, + ReportStatusListApiResponse, StatisticsInputStatusService, StatsInputStatusListApiResponse, StatsInputStatusListRequestParams, @@ -17,6 +19,16 @@ export class StatisticsInputStatusApiService implements StatisticsInputStatusSer ); return response.data; } + + public async getStatsValidationReport( + request: ReportSearchRequestDto, + ): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUT}/stats-input-status/validation-report`, + { params: request }, + ); + return response.data; + } } export const StatisticsInputStatusApi = new StatisticsInputStatusApiService(); diff --git a/apps/st/src/entities/statistics/api/queries.ts b/apps/st/src/entities/statistics/api/queries.ts index 3b5fad7d8..b59af9f07 100644 --- a/apps/st/src/entities/statistics/api/queries.ts +++ b/apps/st/src/entities/statistics/api/queries.ts @@ -122,18 +122,24 @@ export const useStatsVrfcRptData = ( return useQuery({ queryKey: [ ENDPOINT.ST_SERVICE.INPUT, - "statistics-input-status", + "validation-report", submittedMainFilters?.statsYr ?? "", submittedMainFilters?.ctpvNm ?? "", submittedMainFilters?.sggNm ?? "", submittedMainFilters?.sewerBzmnCd ?? "", submittedMainFilters?.statsCd ?? "", + submittedMainFilters?.pbcpSbmsnSttsCd ?? "", ], queryFn: async () => { if (!submittedMainFilters) return []; + const { + ctpvSewerBzmnCd: _ctpvSewerBzmnCd, + sggSewerBzmnCd: _sggSewerBzmnCd, + ...request + } = submittedMainFilters; const response = - await StatisticsInputStatusApi.getStatisticsInputStatusList( - submittedMainFilters, + await StatisticsInputStatusApi.getStatsValidationReport( + request, ); if (response.status !== "success") throw new Error(response.message); diff --git a/apps/st/src/entities/statistics/index.ts b/apps/st/src/entities/statistics/index.ts index 90dfb00f6..8123b72f4 100644 --- a/apps/st/src/entities/statistics/index.ts +++ b/apps/st/src/entities/statistics/index.ts @@ -76,7 +76,11 @@ export { type ImrLogResponse, } from "./model/imr/imr-interface"; export { imrVerificationHistoryColumns } from "./model/imr/imr-verification-history-columns"; -export { type StatsInputStatusList } from "./model/input/statistics-input-status-interface"; +export { + type ReportSearchRequestDto, + type ReportStatusResponseDto, + type StatsInputStatusList, +} from "./model/input/statistics-input-status-interface"; export { userListColumns } from "./model/notice-talk/notice-talk-sending-columns"; export { type BusinessSendRequest, diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts index 82a721d35..d65b3ff32 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-excreta-treatment-facility-interface.ts @@ -97,10 +97,39 @@ export interface ExcretaTreatmentFacilityRequestParams { */ export type ExcretaTreatmentFacilitySaveItem = Omit< ExcretaTreatmentFacilityItem, - "sggNm" | "verificationResult" | "cmptnStrtYmd" | "oprtngStrtYmd" + | "sggNm" + | "verificationResult" + | "cmptnStrtYmd" + | "oprtngStrtYmd" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "fcltNm" + | "fcltPstnCn" + | "telNo" + | "prcsMthdExpln" + | "linkTmplntCn" + | "wtsysNm" + | "trbtrNm" + | "msirbCn" + | "ssirbCn" + | "operMthdCdCn" > & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + fcltPstnCn: string | null; + telNo: string | null; cmptnStrtYmd: string | null; oprtngStrtYmd: string | null; + prcsMthdExpln: string | null; + linkTmplntCn: string | null; + wtsysNm: string | null; + trbtrNm: string | null; + msirbCn: string | null; + ssirbCn: string | null; + operMthdCdCn: string | null; }; /** 분뇨처리시설(ST00070001) 저장 API 요청 */ diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts index b0285ac8d..10c712d7a 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-private-sewage-facility-interface.ts @@ -207,8 +207,8 @@ export interface PrivateSewageDesigStatusRequestParams { * `sggNm`·계산필드(`wholDrtwPrcsFcltCnt`, `wholStaaCnt`, `staaExcpWhol`)·검증결과는 제외. */ export interface SewageTreatFactStatusSaveItem { - admdstCd: string; - admdstNm: string; + admdstCd: string | null; + admdstNm: string | null; step1StaaPrcsQnt: number; step2StaaPrcsQnt: number; step3StaaPrcsQnt: number; @@ -279,8 +279,8 @@ export interface SewageTreatFactStatusSubmitResponse { * `staaExcpWhol`, `staaExcpPrcsMthByWhol`, `staaExcpPrcsCpctByWhol`)·검증결과는 제외. */ export interface SepticTankStatusSaveItem { - admdstCd: string; - admdstNm: string; + admdstCd: string | null; + admdstNm: string | null; staaInsdSttkNprsflCnt: number; step1StaaInsdSttkFrcCnt: number; staaInsdSprktpFltrtbCnt: number; @@ -364,8 +364,8 @@ export interface SepticTankStatusSubmitResponse { * `sggNm`·검증결과는 제외. */ export interface PrivateSewageDesigStatusSaveItem { - admdstCd: string; - admdstNm: string; + admdstCd: string | null; + admdstNm: string | null; indvSwagePbancRgnCnt: number; indvSwageDsgnScpLen: number; spctnkGrssFcltCnt: number; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts index e9cfe9b99..b6abe3d44 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-pumping-station-interface.ts @@ -107,7 +107,21 @@ export type PumpingStationSaveItem = Omit< | "drtwPumpGrssPpqty" | "exlnPumpGrssPpqty" | "oprtngStrtYmd" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "swtrmtFcltNm" + | "fcltNm" + | "pumpSeNm" + | "fcltPstnCn" > & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + swtrmtFcltNm: string | null; + fcltNm: string | null; + pumpSeNm: string | null; + fcltPstnCn: string | null; oprtngStrtYmd: string | null; }; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts index fbfc464b3..891b8fb06 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-related-interface.ts @@ -176,24 +176,54 @@ export type PrivateSewageTreatRelatedIndustryApiResponse = /** ST00120001·ST00120002 저장 — `saveList` / `deleteList` 항목 (`sggNm`·계산필드·검증결과 제외) */ export type PublicInstitutionSewerIndustrySaveItem = Omit< PublicInstitutionSewerIndustryItem, - "sggNm" | "verificationResult" | "empGrssCnt" | "techWholNope" ->; + | "sggNm" + | "verificationResult" + | "empGrssCnt" + | "techWholNope" + | "admdstCd" + | "admdstNm" + | "ognzSn" + | "ognzNm" +> & { + admdstCd: string | null; + admdstNm: string | null; + ognzSn: string | null; + ognzNm: string | null; +}; export type PublicInstitutionExcretaIndustrySaveItem = Omit< PublicInstitutionExcretaIndustryItem, - "sggNm" | "verificationResult" | "empGrssCnt" | "techWholNope" ->; + | "sggNm" + | "verificationResult" + | "empGrssCnt" + | "techWholNope" + | "admdstCd" + | "admdstNm" + | "ognzSn" + | "ognzNm" +> & { + admdstCd: string | null; + admdstNm: string | null; + ognzSn: string | null; + ognzNm: string | null; +}; /** ST00120003·ST00120004 저장 — `saveList` 항목 (`sggNm`·계산필드·검증결과 제외) */ export type ExcretaRelatedIndustrySaveItem = Omit< ExcretaRelatedIndustryItem, - "sggNm" | "verificationResult" | "fcltSecrPrstCnt" ->; + "sggNm" | "verificationResult" | "fcltSecrPrstCnt" | "admdstCd" | "admdstNm" +> & { + admdstCd: string | null; + admdstNm: string | null; +}; export type PrivateSewageTreatRelatedIndustrySaveItem = Omit< PrivateSewageTreatRelatedIndustryItem, - "sggNm" | "verificationResult" ->; + "sggNm" | "verificationResult" | "admdstCd" | "admdstNm" +> & { + admdstCd: string | null; + admdstNm: string | null; +}; export interface SewageExcretaRelatedSaveRequestBase { statsYr: string; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts index 1ad5e2294..c8518b065 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-excreta-slurge-interface.ts @@ -54,7 +54,7 @@ export interface SlurgeProcessingFacStatusItem { sggNm: string; admdstNm: string; fcltSn: string; - swtrmtFcltNm: string; + swtrmtFcltNm: string | null; swtrmtFcltCd: string; gnamWhol: number; itslfQnt: number; @@ -132,7 +132,30 @@ export type SlurgeProcessingFacStatusSaveItem = Omit< | "dryPrcsWhol" | "fuelPrcsWhol" | "otsdCnsgnWhol" ->; + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "swtrmtFcltCd" + | "transfTmplntNm" + | "cmpsPrcsFcltBzentyNm" + | "incnrPrcsFcltNm" + | "stbznPrcsFcltNm" + | "dryPrcsFcltNm" + | "fuelPrcsFcltNm" + | "otsdCnsgnFcltNm" +> & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + swtrmtFcltCd: string | null; + transfTmplntNm: string | null; + cmpsPrcsFcltBzentyNm: string | null; + incnrPrcsFcltNm: string | null; + stbznPrcsFcltNm: string | null; + dryPrcsFcltNm: string | null; + fuelPrcsFcltNm: string | null; + otsdCnsgnFcltNm: string | null; +}; /** ST00110001 저장 API 요청 */ export interface SlurgeProcessingFacStatusSaveRequest { @@ -193,7 +216,7 @@ export interface DigestorOperatingStatusItem { admdstNm: string; fcltSn: string; swtrmtFcltCd: string; - swtrmtFcltNm: string; + swtrmtFcltNm: string | null; digstnEnYn: string; digstnCpct: number; actlDrvnCpct: number; @@ -228,8 +251,21 @@ export type DigestorOperatingStatusApiResponse = */ export type DigestorOperatingStatusSaveItem = Omit< DigestorOperatingStatusItem, - "sggNm" | "verificationResult" | "digstnGasUtztnWholPrst" ->; + | "sggNm" + | "verificationResult" + | "digstnGasUtztnWholPrst" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "swtrmtFcltCd" + | "digstnEnYn" +> & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + swtrmtFcltCd: string | null; + digstnEnYn: string | null; +}; /** ST00110002 저장 API 요청 */ export interface DigestorOperatingStatusSaveRequest { @@ -317,8 +353,22 @@ export type SewageSlurgeProcessingFacStatusApiResponse = */ export type SewageSlurgeProcessingFacStatusSaveItem = Omit< SewageSlurgeProcessingFacStatusItem, - "sggNm" | "verificationResult" | "oprtngRt" | "fcltPstnCn" ->; + | "sggNm" + | "verificationResult" + | "oprtngRt" + | "fcltPstnCn" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "fcltNm" + | "prcsMthExpln" +> & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + prcsMthExpln: string | null; +}; /** ST00110003 저장 API 요청 */ export interface SewageSlurgeProcessingFacStatusSaveRequest { @@ -390,7 +440,7 @@ export interface ExcretaSlurgeProcessingStatusItem { fuelQnt: number; etcQnt: number; nondspsQnt: number; - swtrmtFcltNm: string; + swtrmtFcltNm: string | null; rmrk: string | null; verificationResult?: SewageExcretaSlurgeVerificationResult; } @@ -412,8 +462,22 @@ export type ExcretaSlurgeProcessingStatusApiResponse = */ export type ExcretaSlurgeProcessingStatusSaveItem = Omit< ExcretaSlurgeProcessingStatusItem, - "sggNm" | "verificationResult" | "gnam" | "wholDspsQnt" ->; + | "sggNm" + | "verificationResult" + | "gnam" + | "wholDspsQnt" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "fcltNm" + | "fcltPstnCn" +> & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + fcltPstnCn: string | null; +}; /** ST00110004 저장 API 요청 */ export interface ExcretaSlurgeProcessingStatusSaveRequest { @@ -475,7 +539,7 @@ export interface PublicSewageFacilityItem { admdstCd: string; admdstNm: string; swtrmtFcltCd: string; - swtrmtFcltNm: string; + swtrmtFcltNm: string | null; fcltSn: string; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts index df35d2f76..ca7657561 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-pipe-maintain-interface.ts @@ -97,9 +97,9 @@ export type SewagePipeMaintainDredgingResponse = ApiResponse 개보수 저장·제출 API 행 */ export interface SewagePipeMaintainRepairSaveItem { - admdstCd: string; - sggNm: string; - admdstNm: string; + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; mendngWhol: number; cbsysMendngPrlg: number; sprsysExlnPrlg: number; @@ -164,9 +164,9 @@ export interface SewagePipeMaintainRepairSubmitResponse { /** ST00030002 하수관로 유지관리 > 관로준설 저장·제출 API 행 */ export interface SewagePipeMaintainDredgingSaveItem { - admdstCd: string; - sggNm: string; - admdstNm: string; + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; wholFcltPrlg: number; wholSwagePpl: number; cbsysPplPrlg: number; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts index eb734afca..2ab79fcbd 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewage-reservoir-pump-interface.ts @@ -172,9 +172,39 @@ export interface SewageReservoirRequestParams { */ export type SewageReservoirSaveItem = Omit< SewageReservoirItem, - "sggNm" | "verificationResult" | "oprtngStrtYmd" + | "sggNm" + | "verificationResult" + | "oprtngStrtYmd" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "fcltNm" + | "fcltPstnCn" + | "fldngPrvtnYn" + | "wqPlltnMtgtYn" + | "rtlztnUsgYn" + | "prcsgFcltYn" + | "ddrztnEqpmYn" + | "oslfEtgtYn" + | "dschgWtrsExpln" + | "prcptnLastDspsMthdExpln" + | "ctmnLastDspsMthdExpln" > & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + fcltPstnCn: string | null; oprtngStrtYmd: string | null; + fldngPrvtnYn: string | null; + wqPlltnMtgtYn: string | null; + rtlztnUsgYn: string | null; + prcsgFcltYn: string | null; + ddrztnEqpmYn: string | null; + oslfEtgtYn: string | null; + dschgWtrsExpln: string | null; + prcptnLastDspsMthdExpln: string | null; + ctmnLastDspsMthdExpln: string | null; }; /** 하수저류시설(ST00040001) 저장 API 요청 */ @@ -231,9 +261,37 @@ export interface SewageReservoirSubmitResponse { */ export type RainfallPumpStationSaveItem = Omit< RainfallPumpStationItem, - "sggNm" | "verificationResult" | "oprtngStrtYmd" + | "sggNm" + | "verificationResult" + | "oprtngStrtYmd" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "fcltNm" + | "fcltPstnCn" + | "fcltyKndCn" + | "rsvrYn" + | "prcsgFcltYn" + | "ddrztnEqpmYn" + | "oslfEtgtYn" + | "dschgWtrsExpln" + | "prcptnLastDspsMthdExpln" + | "ctmnLastDspsMthdExpln" > & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + fcltPstnCn: string | null; + fcltyKndCn: string | null; oprtngStrtYmd: string | null; + rsvrYn: string | null; + prcsgFcltYn: string | null; + ddrztnEqpmYn: string | null; + oslfEtgtYn: string | null; + dschgWtrsExpln: string | null; + prcptnLastDspsMthdExpln: string | null; + ctmnLastDspsMthdExpln: string | null; }; /** diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts index 239d3c9d6..a1da3c2a3 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-finance-interface.ts @@ -91,9 +91,9 @@ export interface SewerageFinanceSettleRequestParams { * `SewerageFinanceSettleInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). */ export interface SewerageFinanceSettleSaveItem { - admdstCd: string; - sggNm: string; - admdstNm: string; + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; cnterGovSprtAmt: number; mctpvLcexpAmt: number; wareaSggLclAmt: number; @@ -252,9 +252,9 @@ export interface SewerageChargeRequestParams { * `SewerageChargeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). */ export interface SewerageChargeSaveItem { - admdstCd: string; - sggNm: string; - admdstNm: string; + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; anlLevyQnt: number; levyAmt: number; grssPrmcoAmt: number; @@ -359,9 +359,9 @@ export interface SewerageUsageFeeRequestParams { * `SewerageUsageFeeInpColumn` fieldName과 동일 (`ctpvNm` 그리드 값은 요청 `ctpvNm` 시도명). */ export interface SewerageUsageFeeSaveItem { - admdstCd: string; - sggNm: string; - admdstNm: string; + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; hshusLevyAmt: number; hshusLevyQnt: number; hshusUntprcAmt: number; @@ -469,9 +469,9 @@ export interface CauserBurdenRequestParams { * `trgtByWholClctnEvntcsCnt`는 조회·그리드 전용이며 저장·제출 요청에는 포함하지 않음. */ export interface CauserBurdenSaveItem { - admdstCd: string; - sggNm: string; - admdstNm: string; + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; levyEvntcsCnt: number; levyAmt: number; mltvEqpmEvntcsCnt: number; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts index b2e9f95df..4508e817c 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-sewerage-service-level-interface.ts @@ -128,8 +128,8 @@ export interface SewerageServiceLevelResidentFriendlyFactRequestParams { /** ST00130001 저장 `saveList[]` 한 행 */ export interface SewerageServiceLevelComplaintSaveItem { - admdstCd: string; - admdstNm: string; + admdstCd: string | null; + admdstNm: string | null; swtrmtPpltnCnt: number; dschgWqCnt: number; odorCnt: number; @@ -162,11 +162,11 @@ export interface SewerageServiceLevelComplaintSaveResponse { /** ST00130002 저장 `saveList[]` 한 행 */ export interface SewerageServiceLevelResidentFriendlyFactSaveItem { - admdstCd: string; - admdstNm: string; - swtrmtFcltCd: string; - swtrmtFcltNm: string; - fcltSn: string; + admdstCd: string | null; + admdstNm: string | null; + swtrmtFcltCd: string | null; + swtrmtFcltNm: string | null; + fcltSn: string | null; tmplntArea: number; brkFcltArea: number; childFrshpSpacArea: number; diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts index 93102dc27..85638dc88 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-status-interface.ts @@ -33,8 +33,63 @@ export type StatsInputStatusListApiResponse = ApiResponse< message: string; }; +/** 통계 검증 리포트 RequestDto */ +export interface ReportSearchRequestDto { + sewerBzmnCd?: string | null; + ctpvNm?: string | null; + sggNm?: string | null; + statsYr: string; + statsCd?: string | null; + /** 공단 상태 코드 (0:미작성, 1:작성중, 2:제출, 3:승인, 4:반려, 5:검토중) */ + pbcpSbmsnSttsCd?: string | null; +} + +/** 통계 검증 리포트 현황 조회 ResponseDto */ +export interface ReportStatusResponseDto { + ctpvNm?: string; + sggNm?: string; + sewerBzmnCd?: string; + statsCd?: string; + menuTabName?: string; + laeSbmsnSttsCd?: string; + laeSbmsnSttsNm?: string; + laeSbmsnYmd?: string; + statPicSbmsnSttsCd?: string; + statPicSbmsnSttsNm?: string; + ctpvSbmsnYmd?: string; + pbcpSbmsnSttsCd?: string; + pbcpSbmsnSttsNm?: string; + pbcpCfmtnYmd?: string; + laePicNm?: string; + laePicTelNo?: string; + laeKbrdrDeptNm?: string; + laeKbrdrJbgdNm?: string; + laeIdfrNm?: string; + laeIdfrTelNo?: string; + laeIdfrDeptNm?: string; + laeIdfrJbgdNm?: string; + mctpvIdfrNm?: string; + mctpvIdfrTelNo?: string; + mctpvIdfrDeptNm?: string; + mctpvIdfrJbgdNm?: string; + lastCfmtnPicNm?: string; + lastCfmtnPicTelNo?: string; + lastCfmtnPicDeptNm?: string; + lastCfmtnPicJbgdNm?: string; + rjctOpnnCn?: string; +} + +export type ReportStatusListApiResponse = ApiResponse< + ReportStatusResponseDto[] +> & { + message: string; +}; + export interface StatisticsInputStatusService { getStatisticsInputStatusList( param: StatsInputStatusListRequestParams, ): Promise; + getStatsValidationReport( + request: ReportSearchRequestDto, + ): Promise; } diff --git a/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts index b40a17da5..dc5796286 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-input-water-reuse-interface.ts @@ -105,18 +105,18 @@ export type PublicSewageTreatWaterReuseApiResponse = /** ST00100001 저장 — `saveList` 항목 */ export interface PublicSewageTreatWaterReuseSaveItem { - admdstCd: string; - admdstNm: string; - fcltSn: string; - swtrmtFcltCd: string; - swtrmtFcltNm: string; + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + swtrmtFcltCd: string | null; + swtrmtFcltNm: string | null; wholPrcsQnt: number; - lglYn: string; - instlYn: string; + lglYn: string | null; + instlYn: string | null; oprtngStrtYmd: string | null; instlAprvYmd: string | null; rprcsFcltCpct: number; - mtcnsNm: string; + mtcnsNm: string | null; ruswtPplLen: number; ruswtPumpLen: number; rmrk: string | null; @@ -209,10 +209,29 @@ export type RecycledWaterStatusApiResponse = ApiResponse & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + bldgSeCn: string | null; + fcltPstnCn: string | null; + lglTrgtYn: string | null; cmptnStrtYmd: string | null; oprtngStrtYmd: string | null; + mnUsgCn: string | null; }; /** ST00100002 저장 API 요청 */ @@ -303,9 +322,31 @@ export type RainFallUtilFactStatusApiResponse = /** ST00100003 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ export type RainFallUtilFactStatusSaveItem = Omit< RainFallUtilFactStatusItem, - "sggNm" | "verificationResult" | "cmptYmd" + | "sggNm" + | "verificationResult" + | "cmptYmd" + | "admdstCd" + | "admdstNm" + | "fcltSn" + | "fcltNm" + | "bdstTypCn" + | "fcltPstnCn" + | "lglTrgtYn" + | "rcptMynCn" + | "filtrPrcsFcltYnExpln" + | "exlnUtlzUsgExpln" > & { + admdstCd: string | null; + admdstNm: string | null; + fcltSn: string | null; + fcltNm: string | null; + bdstTypCn: string | null; + fcltPstnCn: string | null; + lglTrgtYn: string | null; cmptYmd: string | null; + rcptMynCn: string | null; + filtrPrcsFcltYnExpln: string | null; + exlnUtlzUsgExpln: string | null; }; /** ST00100003 저장 API 요청 */ @@ -388,8 +429,22 @@ export type TreatWaterReuseDesignConstStatusApiResponse = /** ST00100004 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ export type TreatWaterReuseDesignConstStatusSaveItem = Omit< TreatWaterReuseDesignConstStatusItem, - "sggNm" | "verificationResult" | "coRegYmd" + | "sggNm" + | "verificationResult" + | "coRegYmd" + | "admdstCd" + | "admdstNm" + | "bplcSn" + | "bplcNm" + | "rprsvNm" + | "bplcPstnCn" > & { + admdstCd: string | null; + admdstNm: string | null; + bplcSn: string | null; + bplcNm: string | null; + rprsvNm: string | null; + bplcPstnCn: string | null; coRegYmd: string | null; }; @@ -474,8 +529,22 @@ export type TreatWaterReuseBusinessApprStatusApiResponse = /** ST00100005 저장 — `saveList` / `deleteList` 항목 (`sggNm`·검증결과 제외) */ export type TreatWaterReuseBusinessApprStatusSaveItem = Omit< TreatWaterReuseBusinessApprStatusItem, - "sggNm" | "verificationResult" | "coRegYmd" + | "sggNm" + | "verificationResult" + | "coRegYmd" + | "admdstCd" + | "admdstNm" + | "bplcSn" + | "bplcNm" + | "rprsvNm" + | "bplcPstnCn" > & { + admdstCd: string | null; + admdstNm: string | null; + bplcSn: string | null; + bplcNm: string | null; + rprsvNm: string | null; + bplcPstnCn: string | null; coRegYmd: string | null; }; diff --git a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts index 7d685fdcb..6b8230e93 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-public-sewage-facility-input-interface.ts @@ -170,12 +170,12 @@ export interface PublicSewageMappingTargetsResponse { /** ST00060002 중심처리시설 매핑 저장 — saveList 항목 */ export interface PublicSewageMainFacilityMappingSaveItem { - fcltSn: string; - admdstCd: string; - admdstNm: string; - swtrmtFcltCd: string; - swtrmtFcltNm: string; - isMainYn: string; + fcltSn: string | null; + admdstCd: string | null; + admdstNm: string | null; + swtrmtFcltCd: string | null; + swtrmtFcltNm: string | null; + isMainYn: string | null; } /** ST00060002 중심처리시설 매핑 저장 API 요청 */ @@ -205,11 +205,11 @@ export interface PublicSewageMainFacilityMappingSaveResponse { /** ST00060001 공공하수처리시설 저장 — saveList 항목 */ export interface PublicSewageFacilitySaveItem { - admdstCd: string; - admdstNm: string; - swtrmtFcltCd: string; - swtrmtFcltNm: string; - fcltSn: string; + admdstCd: string | null; + admdstNm: string | null; + swtrmtFcltCd: string | null; + swtrmtFcltNm: string | null; + fcltSn: string | null; fcltPstnCn: string | null; inflwSwagePhysQnt: number; diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts index 454eb4446..9854acce2 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-pipe-input-interface.ts @@ -96,14 +96,34 @@ export interface SeweragePipeInputRegionalResponse { /** 지역별 시설현황(Tab1) 저장 — saveList 행 (검증 결과 제외) */ export type SeweragePipeInputRegionalFactStatusSaveItem = Omit< SeweragePipeInputRegionalFactStatusItem, - "verificationResult" ->; + "verificationResult" | "admdstCd" | "sggNm" | "admdstNm" | "goalYr" +> & { + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; + goalYr: string | null; +}; /** Tab2 저장 — saveList */ export type SeweragePipeInputFactInstallStatusSaveItem = Omit< SeweragePipeInputFactInstallStatusItem, - "verificationResult" ->; + | "verificationResult" + | "admdstCd" + | "sggNm" + | "admdstNm" + | "fcltSn" + | "swtrmtFcltCd" + | "swtrmtFcltNm" + | "rmrk" +> & { + admdstCd: string | null; + sggNm: string | null; + admdstNm: string | null; + fcltSn: string | null; + swtrmtFcltCd: string | null; + swtrmtFcltNm: string | null; + rmrk: string | null; +}; /** 지역별 시설현황 저장 API 요청 */ export interface SeweragePipeInputRegionalFactStatusSaveRequest { @@ -283,11 +303,11 @@ export interface SeweragePipeInputPipeTypeResponse { /** 관종별 현황 저장 — saveList 항목 */ export interface SeweragePipeInputPipeTypeStatusSaveItem { - admdstCd: string; - admdstNm: string; - pipeSeCd: string; + admdstCd: string | null; + admdstNm: string | null; + pipeSeCd: string | null; yearlyStatList: SeweragePipeInputPipeTypeYearlyStatItem[]; - rmrk: string; + rmrk: string | null; lyngPrlgWhol: number; dmltnPrlgWhol: number; pipeByPrstWhol: number; @@ -351,7 +371,7 @@ export interface SeweragePipeInputPipeDiameterResponse { /** 관경별 현황 저장 — saveList 항목 */ export interface SeweragePipeInputPipeDiameterStatusSaveItem { - pipeSeCd: string; + pipeSeCd: string | null; step1PdmtPrlg: number; step2PdmtPrlg: number; step3PdmtPrlg: number; @@ -359,10 +379,10 @@ export interface SeweragePipeInputPipeDiameterStatusSaveItem { step5PdmtPrlg: number; sqrePdmtPrlg: number; etcPdmtPrlg: number; - rmrk: string; + rmrk: string | null; pdmtByTsmWhol: number; - admdstCd: string; - admdstNm: string; + admdstCd: string | null; + admdstNm: string | null; } /** 관경별 현황 저장 API 요청 */ @@ -456,11 +476,11 @@ export interface SeweragePipeInputManholeStatusSaveYearlyStatItem { /** 맨홀 현황 저장 — saveList 항목 */ export interface SeweragePipeInputManholeStatusSaveItem { - emphsMngSeCd: string; - admdstCd: string; - admdstNm: string; + emphsMngSeCd: string | null; + admdstCd: string | null; + admdstNm: string | null; yearlyStats: SeweragePipeInputManholeStatusSaveYearlyStatItem[]; - rmrk: string; + rmrk: string | null; } /** 맨홀 현황 저장 API 요청 */ diff --git a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts index 626ff8a67..79979cc8b 100644 --- a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts +++ b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-columns.ts @@ -29,14 +29,14 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = { { name: "laePicNm", fieldName: "laePicNm", - header: { text: "작성자" }, + header: { text: "지자체작성자" }, styleName: READ_ONLY_STYLE, editable: false, }, { name: "laePicTelNo", fieldName: "laePicTelNo", - header: { text: "작성자연락처" }, + header: { text: "연락처" }, width: 130, styleName: READ_ONLY_STYLE, editable: false, @@ -58,7 +58,7 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = { { name: "laeIdfrNm", fieldName: "laeIdfrNm", - header: { text: "승인자" }, + header: { text: "지자체승인자" }, styleName: READ_ONLY_STYLE, editable: false, }, @@ -76,6 +76,27 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = { styleName: READ_ONLY_STYLE, editable: false, }, + { + name: "lastCfmtnPicNm", + fieldName: "lastCfmtnPicNm", + header: { text: "공단확인자" }, + styleName: READ_ONLY_STYLE, + editable: false, + }, + { + name: "lastCfmtnPicDeptNm", + fieldName: "lastCfmtnPicDeptNm", + header: { text: "부서" }, + styleName: READ_ONLY_STYLE, + editable: false, + }, + { + name: "lastCfmtnPicJbgdNm", + fieldName: "lastCfmtnPicJbgdNm", + header: { text: "직급" }, + styleName: READ_ONLY_STYLE, + editable: false, + }, { name: "rjctOpnnCn", fieldName: "rjctOpnnCn", @@ -108,6 +129,9 @@ export const statisticsVerificationReportColumnInfo: ColumnInfo = { "laeIdfrNm", "laeIdfrDeptNm", "laeIdfrJbgdNm", + "lastCfmtnPicNm", + "lastCfmtnPicDeptNm", + "lastCfmtnPicJbgdNm", "rjctOpnnCn", "sewerBzmnCd", "statsCd", diff --git a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts index 46e74c8b5..d902c9cd6 100644 --- a/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts +++ b/apps/st/src/entities/statistics/model/statistics-verification-report/statistics-verification-report-types.ts @@ -1,12 +1,11 @@ import type { VerifyResultSearchRequest } from "../general-verification-interface"; -import type { StatsInputStatusListRequestParams } from "../input/statistics-input-status-interface"; +import type { ReportSearchRequestDto } from "../input/statistics-input-status-interface"; -export type StatisticsVerificationFilterState = - StatsInputStatusListRequestParams & { - // 셀렉트 표시용 시도, 시군구 코드 - ctpvSewerBzmnCd?: string | null; - sggSewerBzmnCd?: string | null; - }; +export type StatisticsVerificationFilterState = ReportSearchRequestDto & { + // 셀렉트 표시용 시도, 시군구 코드 + ctpvSewerBzmnCd?: string | null; + sggSewerBzmnCd?: string | null; +}; export type StatisticsVerificationSubFilterState = VerifyResultSearchRequest;