diff --git a/apps/st/src/components/module/CompactDataGrid.tsx b/apps/st/src/components/module/CompactDataGrid.tsx index c96f22973..6de651a1d 100644 --- a/apps/st/src/components/module/CompactDataGrid.tsx +++ b/apps/st/src/components/module/CompactDataGrid.tsx @@ -71,6 +71,38 @@ function createNumberComparer(dp: RealGridDataProviderComparer): (field: string }; } +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function realGridHeaderWithTooltip( + header: T, +): T { + const tip = header.tooltip?.trim(); + if (!tip) return header; + return { + ...header, + tooltip: tip, + showTooltip: header.showTooltip ?? true, + template: + header.template ?? + `${escapeHtml(header.text)}`, + }; +} + +function resolveColumnTooltip(column: { + tooltip?: string; + header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string }; +}): string | undefined { + const tip = column.tooltip?.trim() || column.header.tooltip?.trim(); + return tip && tip.length > 0 ? tip : undefined; +} + const CompactDataGrid = ({ columnInfo, fixedColumns, @@ -88,8 +120,10 @@ const CompactDataGrid = ({ }: DataGridProps) => { const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); + const columnsRef = useRef(columns); const rowDataRef = useRef[]>(rowData); const cellStyleCallbackRef = useRef(cellStyleCallback); + columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; @@ -155,6 +189,66 @@ const CompactDataGrid = ({ }); gridView.header.height = 60; + if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) { + (gridView.header as { showTooltip?: boolean }).showTooltip = true; + } + columns.forEach((column) => { + ( + gridView as { + setColumnProperty?: ( + col: string, + key: string, + value: unknown, + ) => void; + } + ).setColumnProperty?.( + column.name, + "header", + realGridHeaderWithTooltip({ + ...column.header, + tooltip: resolveColumnTooltip(column), + }), + ); + }); + ( + gridView as { + onShowHeaderTooltip?: ( + grid: unknown, + column: { + name?: string; + fieldName?: string; + header?: { tooltip?: string }; + }, + value: string, + layout?: { + name?: string; + fieldName?: string; + column?: string | { name?: string; fieldName?: string }; + }, + isEllipsis?: boolean, + ) => string | undefined; + } + ).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => { + const currentColumns = columnsRef.current; + const directTip = headerColumn?.header?.tooltip?.trim(); + if (directTip) return directTip; + + const layoutColumn = + typeof layout?.column === "string" + ? layout.column + : layout?.column?.name ?? layout?.column?.fieldName; + + const col = currentColumns.find( + (c) => + c.name === headerColumn?.name || + c.fieldName === headerColumn?.fieldName || + c.name === layout?.name || + c.fieldName === layout?.fieldName || + c.name === layoutColumn || + c.fieldName === layoutColumn, + ); + return col ? resolveColumnTooltip(col) : undefined; + }; // 헤더 체크박스 표시 여부 설정 gridView.setOptions({ @@ -235,7 +329,10 @@ const CompactDataGrid = ({ const colInfo: RealGridColumnInfo = { name: col.name, fieldName: col.fieldName, - header: col.header, + header: realGridHeaderWithTooltip({ + ...col.header, + tooltip: resolveColumnTooltip(col), + }), editable: col.editable, valueType: col.valueType, dataFormat: col.dataFormat, @@ -427,7 +524,10 @@ const CompactDataGrid = ({ fieldName={column.fieldName} valueType={column.valueType ? column.valueType : ValueType.TEXT} {...(column.valueType === ValueType.NUMBER && { numberFormat: column.dataFormat ? column.dataFormat : '#,##0.000' })} - header={column.header} + header={realGridHeaderWithTooltip({ + ...column.header, + tooltip: resolveColumnTooltip(column), + })} editable={column.editable} {...(column.styleName != null ? { styleName: column.styleName } diff --git a/apps/st/src/components/module/DataGrid.tsx b/apps/st/src/components/module/DataGrid.tsx index acadc607a..d513e90a9 100644 --- a/apps/st/src/components/module/DataGrid.tsx +++ b/apps/st/src/components/module/DataGrid.tsx @@ -81,6 +81,42 @@ function createNumberComparer( }; } +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +/** + * RealGrid 헤더 툴팁: `grid.header.showTooltip`이 비어 있으면 `column.header.showTooltip === true`인 컬럼만 표시됨. + * `tooltip`이 있는 헤더는 info-circle 아이콘이 함께 보이도록 template도 자동 구성한다. + */ +function realGridHeaderWithTooltip( + header: T, +): T { + const tip = header.tooltip?.trim(); + if (!tip) return header; + return { + ...header, + tooltip: tip, + showTooltip: header.showTooltip ?? true, + template: + header.template ?? + `${escapeHtml(header.text)}`, + }; +} + +function resolveColumnTooltip(column: { + tooltip?: string; + header: { text: string; tooltip?: string; showTooltip?: boolean; template?: string }; +}): string | undefined { + const tip = column.tooltip?.trim() || column.header.tooltip?.trim(); + return tip && tip.length > 0 ? tip : undefined; +} + /** 정렬 필드 값을 숫자로 파싱 (쉼표 제거) */ function toNum(v: unknown): number { if (v == null) return NaN; @@ -354,6 +390,7 @@ const DataGrid = ({ const { columns, columnLayout } = columnInfo; const gridRef = useRef(null); const fileInputRef = useRef(null); + const columnsRef = useRef(columns); const sortDirRef = useRef>({}); const originalRowsRef = useRef< Record[]> @@ -372,6 +409,7 @@ const DataGrid = ({ const onItemCheckedRef = useRef(onItemChecked); const onItemAllCheckedRef = useRef(onItemAllChecked); + columnsRef.current = columns; rowDataRef.current = rowData; cellStyleCallbackRef.current = cellStyleCallback; cellTooltipCallbackRef.current = cellTooltipCallback; @@ -813,6 +851,66 @@ const DataGrid = ({ GridFitStyle.EVEN; gridView.displayOptions.rowHeight = 30; gridView.header.height = 60; + if (columns.some((c) => Boolean(resolveColumnTooltip(c)))) { + (gridView.header as { showTooltip?: boolean }).showTooltip = true; + } + columns.forEach((column) => { + ( + gridView as { + setColumnProperty?: ( + col: string, + key: string, + value: unknown, + ) => void; + } + ).setColumnProperty?.( + column.name, + "header", + realGridHeaderWithTooltip({ + ...column.header, + tooltip: resolveColumnTooltip(column), + }), + ); + }); + ( + gridView as { + onShowHeaderTooltip?: ( + grid: unknown, + column: { + name?: string; + fieldName?: string; + header?: { tooltip?: string }; + }, + value: string, + layout?: { + name?: string; + fieldName?: string; + column?: string | { name?: string; fieldName?: string }; + }, + isEllipsis?: boolean, + ) => string | undefined; + } + ).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => { + const currentColumns = columnsRef.current; + const directTip = headerColumn?.header?.tooltip?.trim(); + if (directTip) return directTip; + + const layoutColumn = + typeof layout?.column === "string" + ? layout.column + : layout?.column?.name ?? layout?.column?.fieldName; + + const col = currentColumns.find( + (c) => + c.name === headerColumn?.name || + c.fieldName === headerColumn?.fieldName || + c.name === layout?.name || + c.fieldName === layout?.fieldName || + c.name === layoutColumn || + c.fieldName === layoutColumn, + ); + return col ? resolveColumnTooltip(col) : undefined; + }; // 헤더 체크박스 표시 여부 설정 gridView.setOptions({ @@ -917,7 +1015,7 @@ const DataGrid = ({ const colInfo: RealGridColumnInfo = { name: col.name, fieldName: col.fieldName, - header: col.header, + header: realGridHeaderWithTooltip(col.header), editable: col.editable, valueType: col.valueType, dataFormat: col.dataFormat, @@ -1334,6 +1432,48 @@ const DataGrid = ({ setTimeout(() => { const gv = gridRef.current?.gridView; if (!gv?.columnByName) return; + // 공식 문서: 헤더 툴팁 = header.showTooltip + onShowHeaderTooltip + ( + gv as { + onShowHeaderTooltip?: ( + grid: unknown, + column: { + name?: string; + fieldName?: string; + header?: { + tooltip?: string; + }; + }, + value: string, + layout?: { + name?: string; + fieldName?: string; + column?: string | { name?: string; fieldName?: string }; + }, + isEllipsis?: boolean, + ) => string | undefined; + } + ).onShowHeaderTooltip = (_grid, headerColumn, _value, layout) => { + const currentColumns = columnsRef.current; + const directTip = headerColumn?.header?.tooltip?.trim(); + if (directTip) return directTip; + + const layoutColumn = + typeof layout?.column === "string" + ? layout.column + : layout?.column?.name ?? layout?.column?.fieldName; + + const col = currentColumns.find( + (c) => + c.name === headerColumn?.name || + c.fieldName === headerColumn?.fieldName || + c.name === layout?.name || + c.fieldName === layout?.fieldName || + c.name === layoutColumn || + c.fieldName === layoutColumn, + ); + return col ? resolveColumnTooltip(col) : undefined; + }; // 공식 문서: 데이터 영역 툴팁 = renderer.showTooltip + onShowTooltip (displayOptions는 Ellipsis용이라 사용 안 함) if (cellTooltipCallbackRef.current) { ( @@ -1387,6 +1527,22 @@ const DataGrid = ({ renderer?: { showTooltip?: boolean }; } | null; if (gridCol) { + ( + gv as { + setColumnProperty?: ( + col: string, + key: string, + value: unknown, + ) => void; + } + ).setColumnProperty?.( + column.name, + "header", + realGridHeaderWithTooltip({ + ...column.header, + tooltip: resolveColumnTooltip(column), + }), + ); if (cellTooltipCallbackRef.current) { ( gv as { @@ -1456,7 +1612,10 @@ const DataGrid = ({ ? column.dataFormat : "#,##0.000", })} - header={column.header} + header={realGridHeaderWithTooltip({ + ...column.header, + tooltip: resolveColumnTooltip(column), + })} editable={column.editable} {...(column.width != null && { width: column.width, diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx index 1026fd82d..bc4e9d9e9 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -13,6 +13,10 @@ import { DialogHeader, DialogTitle, LoadingWithAnimation, + Tabs, + TabsContent, + TabsList, + TabsTrigger, } from "@common/components"; import DataGrid from "@/components/module/DataGrid"; @@ -20,6 +24,13 @@ import { VerificationResultHistoryPagination } from "@/components/module/Verific import type { CellStyleCallbackParams, ColumnInfo } from "@/components/type/DataGrid.type"; type VerificationResultGridRow = Record; +type VerificationHistoryTabKey = "NV" | "IMR" | "HR"; + +const verificationHistoryTabs: { value: VerificationHistoryTabKey; label: string }[] = [ + { value: "NV", label: "일반검증 내역" }, + { value: "IMR", label: "I-MR 검증내역" }, + { value: "HR", label: "HR 검증내역" }, +]; function prcsYnCodeToNumber(raw: string | number | undefined): number | undefined { if (raw === undefined || raw === null) return undefined; @@ -76,30 +87,40 @@ export function VerificationResultHistoryDialog({ title = "검증 결과 내역", emptyMessage = "표시할 검증 결과가 없습니다.", }: VerificationResultHistoryDialogProps) { + const [activeTab, setActiveTab] = useState("NV"); const [normalEntries, setNormalEntries] = useState([]); const [imrEntries, setImrEntries] = useState([]); + const [hrEntries, setHrEntries] = useState([]); const [normalPageIndex, setNormalPageIndex] = useState(0); const [imrPageIndex, setImrPageIndex] = useState(0); + const [hrPageIndex, setHrPageIndex] = useState(0); const [normalPageCount, setNormalPageCount] = useState(1); const [imrPageCount, setImrPageCount] = useState(1); + const [hrPageCount, setHrPageCount] = useState(1); const [normalLoading, setNormalLoading] = useState(false); const [imrLoading, setImrLoading] = useState(false); + const [hrLoading, setHrLoading] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const resetState = useCallback(() => { + setActiveTab("NV"); setNormalEntries([]); setImrEntries([]); + setHrEntries([]); setNormalPageIndex(0); setImrPageIndex(0); + setHrPageIndex(0); setNormalPageCount(1); setImrPageCount(1); + setHrPageCount(1); setNormalLoading(false); setImrLoading(false); + setHrLoading(false); setErrorMessage(null); }, []); const fetchVerificationResultPage = useCallback( - async (vrfcMthdCd: "NV" | "IMR", pageIndex: number) => { + async (vrfcMthdCd: VerificationHistoryTabKey, pageIndex: number) => { if (!queryContext) return null; const param: VerificationResultListRequest = { sewerBzmnCd: queryContext.sewerBzmnCd, @@ -126,26 +147,34 @@ export function VerificationResultHistoryDialog({ setErrorMessage(null); setNormalLoading(true); setImrLoading(true); + setHrLoading(true); setNormalPageIndex(0); setImrPageIndex(0); + setHrPageIndex(0); try { - const [nvRes, imrRes] = await Promise.all([ + const [nvRes, imrRes, hrRes] = await Promise.all([ fetchVerificationResultPage("NV", 0), fetchVerificationResultPage("IMR", 0), + fetchVerificationResultPage("HR", 0), ]); setNormalEntries(nvRes?.data?.verifyResultList ?? []); setImrEntries(imrRes?.data?.verifyResultList ?? []); + setHrEntries(hrRes?.data?.verifyResultList ?? []); setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1); setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1); + setHrPageCount(hrRes?.data?.pageInfo?.totalPages ?? 1); } catch { setNormalEntries([]); setImrEntries([]); + setHrEntries([]); setNormalPageCount(1); setImrPageCount(1); + setHrPageCount(1); setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다."); } finally { setNormalLoading(false); setImrLoading(false); + setHrLoading(false); } }, [fetchVerificationResultPage, queryContext]); @@ -187,6 +216,25 @@ export function VerificationResultHistoryDialog({ [fetchVerificationResultPage, imrPageIndex, queryContext], ); + const handleHrPageChange = useCallback( + async (pageIndex: number) => { + if (pageIndex === hrPageIndex || !queryContext) return; + setErrorMessage(null); + setHrLoading(true); + try { + const response = await fetchVerificationResultPage("HR", pageIndex); + setHrEntries(response?.data?.verifyResultList ?? []); + setHrPageCount(response?.data?.pageInfo?.totalPages ?? 1); + setHrPageIndex(pageIndex); + } catch { + setErrorMessage("HR 검증내역 조회 중 오류가 발생했습니다."); + } finally { + setHrLoading(false); + } + }, + [fetchVerificationResultPage, hrPageIndex, queryContext], + ); + useEffect(() => { if (!open) { resetState(); @@ -212,6 +260,7 @@ export function VerificationResultHistoryDialog({ const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]); const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]); + const hrRows = useMemo(() => mapRows(hrEntries), [hrEntries]); const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch< SetStateAction[]> @@ -269,14 +318,24 @@ export function VerificationResultHistoryDialog({ {errorMessage}

)} -
-

