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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user