- {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}%
+
+
+
+
+ );
+};