From b866242d43e59225cfd0d22738576022d83137ce Mon Sep 17 00:00:00 2001 From: sohot8653 Date: Wed, 24 Sep 2025 10:53:08 +0900 Subject: [PATCH] =?UTF-8?q?[=EB=A1=9C=EB=94=A9=20=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EC=B6=94=EA=B0=80]=20=EC=A0=84=EC=97=AD=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EC=98=A4=EB=B2=84=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EC=83=81=ED=83=9C=20=EA=B4=80=EB=A6=AC=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EC=96=B4=20=EA=B5=AC=ED=98=84,=20=EB=A1=9C=EB=94=A9?= =?UTF-8?q?=20=EC=B9=B4=EC=9A=B4=ED=84=B0=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=9E=91=EC=97=85=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.vue | 3 + components/layout/GlobalLoading.vue | 220 ++++++++++++++++++++++++++++ composables/useLoading.ts | 65 ++++++++ composables/usePermission.ts | 1 - pages/[tabId]/test/loading-test.vue | 205 ++++++++++++++++++++++++++ stores/loading.ts | 75 ++++++++++ stores/permissions.ts | 15 +- 7 files changed, 571 insertions(+), 13 deletions(-) create mode 100644 components/layout/GlobalLoading.vue create mode 100644 composables/useLoading.ts create mode 100644 pages/[tabId]/test/loading-test.vue create mode 100644 stores/loading.ts diff --git a/app.vue b/app.vue index 788184a..07a011b 100644 --- a/app.vue +++ b/app.vue @@ -2,6 +2,9 @@ + + + +
+
+
+ + +
+

{{ currentMessage }}

+

+ ({{ loadingCount }}개 작업 진행 중) +

+
+ + +
+
+ 진행 중인 작업 보기 +
    +
  • + {{ message }} +
  • +
