diff --git a/apps/st/src/app/providers/app-provider.tsx b/apps/st/src/app/providers/app-provider.tsx index 70e69eee8..4e64d241c 100644 --- a/apps/st/src/app/providers/app-provider.tsx +++ b/apps/st/src/app/providers/app-provider.tsx @@ -12,6 +12,7 @@ import { AlertDialog } from "@/shared/ui/alert-dialog"; import { PromptDialog } from "@/shared/ui/prompt-dialog"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import { SelectPromptDialog } from "@/shared/ui/select-prompt-dialog"; +import { GlobalProgressOverlay } from "@/shared/ui/global-progress-overlay"; import { PromptProvider, StoreProvider } from "@/shared/providers"; import { AlertDialog as RsoneAlertDialog } from "@rsone/components"; @@ -36,6 +37,7 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => { + diff --git a/apps/st/src/components/button/VerificationResultButton.tsx b/apps/st/src/components/button/VerificationResultButton.tsx new file mode 100644 index 000000000..580f6d0d0 --- /dev/null +++ b/apps/st/src/components/button/VerificationResultButton.tsx @@ -0,0 +1,16 @@ +import { Button } from "@common/components"; + +interface VerificationResultButtonProps { + VerificationResultButtonFunc: () => void; +} + +const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => { + return ( + + ); +}; + +export default VerificationResultButton; \ No newline at end of file diff --git a/apps/st/src/components/css/DataGrid.css b/apps/st/src/components/css/DataGrid.css index 8d56c48f8..c1b346ad5 100644 --- a/apps/st/src/components/css/DataGrid.css +++ b/apps/st/src/components/css/DataGrid.css @@ -16,12 +16,6 @@ @apply !bg-[#5987bf] !text-[#fff]; } - .rg-grid .rg-header .rg-table tr td, - .rg-grid .rg-head .rg-table tr td { - border: none; - border-bottom: 1px solid #e5e7eb; - } - .rg-grid .rg-body .rg-table .rg-data-row .rg-data-cell { @apply !bg-white; } @@ -138,4 +132,10 @@ .rg-grid .rg-body .rg-table tr.rg-alternate-row td, .rg-grid .rg-body .rg-table tr.rg-alternate-row .rg-data-cell { background-color: #ffffff !important; +} + +.rg-grid .rg-header .rg-table tr td, +.rg-grid .rg-head .rg-table tr td { + border: none; + border-bottom: 1px solid #e5e7eb; } \ No newline at end of file diff --git a/apps/st/src/components/module/SearchForm.tsx b/apps/st/src/components/module/SearchForm.tsx index 4d16417ed..b5cdb06a4 100644 --- a/apps/st/src/components/module/SearchForm.tsx +++ b/apps/st/src/components/module/SearchForm.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from "react"; import useSWR from "swr"; import { useAlertStore, useUserStore } from "@/shared/lib"; -import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { ApiResponse, SearchCondition, @@ -33,14 +33,14 @@ import { doList } from "../util/column/input/DoList"; const getSidoListFetcher = async ( key: [string, string], ): Promise> => { - return statisticsAPi.getSidoList({ instSeCd: key[1] }); + return statisticsApi.getSidoList({ instSeCd: key[1] }); }; const getSggListFetcher = async ( key: [string, string, string, string], ): Promise> => { const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y"; - return statisticsAPi.getSggAllList(key[1], { + return statisticsApi.getSggAllList(key[1], { instSeCd: key[2], gugunSkipYn, }); diff --git a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx index 13b436ce4..d8647037e 100644 --- a/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx +++ b/apps/st/src/components/module/SidoSggFacilityNameSearchForm.tsx @@ -19,7 +19,7 @@ import CompactDataGrid from "./CompactDataGrid"; import SaveButton from "../button/SaveButton"; import { useAlertStore } from "@/shared/lib"; -import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { ApiResponse, SearchCondition, @@ -91,13 +91,13 @@ const adminRegionColumnInfo: ColumnInfo = { }; const getSidoListFetcher = async (): Promise> => { - return statisticsAPi.getSidoList(); + return statisticsApi.getSidoList(); }; const getSggAllListFetcher = async ( key: [string, string], ): Promise> => { - return statisticsAPi.getSggAllList(key[1]); + return statisticsApi.getSggAllList(key[1]); }; type SidoSggFacilityNameSearchFormProps = { diff --git a/apps/st/src/components/module/VerificationResultDialog.tsx b/apps/st/src/components/module/VerificationResultDialog.tsx new file mode 100644 index 000000000..5e11dd6ac --- /dev/null +++ b/apps/st/src/components/module/VerificationResultDialog.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@common/components"; + +export interface VerificationResultDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function VerificationResultDialog({ + open, + onOpenChange, +}: VerificationResultDialogProps) { + return ( + + + + 검증 결과 + +
+ + + + + + +
+ ); +} + diff --git a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx new file mode 100644 index 000000000..3be4a6a9e --- /dev/null +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + LoadingWithAnimation, +} from "@common/components"; +import DataGrid from "@/components/module/DataGrid"; +import { VerificationResultHistoryPagination } from "@/components/module/VerificationResultHistoryPagination"; +import type { ColumnInfo } from "@/components/type/DataGrid.type"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; +import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; + +type VerificationResultGridRow = Record; + +const PRCS_YN_LABEL: Record = { + "0": "미통과", + "1": "통과", + "2": "예외처리", + "3": "반려", +}; + +const verificationResultColumnInfo: 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: "prcsYn", fieldName: "prcsYn", 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 }, + ], + columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYn", "pbcpAprvYn", "regDt", "mdfcnDt"], +}; + +export interface VerificationResultHistoryDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + queryContext?: { + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; + statsYr: string; + statsCd: string; + } | null; + title?: string; + emptyMessage?: string; +} + +export function VerificationResultHistoryDialog({ + open, + onOpenChange, + queryContext, + title = "검증 결과 내역", + emptyMessage = "표시할 검증 결과가 없습니다.", +}: VerificationResultHistoryDialogProps) { + const [normalEntries, setNormalEntries] = useState([]); + const [imrEntries, setImrEntries] = useState([]); + const [normalPageIndex, setNormalPageIndex] = useState(0); + const [imrPageIndex, setImrPageIndex] = useState(0); + const [normalPageCount, setNormalPageCount] = useState(1); + const [imrPageCount, setImrPageCount] = useState(1); + const [normalLoading, setNormalLoading] = useState(false); + const [imrLoading, setImrLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const resetState = useCallback(() => { + setNormalEntries([]); + setImrEntries([]); + setNormalPageIndex(0); + setImrPageIndex(0); + setNormalPageCount(1); + setImrPageCount(1); + setNormalLoading(false); + setImrLoading(false); + setErrorMessage(null); + }, []); + + const fetchVerificationResultPage = useCallback( + async (vrfcMthdCd: "NV" | "IMR", 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: "0,2,3", + page: pageIndex + 1, + size: 30, + }; + return statisticsApi.getVerificationResultList(param); + }, + [queryContext], + ); + + const loadInitial = useCallback(async () => { + if (!queryContext) { + setErrorMessage("조회 조건이 없어 검증 결과를 불러올 수 없습니다."); + return; + } + setErrorMessage(null); + setNormalLoading(true); + setImrLoading(true); + setNormalPageIndex(0); + setImrPageIndex(0); + try { + const [nvRes, imrRes] = await Promise.all([ + fetchVerificationResultPage("NV", 0), + fetchVerificationResultPage("IMR", 0), + ]); + setNormalEntries(nvRes?.data?.verifyResultList ?? []); + setImrEntries(imrRes?.data?.verifyResultList ?? []); + setNormalPageCount(nvRes?.data?.pageInfo?.totalPages ?? 1); + setImrPageCount(imrRes?.data?.pageInfo?.totalPages ?? 1); + } catch { + setNormalEntries([]); + setImrEntries([]); + setNormalPageCount(1); + setImrPageCount(1); + setErrorMessage("검증 결과 내역 조회 중 오류가 발생했습니다."); + } finally { + setNormalLoading(false); + setImrLoading(false); + } + }, [fetchVerificationResultPage, queryContext]); + + const handleNormalPageChange = useCallback( + async (pageIndex: number) => { + if (pageIndex === normalPageIndex || !queryContext) return; + setErrorMessage(null); + setNormalLoading(true); + try { + const response = await fetchVerificationResultPage("NV", pageIndex); + setNormalEntries(response?.data?.verifyResultList ?? []); + setNormalPageCount(response?.data?.pageInfo?.totalPages ?? 1); + setNormalPageIndex(pageIndex); + } catch { + setErrorMessage("일반검증 내역 조회 중 오류가 발생했습니다."); + } finally { + setNormalLoading(false); + } + }, + [fetchVerificationResultPage, normalPageIndex, queryContext], + ); + + const handleImrPageChange = useCallback( + async (pageIndex: number) => { + if (pageIndex === imrPageIndex || !queryContext) return; + setErrorMessage(null); + setImrLoading(true); + try { + const response = await fetchVerificationResultPage("IMR", pageIndex); + setImrEntries(response?.data?.verifyResultList ?? []); + setImrPageCount(response?.data?.pageInfo?.totalPages ?? 1); + setImrPageIndex(pageIndex); + } catch { + setErrorMessage("I-MR 검증내역 조회 중 오류가 발생했습니다."); + } finally { + setImrLoading(false); + } + }, + [fetchVerificationResultPage, imrPageIndex, queryContext], + ); + + useEffect(() => { + if (!open) { + resetState(); + return; + } + void loadInitial(); + }, [loadInitial, open, resetState]); + + const mapRows = (entries: VerificationResultReportItem[]): VerificationResultGridRow[] => + entries.map((item) => ({ + ctpvNm: item.ctpvNm, + sggNm: item.sggNm, + vrfcTrgtNm: item.vrfcTrgtNm, + rsnExpln: item.rsnExpln, + nowVlCn: item.nowVlCn ?? "-", + compVlCn: item.compVlCn ?? "-", + prcsYn: PRCS_YN_LABEL[item.prcsYn] ?? item.prcsYn, + pbcpAprvYn: item.pbcpAprvYn, + regDt: (item.regDt ?? "").slice(0, 16), + mdfcnDt: (item.mdfcnDt ?? "").slice(0, 16), + })); + + const normalRows = useMemo(() => mapRows(normalEntries), [normalEntries]); + const imrRows = useMemo(() => mapRows(imrEntries), [imrEntries]); + + const noopSetRows = ((_: SetStateAction[]>) => {}) as Dispatch< + SetStateAction[]> + >; + + const renderGrid = ( + rows: VerificationResultGridRow[], + pageIndex: number, + pageCount: number, + onPageIndexChange: (page: number) => void, + loading: boolean, + ) => { + if (!loading && rows.length === 0) { + return

