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 index 0e0124a99..580f6d0d0 100644 --- a/apps/st/src/components/button/VerificationResultButton.tsx +++ b/apps/st/src/components/button/VerificationResultButton.tsx @@ -6,9 +6,9 @@ interface VerificationResultButtonProps { const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => { return ( - ); }; 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 index 067a11655..3be4a6a9e 100644 --- a/apps/st/src/components/module/VerificationResultHistoryDialog.tsx +++ b/apps/st/src/components/module/VerificationResultHistoryDialog.tsx @@ -14,7 +14,7 @@ import { 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 { statisticsApi } from "@/entities/statistics/api/statistics-service"; import type { VerificationResultListRequest, VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface"; type VerificationResultGridRow = Record; @@ -37,8 +37,9 @@ const verificationResultColumnInfo: ColumnInfo = { { 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"], + columnLayout: ["ctpvNm", "sggNm", "vrfcTrgtNm", "rsnExpln", "nowVlCn", "compVlCn", "prcsYn", "pbcpAprvYn", "regDt", "mdfcnDt"], }; export interface VerificationResultHistoryDialogProps { @@ -93,12 +94,13 @@ export function VerificationResultHistoryDialog({ 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); + return statisticsApi.getVerificationResultList(param); }, [queryContext], ); @@ -191,6 +193,7 @@ export function VerificationResultHistoryDialog({ 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]); @@ -243,7 +246,7 @@ export function VerificationResultHistoryDialog({ - {title} + {`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}
{errorMessage && ( diff --git a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx index f9ac2a347..c37700dae 100644 --- a/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx +++ b/apps/st/src/components/statistics/input/SeweragePenetrationRateInp.tsx @@ -9,14 +9,15 @@ import LastYearDataButton from "@/components/button/LastYearDataButton"; 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"; @@ -33,6 +34,7 @@ import type { } from "@/entities/statistics/model/input/statistics-sewage-supply-rate-input-interface"; import type { cancelSubmitRequest, + ImrStatusResponse, SubmitExceptionRequest, } from "@/entities/statistics/model/statistics-interface"; import type { StatisticsInputSubmitInfoFormData } from "@/entities/statistics/model/statistics-input-submit-info-schema"; @@ -49,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 } @@ -74,11 +102,16 @@ const SeweragePenetrationRateInp = () => { 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(); @@ -218,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); @@ -277,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?.(); @@ -354,6 +387,7 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputSewSupplyRateApi.submitSewerageSupplyRate(requestData); if (response.status === 'success') { + setGlobalProgress(50); if (response.data.finalStatus === '5') { const imrParam = { @@ -366,16 +400,32 @@ const SeweragePenetrationRateInp = () => { }; try { - await statisticsAPi.callImrService(imrParam); + 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); - alert('제출되었습니다.'); - } catch { + setVerificationResultDialogOpen(true); + } catch (e) { await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); setIsLoading(false); - alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'); + const msg = + e instanceof Error && e.message + ? e.message + : 'IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'; + alert(msg); return; } } else { + setGlobalProgress(100); await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), false); alert('제출되었습니다.'); } @@ -385,14 +435,14 @@ const SeweragePenetrationRateInp = () => { } 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?.(); @@ -477,31 +527,10 @@ const SeweragePenetrationRateInp = () => { const response = await statisticsInputSewSupplyRateApi.saveSewerageSupplyRate(requestData); if (response.status === 'success') { - - if (response.data === '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' ? '3' : '1'), - gen_verify_stts_cd: '1', - }; - - try { - await statisticsAPi.callImrService(imrParam); - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); - alert('저장되었습니다.'); - } catch { - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); - setIsLoading(false); - alert('IMR 검증 중 오류가 발생했습니다.\n관리자에게 문의하세요.'); - return; - } - } else { - await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); - alert('저장되었습니다.'); - } + setGlobalProgress(50); + setGlobalProgress(100); + await fetchSewerageSupplyRate(getSewerageSupplyRateParams(searchValues ?? {}), true); + alert('저장되었습니다.'); } else { alert(response.message || '데이터 저장 중 오류가 발생했습니다.'); } @@ -511,7 +540,7 @@ const SeweragePenetrationRateInp = () => { } catch { alert('데이터 저장 중 오류가 발생했습니다.'); } finally { - setIsLoading(false); + hideGlobalProgress(); } }; @@ -530,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); @@ -585,7 +614,7 @@ const SeweragePenetrationRateInp = () => {
)} - {isLoading &&
+ {isLoading && !globalProgressVisible &&
@@ -654,6 +683,10 @@ const SeweragePenetrationRateInp = () => { : null } /> + ); }; 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/statistics-service.ts b/apps/st/src/entities/statistics/api/statistics-service.ts index f899aee6c..b7aec86ac 100644 --- a/apps/st/src/entities/statistics/api/statistics-service.ts +++ b/apps/st/src/entities/statistics/api/statistics-service.ts @@ -24,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> { @@ -70,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, @@ -157,4 +166,4 @@ export class StatisticsAPiService implements StatisticsService { } } -export const statisticsAPi = new StatisticsAPiService(); +export const statisticsApi = new StatisticsApiService(); diff --git a/apps/st/src/entities/statistics/model/statistics-interface.ts b/apps/st/src/entities/statistics/model/statistics-interface.ts index 800849c5e..5c95fb8bf 100644 --- a/apps/st/src/entities/statistics/model/statistics-interface.ts +++ b/apps/st/src/entities/statistics/model/statistics-interface.ts @@ -44,6 +44,30 @@ export interface ImrRequest { 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; @@ -249,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; @@ -258,4 +282,5 @@ export interface StatisticsService { 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}% +

+ +
+
+ ); +};