refactor: 빈/상태 메시지 박스 EmptyState 로 통일 (Phase 3-3)
목록 로딩·빈 상태 박스(state-msg/feed-state/list-empty)를 전역 .empty-state 카드 스타일 + EmptyState 컴포넌트로 통일. 5개 페이지(프로젝트 목록/상세, 내 업무, 활동, 멤버)의 중복 박스 CSS 제거. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -677,3 +677,17 @@ body {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 빈/상태 메시지 박스 (목록 로딩·빈 상태 공통)
|
||||
* ============================================================ */
|
||||
.empty-state {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.625rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 2.5rem 1.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
// 빈/상태 메시지 박스 공용 컴포넌트 — 목록 로딩·빈 상태 등에 사용.
|
||||
// 전역 .empty-state 카드 스타일을 쓰고, 메시지는 기본 슬롯으로 전달한다.
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -8,6 +8,7 @@ import AppShell from '@/layouts/AppShell.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import DropdownSelect from '@/components/DropdownSelect.vue'
|
||||
import LoadMore from '@/components/common/LoadMore.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import { useMyTaskStore } from '@/stores/my-task.store'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import { countActive, type MyTaskRow } from '@/mock/relay.mock'
|
||||
@@ -216,18 +217,12 @@ function loadMore() {
|
||||
</div>
|
||||
|
||||
<!-- 로딩 / 빈 상태 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="state-msg"
|
||||
>
|
||||
<EmptyState v-if="loading">
|
||||
내 업무를 불러오는 중…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isEmpty"
|
||||
class="state-msg"
|
||||
>
|
||||
</EmptyState>
|
||||
<EmptyState v-else-if="isEmpty">
|
||||
{{ view === 'assigned' ? '담당 중인 업무가 없습니다.' : '지시한 업무가 없습니다.' }}
|
||||
</div>
|
||||
</EmptyState>
|
||||
|
||||
<!-- 상태 그룹 -->
|
||||
<div
|
||||
@@ -534,16 +529,6 @@ function loadMore() {
|
||||
}
|
||||
|
||||
/* 로딩/빈 상태 */
|
||||
.state-msg {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.625rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 2.5rem 1.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text-3);
|
||||
}
|
||||
|
||||
/* 상태 그룹 */
|
||||
.group {
|
||||
|
||||
@@ -9,6 +9,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import DropdownSelect from '@/components/DropdownSelect.vue'
|
||||
import SegmentedTabs from '@/components/common/SegmentedTabs.vue'
|
||||
import LoadMore from '@/components/common/LoadMore.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import { useActivityStore } from '@/stores/activity.store'
|
||||
import type { Project, ActivityIcon } from '@/mock/relay.mock'
|
||||
@@ -178,18 +179,12 @@ const ActIcon = defineComponent({
|
||||
</div>
|
||||
|
||||
<!-- 로딩 / 빈 상태 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="feed-state"
|
||||
>
|
||||
<EmptyState v-if="loading">
|
||||
활동을 불러오는 중입니다…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isEmpty"
|
||||
class="feed-state"
|
||||
>
|
||||
</EmptyState>
|
||||
<EmptyState v-else-if="isEmpty">
|
||||
아직 기록된 활동이 없습니다.
|
||||
</div>
|
||||
</EmptyState>
|
||||
|
||||
<!-- 활동 피드 -->
|
||||
<!-- act-text 의 v-html 은 store 에서 사용자 입력을 escapeHtml 로 이스케이프한 뒤 신뢰 태그만 조립한 마크업이다 (XSS 위험 없음) -->
|
||||
@@ -366,15 +361,4 @@ const ActIcon = defineComponent({
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
/* 로딩/빈 상태 */
|
||||
.feed-state {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.625rem;
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 2.5rem 1.25rem;
|
||||
text-align: center;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,6 +11,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import DropdownSelect from '@/components/DropdownSelect.vue'
|
||||
import SegmentedTabs from '@/components/common/SegmentedTabs.vue'
|
||||
import LoadMore from '@/components/common/LoadMore.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import { useTaskStore } from '@/stores/task.store'
|
||||
import type { TaskListQuery, TaskSegment, TaskSort } from '@/types/task'
|
||||
@@ -243,18 +244,12 @@ const STATUS_LABEL: Record<TaskStatus, string> = {
|
||||
</div>
|
||||
|
||||
<!-- 업무 목록 -->
|
||||
<div
|
||||
v-if="loading && tasks.length === 0"
|
||||
class="state-msg"
|
||||
>
|
||||
<EmptyState v-if="loading && tasks.length === 0">
|
||||
업무를 불러오는 중…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="tasks.length === 0"
|
||||
class="state-msg"
|
||||
>
|
||||
</EmptyState>
|
||||
<EmptyState v-else-if="tasks.length === 0">
|
||||
{{ isFiltered ? '조건에 맞는 업무가 없습니다.' : '아직 등록된 업무가 없습니다. 새 업무를 지시해 보세요.' }}
|
||||
</div>
|
||||
</EmptyState>
|
||||
<div
|
||||
v-else
|
||||
class="list"
|
||||
@@ -576,13 +571,4 @@ const STATUS_LABEL: Record<TaskStatus, string> = {
|
||||
text-align: center;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.state-msg {
|
||||
padding: 2.5rem 1.125rem;
|
||||
text-align: center;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@ import AppShell from '@/layouts/AppShell.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import DropdownSelect from '@/components/DropdownSelect.vue'
|
||||
import LoadMore from '@/components/common/LoadMore.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import type { ProjectListQuery, ProjectSort } from '@/types/project'
|
||||
|
||||
@@ -204,18 +205,12 @@ const isFiltered = computed(() => keyword.value.trim() !== '')
|
||||
</RouterLink>
|
||||
|
||||
<!-- 로딩 / 빈 상태 -->
|
||||
<div
|
||||
v-if="loading && projects.length === 0"
|
||||
class="list-empty"
|
||||
>
|
||||
<EmptyState v-if="loading && projects.length === 0">
|
||||
불러오는 중…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="projects.length === 0"
|
||||
class="list-empty"
|
||||
>
|
||||
</EmptyState>
|
||||
<EmptyState v-else-if="projects.length === 0">
|
||||
{{ isFiltered ? '검색 결과가 없습니다.' : '아직 프로젝트가 없습니다. 새 프로젝트를 만들어 보세요.' }}
|
||||
</div>
|
||||
</EmptyState>
|
||||
</div>
|
||||
|
||||
<!-- 더보기 -->
|
||||
@@ -368,10 +363,4 @@ const isFiltered = computed(() => keyword.value.trim() !== '')
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
.list-empty {
|
||||
padding: 2.5rem 1.25rem;
|
||||
text-align: center;
|
||||
color: var(--text-3);
|
||||
font-size: 0.844rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -9,6 +9,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import DropdownSelect from '@/components/DropdownSelect.vue'
|
||||
import BaseModal from '@/components/common/BaseModal.vue'
|
||||
import SegmentedTabs from '@/components/common/SegmentedTabs.vue'
|
||||
import EmptyState from '@/components/common/EmptyState.vue'
|
||||
import { useProjectStore } from '@/stores/project.store'
|
||||
import { useMemberStore } from '@/stores/member.store'
|
||||
import { useUser } from '@/composables/useUser'
|
||||
@@ -325,18 +326,12 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||
</div>
|
||||
|
||||
<!-- 멤버 목록 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="state-msg"
|
||||
>
|
||||
<EmptyState v-if="loading">
|
||||
멤버를 불러오는 중…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="filteredMembers.length === 0"
|
||||
class="state-msg"
|
||||
>
|
||||
</EmptyState>
|
||||
<EmptyState v-else-if="filteredMembers.length === 0">
|
||||
표시할 멤버가 없습니다.
|
||||
</div>
|
||||
</EmptyState>
|
||||
<div
|
||||
v-else
|
||||
class="list"
|
||||
@@ -770,15 +765,6 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||
}
|
||||
|
||||
/* 로딩/빈 상태 메시지 */
|
||||
.state-msg {
|
||||
padding: 2.5rem 1.125rem;
|
||||
text-align: center;
|
||||
font-size: 0.844rem;
|
||||
color: var(--text-3);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* 역할 변경 드롭다운 */
|
||||
/* 전역 .list 의 overflow:hidden 이 드롭다운을 잘라내므로, 이 화면에서는 보이도록 해제.
|
||||
|
||||
Reference in New Issue
Block a user