일반검증 내역

- {renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)} -
-
-

I-MR 검증내역

- {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)} -
+ setActiveTab(value as VerificationHistoryTabKey)} className="w-full" variant="default"> + + {verificationHistoryTabs.map((tab) => ( + + {tab.label} + + ))} + + + {renderGrid(normalRows, normalPageIndex, normalPageCount, handleNormalPageChange, normalLoading)} + + + {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)} + + + {renderGrid(hrRows, hrPageIndex, hrPageCount, handleHrPageChange, hrLoading)} + + diff --git a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx index f0334fee8..da4e74843 100644 --- a/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx +++ b/apps/st/src/components/statistics/input/PublicSewageFacilityInp.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useConfirm } from '@frontend-opensource/use-react-hooks'; @@ -131,6 +131,10 @@ type MainFacilityMappingSaveDraft = { initialPrxyCtrtCn: string; }; +const PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS = Array.from( + new Set(PublicSewageFactInpColumnList.map((tab) => tab.tabKey).filter(Boolean)), +); + // 하수도 통계 관리 > 06. 공공하수처리시설 const PublicSewageFacilityInp = () => { const { setMessage: alert } = useAlertStore((state) => state); @@ -149,6 +153,8 @@ const PublicSewageFacilityInp = () => { const [lastSearchedValues, setLastSearchedValues] = useState(null); const [tabStatusInfoMap, setTabStatusInfoMap] = useState>(new Map()); const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); + /** 탭키별 `engColNm -> columnGuide` 캐시 */ + const [columnGuidesByTab, setColumnGuidesByTab] = useState>>({}); const [toolbarSnapshotByTab, setToolbarSnapshotByTab] = useState>(() => new Map()); const [inputPeriodEnterable, setInputPeriodEnterable] = useState(true); const [periodCheckList, setPeriodCheckList] = useState([]); @@ -489,6 +495,36 @@ const PublicSewageFacilityInp = () => { const firstTabKey = PublicSewageFactInpColumnList[0]?.tabKey ?? 'ST00060001'; const searchParamRef = useRef(null); + useEffect(() => { + let cancelled = false; + void (async () => { + const entries = await Promise.all( + PUBLIC_SEWAGE_FACILITY_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => { + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== 'success' || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [tabKey, next] as const; + } catch { + return [tabKey, {}] as const; + } + }), + ); + if (cancelled) return; + setColumnGuidesByTab(Object.fromEntries(entries)); + })(); + return () => { + cancelled = true; + }; + }, []); + const commitToolbarSnapshotForLoadedTab = ( tabKey: string, statusInfo: SewerageStatusInfo | null, @@ -783,7 +819,7 @@ const PublicSewageFacilityInp = () => { const requestData: cancelSubmitRequest = { sewerBzmnCd: lastSearchedValues?.bsnmCd ?? '', ctpvNm: lastSearchedValues?.sido ?? '', - sggNm: lastSearchedValues?.gugun ?? '', + sggNm: lastSearchedValues?.gugun === 'ALL' ? '' : lastSearchedValues?.gugun ?? '', statsYr: lastSearchedValues?.year ?? '', mdfrId: userInfo.userUnqId, statsCd: statsCdForCancel, @@ -811,8 +847,15 @@ const PublicSewageFacilityInp = () => { const getGridColumnInfo = ( tab: ColumnInfoList, + guideTabKey?: string, ): ColumnInfo => ({ - columns: tab.columns ?? [], + columns: (tab.columns ?? []).map((col) => { + const guide = columnGuidesByTab[guideTabKey ?? tab.tabKey]?.[col.fieldName]?.trim(); + return { + ...col, + tooltip: guide && guide.length > 0 ? guide : undefined, + }; + }), columnLayout: tab.columnLayout ?? [], }); @@ -955,8 +998,8 @@ const PublicSewageFacilityInp = () => { return ( & { }; const REGIONAL_FACT_TAB_KEY = "ST00020001"; +const SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS = Array.from( + new Set(SewagePipeInpColumnInfoList.map((tab) => tab.tabKey).filter(Boolean)), +); function hasRegionalFactVerificationErrors(verification?: SeweragePipeInputVerificationResult | null): boolean { if (!verification) return false; @@ -355,6 +358,8 @@ const SewagePipeInp = () => { const [tabStatsCdMap, setTabStatsCdMap] = useState>(new Map()); const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); const [periodCheckList, setPeriodCheckList] = useState([]); + /** 탭키별 `engColNm -> columnGuide` 캐시 */ + const [columnGuidesByTab, setColumnGuidesByTab] = useState>>({}); /** API 연결 전에도 검색 후 툴바 등 UI를 쓸 수 있게 함 */ const [hasSearched, setHasSearched] = useState(false); /** checkInputPeriod — columnInfo useMemo가 입력기간 변경을 반영하도록 state로도 유지 */ @@ -417,6 +422,37 @@ const SewagePipeInp = () => { } }, [activeTabId, inputFocusNonce]); + useEffect(() => { + if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWAGE_PIPE) return; + let cancelled = false; + void (async () => { + const entries = await Promise.all( + SEWAGE_PIPE_COLUMN_GUIDE_TAB_KEYS.map(async (tabKey) => { + try { + const res = await statisticsApi.getColumnGuides(tabKey); + if (res.status !== "success" || !Array.isArray(res.data)) { + return [tabKey, {}] as const; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + return [tabKey, next] as const; + } catch { + return [tabKey, {}] as const; + } + }), + ); + if (cancelled) return; + setColumnGuidesByTab(Object.fromEntries(entries)); + })(); + return () => { + cancelled = true; + }; + }, [activeTabId]); + const syncTopStatusIfStillOnTab = (loadedTabKey: string, nextStatsCd: string) => { if (loadedTabKey === activeTabRef.current) { setStatsCd(nextStatsCd); @@ -445,6 +481,14 @@ const SewagePipeInp = () => { }; const columnInfo = useMemo(() => { + const applyColumnGuides = (cols: ColumnConfig[] | undefined, tabKey: string) => + (cols ?? []).map((col: ColumnConfig) => { + const guide = columnGuidesByTab[tabKey]?.[col.fieldName]?.trim(); + return { + ...col, + tooltip: guide && guide.length > 0 ? guide : undefined, + }; + }); return SewagePipeInpColumnInfoList.map((tab: ColumnInfoList) => { const canEdit = computeTabEnterable(tabStatusInfoMap.get(tab.tabKey) ?? null, inputPeriodEnterable) || isAdminInfo; @@ -458,16 +502,16 @@ const SewagePipeInp = () => { if (tab.left && tab.right) { return { ...tab, - left: { ...tab.left, columns: applyEditable(tab.left.columns) }, - right: { ...tab.right, columns: applyEditable(tab.right.columns) }, + left: { ...tab.left, columns: applyColumnGuides(applyEditable(tab.left.columns), tab.tabKey) }, + right: { ...tab.right, columns: applyColumnGuides(applyEditable(tab.right.columns), tab.tabKey) }, }; } return { ...tab, - columns: applyEditable(tab.columns), + columns: applyColumnGuides(applyEditable(tab.columns), tab.tabKey), }; }); - }, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable]); + }, [tabStatusInfoMap, isAdminInfo, inputPeriodEnterable, columnGuidesByTab]); const commitToolbarSnapshotForLoadedTab = ( tabKey: string, @@ -1251,7 +1295,7 @@ const SewagePipeInp = () => { const requestData: cancelSubmitRequest = { sewerBzmnCd: searchValues?.bsnmCd ?? '', ctpvNm: searchValues?.sido ?? '', - sggNm: searchValues?.gugun ?? '', + sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""), statsYr: searchValues?.year ?? '', mdfrId: userInfo.userUnqId, statsCd: tabStatsCdMap.get(activeTab) ?? statsCd, diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 4db880b03..4c048bf21 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -1,4 +1,4 @@ -import { useEffect,useLayoutEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useConfirm } from "@frontend-opensource/use-react-hooks"; @@ -57,6 +57,9 @@ type GridRowWithVerification = Record & { verificationResult?: SewerageSupplyRateVerificationResult; }; +/** 컬럼 가이드 API 조회용 통계코드 (하수도 보급률) */ +const SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD = "ST00010001"; + const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResult | null): boolean => { if (!verification) return false; return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0); @@ -119,6 +122,8 @@ const SeweragePenetrationRateInp = () => { /** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */ const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false); const [statsCd, setStatsCd] = useState(''); + /** `engColNm` → `columnGuide` (헤더 hover) */ + const [columnGuidesByEngCol, setColumnGuidesByEngCol] = useState>({}); const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null); const { confirm } = useConfirm(); const showGlobalProgress = useGlobalProgressStore((state) => state.show); @@ -149,6 +154,35 @@ const SeweragePenetrationRateInp = () => { setSearchFormRemountKey((k) => k + 1); }, [activeTabId, inputFocusNonce]); + useEffect(() => { + if (activeTabId !== STATISTICS_INPUT_URL_PATH.SEWERAGE_PENETRATION_RATE) { + return; + } + let cancelled = false; + void (async () => { + try { + const res = await statisticsApi.getColumnGuides(SEWERAGE_PENETRATION_COLUMN_GUIDE_STATS_CD); + if (cancelled) return; + if (res.status !== "success" || !Array.isArray(res.data)) { + setColumnGuidesByEngCol({}); + return; + } + const next: Record = {}; + for (const item of res.data) { + const key = item.engColNm?.trim(); + const guide = item.columnGuide?.trim(); + if (key && guide) next[key] = guide; + } + setColumnGuidesByEngCol(next); + } catch { + if (!cancelled) setColumnGuidesByEngCol({}); + } + })(); + return () => { + cancelled = true; + }; + }, [activeTabId]); + const { isAdminInfo } = useAdminStore(); const getGridDataRef = useRef<(() => Record[]) | null>(null); @@ -167,9 +201,17 @@ const SeweragePenetrationRateInp = () => { const { columns, columnLayout } = SeweragePenetrationRateInpColumn; const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const })); + const withHeaderTooltips = base.map((col) => { + const guide = columnGuidesByEngCol[col.fieldName]?.trim(); + return { + ...col, + tooltip: guide && guide.length > 0 ? guide : undefined, + }; + }); + return { columnLayout, - columns: base.map((col) => { + columns: withHeaderTooltips.map((col) => { if (col.fieldName === 'pbadmsWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnWholPpltnCnt', '#,##0') }; if (col.fieldName === 'domplWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnDomplWholPpltnCnt', '#,##0') }; if (col.fieldName === 'frgnrWholPpltnCnt') return { ...col, footer: footerWithApiDefault('sewerBzmnFrgnrWholPpltnCnt', '#,##0') }; @@ -178,7 +220,7 @@ const SeweragePenetrationRateInp = () => { return col; }), }; - }, [isEnterable, isAdminInfo, footerFromApi]); + }, [isEnterable, isAdminInfo, footerFromApi, columnGuidesByEngCol]); const handleReset = () => { setRowData([]); @@ -276,7 +318,7 @@ const SeweragePenetrationRateInp = () => { if (!values) return; postVerificationSearchRef.current = null; void handleSearch(values); - + }, [searchFormRemountKey]); const SubmitExceptionFunc = async (submitInfo: StatisticsInputSubmitInfoFormData) => { @@ -291,7 +333,7 @@ const SeweragePenetrationRateInp = () => { ...submitInfo, sewerBzmnCd: searchValues?.bsnmCd ?? '', ctpvNm: searchValues?.sido ?? '', - sggNm: searchValues?.gugun ?? '', + sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''), statsYr: searchValues?.year ?? '', userId: userInfo.userUnqId, statsCd: statsCd, @@ -428,7 +470,7 @@ const SeweragePenetrationRateInp = () => { const requestData: SewerageSupplyRateSaveRequest = { sewerBzmnCd: searchValues?.bsnmCd ?? '', ctpvNm: searchValues?.sido ?? '', - sggNm: searchValues?.gugun ?? '', + sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''), statsYr: searchValues?.year ?? '', userId: userInfo.userUnqId, submitList: convertedData, @@ -579,7 +621,7 @@ const SeweragePenetrationRateInp = () => { const requestData: SewerageSupplyRateSaveRequest = { sewerBzmnCd: searchValues?.bsnmCd ?? '', // 하수도 사업자 코드 ctpvNm: searchValues?.sido ?? '', // 시도 - sggNm: searchValues?.gugun ?? '', + sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ''), statsYr: searchValues?.year ?? '', // 통계 연도 userId: userInfo.userUnqId, // 사용자 saveList: convertedData, // 저장 데이터 @@ -624,7 +666,7 @@ const SeweragePenetrationRateInp = () => { const requestData: cancelSubmitRequest = { sewerBzmnCd: searchValues?.bsnmCd ?? '', ctpvNm: searchValues?.sido ?? '', - sggNm: searchValues?.gugun ?? '', + sggNm: searchValues?.gugun === "ALL" ? "" : (searchValues?.gugun ?? ""), statsYr: searchValues?.year ?? '', mdfrId: userInfo.userUnqId, statsCd: statsCd, diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx index fdb9a4e1a..2c4e68dd3 100644 --- a/apps/st/src/components/statistics/management/LogHistory.tsx +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -8,6 +8,7 @@ import useSWR from "swr"; import { useAlertStore } from "@/shared/lib"; import { CodeSelectWithAll } from "@/entities/code"; import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { StatisticsLogResponse } from "@/entities/statistics/model/statistics-interface"; import type { ApiResponse, SearchCondition, @@ -129,7 +130,7 @@ const LogHistory = () => { const selectedBsnm = value === "ALL" ? (sidoList?.data.sidoList.find((v) => v.sido === sido) - ?.bsnmCd ?? "") + ?.bsnmCd ?? "") : (listForBsnm.find((v) => v.gugun === value)?.bsnmCd ?? ""); setGugun(value); setBsnmCd(selectedBsnm); @@ -150,8 +151,8 @@ const LogHistory = () => { ...(bsnmCd && { sewerBzmnCd: bsnmCd }), ...(statsDataChgComCd && statsDataChgComCd !== "ALL" && { - statsDataChgComCd, - }), + statsDataChgComCd, + }), }; try { @@ -160,15 +161,19 @@ const LogHistory = () => { const response = await statisticsApi.getLogList(param); if (response.status === "success") { - const lostList = response.data?.statsLogList ?? []; + const logListRaw = response.data?.statsLogList ?? []; + const logList = logListRaw.map((l) => ({ + ...l, + sggNm: String(l.sggNm ?? "").trim() || "-", + })); - setRowData(toGridRows(lostList)); + setRowData(toGridRows(logList)); setTotalPages(response.data?.pageInfo?.totalPages ?? 0); setPageIndex(Math.max(0, page1Based - 1)); } else { alert( response.message || - "데이터 조회 중 오류가 발생했습니다.", + "데이터 조회 중 오류가 발생했습니다.", ); } } catch { @@ -331,7 +336,7 @@ const LogHistory = () => { columnInfo={LogHistoryColumn} isExcelImport={false} rowData={rowData} - setRowData={() => {}} + setRowData={() => { }} onRowClick={handleRowClick} /> ): "INPT" | "VRFC" return "INPT"; } -/** API `YYYYMMDD` 등 → Date */ function parseYmdToDate(ymd: unknown): Date | undefined { const s = String(ymd ?? "").replace(/\D/g, ""); if (s.length !== 8) return undefined; @@ -71,10 +71,11 @@ const StatisticsInputManagement = () => { setIsLoading(true); const params: StatsInputPeriodListRequest = { statsYr: values.year, + seType: + values.clsfCd && values.clsfCd !== "ALL" + ? values.clsfCd + : "", }; - if (values.clsfCd && values.clsfCd !== "ALL") { - params.seType = values.clsfCd; - } const res = await statisticsApi.getStatsInputPeriodList(params); if (res.status === "success" && Array.isArray(res.data)) { const rowsForGrid = res.data.map((row) => ({ @@ -112,6 +113,11 @@ const StatisticsInputManagement = () => { }; const handleRowClick = (row: Record) => { + const inptYmdSnRaw = row.inptYmdSn; + const inptYmdSnNum = + typeof inptYmdSnRaw === "number" + ? inptYmdSnRaw + : parseInt(String(inptYmdSnRaw ?? ""), 10); const seCdRaw = row.seCd; const seCdNum = typeof seCdRaw === "number" @@ -121,6 +127,7 @@ const StatisticsInputManagement = () => { setPeriodDialogInitial({ statsYr: String(row.statsYr ?? ""), seType: rowSeTypeToCode(row), + inptYmdSn: Number.isFinite(inptYmdSnNum) ? inptYmdSnNum : 0, seCd: Number.isFinite(seCdNum) ? seCdNum : 0, inptBgngYmd: parseYmdToDate(row.inptBgngYmd), inptEndYmd: parseYmdToDate(row.inptEndYmd), @@ -159,6 +166,13 @@ const StatisticsInputManagement = () => { header: { text: "종료일" }, editable: false, }, + { + name: "inptYmdSn", + fieldName: "inptYmdSn", + header: { text: "inptYmdSn" }, + visible: false, + editable: false, + }, ]; const columnLayout = [ diff --git a/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx b/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx index 06a70a10d..a6765f87e 100644 --- a/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx +++ b/apps/st/src/components/statistics/management/StatisticsInputPeriodDialog.tsx @@ -33,6 +33,7 @@ import { getUserInfo } from "../../../../../../packages/common/components/src/ap export type StatisticsInputPeriodDialogValues = { statsYr: string; seType: "INPT" | "VRFC"; + inptYmdSn: number; seCd: number; inptBgngYmd: Date | undefined; inptEndYmd: Date | undefined; @@ -137,15 +138,22 @@ export function StatisticsInputPeriodDialog({ try { setIsSubmitting(true); const userInfo = await getUserInfo(); + const inptYmdSn = + mode === "create" + ? 0 + : (initial && Number.isFinite(initial.inptYmdSn) ? initial.inptYmdSn : 0); const param: StatsInputPeriodSaveRequest = { statsYr: yr, seType, + inptYmdSn, seCd: seCdParsed, inptBgngYmd: bgng, inptEndYmd: end, rgtrId: userInfo.userUnqId, }; - const res = await statisticsApi.saveStatsInputPeriod(param); + const res = isCreate + ? await statisticsApi.saveStatsInputPeriod(param) + : await statisticsApi.updateStatsInputPeriod(param); if (res.status === "success") { alert(`${isCreate ? '등록 되었습니다.' : '수정 되었습니다.'}`); onOpenChange(false); @@ -184,6 +192,8 @@ export function StatisticsInputPeriodDialog({ const param: StatsInputPeriodDeleteRequest = { statsYr: yr, seType: initial.seType, + inptYmdSn: + Number.isFinite(initial.inptYmdSn) ? initial.inptYmdSn : 0, seCd, inptBgngYmd: bgng, inptEndYmd: end, diff --git a/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx b/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx index b929e1e3f..f5d6fb32f 100644 --- a/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx +++ b/apps/st/src/components/statistics/review/PublicSewageFacilityRvw.tsx @@ -1,47 +1,273 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; -import { DataGrid } from "@rsone/components"; +import { LoadingWithAnimation } from "@common/components"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; +import { useAlertStore } from "@/shared/lib"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; +import type { PublicSewageFacilityReviewItem } from "@/entities/statistics/model/statistics-public-sewage-facility-review-interface"; +import type { StatsStatusInfo } from "@/entities/statistics/model/statistics-sewage-pipe-review-interface"; +import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface"; +import DataGrid from "@/components/module/DataGrid"; import SearchForm from "@/components/module/SearchForm"; -import SaveButton from "@/components/button/SaveButton"; -import ApproveButton from "@/components/button/ApproveButton"; +import SubmitButton from "@/components/button/SumitButton"; import ReviewOpinionButton from "@/components/button/ReviewOpinionButton"; import ReturnButton from "@/components/button/ReturnButton"; +import { ReturnReasonDialog } from "@/components/statistics/review/ReturnReasonDialog"; +import { + ReviewOpinionDialog, + type ReviewOpinionDialogContext, +} from "@/components/statistics/review/ReviewOpinionDialog"; +import type { SearchFormValues } from "@/components/type/SearchForm.type"; +import { toGridRows } from "@/components/util/grid/gridFunction"; import { PublicSewageFacilityInfoList } from "@/components/util/column/review/PublicSewageFacilityRvwColumn"; // 하수도 통계 관리 06.공공하수처리시설 const PublicSewageFacilityRvw = () => { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); const [rowData, setRowData] = useState[]>([]); + const [isLoading, setIsLoading] = useState(false); + const [statsStatusInfo, setStatsStatusInfo] = useState({}); + const [reviewOpinionOpen, setReviewOpinionOpen] = useState(false); + const [returnDialogOpen, setReturnDialogOpen] = useState(false); + const [reviewOpinionContext, setReviewOpinionContext] = + useState(null); + const [latestReviewOpinion, setLatestReviewOpinion] = useState(""); + const [latestProcessHistory, setLatestProcessHistory] = useState(""); + const [lastSearchValues, setLastSearchValues] = useState(null); - const approveFunc = () => { - console.log('approveFunc'); + const columnInfo = useMemo(() => { + const { columns, columnLayout, fixedColumns, rowStartNum } = PublicSewageFacilityInfoList; + return { + columnLayout, + fixedColumns, + rowStartNum, + columns: columns.map((col) => ({ + ...col, + editable: false as const, + })), + }; + }, []); + + const { showReviewOpinionButton, showReturnButton, showSubmitButton } = useMemo(() => { + const submit = statsStatusInfo.submitStatusCd != null ? String(statsStatusInfo.submitStatusCd) : ""; + const prvn = statsStatusInfo.prvnStatusCd != null ? String(statsStatusInfo.prvnStatusCd) : ""; + const submitIs3 = submit === "3"; + const reviewSubmitVisible = submitIs3 && (prvn === "0" || prvn === "1"); + return { + showReviewOpinionButton: reviewSubmitVisible, + showSubmitButton: reviewSubmitVisible, + showReturnButton: submitIs3 && prvn !== "3", + }; + }, [statsStatusInfo]); + + const showActionBar = + showReviewOpinionButton || showReturnButton || showSubmitButton; + + const handleReset = () => { + setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + setLastSearchValues(null); }; - const saveFunc = () => { - console.log('submit'); + const handleSearch = async (values: SearchFormValues) => { + if (!values.sido || values.sido === "ALL") { + alert("시도를 선택해주세요."); + return; + } + setLastSearchValues(values); + + try { + setIsLoading(true); + + const response = await statisticsManagementApi.getPublicSewageFacilityReviewList({ + ctpvNm: values.sido, + sggNm: values.gugun === "ALL" ? "" : values.gugun, + sewerBzmnCd: values.bsnmCd, + statsYr: values.year, + fctName: values.fctName, + }); + + if (response.status === "success" && response.data) { + const rawList = response.data.publicSewageList; + const convertedData: PublicSewageFacilityReviewItem[] = rawList.map((item) => ({ + ...item, + impdFcltPstnCn: item.fcltPstnCn ?? "", + prxyBzentyNmCn: item.prxyBzentyNmCn ?? "", + prxyCtrtCn: item.prxyCtrtCn ?? "", + intgPrcsFcltCn: item.intgPrcsFcltCn ?? "", + rmrk: item.rmrk ?? "", + })) as PublicSewageFacilityReviewItem[]; + + setStatsStatusInfo(response.data.statsStatusInfo); + setReviewOpinionContext({ + statsYr: values.year ?? "", + sewerBzmnCd: values.bsnmCd ?? "", + statsCd: response.data.statsStatusInfo.statsCd ?? "", + }); + setLatestReviewOpinion( + response.data.statsStatusInfo.splmntOpnnCn ?? "", + ); + setLatestProcessHistory( + response.data.statsStatusInfo.wrterOpnnCn ?? "", + ); + setRowData(toGridRows(convertedData as unknown as object[])); + } else { + setRowData([]); + setStatsStatusInfo({}); + setReviewOpinionContext(null); + setLatestReviewOpinion(""); + setLatestProcessHistory(""); + alert(response.message || "데이터가 없습니다."); + } + } catch (error) { + console.error("공공하수처리시설 조회 중 에러 발생:", error); + alert("데이터 조회 중 오류가 발생했습니다."); + } finally { + setIsLoading(false); + } }; - const reviewOpinionFunc = () => { - console.log('reviewOpinionFunc'); + const submitFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 제출할 수 있습니다."); + return; + } + + if (!(await confirm("제출 하시겠습니까?"))) { + return; + } + + try { + const userInfo = await getUserInfo(); + const response = await statisticsApi.submitReview({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + mdfrId: userInfo.userUnqId, + }); + + if (response.status !== "success") { + alert(response.message || "제출에 실패했습니다."); + return; + } + + alert(response.message || "제출되었습니다."); + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } + } catch { + alert("제출 중 오류가 발생했습니다."); + } }; - const returnFunc = () => { - console.log('returnFunc'); + const reviewOpinionFunc = async () => { + if (reviewOpinionContext == null) { + alert("조회 후 검토의견을 작성할 수 있습니다."); + return; + } + + try { + const detailRes = await statisticsApi.getReviewOpinionDetail({ + statsYr: reviewOpinionContext.statsYr, + sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, + statsCd: reviewOpinionContext.statsCd, + }); + + if (detailRes.status !== "success") { + alert(detailRes.message || "검토의견 조회에 실패했습니다."); + return; + } + + const row = detailRes.data?.[0]; + setLatestReviewOpinion(row?.splmntOpnnCn ?? ""); + setLatestProcessHistory(row?.wrterOpnnCn ?? ""); + setReviewOpinionOpen(true); + } catch { + alert("검토의견 조회 중 오류가 발생했습니다."); + } + }; + + const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { + try { + const response = await statisticsApi.saveReviewOpinion(param); + if (response.status !== "success") { + alert(response.message || "저장에 실패했습니다."); + return; + } + alert(response.message || "저장되었습니다."); + setStatsStatusInfo((prev) => ({ + ...prev, + splmntOpnnCn: param.splmntOpnnCn, + wrterOpnnCn: param.wrterOpnnCn, + })); + setLatestReviewOpinion(param.splmntOpnnCn); + setLatestProcessHistory(param.wrterOpnnCn); + } catch { + alert("저장 중 오류가 발생했습니다."); + } + }; + + const returnFunc = () => setReturnDialogOpen(true); + + const handleSaveReturnReason = async () => { + if (lastSearchValues != null) { + await handleSearch(lastSearchValues); + } }; return ( - <> - -
-
- - - - +
+ + {showActionBar && ( +
+
+ {showReviewOpinionButton && ( + + )} + {showReturnButton && } + {showSubmitButton && } +
-
- - + )} + + {isLoading && ( +
+
+ +
+
+ )} + + + + +
); }; diff --git a/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx b/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx index b2955bc9a..05df3752b 100644 --- a/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx +++ b/apps/st/src/components/statistics/review/ReturnReasonDialog.tsx @@ -1,6 +1,8 @@ "use client"; import { useEffect, useState } from "react"; +import { getUserInfo } from "../../../../../../packages/common/components/src/api"; +import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { Button, @@ -14,25 +16,47 @@ import { Textarea, } from "@common/components"; +import type { ReviewReturnSaveRequest } from "@/entities/statistics/model/statistics-interface"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import { useAlertStore } from "@/shared/lib"; + +export type ReturnReasonDialogContext = Pick< + ReviewReturnSaveRequest, + "statsYr" | "sewerBzmnCd" | "statsCd" +>; + export interface ReturnReasonDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - onSave?: (reason: string) => void | Promise; + context: ReturnReasonDialogContext | null; + onSave?: () => void | Promise; } export function ReturnReasonDialog({ open, onOpenChange, + context, onSave, }: ReturnReasonDialogProps) { + const { setMessage: alert } = useAlertStore((state) => state); + const { confirm } = useConfirm(); const [reason, setReason] = useState(""); const [isSaving, setIsSaving] = useState(false); const [reasonError, setReasonError] = useState(false); - useEffect(() => { - if (!open) return; + const resetForm = () => { setReason(""); setReasonError(false); + }; + + const handleDialogOpenChange = (nextOpen: boolean) => { + if (!nextOpen) resetForm(); + onOpenChange(nextOpen); + }; + + useEffect(() => { + if (!open) return; + resetForm(); }, [open]); const handleSave = async () => { @@ -41,24 +65,65 @@ export function ReturnReasonDialog({ return; } + if ( + context == null || + context.statsYr === "" || + context.sewerBzmnCd === "" || + context.statsCd === "" + ) { + alert("시도·시군구·년도로 조회한 뒤 반려 처리할 수 있습니다."); + return; + } + + if (!(await confirm("반려 처리 하시겠습니까?"))) { + return; + } + try { setIsSaving(true); - await onSave?.(reason); + let userInfo: Awaited>; + try { + userInfo = await getUserInfo(); + } catch { + alert("사용자 정보를 불러오지 못했습니다. 다시 시도해주세요."); + return; + } + + const param: ReviewReturnSaveRequest = { + type: "REVIEW", + statsYr: context.statsYr, + sewerBzmnCd: context.sewerBzmnCd, + statsCd: context.statsCd, + rjctOpnnCn: reason, + mdfrId: userInfo.userUnqId, + }; + + const response = await statisticsApi.saveReviewReturn(param); + if (response.status !== "success") { + alert(response.message || "반려 처리 중 오류가 발생했습니다."); + return; + } + + alert(response.message || "반려 처리 되었습니다."); + await onSave?.(); + resetForm(); onOpenChange(false); + } catch { + alert("반려 처리 중 오류가 발생했습니다."); } finally { setIsSaving(false); } }; return ( - + - 반려 사유 + 반려
- +