{emptyMessage}

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

+ {errorMessage} +

+ )} +
+

일반검증 내역

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

I-MR 검증내역

+ {renderGrid(imrRows, imrPageIndex, imrPageCount, handleImrPageChange, imrLoading)} +
+
+ + + + + +
+
+ ); +} diff --git a/apps/st/src/components/module/VerificationResultHistoryPagination.tsx b/apps/st/src/components/module/VerificationResultHistoryPagination.tsx new file mode 100644 index 000000000..e3fa59851 --- /dev/null +++ b/apps/st/src/components/module/VerificationResultHistoryPagination.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { + ChevronLeftIcon, + ChevronRightIcon, + DoubleArrowLeftIcon, + DoubleArrowRightIcon, +} from "@radix-ui/react-icons"; +import { Button, cn } from "@common/components"; + +export type VerificationResultHistoryPaginationProps = { + pageCount: number; + pageIndex: number; + onPageIndexChange: (pageIndex: number) => void; + className?: string; +}; + +export function VerificationResultHistoryPagination({ + pageCount, + pageIndex: currentPage, + onPageIndexChange, + className, +}: VerificationResultHistoryPaginationProps) { + const maxVisiblePages = 10; + let startPage = Math.max(0, currentPage - maxVisiblePages / 2); + const endPage = Math.min(pageCount, startPage + maxVisiblePages); + + if (endPage - startPage < maxVisiblePages) { + startPage = Math.max(0, endPage - maxVisiblePages); + } + + const pageNumbers = Array(endPage - startPage) + .fill(0) + .map((_, i) => startPage + i); + + const canPrevious = currentPage > 0; + const canNext = pageCount > 0 && currentPage < pageCount - 1; + + return ( +
+ + +
+ {pageNumbers.map((pageNumber) => ( + + ))} +
+ + +
+ ); +} diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index 5b8d9c678..c37700dae 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -6,16 +6,18 @@ import DataGrid from "@/components/module/DataGrid"; import SubmitButton from "@/components/button/SumitButton"; import SaveButton from "@/components/button/SaveButton"; import LastYearDataButton from "@/components/button/LastYearDataButton"; -import SubmitExceptionButton from "@/components/button/SubmitExceptionButton"; import CancelSubmitButton from "@/components/button/CancelSubmitButton"; +import VerificationResultButton from "@/components/button/VerificationResultButton"; +import { VerificationResultHistoryDialog } from "@/components/module/VerificationResultHistoryDialog"; +import { VerificationResultDialog } from "@/components/module/VerificationResultDialog"; import { LoadingWithAnimation } from "@common/components"; import { ConfirmDialog } from "@/shared/ui/confirm-dialog"; import SubmitDailog from "./SubmitDailog"; -import { useAlertStore } from "@/shared/lib"; +import { useAlertStore, useGlobalProgressStore } from "@/shared/lib"; import { useAdminStore } from "@/store/admin-store"; import { statisticsInputSewSupplyRateApi } from "@/entities/statistics/api/input/statistics-input-sew-supply-rate-service"; -import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { toGridRows } from "@/components/util/grid/gridFunction"; import { getFooterSummary } from "@/components/util/grid/footerSummary"; import { getUserInfo } from "../../../../../../packages/common/components/src/api"; @@ -30,7 +32,11 @@ import type { SewerageSupplyRateSaveRequest, SewerageSupplyRateStatusInfo, } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; -import type { cancelSubmitRequest, SubmitExceptionRequest } from "@/entities/statistics/model/statistics-interface"; +import type { + cancelSubmitRequest, + ImrStatusResponse, + SubmitExceptionRequest, +} from "@/entities/statistics/model/statistics-interface"; import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; import { SeweragePenetrationRateInpColumn } from "@/components/util/column/input/SeweragePenetrationRateInpColumn"; import { doList } from "@/components/util/column/input/DoList"; @@ -45,6 +51,32 @@ const hasVerificationErrors = (verification?: SewerageSupplyRateVerificationResu return Object.values(verification).some((errors) => Array.isArray(errors) && errors.length > 0); }; +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +/** IMR 상태가 COMPLETED 이고 job_prog 가 100일 때까지 폴링 */ +async function waitForImrJobCompleted( + jobId: string, + getStatus: (id: string) => Promise, + options?: { + pollIntervalMs?: number; + maxAttempts?: number; + onProgress?: (jobProgress: number) => void; + }, +): Promise { + const pollIntervalMs = options?.pollIntervalMs ?? 2000; + const maxAttempts = options?.maxAttempts ?? 150; + for (let i = 0; i < maxAttempts; i++) { + const res = await getStatus(jobId); + options?.onProgress?.(Math.max(0, Math.min(100, Number(res.job_prog) || 0))); + if (res.status === "COMPLETED" && Number(res.job_prog) === 100) return; + if (res.status === "FAILED" || res.status === "ERROR" || res.status === "CANCELLED") { + throw new Error(res.error_msg || `IMR 검증이 ${res.status} 상태로 종료되었습니다.`); + } + await sleep(pollIntervalMs); + } + throw new Error("IMR 검증 완료 대기 시간이 초과되었습니다."); +} + function getSewerageSupplyRateParams( values: SearchFormValues, overrides?: { yr?: string; demoYr?: string; mode?: string } @@ -69,11 +101,17 @@ const SeweragePenetrationRateInp = () => { const [lastSearchedValues, setLastSearchedValues] = useState(null); const [statusInfo, setStatusInfo] = useState(null); const [showDailog, setShowDailog] = useState(false); + const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false); + const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false); /** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */ const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false); const [statsCd, setStatsCd] = useState(''); const [footerFromApi, setFooterFromApi] = useState<{ sewerBzmnWholPpltnCnt?: number; sewerBzmnDomplWholPpltnCnt?: number; sewerBzmnFrgnrWholPpltnCnt?: number; sewerBzmnArea?: number } | null>(null); const { confirm } = useConfirm(); + const showGlobalProgress = useGlobalProgressStore((state) => state.show); + const setGlobalProgress = useGlobalProgressStore((state) => state.setProgress); + const hideGlobalProgress = useGlobalProgressStore((state) => state.hide); + const globalProgressVisible = useGlobalProgressStore((state) => state.visible); const { isAdminInfo } = useAdminStore(); @@ -88,6 +126,7 @@ const SeweragePenetrationRateInp = () => { }); // 최상위 관리자일땐 모든 상황에서 edit 가능하게 수정 필요. + const columnInfo = useMemo(() => { const { columns, columnLayout } = SeweragePenetrationRateInpColumn; const base = (isEnterable || isAdminInfo) ? columns : columns.map((col) => ({ ...col, editable: false as const })); @@ -212,7 +251,7 @@ const SeweragePenetrationRateInp = () => { try { setIsLoading(true); - const response = await statisticsAPi.submitException(params); + const response = await statisticsApi.submitException(params); if (response.status === 'success') { await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); @@ -271,7 +310,7 @@ const SeweragePenetrationRateInp = () => { const executeSubmit = async (submitInfo: StatisticsInputSubmitInfoFormData) => { try { - setIsLoading(true); + showGlobalProgress({ label: "제출 진행 중...", progress: 0 }); const userInfo = await getUserInfo(); const currentData = getGridDataRef.current?.(); @@ -348,39 +387,62 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); if (response.status === 'success') { - const imrParam = { - user_id: userInfo.userUnqId, - sewer_bzmn_cd: searchValues?.bsnmCd ?? '', - stats_cd: statsCd, - stats_yr: searchValues?.year ?? '', - lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'), - }; + setGlobalProgress(50); + + if (response.data.finalStatus === '5') { + const imrParam = { + user_id: userInfo.userUnqId, + sewer_bzmn_cd: searchValues?.bsnmCd ?? '', + stats_cd: statsCd, + stats_yr: searchValues?.year ?? '', + lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '4' : '2'), + gen_verify_stts_cd: response.data.nvpassed ? '1' : '2', + }; - try { - await statisticsAPi.callImrService(imrParam); - } catch { - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); - setIsLoading(false); - alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'); - return; + try { + const imrResponse = await statisticsApi.callImrService(imrParam); + if (imrResponse.job_id) { + await waitForImrJobCompleted(imrResponse.job_id, (id) => + statisticsApi.getImrStatus(id), + { + onProgress: (jobProgress) => { + setGlobalProgress(50 + Math.round(jobProgress / 2)); + }, + }, + ); + } + setGlobalProgress(100); + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); + setVerificationResultDialogOpen(true); + } catch (e) { + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); + setIsLoading(false); + const msg = + e instanceof Error && e.message + ? e.message + : 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'; + alert(msg); + return; + } + } else { + setGlobalProgress(100); + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); + alert('제출되었습니다.'); } - - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); - alert('제출되었습니다.'); } else { alert(response.message || '데이터 제출 중 오류가 발생했습니다.'); } } catch { alert('데이터 제출 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); + hideGlobalProgress(); } }; const saveFunc = async () => { try { if (await confirm('저장 하시겠습니까?')) { - setIsLoading(true); + showGlobalProgress({ label: "저장 진행 중...", progress: 0 }); const userInfo = await getUserInfo(); const currentData = getGridDataRef.current?.(); @@ -465,23 +527,8 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); if (response.status === 'success') { - // const imrParam = { - // user_id: userInfo.userUnqId, - // sewer_bzmn_cd: searchValues?.bsnmCd ?? '', - // stats_cd: statsCd, - // stats_yr: searchValues?.year ?? '', - // lae_sbmsn_stts_cd: (statusInfo?.laeSbmsnSttsCd === '4' ? '3' : '1'), - // }; - - // try { - // await statisticsAPi.callImrService(imrParam); - // } catch { - // await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); - // setIsLoading(false); - // alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'); - // return; - // } - + setGlobalProgress(50); + setGlobalProgress(100); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); alert('저장되었습니다.'); } else { @@ -493,7 +540,7 @@ const SeweragePenetrationRateInp = () => { } catch { alert('데이터 저장 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); + hideGlobalProgress(); } }; @@ -512,7 +559,7 @@ const SeweragePenetrationRateInp = () => { statsCd: statsCd, }; - const response = await statisticsAPi.cancelSubmit(requestData); + const response = await statisticsApi.cancelSubmit(requestData); if (response.status === 'success') { fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); @@ -526,6 +573,14 @@ const SeweragePenetrationRateInp = () => { } }; + const openVerificationHistoryDialog = () => { + if (!searchValues?.year || !searchValues?.sido || !searchValues?.bsnmCd || !statsCd) { + alert("먼저 조회를 진행해주세요."); + return; + } + setVerificationHistoryOpen(true); + }; + const gugunOkOrNotDoList = doList.includes(lastSearchedValues?.bsnmCd || '') ? Boolean(lastSearchedValues?.gugun && lastSearchedValues.gugun !== 'ALL') : true; @@ -547,12 +602,11 @@ const SeweragePenetrationRateInp = () => {
{showEnterableToolbarButtons && ( <> - {isAdminInfo && statusInfo && statusInfo.laeSbmsnSttsCd !== '0' && ( - submitFunc(true)} /> - )} + getLastYearDataFunc(searchValues ?? {})} /> submitFunc(false)} /> - )} {statusInfo?.laeSbmsnSttsCd === '2' && } @@ -560,7 +614,7 @@ const SeweragePenetrationRateInp = () => {
)} - {isLoading &&
+ {isLoading && !globalProgressVisible &&
@@ -593,7 +647,7 @@ const SeweragePenetrationRateInp = () => { return errors .map((e) => { const prefix = e.vrfcCd === 'IMR' ? 'IMR검증 - ' : '일반검증 - '; - return `${prefix}${e.rsnExpln}`; + return `${prefix}${e.rsnExpln}(검증 기준값: ${e.compVlCn} / 현재 입력값: ${e.nowVlCn})`; }) .filter(Boolean) .join('\n'); @@ -614,6 +668,25 @@ const SeweragePenetrationRateInp = () => { submitMode={isExceptionSubmitFlow ? 'exception' : 'normal'} isLoading={isLoading} /> + + ); }; diff --git a/apps/st/src/components/statistics/management/LogHistory.tsx b/apps/st/src/components/statistics/management/LogHistory.tsx index ff1747c33..e2abaf60d 100644 --- a/apps/st/src/components/statistics/management/LogHistory.tsx +++ b/apps/st/src/components/statistics/management/LogHistory.tsx @@ -7,7 +7,7 @@ import useSWR from "swr"; import { useAlertStore } from "@/shared/lib"; import { CodeSelectWithAll } from "@/entities/code"; -import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { ApiResponse, SearchCondition, @@ -36,13 +36,13 @@ import { toGridRows } from "@/components/util/grid/gridFunction"; const PAGE_SIZE = 100; const getSidoListFetcher = async (): Promise> => { - return statisticsAPi.getSidoList(); + return statisticsApi.getSidoList(); }; const getSggListFetcher = async ( key: [string, string], ): Promise> => { - return statisticsAPi.getSggList(key[1]); + return statisticsApi.getSggList(key[1]); }; const LogHistory = () => { @@ -158,7 +158,7 @@ const LogHistory = () => { try { setIsLoading(true); - const response = await statisticsAPi.getLogList(param); + const response = await statisticsApi.getLogList(param); if (response.status === "success") { const lostList = response.data?.statsLogList ?? []; diff --git a/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx b/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx index b451b3a14..09667bc88 100644 --- a/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx +++ b/apps/st/src/components/statistics/management/LogHistoryDetailDialog.tsx @@ -12,7 +12,7 @@ import { DialogTitle, } from "@common/components"; -import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; export interface LogHistoryDetailDialogProps { open: boolean; @@ -30,7 +30,7 @@ export function LogHistoryDetailDialog({ const sn = row.statsLogSn; void (async () => { - const response = await statisticsAPi.getLogDetail( + const response = await statisticsApi.getLogDetail( sn !== undefined && sn !== null ? String(sn) : "", ); console.log(response.data); diff --git a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx index 3af748260..b0f38ed02 100644 --- a/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx +++ b/apps/st/src/components/statistics/review/SeweragePenetrationRateRvw.tsx @@ -13,7 +13,7 @@ import { useConfirm } from "@frontend-opensource/use-react-hooks"; import { SeweragePenetrationRateInfoList } from "@/components/util/column/review/SeweragePenetrationRateRvwColumn"; import type { SewerageSupplyRateReviewItem, statsStatusInfo } from "@/entities/statistics/model/statistics-sewerage-penetration-review-interface"; import type { ReviewOpinionSaveRequest } from "@/entities/statistics/model/statistics-interface"; -import { statisticsAPi } from "@/entities/statistics/api/statistics-service"; +import { statisticsApi } from "@/entities/statistics/api/statistics-service"; import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service"; import type { SearchFormValues } from "@/components/type/SearchForm.type"; import { toGridRows } from "@/components/util/grid/gridFunction"; @@ -141,7 +141,7 @@ const SeweragePenetrationRateRvw = () => { try { const userInfo = await getUserInfo(); - const response = await statisticsAPi.submitReview({ + const response = await statisticsApi.submitReview({ statsYr: reviewOpinionContext.statsYr, sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, statsCd: reviewOpinionContext.statsCd, @@ -169,7 +169,7 @@ const SeweragePenetrationRateRvw = () => { } try { - const detailRes = await statisticsAPi.getReviewOpinionDetail({ + const detailRes = await statisticsApi.getReviewOpinionDetail({ statsYr: reviewOpinionContext.statsYr, sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, statsCd: reviewOpinionContext.statsCd, @@ -191,7 +191,7 @@ const SeweragePenetrationRateRvw = () => { const saveReviewOpinion = async (param: ReviewOpinionSaveRequest) => { try { - const response = await statisticsAPi.saveReviewOpinion(param); + const response = await statisticsApi.saveReviewOpinion(param); if (response.status !== "success") { alert(response.message || "저장에 실패했습니다."); return; @@ -220,7 +220,7 @@ const SeweragePenetrationRateRvw = () => { if (await confirm('반려 처리 하시겠습니까?')) { try { const userInfo = await getUserInfo(); - const response = await statisticsAPi.saveReviewReturn({ + const response = await statisticsApi.saveReviewReturn({ statsYr: reviewOpinionContext.statsYr, sewerBzmnCd: reviewOpinionContext.sewerBzmnCd, statsCd: reviewOpinionContext.statsCd, diff --git a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts index 32dfb9211..6adccc917 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-input-sew-supply-rate-service.ts @@ -7,7 +7,8 @@ import { type SewerageSupplyRateApiResponse, type CheckInputPeriodResponse, type SewerageSupplyRateSaveRequest, - type SewerageSupplyRateSaveApiResponse, + type SewerageSupplyRateSubmitResponse, + type SewerageSupplyRateSaveResponse, } from "../../model/input/statistics-sewage-supply-rate-input-interface"; export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSewSupplyRateService { @@ -26,16 +27,16 @@ export class StatisticsInputSewSupplyRateApiService implements StatisticsInputSe return response.data; } - public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { - const response = await stClient.post( + public async saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/save`, params, ); return response.data; } - public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { - const response = await stClient.post( + public async submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise { + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewerage-supply-rate/submit`, params, ); diff --git a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts index af6aad7fb..02d0144c4 100644 --- a/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts +++ b/apps/st/src/entities/statistics/api/input/statistics-sewage-pipe-input-service.ts @@ -11,7 +11,7 @@ import type { SeweragePipeInputPipeDiameterResponse, SeweragePipeInputPipeTypeResponse, SeweragePipeInputRegionalFactStatusSaveRequest, - SeweragePipeInputRegionalFactStatusSaveApiResponse, + SeweragePipeInputRegionalFactStatusSaveResponse, SeweragePipeInputRegionalResponse, SeweragePipeRequestParams, StatisticsInputSewPipeService, @@ -82,8 +82,8 @@ export class StatisticsInputSewPipeApiService implements StatisticsInputSewPipeS return response.data; } - public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise { - const response = await stClient.post( + public async saveRegionalFactStatus(param: SeweragePipeInputRegionalFactStatusSaveRequest): Promise { + const response = await stClient.post( `${ENDPOINT.ST_SERVICE.INPUT}/sewage-pipe/regional-facility-status/save`, param, ); diff --git a/apps/st/src/entities/statistics/api/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index 648853663..b7aec86ac 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -14,6 +14,8 @@ import { type ReviewReturnSaveResponse, type ReviewSubmitRequest, type ReviewSubmitResponse, + type VerificationResultListRequest, + type VerificationResultListResponse, type SearchCondition, type Sgg, type StatisticsLogDetailResponse, @@ -22,9 +24,11 @@ import { type StatisticsService, type SubmitExceptionRequest, type SubmitExceptionResponse, + type ImrResponse, + type ImrStatusResponse, } from "../model/statistics-interface"; -export class StatisticsAPiService implements StatisticsService { +export class StatisticsApiService implements StatisticsService { public async getSidoList(params?: { instSeCd?: string; }): Promise> { @@ -68,14 +72,21 @@ export class StatisticsAPiService implements StatisticsService { return response.data; } - public async callImrService(param: ImrRequest): Promise { - const response = await client.post( + public async callImrService(param: ImrRequest): Promise { + const response = await client.post( `${ENDPOINT.STATVERIFY_SERVICE.IMR}`, param, ); return response.data; } + public async getImrStatus(param: string): Promise { + const response = await client.get( + `${ENDPOINT.STATVERIFY_SERVICE.IMR}/status/${param}`, + ); + return response.data; + } + // 예외 제출 public async submitException( param: SubmitExceptionRequest, @@ -145,6 +156,14 @@ export class StatisticsAPiService implements StatisticsService { ); return response.data; } + + public async getVerificationResultList(param: VerificationResultListRequest): Promise { + const response = await stClient.get( + `${ENDPOINT.ST_SERVICE.INPUTVERIFY}/results`, + { params: param }, + ); + return response.data; + } } -export const statisticsAPi = new StatisticsAPiService(); +export const statisticsApi = new StatisticsApiService(); 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 79f9fd1db..299078872 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 @@ -1,5 +1,3 @@ -import type { SewerageSupplyRateSaveApiResponse } from "./statistics-sewage-supply-rate-input-interface"; - export interface SeweragePipeRequestParams { ctpvNm?: string; sggNm?: string; @@ -119,8 +117,11 @@ export interface SeweragePipeInputRegionalFactStatusSaveRequest { saveList: SeweragePipeInputRegionalFactStatusSaveItem[]; } -/** 지역별 시설현황 저장 API 응답 — 보급률 저장과 동일 래핑 */ -export type SeweragePipeInputRegionalFactStatusSaveApiResponse = SewerageSupplyRateSaveApiResponse; +export interface SeweragePipeInputRegionalFactStatusSaveResponse { + data: string; + message?: string; + status: string; +} /** 시설별 설치 현황 — 목록 항목 */ export interface SeweragePipeInputFactInstallStatusItem { @@ -519,5 +520,5 @@ export interface StatisticsInputSewPipeService { getFacilityAdminAreaList(param: SeweragePipeFacilityAdminAreaListRequest): Promise; saveRegionalFactStatus( param: SeweragePipeInputRegionalFactStatusSaveRequest, - ): Promise; + ): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts index 59481dc5b..1c14dfab8 100644 --- a/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts +++ b/apps/st/src/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface.ts @@ -36,6 +36,8 @@ export interface SewerageSupplyRateCommonInfo { export interface VerificationResultItem { vrfcCd: string; rsnExpln: string; + nowVlCn: string; + compVlCn: string; } /** 필드명 → 검증 결과 배열. 예: { popltnSm: [{ type, rsn }], wstrPrmTrt: [...] } */ @@ -150,12 +152,20 @@ export interface SewerageSupplyRateSaveRequest { superAdmin?: string; } -export interface SewerageSupplyRateSaveData { - httpStatus?: number; - code?: string; +export interface SewerageSupplyRateSaveResponse { + data: string; + message?: string; + status: string; } -export type SewerageSupplyRateSaveApiResponse = ApiResponse; +export interface SewerageSupplyRateSubmitResponse { + data: { + finalStatus: string; + nvpassed: boolean; + } + message?: string; + status: string; +} // 하수도 통계 입력 기간 조회 export interface CheckInputPeriodResponse { @@ -167,6 +177,6 @@ export interface CheckInputPeriodResponse { export interface StatisticsInputSewSupplyRateService { getSewerageSupplyRateList(params: SewerageSupplyRateRequestParams): Promise; checkInputPeriod(params: string): Promise; - saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; - submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; + saveSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; + submitSewerageSupplyRate(params: SewerageSupplyRateSaveRequest): Promise; } \ No newline at end of file diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index c20b175e0..5c95fb8bf 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -41,6 +41,91 @@ export interface ImrRequest { stats_cd: string; stats_yr: string; lae_sbmsn_stts_cd: string; + gen_verify_stts_cd?: string; +} + +export interface ImrResponse { + anls_sn: number; + job_id: string; + total_otlr_cnt: number; + status_code: string; + error_msg: string; + processing_time_ms: number; + job_prog: number; + queue_position: number; +} + +export interface ImrStatusResponse { + job_id: string; + anls_sn: number; + status: string; + job_prog: number; + total_otlr_cnt: number; + total_nml_cnt: number; + total_excl_cnt: number; + processing_time_ms: number; + error_msg: string; + queue_position: string; +} + +export interface VerificationResultListRequest { + statsYr: string; + sewerBzmnCd?: string; + ctpvNm?: string; + sggNm?: string; + statsCd?: string; + /** 검증 방법 코드 (예: IMR) */ + vrfcMthdCd?: string; + /** 처리여부 코드 목록, 콤마 구분 */ + prcsYnList?: string; + pbcpAprvYn?: string; + page?: number; + size?: number; +} + +/** 검증 결과 리포트 목록 한 행 (`verifyResultList` 원소) */ +export interface VerificationResultReportItem { + statsYr: string; + statsCd: string; + statsNm: string; + sewerBzmnCd: string; + ctpvNm: string; + sggNm: string; + admdstNm: string; + vrfcSn: number; + vrfcMthdCd: string; + vrfcMthdNm: string; + vrfcTrgtNm: string; + regDt: string; + rsnExpln: string; + nowVlCn: string; + compVlCn: string | null; + prcsYn: string; + prcsOpnnExpln: string | null; + prcrId: string | null; + rjctOpnnCn: string | null; + pbcpAprvYn: string; + mdfrId: string | null; + mdfcnDt: string; +} + +export interface VerificationResultListPageInfo { + totalElements: number; + totalPages: number; + currentPage: number; + size: number; +} + +export interface VerificationResultListData { + verifyResultList: VerificationResultReportItem[]; + pageInfo: VerificationResultListPageInfo; +} + +/** 검증 결과 리포트 조회 API 응답 */ +export interface VerificationResultListResponse { + status: string; + message?: string; + data?: VerificationResultListData; } export interface SubmitExceptionRequest { @@ -188,7 +273,7 @@ export interface StatisticsService { getSggList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>; getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise>; cancelSubmit(param: cancelSubmitRequest): Promise; - callImrService(param: ImrRequest): Promise; + callImrService(param: ImrRequest): Promise; submitException(param: SubmitExceptionRequest): Promise; getLogList(param: StatisticsLogRequest): Promise; getLogDetail(param: string): Promise; @@ -196,4 +281,6 @@ export interface StatisticsService { getReviewOpinionDetail(param: getReviewOpinionDetailRequest): Promise; saveReviewReturn(param: ReviewReturnSaveRequest): Promise; submitReview(param :ReviewSubmitRequest): Promise; + getVerificationResultList(param: VerificationResultListRequest): Promise; + getImrStatus(param: string): Promise; } \ No newline at end of file diff --git a/apps/st/src/shared/lib/index.ts b/apps/st/src/shared/lib/index.ts index 5ac79bf08..e2ddab13d 100644 --- a/apps/st/src/shared/lib/index.ts +++ b/apps/st/src/shared/lib/index.ts @@ -2,6 +2,7 @@ export { getCookie, setCookie, removeCookie } from "./cookie"; export { useMutationWrapper } from "./use-mutation-wrapper"; export { useAlertStore } from "./use-alert-store"; export { useDataTable } from "./use-data-table"; +export { useGlobalProgressStore } from "./use-global-progress-store"; export { useMenuStore } from "./use-menu-store"; export { useUserStore } from "./use-user-store"; export { usePrompt } from "./use-prompt"; diff --git a/apps/st/src/shared/lib/use-global-progress-store.ts b/apps/st/src/shared/lib/use-global-progress-store.ts new file mode 100644 index 000000000..4161ad1d4 --- /dev/null +++ b/apps/st/src/shared/lib/use-global-progress-store.ts @@ -0,0 +1,16 @@ +import { useContext } from "react"; +import { useStore } from "zustand"; +import type { GlobalProgressStore } from "../store/global-progress-store"; +import { StoreContext } from "../providers/store-provider"; + +export const useGlobalProgressStore = ( + selector: (store: GlobalProgressStore) => T, +): T => { + const storeContext = useContext(StoreContext); + + if (!storeContext) { + throw new Error("useGlobalProgressStore must be used within StoreProvider"); + } + + return useStore(storeContext.globalProgressStore, selector); +}; diff --git a/apps/st/src/shared/providers/store-provider.tsx b/apps/st/src/shared/providers/store-provider.tsx index a0ec17b61..44d6d89f0 100644 --- a/apps/st/src/shared/providers/store-provider.tsx +++ b/apps/st/src/shared/providers/store-provider.tsx @@ -4,6 +4,7 @@ import { createContext, useRef } from "react"; import { createAlertStore, createAttachmentStore, + createGlobalProgressStore, createMenuStore, createUserStore, } from "../store"; @@ -11,6 +12,7 @@ import { type StoreContextType = { alertStore: ReturnType; attachmentStore: ReturnType; + globalProgressStore: ReturnType; menuStore: ReturnType; userStore: ReturnType; }; @@ -25,6 +27,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => { storeRef.current = { alertStore: createAlertStore(), attachmentStore: createAttachmentStore(), + globalProgressStore: createGlobalProgressStore(), menuStore: createMenuStore(), userStore: createUserStore(), }; diff --git a/apps/st/src/shared/store/global-progress-store.ts b/apps/st/src/shared/store/global-progress-store.ts new file mode 100644 index 000000000..70a08debf --- /dev/null +++ b/apps/st/src/shared/store/global-progress-store.ts @@ -0,0 +1,45 @@ +import { createStore } from "zustand/vanilla"; + +export type GlobalProgressState = { + visible: boolean; + label: string; + progress: number; +}; + +export type GlobalProgressActions = { + show: (payload?: { label?: string; progress?: number }) => void; + setLabel: (label: string) => void; + setProgress: (progress: number) => void; + hide: () => void; +}; + +export type GlobalProgressStore = GlobalProgressState & GlobalProgressActions; + +export const defaultGlobalProgressState: GlobalProgressState = { + visible: false, + label: "진행 중...", + progress: 0, +}; + +export const createGlobalProgressStore = ( + initState: GlobalProgressState = defaultGlobalProgressState, +) => { + return createStore()((set) => ({ + ...initState, + show: (payload) => + set(() => ({ + visible: true, + label: payload?.label ?? defaultGlobalProgressState.label, + progress: Math.max(0, Math.min(100, payload?.progress ?? 0)), + })), + setLabel: (label) => set(() => ({ label })), + setProgress: (progress) => + set(() => ({ progress: Math.max(0, Math.min(100, progress)) })), + hide: () => + set(() => ({ + visible: false, + progress: 0, + label: defaultGlobalProgressState.label, + })), + })); +}; diff --git a/apps/st/src/shared/store/index.ts b/apps/st/src/shared/store/index.ts index a41d6f928..c5014150e 100644 --- a/apps/st/src/shared/store/index.ts +++ b/apps/st/src/shared/store/index.ts @@ -1,4 +1,5 @@ export { createAlertStore } from "./alert-store"; export { createAttachmentStore } from "./attachment-store"; +export { createGlobalProgressStore } from "./global-progress-store"; export { createMenuStore, type BbsType } from "./menu-store"; export { createUserStore, type IUserInfo } from "./user-store"; diff --git a/apps/st/src/shared/ui/global-progress-overlay.tsx b/apps/st/src/shared/ui/global-progress-overlay.tsx new file mode 100644 index 000000000..c9007148b --- /dev/null +++ b/apps/st/src/shared/ui/global-progress-overlay.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { Progress } from "@common/components"; +import { useGlobalProgressStore } from "@/shared/lib"; + +export const GlobalProgressOverlay = () => { + const visible = useGlobalProgressStore((state) => state.visible); + const label = useGlobalProgressStore((state) => state.label); + const progress = useGlobalProgressStore((state) => state.progress); + + if (!visible) return null; + + return ( +
+
+

+ {label} {progress}% +

+ +
+
+ ); +};