From 0e067bb5fcfd9d16a5ed3095363e537d5171e395 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 12 May 2026 16:04:51 +0900 Subject: [PATCH 001/389] =?UTF-8?q?[as]=20chore:=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/src/pages/auth/login/ui/login-page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/as/src/pages/auth/login/ui/login-page.tsx b/apps/as/src/pages/auth/login/ui/login-page.tsx index 2926a2ebe..60faf66bb 100644 --- a/apps/as/src/pages/auth/login/ui/login-page.tsx +++ b/apps/as/src/pages/auth/login/ui/login-page.tsx @@ -33,11 +33,11 @@ function LoginPage() {

- 하수도정보시스템 + 유역하수도통합플랫폼
- CMS 통합관리시스템 + 디지털 행정지원 시스템

-

하수도정보시스템 CMS 통합관리시스템에 오신 것을 환영합니다.

+

유역하수도통합플랫폼 디지털행정지원 시스템에 오신 것을 환영합니다.

logo From c1cab1455f0c0b5ebc6272e05b3ef03619450566 Mon Sep 17 00:00:00 2001 From: yangsein Date: Wed, 13 May 2026 17:30:09 +0900 Subject: [PATCH 002/389] =?UTF-8?q?feat:=2002.=ED=95=98=EC=88=98=EA=B4=80?= =?UTF-8?q?=EB=A1=9C=20tab3=20=EA=B4=80=EC=A2=85=EB=B3=84=20=ED=98=84?= =?UTF-8?q?=ED=99=A9=20dto=20=EC=BB=AC=EB=9F=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/total/SewagePipeTot.tsx | 30 ++-- .../util/column/total/SewagePipeTotColumn.ts | 158 +++++++----------- .../statistics-sewage-pipe-total-interface.ts | 108 ++---------- 3 files changed, 90 insertions(+), 206 deletions(-) diff --git a/apps/st/src/components/statistics/total/SewagePipeTot.tsx b/apps/st/src/components/statistics/total/SewagePipeTot.tsx index 061405229..08aac9eb0 100644 --- a/apps/st/src/components/statistics/total/SewagePipeTot.tsx +++ b/apps/st/src/components/statistics/total/SewagePipeTot.tsx @@ -8,7 +8,7 @@ import SearchForm from "@/components/module/SearchForm"; import DataGrid from "@/components/module/DataGrid"; import type { ColumnInfo } from "@/components/type/DataGrid.type"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; -import { SewagePipeColumnInfoListTot } from "@/components/util/column/total/SewagePipeTotColumn"; +import { SewagePipeColumnInfoListTot, getST00020003Config } from "@/components/util/column/total/SewagePipeTotColumn"; import { toGridRows } from "@/components/util/grid/gridFunction"; import type { SewagePipeTotalRequest } from "@/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface"; import CellColorInfo from "./CellColorInfo"; @@ -46,7 +46,6 @@ const SewagePipeTot = () => { lastSearchedValuesRef.current = null; }; - /** SewagePipeInp의 `loadSewagePipeActiveTabData`와 동일 역할: 탭키별 API 호출 후 `rowDataMap` 반영 */ const loadSewagePipeTotTabData = async ( tabKey: string, values: SearchFormValues, @@ -107,7 +106,8 @@ const SewagePipeTot = () => { alert(response.message || "데이터 조회 중 오류가 발생했습니다."); return false; } - if (tabKey === "ST00020005") { + + if (tabKey === "ST00020005") { const response = await statisticsTotalStatusApi.getSewagePipePipeMnhleStatus(param); if (response.status === "success" && response.data) { applyRows(toGridRows(response.data as object[])); @@ -124,7 +124,6 @@ const SewagePipeTot = () => { try { setIsLoading(true); - // SewagePipeInp과 동일: 검색 시 활성 탭 데이터만 조회·맵 병합 let ok = false; if ( activeTab === "ST00020001" || @@ -175,8 +174,6 @@ const SewagePipeTot = () => { } return; } - - console.log("[SewagePipeTot] tabKey (API 미연결):", tabKey); }; return ( @@ -206,22 +203,29 @@ const SewagePipeTot = () => { {columnInfo.map((info) => { const tabKey = info.tabKey; - if (!info.columns || !info.columnLayout) { + + // ★ [동적 처리] ST00020003인 경우 함수를 통해 컬럼 정보를 실시간으로 생성 + const currentYear = lastSearchedValuesRef.current?.year || new Date().getFullYear(); + const finalTabConfig = tabKey === "ST00020003" + ? getST00020003Config(currentYear) + : info; + + if (!finalTabConfig.columns || !finalTabConfig.columnLayout) { return null; } const rowData = rowDataMap.get(tabKey) ?? []; const gridColumnInfo: ColumnInfo = { - columns: info.columns, - columnLayout: info.columnLayout, + columns: finalTabConfig.columns, + columnLayout: finalTabConfig.columnLayout, }; return ( { setRowDataMap((prev) => { @@ -236,7 +240,7 @@ const SewagePipeTot = () => { isExcelImport={false} isExcelExport showHeaderCheckbox={false} - fixedColumns={info.fixedColumns} + fixedColumns={finalTabConfig.fixedColumns} enableSort={false} cellStyleCallback={({ row }) => { const rowType = String(row.rowType ?? ''); @@ -255,4 +259,4 @@ const SewagePipeTot = () => { ); }; -export default SewagePipeTot; +export default SewagePipeTot; \ No newline at end of file diff --git a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts index e2a30d9e0..cfb46b13f 100644 --- a/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts +++ b/apps/st/src/components/util/column/total/SewagePipeTotColumn.ts @@ -605,103 +605,6 @@ const rawSewagePipeTotList = [ }, ], }, - - { - tabKey: "ST00020003", - tabLabel: "관종별 현황", - rowStartNum: 2, - fixedColumns: 2, - columns: [ - { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, - { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, - { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, editable: false }, - { name: "swrgPipeNm", fieldName: "swrgPipeNm", header: { text: "관종" }, editable: false }, - { - name: "pipeByPrstWholSum", - fieldName: "pipeByPrstWhol", - header: { text: "계" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.0", - editable: false, - }, - { - name: "lyngPrlgSum", - fieldName: "lyngPrlg", - header: { text: "매설연장(m)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.0", - editable: false, - }, - { - name: "dmltnPrlgSum", - fieldName: "dmltnPrlg", - header: { text: "철거연장(m)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.0", - editable: false, - }, - ...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX }, (_, i) => { - const yyIdx = i + 1; - const wholField = `yy${yyIdx}PrlgWhol`; - const lyngField = `yy${yyIdx}LyngPrlg`; - const dmltnField = `yy${yyIdx}DmltnPrlg`; - return [ - { - name: wholField, - fieldName: wholField, - header: { text: "계" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.0", - editable: false, - }, - { - name: lyngField, - fieldName: lyngField, - header: { text: "매설연장(m)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.0", - editable: false, - }, - { - name: dmltnField, - fieldName: dmltnField, - header: { text: "철거연장(m)" }, - valueType: ValueType.NUMBER, - dataFormat: "#,##0.0", - editable: false, - }, - ]; - }).flat(), - ], - columnLayout: [ - { - name: "areaGroup", - header: { text: "행정구역" }, - items: ["ctpvNm", "sggNm", "admdstNm"], - }, - "swrgPipeNm", - { - name: "summaryGroup", - header: { text: "총계" }, - items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"], - }, - { - name: "year1996Before", - header: { text: "1996년이전" }, - items: ["yy1PrlgWhol", "yy1LyngPrlg", "yy1DmltnPrlg"], - }, - ...Array.from({ length: YY_PIPE_TYPE_FACILITY_MAX - 1 }, (_, i) => { - const yyIdx = i + 2; - const year = 1994 + yyIdx; - return { - name: `group_yy${yyIdx}`, - header: { text: `${year}년` }, - items: [`yy${yyIdx}PrlgWhol`, `yy${yyIdx}LyngPrlg`, `yy${yyIdx}DmltnPrlg`], - }; - }), - ], - }, - { tabKey: "ST00020004", tabLabel: "관경별 현황", @@ -1019,13 +922,70 @@ const rawSewagePipeTotList = [ ], }, "rmrk", -], + ], }, - + ]; +// 2. 동적 컬럼(ST00020003) 생성 함수 +export const getST00020003Config = (statsYr: string | number) => { + const startYear = 1995; + const endYear = Number(statsYr) || new Date().getFullYear(); + + return { + tabKey: "ST00020003", + tabLabel: "관종별 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [ + { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, + { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, + { name: "admdstNm", fieldName: "admdstNm", header: { text: "행정구역명" }, editable: false }, + { name: "swrgPipeNm", fieldName: "swrgPipeNm", header: { text: "관종" }, editable: false }, + { name: "pipeByPrstWholSum", fieldName: "pipeByPrstWhol", header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "lyngPrlgSum", fieldName: "lyngPrlg", header: { text: "매설연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: "dmltnPrlgSum", fieldName: "dmltnPrlg", header: { text: "철거연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + ...(() => { + const yearColumns = []; + for (let yr = startYear; yr <= endYear; yr++) { + yearColumns.push( + { name: `prlg_whol_${yr}`, fieldName: `prlg_whol_${yr}`, header: { text: "계" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: `lyng_prlg_${yr}`, fieldName: `lyng_prlg_${yr}`, header: { text: "매설연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false }, + { name: `dmltn_prlg_${yr}`, fieldName: `dmltn_prlg_${yr}`, header: { text: "철거연장(m)" }, valueType: ValueType.NUMBER, dataFormat: "#,##0.0", editable: false } + ); + } + return yearColumns; + })(), + ], + columnLayout: [ + { name: "areaGroup", header: { text: "행정구역" }, items: ["ctpvNm", "sggNm", "admdstNm"] }, + "swrgPipeNm", + { name: "summaryGroup", header: { text: "총계" }, items: ["pipeByPrstWholSum", "lyngPrlgSum", "dmltnPrlgSum"] }, + ...(() => { + const layouts = []; + for (let yr = startYear; yr <= endYear; yr++) { + layouts.push({ + name: `group_${yr}`, + header: { text: yr === 1995 ? "1996년이전" : `${yr}년` }, + items: [`prlg_whol_${yr}`, `lyng_prlg_${yr}`, `dmltn_prlg_${yr}`], + }); + } + return layouts; + })(), + ], + }; +}; + export const SewagePipeColumnInfoListTot = rawSewagePipeTotList.map((tab) => ({ ...tab, columns: tab.columns.map((col) => ({ ...col, editable: false as const })), })); +SewagePipeColumnInfoListTot.splice(2, 0, { + tabKey: "ST00020003", + tabLabel: "관종별 현황", + rowStartNum: 2, + fixedColumns: 2, + columns: [], + columnLayout: [], +} as any); \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts index 8643d41d6..76132a1ac 100644 --- a/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-total/statistics-sewage-pipe-total-interface.ts @@ -102,99 +102,7 @@ export interface SewagePipeFactInstallStatusResponse { /** ST00020003 합계 — `GET .../sewer-pipe/pipe-type-facility-status` */ export interface SewagePipePipeTypeFacilityStatusItem { - pipeByPrstWhol: number; - lyngPrlg: number; - dmltnPrlg: number; - yy1PrlgWhol: number; - yy1LyngPrlg: number; - yy1DmltnPrlg: number; - yy2PrlgWhol: number; - yy2LyngPrlg: number; - yy2DmltnPrlg: number; - yy3PrlgWhol: number; - yy3LyngPrlg: number; - yy3DmltnPrlg: number; - yy4PrlgWhol: number; - yy4LyngPrlg: number; - yy4DmltnPrlg: number; - yy5PrlgWhol: number; - yy5LyngPrlg: number; - yy5DmltnPrlg: number; - yy6PrlgWhol: number; - yy6LyngPrlg: number; - yy6DmltnPrlg: number; - yy7PrlgWhol: number; - yy7LyngPrlg: number; - yy7DmltnPrlg: number; - yy8PrlgWhol: number; - yy8LyngPrlg: number; - yy8DmltnPrlg: number; - yy9PrlgWhol: number; - yy9LyngPrlg: number; - yy9DmltnPrlg: number; - yy10PrlgWhol: number; - yy10LyngPrlg: number; - yy10DmltnPrlg: number; - yy11PrlgWhol: number; - yy11LyngPrlg: number; - yy11DmltnPrlg: number; - yy12PrlgWhol: number; - yy12LyngPrlg: number; - yy12DmltnPrlg: number; - yy13PrlgWhol: number; - yy13LyngPrlg: number; - yy13DmltnPrlg: number; - yy14PrlgWhol: number; - yy14LyngPrlg: number; - yy14DmltnPrlg: number; - yy15PrlgWhol: number; - yy15LyngPrlg: number; - yy15DmltnPrlg: number; - yy16PrlgWhol: number; - yy16LyngPrlg: number; - yy16DmltnPrlg: number; - yy17PrlgWhol: number; - yy17LyngPrlg: number; - yy17DmltnPrlg: number; - yy18PrlgWhol: number; - yy18LyngPrlg: number; - yy18DmltnPrlg: number; - yy19PrlgWhol: number; - yy19LyngPrlg: number; - yy19DmltnPrlg: number; - yy20PrlgWhol: number; - yy20LyngPrlg: number; - yy20DmltnPrlg: number; - yy21PrlgWhol: number; - yy21LyngPrlg: number; - yy21DmltnPrlg: number; - yy22PrlgWhol: number; - yy22LyngPrlg: number; - yy22DmltnPrlg: number; - yy23PrlgWhol: number; - yy23LyngPrlg: number; - yy23DmltnPrlg: number; - yy24PrlgWhol: number; - yy24LyngPrlg: number; - yy24DmltnPrlg: number; - yy25PrlgWhol: number; - yy25LyngPrlg: number; - yy25DmltnPrlg: number; - yy26PrlgWhol: number; - yy26LyngPrlg: number; - yy26DmltnPrlg: number; - yy27PrlgWhol: number; - yy27LyngPrlg: number; - yy27DmltnPrlg: number; - yy28PrlgWhol: number; - yy28LyngPrlg: number; - yy28DmltnPrlg: number; - yy29PrlgWhol: number; - yy29LyngPrlg: number; - yy29DmltnPrlg: number; - yy30PrlgWhol: number; - yy30LyngPrlg: number; - yy30DmltnPrlg: number; + // 고정 필드 ctpvNm: string; sggNm: string | null; admdstCd: string; @@ -203,7 +111,19 @@ export interface SewagePipePipeTypeFacilityStatusItem { statsYr: string; rowType: string; rmrk: string | null; -} + + // 총계 필드 + pipeByPrstWhol: number; + lyngPrlg: number; + dmltnPrlg: number; + + /** + * 연도별 동적 데이터 필드 + "prlg_whol_1995", "lyng_prlg_1995", "dmltn_prlg_1995", + + */ + [key: string]: string | number | null; +} export interface SewagePipePipeTypeFacilityStatusResponse { status: string; From 1cde3b0998484ca3cde287a763623fe77ee8e5a8 Mon Sep 17 00:00:00 2001 From: rsone001 Date: Thu, 14 May 2026 00:18:30 +0900 Subject: [PATCH 003/389] =?UTF-8?q?[st]=20-=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=EB=B0=98=EB=A0=A4,=EC=8A=B9=EC=9D=B8=20/=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=20=EC=8A=B9=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/button/ApproveButton.tsx | 2 +- apps/st/src/components/css/DataGrid.css | 247 +++++-- apps/st/src/components/module/DataGrid.tsx | 22 +- .../ExceptionProcessingHistoryDialog.tsx | 617 ++++++++++++++++++ .../VerificationResultHistoryDialog.tsx | 2 +- .../input/PublicSewageFacilityInp.tsx | 16 +- .../statistics/input/SewagePipeInp.tsx | 52 +- .../input/SewagePipeMaintainInp.tsx | 2 +- .../input/SeweragePenetrationRateInp.tsx | 8 +- .../statistics/input/StatsInputStatusInp.tsx | 139 +++- .../statistics/management/LogHistory.tsx | 2 +- .../statistics/review/StatsInputStatusRvw.tsx | 174 +++-- apps/st/src/components/type/DataGrid.type.ts | 12 + .../column/input/StatsInputStatusInpColumn.ts | 9 + .../column/management/LogHistoryColumn.ts | 2 +- .../review/StatsInputStatusRvwColumn.ts | 8 + .../util/column/total/SewagePipeTotColumn.ts | 2 +- .../statistics/api/statistics-service.ts | 54 ++ apps/st/src/entities/statistics/index.ts | 6 + .../statistics/model/statistics-interface.ts | 39 ++ 20 files changed, 1241 insertions(+), 174 deletions(-) create mode 100644 apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx diff --git a/apps/st/src/components/button/ApproveButton.tsx b/apps/st/src/components/button/ApproveButton.tsx index 36455f7ff..76d3618f5 100644 --- a/apps/st/src/components/button/ApproveButton.tsx +++ b/apps/st/src/components/button/ApproveButton.tsx @@ -1,7 +1,7 @@ import { Button } from "@common/components"; interface ApproveButtonProps { - approveFunc: () => void; + approveFunc: () => void | Promise; } const ApproveButton = ({ approveFunc }: ApproveButtonProps) => { diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index a2e447269..190d41a79 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -1,21 +1,6 @@ @tailwind components; @layer components { - .rg-grid .rg-header .rg-table .rg-header-group-cell { - @apply !bg-[#466995] !text-[#fff]; - } - .rg-grid .rg-header .rg-table { - @apply !bg-[#5987bf] !text-[#fff]; - } - - /* 상태바·체크바 헤더 */ - .rg-grid .rg-head .rg-table tr:first-child td:first-child, - .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), - .rg-grid .rg-header .rg-table tr:first-child td:first-child, - .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { - @apply !bg-[#5987bf] !text-[#fff]; - } - .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { @apply !bg-white; } @@ -36,14 +21,67 @@ .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { @apply !font-semibold !text-[#3372cf]; } + /* 개별 반려: 높이/레이아웃은 RealGrid 행에 그대로 두고, 색·테두리·정렬만 버튼 느낌 */ + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn { + text-align: center !important; + vertical-align: middle !important; + box-sizing: border-box !important; + padding-left: 8px !important; + padding-right: 8px !important; + border-radius: 0.25rem !important; + border: 1px solid rgb(248 113 113 / 0.55) !important; + background-color: rgb(254 242 242) !important; + color: rgb(185 28 28) !important; + font-size: 1.2rem !important; + font-weight: 600 !important; + white-space: nowrap !important; + cursor: pointer !important; + text-decoration: none !important; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:hover { + background-color: rgb(254 226 226) !important; + border-color: rgb(239 68 68 / 0.85) !important; + color: rgb(153 27 27) !important; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:active { + background-color: rgb(254 202 202) !important; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn { + text-align: center !important; + vertical-align: middle !important; + box-sizing: border-box !important; + padding-left: 8px !important; + padding-right: 8px !important; + border-radius: 0.25rem !important; + border: 1px solid rgb(148 163 184 / 0.7) !important; + background-color: rgb(248 250 252) !important; + color: rgb(51 65 85) !important; + font-size: 1.2rem !important; + font-weight: 600 !important; + white-space: nowrap !important; + cursor: pointer !important; + text-decoration: none !important; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:hover { + background-color: rgb(241 245 249) !important; + border-color: rgb(100 116 139 / 0.85) !important; + color: rgb(30 41 59) !important; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:active { + background-color: rgb(226 232 240) !important; + } + .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-action-link { + @apply !font-semibold !text-[#2563eb] underline; + cursor: pointer !important; + } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-pending { @apply !text-gray-500; } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-submitted { - @apply !font-semibold + @apply !font-semibold; } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-approved { - @apply !font-semibold + @apply !font-semibold; } .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-stats-status-rejected { @apply !font-semibold !text-[#ff6c64]; @@ -117,60 +155,6 @@ .data-grid-status-bubble > span + span { @apply ml-4 border-l border-slate-200/70 pl-4; } - - [data-theme="black"] { - .rg-grid .rg-header .rg-table .rg-header-group-cell { - @apply !bg-[#3C4043] !text-[#fff]; - } - .rg-grid .rg-header .rg-table .rg-header-cell { - @apply !bg-[#62686d] !text-[#fff]; - } - .rg-grid .rg-head .rg-table tr:first-child td:first-child, - .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), - .rg-grid .rg-header .rg-table tr:first-child td:first-child, - .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { - @apply !bg-[#62686d] !text-[#fff]; - } - .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { - @apply !bg-[#545556] !text-[#fff]; - } - .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad { - @apply !text-[#f87171]; - } - .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { - @apply !text-[#3372cf]; - } - } - - [data-theme="red"] { - .rg-grid .rg-header .rg-table .rg-header-group-cell { - @apply !bg-[#646469] !text-[#fff]; - } - .rg-grid .rg-header .rg-table .rg-header-cell { - @apply !bg-[#8d8d94] !text-[#fff]; - } - .rg-grid .rg-head .rg-table tr:first-child td:first-child, - .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), - .rg-grid .rg-header .rg-table tr:first-child td:first-child, - .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { - @apply !bg-[#8d8d94] !text-[#fff]; - } - } - - [data-theme="green"] { - .rg-grid .rg-header .rg-table .rg-header-group-cell { - @apply !bg-[#518585] !text-[#fff]; - } - .rg-grid .rg-header .rg-table .rg-header-cell { - @apply !bg-[#6bafae] !text-[#fff]; - } - .rg-grid .rg-head .rg-table tr:first-child td:first-child, - .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), - .rg-grid .rg-header .rg-table tr:first-child td:first-child, - .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { - @apply !bg-[#6bafae] !text-[#fff]; - } - } } @keyframes rg-cell-stats-status-dot-ping { @@ -203,4 +187,123 @@ .rg-grid .rg-head .rg-table tr td { border: none; border-bottom: 1px solid #e5e7eb; -} \ No newline at end of file +} + +/* + * ST 통계 그리드 헤더 색 — @layer 밖(일반 규칙). + * packages/rsone/components/grid/css/DataGrid.css 등 @layer 밖 !important와 동일 층에서 + * .rg-grid … 특이도로 덮어씀. + */ +.rg-grid .rg-header .rg-table .rg-header-group-cell { + background-color: #466995 !important; + color: #ffffff !important; +} +.rg-grid .rg-header .rg-table { + background-color: #5987bf !important; + color: #ffffff !important; +} +.rg-grid .rg-head .rg-table tr:first-child td:first-child, +.rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), +.rg-grid .rg-header .rg-table tr:first-child td:first-child, +.rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + background-color: #5987bf !important; + color: #ffffff !important; +} +.rg-grid .rg-header .rg-cell, +.rg-grid .rg-head .rg-cell { + background-color: #5987bf !important; + color: #ffffff !important; +} + +[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-group-cell { + background-color: #3c4043 !important; + color: #ffffff !important; +} +[data-theme="black"] .rg-grid .rg-header .rg-table .rg-header-cell { + background-color: #62686d !important; + color: #ffffff !important; +} +[data-theme="black"] .rg-grid .rg-head .rg-table tr:first-child td:first-child, +[data-theme="black"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), +[data-theme="black"] .rg-grid .rg-header .rg-table tr:first-child td:first-child, +[data-theme="black"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + background-color: #62686d !important; + color: #ffffff !important; +} +[data-theme="black"] .rg-grid .rg-header .rg-cell, +[data-theme="black"] .rg-grid .rg-head .rg-cell { + background-color: #62686d !important; + color: #ffffff !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { + background-color: #545556 !important; + color: #ffffff !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-bad { + color: #f87171 !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-vrf-prcs-ok { + color: #3372cf !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn { + background-color: rgb(127 29 29 / 0.42) !important; + border-color: rgb(248 113 113 / 0.5) !important; + color: #fecaca !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-reject-mini-btn:hover { + background-color: rgb(153 27 27 / 0.55) !important; + border-color: rgb(252 165 165 / 0.75) !important; + color: #fee2e2 !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn { + background-color: rgb(71 85 105 / 0.35) !important; + border-color: rgb(148 163 184 / 0.55) !important; + color: #e2e8f0 !important; +} +[data-theme="black"] .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell.rg-cell-outline-action-btn:hover { + background-color: rgb(71 85 105 / 0.5) !important; + border-color: rgb(203 213 225 / 0.65) !important; + color: #f8fafc !important; +} + +[data-theme="red"] .rg-grid .rg-header .rg-table .rg-header-group-cell { + background-color: #646469 !important; + color: #ffffff !important; +} +[data-theme="red"] .rg-grid .rg-header .rg-table .rg-header-cell { + background-color: #8d8d94 !important; + color: #ffffff !important; +} +[data-theme="red"] .rg-grid .rg-head .rg-table tr:first-child td:first-child, +[data-theme="red"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), +[data-theme="red"] .rg-grid .rg-header .rg-table tr:first-child td:first-child, +[data-theme="red"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + background-color: #8d8d94 !important; + color: #ffffff !important; +} +[data-theme="red"] .rg-grid .rg-header .rg-cell, +[data-theme="red"] .rg-grid .rg-head .rg-cell { + background-color: #8d8d94 !important; + color: #ffffff !important; +} + +[data-theme="green"] .rg-grid .rg-header .rg-table .rg-header-group-cell { + background-color: #518585 !important; + color: #ffffff !important; +} +[data-theme="green"] .rg-grid .rg-header .rg-table .rg-header-cell { + background-color: #6bafae !important; + color: #ffffff !important; +} +[data-theme="green"] .rg-grid .rg-head .rg-table tr:first-child td:first-child, +[data-theme="green"] .rg-grid .rg-head .rg-table tr:first-child td:nth-child(2), +[data-theme="green"] .rg-grid .rg-header .rg-table tr:first-child td:first-child, +[data-theme="green"] .rg-grid .rg-header .rg-table tr:first-child td:nth-child(2) { + background-color: #6bafae !important; + color: #ffffff !important; +} +[data-theme="green"] .rg-grid .rg-header .rg-cell, +[data-theme="green"] .rg-grid .rg-head .rg-cell { + background-color: #6bafae !important; + color: #ffffff !important; +} diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index a9b615c7e..0ae40300a 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -378,6 +378,7 @@ const DataGrid = ({ cellTooltipCallback, onCellEdited, onRowClick, + onDataCellClick, onItemChecked, onItemAllChecked, onScrollToBottom, @@ -403,6 +404,7 @@ const DataGrid = ({ cellTooltipCallback, ); const onCellEditedRef = useRef(onCellEdited); + const onDataCellClickRef = useRef(onDataCellClick); const onScrollToBottomRef = useRef<(() => void) | undefined>( onScrollToBottom, ); @@ -417,6 +419,7 @@ const DataGrid = ({ onItemCheckedRef.current = onItemChecked; onItemAllCheckedRef.current = onItemAllChecked; onCellEditedRef.current = onCellEdited; + onDataCellClickRef.current = onDataCellClick; // highlightedRowId가 변경될 때 해당 행으로 스크롤 및 선택 useEffect(() => { @@ -1180,7 +1183,11 @@ const DataGrid = ({ // 행 클릭 이벤트 + 검증 오류 셀 클릭 시 showMessage (공식 문서 방식) useEffect(() => { const gridView = gridRef.current?.gridView; - if (!gridView || (!onRowClick && !cellTooltipCallback)) return; + if ( + !gridView || + (!onRowClick && !cellTooltipCallback && !onDataCellClick) + ) + return; gridView.onCellClicked = (grid, clickData) => { if ( @@ -1199,10 +1206,10 @@ const DataGrid = ({ }; } ).getCurrent?.(); + const fieldName = + cellIndex?.fieldName ?? cellIndex?.column ?? ""; // cellTooltipCallback 있으면 해당 셀 메시지로 showMessage (공식 문서: TooltipOptions + CellIndex) if (cellTooltipCallbackRef.current && row && cellIndex) { - const fieldName = - cellIndex.fieldName ?? cellIndex.column ?? ""; const msg = cellTooltipCallbackRef.current({ row: row as Record, fieldName, @@ -1232,6 +1239,13 @@ const DataGrid = ({ ); } } + if (onDataCellClickRef.current && row !== undefined) { + onDataCellClickRef.current({ + row: (row ?? {}) as Record, + fieldName, + itemIndex: clickData.itemIndex, + }); + } if (onRowClick) onRowClick((row ?? {}) as Record); } catch (error) { @@ -1242,7 +1256,7 @@ const DataGrid = ({ return () => { if (gridView.onCellClicked) gridView.onCellClicked = undefined; }; - }, [onRowClick, cellTooltipCallback, columns]); + }, [onRowClick, onDataCellClick, cellTooltipCallback, columns]); useEffect(() => { const gridView = gridRef.current?.gridView; diff --git a/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx b/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx new file mode 100644 index 000000000..04cd06dc9 --- /dev/null +++ b/apps/st/src/components/module/ExceptionProcessingHistoryDialog.tsx @@ -0,0 +1,617 @@ +"use client"; + +import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { RealGridReact } from "realgrid-react"; + +import { getUserInfo } from "../../../../../packages/common/components/src/api"; +import { useAlertStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { + ReviewInputSaveRequest, + VerificationResultListRequest, + VerificationResultReportItem, +} from "@/entities/statistics/model/statistics-interface"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + Label, + LoadingWithAnimation, + Tabs, + TabsContent, + TabsList, + TabsTrigger, + Textarea, +} from "@common/components"; + +import DataGrid from "@/components/module/DataGrid"; +import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; +import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; + +/** 통계 입력 현황 > 행 클릭 시 예외처리(prcsYn=3) 내역만 조회 */ +export interface ExceptionProcessingHistoryQueryContext { + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; + statsYr: string; + statsCd: string; +} + +export interface ExceptionProcessingHistoryDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + queryContext?: ExceptionProcessingHistoryQueryContext | null; + emptyMessage?: string; + /** 반려 API 성공 후 (다이얼로그 내 목록 재조회 이후) 상위 화면 등에서 호출 */ + onRejectSuccess?: () => void | Promise; +} + +type ExceptionHistoryGridRow = Record; +type ExceptionHistoryTabKey = "NV" | "IMR" | "HB"; + +const exceptionHistoryTabs: { value: ExceptionHistoryTabKey; label: string }[] = [ + { value: "NV", label: "일반검증 내역" }, + { value: "IMR", label: "I-MR 검증내역" }, + { value: "HB", label: "HB 검증내역" }, +]; + +const TITLE = "예외처리 내역 조회"; +/** 예외처리 건만 */ +const PRCS_YN_LIST = "2"; + +export type ExceptionRejectSubmitItem = { + vrfcSn: number; + statsYr: string; + sewerBzmnCd: string; + statsCd: string; + mdfrId: string; + rjctOpnnCn?: string; +}; + +function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined { + if (raw === undefined || raw === null) return undefined; + if (typeof raw === "number" && Number.isFinite(raw)) return raw; + const s = String(raw).trim(); + if (s === "") return undefined; + const n = Number.parseInt(s, 10); + return Number.isFinite(n) ? n : undefined; +} + +function exceptionPrcsYnNmCellStyle({ fieldName, row }: CellStyleCallbackParams): string | undefined { + if (fieldName !== "prcsYnNm") return undefined; + const code = prcsYnCodeToNumber(row.prcsYn); + if (code === 0 || code === 4) return "rg-cell-vrf-prcs-bad"; + return "rg-cell-vrf-prcs-ok"; +} + +const exceptionHistoryColumnInfo: ColumnInfo = { + columns: [ + { name: "ctpvNm", fieldName: "ctpvNm", header: { text: "시도" }, editable: false }, + { name: "sggNm", fieldName: "sggNm", header: { text: "시군구" }, editable: false }, + { name: "vrfcTrgtNm", fieldName: "vrfcTrgtNm", header: { text: "검증대상" }, editable: false }, + { name: "rsnExpln", fieldName: "rsnExpln", header: { text: "사유" }, editable: false }, + { name: "nowVlCn", fieldName: "nowVlCn", header: { text: "입력값" }, editable: false }, + { name: "compVlCn", fieldName: "compVlCn", header: { text: "기준값" }, editable: false }, + { name: "prcsYnNm", fieldName: "prcsYnNm", header: { text: "처리여부" }, editable: false }, + { name: "pbcpAprvYn", fieldName: "pbcpAprvYn", header: { text: "공단승인여부" }, editable: false }, + { name: "regDt", fieldName: "regDt", header: { text: "등록일시" }, editable: false }, + { name: "mdfcnDt", fieldName: "mdfcnDt", header: { text: "갱신일시" }, editable: false }, + { + name: "individualReject", + fieldName: "individualReject", + header: { text: "개별 반려" }, + editable: false, + width: 104, + styleName: "rg-cell-reject-mini-btn", + }, + { name: "prcsYn", fieldName: "prcsYn", header: { text: "검증결과상태코드" }, editable: false, visible: false }, + { name: "vrfcSn", fieldName: "vrfcSn", header: { text: "vrfcSn" }, editable: false, visible: false }, + ], + columnLayout: [ + "ctpvNm", + "sggNm", + "vrfcTrgtNm", + "rsnExpln", + "nowVlCn", + "compVlCn", + "prcsYnNm", + "pbcpAprvYn", + "regDt", + "mdfcnDt", + "individualReject", + "prcsYn", + "vrfcSn", + ], +}; + +export function ExceptionProcessingHistoryDialog({ + open, + onOpenChange, + queryContext, + emptyMessage = "표시할 예외처리 내역이 없습니다.", + onRejectSuccess, +}: ExceptionProcessingHistoryDialogProps) { + const { setMessage: alert } = useAlertStore((state) => state); + const nvGridRef = useRef(null); + const imrGridRef = useRef(null); + const hbGridRef = useRef(null); + const [rejectReasonDialogOpen, setRejectReasonDialogOpen] = useState(false); + const [pendingRejectVrfcSns, setPendingRejectVrfcSns] = useState([]); + const [rejectReasonText, setRejectReasonText] = useState(""); + const [rejectSubmitting, setRejectSubmitting] = useState(false); + + const [activeTab, setActiveTab] = useState("NV"); + const [normalEntries, setNormalEntries] = useState([]); + const [imrEntries, setImrEntries] = useState([]); + const [hbEntries, setHbEntries] = useState([]); + const [normalPageIndex, setNormalPageIndex] = useState(0); + const [imrPageIndex, setImrPageIndex] = useState(0); + const [hbPageIndex, setHbPageIndex] = useState(0); + const [normalPageCount, setNormalPageCount] = useState(1); + const [imrPageCount, setImrPageCount] = useState(1); + const [hbPageCount, setHbPageCount] = useState(1); + const [normalLoading, setNormalLoading] = useState(false); + const [imrLoading, setImrLoading] = useState(false); + const [hbLoading, setHbLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const resetState = useCallback(() => { + setActiveTab("NV"); + setNormalEntries([]); + setImrEntries([]); + setHbEntries([]); + setNormalPageIndex(0); + setImrPageIndex(0); + setHbPageIndex(0); + setNormalPageCount(1); + setImrPageCount(1); + setHbPageCount(1); + setNormalLoading(false); + setImrLoading(false); + setHbLoading(false); + setErrorMessage(null); + setRejectReasonDialogOpen(false); + setPendingRejectVrfcSns([]); + setRejectReasonText(""); + setRejectSubmitting(false); + }, []); + + const fetchPage = useCallback( + async (vrfcMthdCd: ExceptionHistoryTabKey, pageIndex: number) => { + if (!queryContext) return null; + const param: VerificationResultListRequest = { + sewerBzmnCd: queryContext.sewerBzmnCd, + ctpvNm: queryContext.ctpvNm, + sggNm: queryContext.sggNm, + statsYr: queryContext.statsYr, + statsCd: queryContext.statsCd, + pbcpAprvYn: "N", + vrfcMthdCd, + prcsYnList: PRCS_YN_LIST, + page: pageIndex + 1, + size: 30, + }; + return statisticsApi.getVerificationResultList(param); + }, + [queryContext], + ); + + const loadInitial = useCallback(async () => { + if (!queryContext) { + setErrorMessage("조회 조건이 없어 예외처리 내역을 불러올 수 없습니다."); + return; + } + setErrorMessage(null); + setNormalLoading(true); + setImrLoading(true); + setHbLoading(true); + setNormalPageIndex(0); + setImrPageIndex(0); + setHbPageIndex(0); + try { + const [nvRes, imrRes, hbRes] = await Promise.all([ + fetchPage("NV", 0), + fetchPage("IMR", 0), + fetchPage("HB", 0), + ]); + setNormalEntries(nvRes?.data?.verifyResultList ?? []); + setImrEntries(imrRes?.data?.verifyResultList ?? []); + setHbEntries(hbRes?.data?.verifyResultList ?? []); + setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1); + setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1); + setHbPageCount(hbRes?.data?.pageInfo?.totalPages ?? 1); + } catch { + setNormalEntries([]); + setImrEntries([]); + setHbEntries([]); + setNormalPageCount(1); + setImrPageCount(1); + setHbPageCount(1); + setErrorMessage("예외처리 내역 조회 중 오류가 발생했습니다."); + } finally { + setNormalLoading(false); + setImrLoading(false); + setHbLoading(false); + } + }, [fetchPage, queryContext]); + + const handleNormalPageChange = useCallback( + async (pageIndex: number) => { + if (pageIndex === normalPageIndex || !queryContext) return; + setErrorMessage(null); + setNormalLoading(true); + try { + const response = await fetchPage("NV", pageIndex); + setNormalEntries(response?.data?.verifyResultList ?? []); + setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1); + setNormalPageIndex(pageIndex); + } catch { + setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다."); + } finally { + setNormalLoading(false); + } + }, + [fetchPage, normalPageIndex, queryContext], + ); + + const handleImrPageChange = useCallback( + async (pageIndex: number) => { + if (pageIndex === imrPageIndex || !queryContext) return; + setErrorMessage(null); + setImrLoading(true); + try { + const response = await fetchPage("IMR", pageIndex); + setImrEntries(response?.data?.verifyResultList ?? []); + setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); + setImrPageIndex(pageIndex); + } catch { + setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); + } finally { + setImrLoading(false); + } + }, + [fetchPage, imrPageIndex, queryContext], + ); + + const handleHbPageChange = useCallback( + async (pageIndex: number) => { + if (pageIndex === hbPageIndex || !queryContext) return; + setErrorMessage(null); + setHbLoading(true); + try { + const response = await fetchPage("HB", pageIndex); + setHbEntries(response?.data?.verifyResultList ?? []); + setHbPageCount(response?.data?.pageInfo?.totalPages ?? 1); + setHbPageIndex(pageIndex); + } catch { + setErrorMessage("HB 검증내역 조회 중 오류가 발생했습니다."); + } finally { + setHbLoading(false); + } + }, + [fetchPage, hbPageIndex, queryContext], + ); + + useEffect(() => { + if (!open) { + resetState(); + return; + } + void loadInitial(); + }, [loadInitial, open, resetState]); + + const mapRows = (entries: VerificationResultReportItem[]): ExceptionHistoryGridRow[] => + entries.map((item) => ({ + ctpvNm: item.ctpvNm, + sggNm: item.sggNm, + vrfcTrgtNm: item.vrfcTrgtNm, + rsnExpln: item.rsnExpln, + nowVlCn: item.nowVlCn ?? "-", + compVlCn: item.compVlCn ?? "-", + prcsYnNm: item.prcsYnNm ?? "-", + prcsYn: item.prcsYn ?? "", + pbcpAprvYn: item.pbcpAprvYn, + regDt: (item.regDt ?? "").slice(0, 16), + mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), + individualReject: "개별 반려", + vrfcSn: item.vrfcSn, + })); + + const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]); + const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]); + const hbRows = useMemo(() => mapRows(hbEntries), [hbEntries]); + + const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch< + SetStateAction[]> + >; + + const handleOpenRejectReasonDialog = useCallback(() => { + if (!queryContext) { + alert("조회 조건이 없습니다."); + return; + } + const rows = + activeTab === "NV" ? normalRows : activeTab === "IMR" ? imrRows : hbRows; + const gridRef = + activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef; + const gridView = gridRef.current?.gridView; + if (!gridView) { + alert("데이터가 존재하지 않습니다."); + return; + } + const checkedIndices = (gridView.getCheckedRows?.(true, false, true) ?? []) as number[]; + if (checkedIndices.length === 0) { + alert("반려할 항목을 체크해주세요."); + return; + } + const vrfcSns: number[] = []; + for (const itemIndex of checkedIndices) { + const row = rows[itemIndex]; + if (!row) continue; + const vrfcSn = Number(row.vrfcSn); + if (!Number.isFinite(vrfcSn)) continue; + vrfcSns.push(vrfcSn); + } + if (vrfcSns.length === 0) { + alert("유효한 검증번호가 있는 행을 선택해주세요."); + return; + } + setPendingRejectVrfcSns(vrfcSns); + setRejectReasonText(""); + setRejectSubmitting(false); + setRejectReasonDialogOpen(true); + }, [activeTab, alert, hbRows, imrRows, normalRows, queryContext]); + + const resetRejectReasonDialog = useCallback(() => { + setRejectReasonDialogOpen(false); + setPendingRejectVrfcSns([]); + setRejectReasonText(""); + setRejectSubmitting(false); + }, []); + + const handleRejectReasonDialogOpenChange = useCallback( + (nextOpen: boolean) => { + if (!nextOpen) resetRejectReasonDialog(); + }, + [resetRejectReasonDialog], + ); + + const handleConfirmReject = useCallback(async () => { + if (!queryContext || pendingRejectVrfcSns.length === 0) { + alert("반려할 항목이 없습니다. 다시 선택해주세요."); + resetRejectReasonDialog(); + return; + } + let userInfo; + try { + userInfo = await getUserInfo(); + } catch { + alert("사용자 정보를 불러오지 못했습니다."); + return; + } + const mdfrId = userInfo.userUnqId; + const trimmedReason = rejectReasonText.trim(); + + const items: ReviewInputSaveRequest[] = pendingRejectVrfcSns.map((vrfcSn) => ({ + type: "INPUT", + statsYr: queryContext.statsYr, + sewerBzmnCd: queryContext.sewerBzmnCd, + statsCd: queryContext.statsCd, + mdfrId, + vrfcSn: String(vrfcSn), + ...(trimmedReason ? { rjctOpnnCn: trimmedReason } : {}), + })); + + setRejectSubmitting(true); + try { + const response = await statisticsApi.saveInputReturn(items); + if (response.resultCode.toLowerCase() !== "success") { + alert(response.message ?? "반려 처리 중 오류가 발생했습니다."); + return; + } + alert("반려 처리되었습니다."); + resetRejectReasonDialog(); + await Promise.all([ + loadInitial(), + onRejectSuccess?.() ?? Promise.resolve(), + ]); + } catch { + alert("반려 처리 중 오류가 발생했습니다."); + } finally { + setRejectSubmitting(false); + } + }, [ + alert, + loadInitial, + onRejectSuccess, + pendingRejectVrfcSns, + queryContext, + rejectReasonText, + resetRejectReasonDialog, + ]); + + const handleGridRowClick = useCallback( + (rowData: Record) => { + const gridRef = + activeTab === "NV" ? nvGridRef : activeTab === "IMR" ? imrGridRef : hbGridRef; + const current = gridRef.current?.gridView?.getCurrent?.() as + | { fieldName?: string; column?: string } + | undefined; + const fieldName = current?.fieldName ?? current?.column; + if (fieldName !== "individualReject") return; + const vrfcSn = Number(rowData.vrfcSn); + if (!Number.isFinite(vrfcSn)) { + alert("유효한 검증번호가 있는 행을 선택해주세요."); + return; + } + setPendingRejectVrfcSns([vrfcSn]); + setRejectReasonText(""); + setRejectSubmitting(false); + setRejectReasonDialogOpen(true); + }, + [activeTab, alert], + ); + + const renderGrid = ( + rows: ExceptionHistoryGridRow[], + pageIndex: number, + pageCount: number, + onPageIndexChange: (page: number) => void, + loading: boolean, + gridRef: MutableRefObject, + ) => { + if (!loading && rows.length === 0) { + return

{emptyMessage}

; + } + + return ( +
+ + + {loading && ( +
+
+ +
+
+ )} +
+ ); + }; + + return ( + <> + + + + {`${TITLE} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`} + +
+ {errorMessage && ( +

+ {errorMessage} +

+ )} + setActiveTab(value as ExceptionHistoryTabKey)} + className="w-full" + variant="default" + > + + {exceptionHistoryTabs.map((tab) => ( + + {tab.label} + + ))} + + + {renderGrid( + normalRows, + normalPageIndex, + normalPageCount, + handleNormalPageChange, + normalLoading, + nvGridRef, + )} + + + {renderGrid( + imrRows, + imrPageIndex, + imrPageCount, + handleImrPageChange, + imrLoading, + imrGridRef, + )} + + + {renderGrid( + hbRows, + hbPageIndex, + hbPageCount, + handleHbPageChange, + hbLoading, + hbGridRef, + )} + + +
+ + + + + + +
+
+ + + + + 예외처리 반려 + +
+ +