221 lines
4.3 KiB
Vue
221 lines
4.3 KiB
Vue
<template>
|
|
<Teleport to="body">
|
|
<Transition
|
|
name="loading-fade"
|
|
enter-active-class="transition-opacity duration-300"
|
|
leave-active-class="transition-opacity duration-300"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div
|
|
v-if="isLoading"
|
|
class="global-loading-overlay"
|
|
@click.self="handleOverlayClick"
|
|
>
|
|
<div class="loading-container">
|
|
<!-- 스피너 -->
|
|
<div class="loading-spinner">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
|
|
<!-- 로딩 메시지 -->
|
|
<div class="loading-message">
|
|
<p class="message-text">{{ currentMessage }}</p>
|
|
<p v-if="loadingCount > 1" class="count-text">
|
|
({{ loadingCount }}개 작업 진행 중)
|
|
</p>
|
|
</div>
|
|
|
|
<!-- 진행 중인 작업 목록 (개발 모드에서만) -->
|
|
<div
|
|
v-if="showDetails && loadingMessages.length > 1"
|
|
class="loading-details"
|
|
>
|
|
<details class="details">
|
|
<summary class="details-summary">진행 중인 작업 보기</summary>
|
|
<ul class="details-list">
|
|
<li
|
|
v-for="(message, index) in loadingMessages"
|
|
:key="index"
|
|
class="details-item"
|
|
>
|
|
{{ message }}
|
|
</li>
|
|
</ul>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
/** 오버레이 클릭 시 로딩 취소 허용 여부 */
|
|
allowCancel?: boolean;
|
|
/** 개발 모드에서 상세 정보 표시 여부 */
|
|
showDetails?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
allowCancel: false,
|
|
showDetails: false,
|
|
});
|
|
|
|
const { isLoading, currentMessage, loadingCount, loadingMessages, clearAll } =
|
|
useLoading();
|
|
|
|
// 오버레이 클릭 처리
|
|
const handleOverlayClick = () => {
|
|
if (props.allowCancel) {
|
|
clearAll();
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.global-loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
z-index: 9999;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
|
|
.loading-container {
|
|
background: white;
|
|
border-radius: 12px;
|
|
padding: 2rem;
|
|
box-shadow:
|
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
text-align: center;
|
|
min-width: 200px;
|
|
max-width: 400px;
|
|
}
|
|
|
|
.loading-spinner {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid #f3f4f6;
|
|
border-top: 4px solid #3b82f6;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: rotate(0deg);
|
|
}
|
|
100% {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.loading-message {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.message-text {
|
|
font-size: 1.125rem;
|
|
font-weight: 500;
|
|
color: #374151;
|
|
margin: 0 0 0.5rem 0;
|
|
}
|
|
|
|
.count-text {
|
|
font-size: 0.875rem;
|
|
color: #6b7280;
|
|
margin: 0;
|
|
}
|
|
|
|
.loading-details {
|
|
margin-top: 1rem;
|
|
text-align: left;
|
|
}
|
|
|
|
.details {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.details-summary {
|
|
cursor: pointer;
|
|
color: #6b7280;
|
|
font-weight: 500;
|
|
list-style: none;
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
}
|
|
|
|
.details-summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
|
|
.details-summary::before {
|
|
content: "▶";
|
|
display: inline-block;
|
|
margin-right: 0.5rem;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.details[open] .details-summary::before {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.details-list {
|
|
margin: 0.5rem 0 0 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.details-item {
|
|
padding: 0.25rem 0;
|
|
color: #6b7280;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.details-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* 다크 모드 지원 */
|
|
@media (prefers-color-scheme: dark) {
|
|
.loading-container {
|
|
background: #1f2937;
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.message-text {
|
|
color: #f9fafb;
|
|
}
|
|
|
|
.count-text,
|
|
.details-summary,
|
|
.details-item {
|
|
color: #d1d5db;
|
|
}
|
|
|
|
.details-summary {
|
|
border-bottom-color: #374151;
|
|
}
|
|
|
|
.details-item {
|
|
border-bottom-color: #374151;
|
|
}
|
|
}
|
|
</style>
|