[st] feat: 입력 - 제출 시 imr 요청 및 상태 확인, 진행 progress bar 추가

This commit is contained in:
2026-04-21 17:57:00 +09:00
parent d0a808882d
commit 9c7e71e65a
19 changed files with 273 additions and 72 deletions
@@ -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 }>) => {
<PromptDialog />
<ConfirmDialog />
<SelectPromptDialog />
<GlobalProgressOverlay />
<Toaster position="top-center" />
</AuthWrapper>
</ConfirmProvider>
@@ -6,9 +6,9 @@ interface VerificationResultButtonProps {
const VerificationResultButton = ({ VerificationResultButtonFunc }: VerificationResultButtonProps) => {
return (
<Button color="white" className="h-[3.2rem] w-[96px]" onClick={VerificationResultButtonFunc}>
<Button color="white" className="h-[3.2rem]" onClick={VerificationResultButtonFunc}>
<i className="diveicon di-list" />
<span>검증 결과</span>
<span>지난 검증 결과</span>
</Button>
);
};
+6 -6
View File
@@ -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;
}
+3 -3
View File
@@ -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<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList({ instSeCd: key[1] });
return statisticsApi.getSidoList({ instSeCd: key[1] });
};
const getSggListFetcher = async (
key: [string, string, string, string],
): Promise<ApiResponse<Sgg[]>> => {
const gugunSkipYn = doList.includes(key[3]) ? "N" : "Y";
return statisticsAPi.getSggAllList(key[1], {
return statisticsApi.getSggAllList(key[1], {
instSeCd: key[2],
gugunSkipYn,
});
@@ -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<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
return statisticsApi.getSidoList();
};
const getSggAllListFetcher = async (
key: [string, string],
): Promise<ApiResponse<Sgg[]>> => {
return statisticsAPi.getSggAllList(key[1]);
return statisticsApi.getSggAllList(key[1]);
};
type SidoSggFacilityNameSearchFormProps = {
@@ -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 (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[640px]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>검증 결과</DialogTitle>
</DialogHeader>
<div className="min-h-[120px]" />
<DialogFooter>
<DialogClose asChild>
<Button type="button" color="gray">
닫기
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -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<string, string | number>;
@@ -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({
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-[80%]" aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogTitle>{`${title} - ${queryContext?.statsYr} ${queryContext?.ctpvNm} ${queryContext?.sggNm}`}</DialogTitle>
</DialogHeader>
<div className="max-h-[85vh] space-y-5 overflow-auto">
{errorMessage && (
@@ -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<void>((resolve) => setTimeout(resolve, ms));
/** IMR 상태가 COMPLETED 이고 job_prog 가 100일 때까지 폴링 */
async function waitForImrJobCompleted(
jobId: string,
getStatus: (id: string) => Promise<ImrStatusResponse>,
options?: {
pollIntervalMs?: number;
maxAttempts?: number;
onProgress?: (jobProgress: number) => void;
},
): Promise<void> {
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<SewerageSupplyRateStatusInfo | null>(null);
const [showDailog, setShowDailog] = useState<boolean>(false);
const [verificationHistoryOpen, setVerificationHistoryOpen] = useState(false);
const [verificationResultDialogOpen, setVerificationResultDialogOpen] = useState(false);
/** 예외제출 버튼으로 연 다이얼로그인지 (확인 시 executeSubmit 대신 SubmitExceptionFunc 경로) */
const [isExceptionSubmitFlow, setIsExceptionSubmitFlow] = useState(false);
const [statsCd, setStatsCd] = useState<string>('');
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 = () => {
</div>
</div>
)}
{isLoading && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
{isLoading && !globalProgressVisible && <div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
<div className="relative size-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
@@ -654,6 +683,10 @@ const SeweragePenetrationRateInp = () => {
: null
}
/>
<VerificationResultDialog
open={verificationResultDialogOpen}
onOpenChange={setVerificationResultDialogOpen}
/>
</>
);
};
@@ -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<ApiResponse<SearchCondition>> => {
return statisticsAPi.getSidoList();
return statisticsApi.getSidoList();
};
const getSggListFetcher = async (
key: [string, string],
): Promise<ApiResponse<Sgg[]>> => {
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 ?? [];
@@ -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);
@@ -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,
@@ -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<ApiResponse<SearchCondition>> {
@@ -70,14 +72,21 @@ export class StatisticsAPiService implements StatisticsService {
return response.data;
}
public async callImrService(param: ImrRequest): Promise<null> {
const response = await client.post<null>(
public async callImrService(param: ImrRequest): Promise<ImrResponse> {
const response = await client.post<ImrResponse>(
`${ENDPOINT.STATVERIFY_SERVICE.IMR}`,
param,
);
return response.data;
}
public async getImrStatus(param: string): Promise<ImrStatusResponse> {
const response = await client.get<ImrStatusResponse>(
`${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();
@@ -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<ApiResponse<Sgg[]>>;
getSggAllList(sidoName: string, params?: { instSeCd?: string; gugunSkipYn?: "Y" | "N" }): Promise<ApiResponse<Sgg[]>>;
cancelSubmit(param: cancelSubmitRequest): Promise<cancelSubmitResponse>;
callImrService(param: ImrRequest): Promise<null>;
callImrService(param: ImrRequest): Promise<ImrResponse>;
submitException(param: SubmitExceptionRequest): Promise<SubmitExceptionResponse>;
getLogList(param: StatisticsLogRequest): Promise<StatisticsLogResponse>;
getLogDetail(param: string): Promise<StatisticsLogDetailResponse>;
@@ -258,4 +282,5 @@ export interface StatisticsService {
saveReviewReturn(param: ReviewReturnSaveRequest): Promise<ReviewReturnSaveResponse>;
submitReview(param :ReviewSubmitRequest): Promise<ReviewSubmitResponse>;
getVerificationResultList(param: VerificationResultListRequest): Promise<VerificationResultListResponse>;
getImrStatus(param: string): Promise<ImrStatusResponse>;
}
+1
View File
@@ -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";
@@ -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 = <T>(
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);
};
@@ -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<typeof createAlertStore>;
attachmentStore: ReturnType<typeof createAttachmentStore>;
globalProgressStore: ReturnType<typeof createGlobalProgressStore>;
menuStore: ReturnType<typeof createMenuStore>;
userStore: ReturnType<typeof createUserStore>;
};
@@ -25,6 +27,7 @@ export const StoreProvider = ({ children }: { children: React.ReactNode }) => {
storeRef.current = {
alertStore: createAlertStore(),
attachmentStore: createAttachmentStore(),
globalProgressStore: createGlobalProgressStore(),
menuStore: createMenuStore(),
userStore: createUserStore(),
};
@@ -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<GlobalProgressStore>()((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,
})),
}));
};
+1
View File
@@ -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";
@@ -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 (
<div className="fixed inset-0 z-[999] bg-black/35 [data-theme='black']:bg-black/55 backdrop-blur-[1px] rounded-lg p-10 flex justify-center items-center">
<div className="w-[34rem] rounded-md border border-border bg-background p-6 shadow-md">
<p className="mb-3 text-center text-[1.3rem] text-foreground">
{label} {progress}%
</p>
<Progress value={progress} />
</div>
</div>
);
};