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>
 |