refactor: 세그먼트 토글 SegmentedTabs 컴포넌트로 통일 (Phase 3-1)

3개 페이지(활동/프로젝트 상세/멤버)에 반복되던 .segmented 버튼 마크업을
options 기반 SegmentedTabs 로 추출(전역 .segmented 스타일은 그대로 사용).
수량(.n) 표기는 option.count 로 처리.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 23:49:09 +09:00
parent 7425d57134
commit d3a916dbd7
4 changed files with 67 additions and 72 deletions
@@ -7,11 +7,18 @@ import ProjectHeader from '@/components/ProjectHeader.vue'
import ProjectTabs from '@/components/ProjectTabs.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import DropdownSelect from '@/components/DropdownSelect.vue'
import SegmentedTabs from '@/components/common/SegmentedTabs.vue'
import { useProjectStore } from '@/stores/project.store'
import { useActivityStore } from '@/stores/activity.store'
import type { Project, ActivityIcon } from '@/mock/relay.mock'
type Filter = 'all' | 'task' | 'member' | 'setting'
const FILTER_OPTIONS: { value: Filter; label: string }[] = [
{ value: 'all', label: '전체' },
{ value: 'task', label: '업무' },
{ value: 'member', label: '멤버' },
{ value: 'setting', label: '설정' },
]
const route = useRoute()
const projectId = computed(() => String(route.params.projectId))
@@ -139,32 +146,10 @@ const ActIcon = defineComponent({
<!-- 툴바 -->
<div class="toolbar">
<div class="segmented">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
전체
</button>
<button
:class="{ active: filter === 'task' }"
@click="filter = 'task'"
>
업무
</button>
<button
:class="{ active: filter === 'member' }"
@click="filter = 'member'"
>
멤버
</button>
<button
:class="{ active: filter === 'setting' }"
@click="filter = 'setting'"
>
설정
</button>
</div>
<SegmentedTabs
v-model="filter"
:options="FILTER_OPTIONS"
/>
<DropdownSelect
v-model="memberFilter"
:options="memberOptions"
+14 -26
View File
@@ -9,6 +9,7 @@ import ProjectHeader from '@/components/ProjectHeader.vue'
import ProjectTabs from '@/components/ProjectTabs.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import DropdownSelect from '@/components/DropdownSelect.vue'
import SegmentedTabs from '@/components/common/SegmentedTabs.vue'
import { useProjectStore } from '@/stores/project.store'
import { useTaskStore } from '@/stores/task.store'
import type { TaskListQuery, TaskSegment, TaskSort } from '@/types/task'
@@ -32,6 +33,15 @@ async function loadProject() {
const keyword = ref('')
const segment = ref<TaskSegment>('all')
// 세그먼트 탭 옵션(수량 포함) — counts 갱신에 반응
const segOptions = computed<{ value: TaskSegment; label: string; count: number }[]>(
() => [
{ value: 'all', label: '전체', count: counts.value.all },
{ value: 'prog', label: '진행 중', count: counts.value.prog },
{ value: 'todo', label: '대기', count: counts.value.todo },
{ value: 'done', label: '완료', count: counts.value.done },
],
)
const sort = ref<TaskSort>('latest')
const SORT_OPTIONS: { value: TaskSort; label: string }[] = [
{ value: 'latest', label: '최신순' },
@@ -188,32 +198,10 @@ const STATUS_LABEL: Record<TaskStatus, string> = {
placeholder="업무 검색…"
>
</div>
<div class="segmented">
<button
:class="{ active: segment === 'all' }"
@click="segment = 'all'"
>
전체 <span class="n">{{ counts.all }}</span>
</button>
<button
:class="{ active: segment === 'prog' }"
@click="segment = 'prog'"
>
진행 <span class="n">{{ counts.prog }}</span>
</button>
<button
:class="{ active: segment === 'todo' }"
@click="segment = 'todo'"
>
대기 <span class="n">{{ counts.todo }}</span>
</button>
<button
:class="{ active: segment === 'done' }"
@click="segment = 'done'"
>
완료 <span class="n">{{ counts.done }}</span>
</button>
</div>
<SegmentedTabs
v-model="segment"
:options="segOptions"
/>
<DropdownSelect
v-model="sort"
:options="SORT_OPTIONS"
+14 -20
View File
@@ -8,6 +8,7 @@ import ProjectTabs from '@/components/ProjectTabs.vue'
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 { useProjectStore } from '@/stores/project.store'
import { useMemberStore } from '@/stores/member.store'
import { useUser } from '@/composables/useUser'
@@ -71,6 +72,15 @@ const counts = computed(() => ({
member: members.value.filter((m) => m.roleType === 'member').length,
}))
// 역할 필터 세그먼트 옵션(수량 포함)
const filterOptions = computed<{ value: RoleFilter; label: string; count: number }[]>(
() => [
{ value: 'all', label: '전체', count: counts.value.all },
{ value: 'admin', label: '관리자', count: counts.value.admin },
{ value: 'member', label: '멤버', count: counts.value.member },
],
)
const filteredMembers = computed(() =>
members.value.filter((m) => {
const matchRole = filter.value === 'all' || m.roleType === filter.value
@@ -286,26 +296,10 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
placeholder="이름·이메일 검색…"
>
</div>
<div class="segmented">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
전체 <span class="n">{{ counts.all }}</span>
</button>
<button
:class="{ active: filter === 'admin' }"
@click="filter = 'admin'"
>
관리자 <span class="n">{{ counts.admin }}</span>
</button>
<button
:class="{ active: filter === 'member' }"
@click="filter = 'member'"
>
멤버 <span class="n">{{ counts.member }}</span>
</button>
</div>
<SegmentedTabs
v-model="filter"
:options="filterOptions"
/>
<button
v-if="isAdmin"
class="btn primary invite"