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:
2026-06-22 23:56:07 +09:00
parent e7760e9ddc
commit d3e2861440
7 changed files with 49 additions and 95 deletions
+14
View File
@@ -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>
+5 -20
View File
@@ -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>
+5 -19
View File
@@ -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>
+5 -16
View File
@@ -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 이 드롭다운을 잘라내므로, 이 화면에서는 보이도록 해제.