+
+
+ + + + + + + + + diff --git a/composables/useLoading.ts b/composables/useLoading.ts new file mode 100644 index 0000000..a6ca155 --- /dev/null +++ b/composables/useLoading.ts @@ -0,0 +1,65 @@ +/** + * 로딩 상태를 쉽게 사용할 수 있는 컴포저블 + * 로딩 카운터 방식으로 여러 비동기 작업을 안전하게 관리합니다. + */ +export const useLoading = () => { + const loadingStore = useLoadingStore(); + + /** + * 로딩 상태와 함께 비동기 함수를 실행합니다. + * 함수 실행 중에는 자동으로 로딩 카운터가 증가하고, 완료되면 감소합니다. + * + * @param asyncFn 실행할 비동기 함수 + * @param message 로딩 메시지 (선택사항) + * @returns Promise 비동기 함수의 결과 + */ + const withLoading = async ( + asyncFn: () => Promise, + message?: string + ): Promise => { + loadingStore.startLoading(message); + try { + return await asyncFn(); + } finally { + loadingStore.stopLoading(message); + } + }; + + /** + * 로딩 상태를 수동으로 관리할 때 사용합니다. + * + * @param message 로딩 메시지 (선택사항) + * @returns 로딩 종료 함수 + */ + const startLoading = (message?: string) => { + loadingStore.startLoading(message); + return () => loadingStore.stopLoading(message); + }; + + /** + * 특정 메시지로 로딩을 시작하고, 해당 메시지로 종료할 수 있는 함수를 반환합니다. + * + * @param message 로딩 메시지 + * @returns 로딩 종료 함수 + */ + const withMessage = (message: string) => { + loadingStore.startLoading(message); + return () => loadingStore.stopLoading(message); + }; + + return { + // 상태 + isLoading: computed(() => loadingStore.isLoading), + loadingCount: computed(() => loadingStore.loadingCount), + currentMessage: computed(() => loadingStore.currentMessage), + loadingMessages: computed(() => loadingStore.loadingMessages), + + // 액션 + startLoading, + stopLoading: loadingStore.stopLoading, + withLoading, + withMessage, + clearAll: loadingStore.clearAllLoading, + reset: loadingStore.reset, + }; +}; diff --git a/composables/usePermission.ts b/composables/usePermission.ts index 48b10e9..d25424c 100644 --- a/composables/usePermission.ts +++ b/composables/usePermission.ts @@ -21,7 +21,6 @@ export const usePermission = () => { // 권한 데이터 직접 접근 permissions: computed(() => permissionsStore.permissions), resources: computed(() => permissionsStore.permissions?.resources), - isLoading: computed(() => permissionsStore.isLoading), // 리소스별 전체 접근 함수 getPageGroups: () => diff --git a/pages/[tabId]/test/loading-test.vue b/pages/[tabId]/test/loading-test.vue new file mode 100644 index 0000000..3246dbd --- /dev/null +++ b/pages/[tabId]/test/loading-test.vue @@ -0,0 +1,205 @@ + + + diff --git a/stores/loading.ts b/stores/loading.ts new file mode 100644 index 0000000..1aa2ce2 --- /dev/null +++ b/stores/loading.ts @@ -0,0 +1,75 @@ +/** + * 전역 로딩 상태 관리 스토어 (로딩 카운터 방식) + * 여러 비동기 작업이 동시에 진행되어도 안전하게 로딩 상태를 관리합니다. + */ +export const useLoadingStore = defineStore( + "loading", + () => { + // 로딩 카운터 - 0보다 크면 로딩 중 + const loadingCount = ref(0); + + // 현재 로딩 중인 작업들의 메시지 + const loadingMessages = ref([]); + + // 로딩 상태 (카운터가 0보다 크면 true) + const isLoading = computed(() => loadingCount.value > 0); + + // 현재 로딩 메시지 (가장 최근 메시지 또는 기본 메시지) + const currentMessage = computed( + () => + loadingMessages.value[loadingMessages.value.length - 1] || "로딩 중..." + ); + + // 로딩 시작 + const startLoading = (message?: string) => { + loadingCount.value++; + if (message) { + loadingMessages.value.push(message); + } + }; + + // 로딩 종료 + const stopLoading = (message?: string) => { + loadingCount.value = Math.max(0, loadingCount.value - 1); + + if (message && loadingMessages.value.length > 0) { + const index = loadingMessages.value.lastIndexOf(message); + if (index > -1) { + loadingMessages.value.splice(index, 1); + } + } else if (loadingMessages.value.length > 0) { + // 메시지가 지정되지 않으면 가장 최근 메시지 제거 + loadingMessages.value.pop(); + } + }; + + // 모든 로딩 강제 종료 + const clearAllLoading = () => { + loadingCount.value = 0; + loadingMessages.value = []; + }; + + // 로딩 상태 리셋 + const reset = () => { + loadingCount.value = 0; + loadingMessages.value = []; + }; + + return { + // 상태 + loadingCount: readonly(loadingCount), + loadingMessages: readonly(loadingMessages), + isLoading, + currentMessage, + + // 액션 + startLoading, + stopLoading, + clearAllLoading, + reset, + }; + }, + { + persist: false, // 로딩 상태는 새로고침 시 초기화되어야 함 + } +); diff --git a/stores/permissions.ts b/stores/permissions.ts index 1bfdb1c..87a1bb9 100644 --- a/stores/permissions.ts +++ b/stores/permissions.ts @@ -13,14 +13,11 @@ export const usePermissionsStore = defineStore( }, }); - // 권한 로딩 상태 - const isLoading = ref(false); - // 서버에서 권한 데이터 가져오기 (현재는 가데이터 사용) const fetchPermissions = async (): Promise => { - try { - isLoading.value = true; + const { withLoading } = useLoading(); + return await withLoading(async () => { // 실제 API 호출 (백엔드 준비되면 주석 해제) /* const { success, data } = await useApi('/auth/permissions', { @@ -53,12 +50,7 @@ export const usePermissionsStore = defineStore( }; return true; - } catch (error) { - console.error("권한 데이터 로드 실패:", error); - return false; - } finally { - isLoading.value = false; - } + }, "권한 정보를 불러오는 중..."); }; const getPagePaths = (): string[] => { @@ -104,7 +96,6 @@ export const usePermissionsStore = defineStore( return { permissions: readonly(permissions), - isLoading: readonly(isLoading), fetchPermissions, clearPermissions,