diff --git a/frontend/src/components/common/ActivityFeed.vue b/frontend/src/components/common/ActivityFeed.vue new file mode 100644 index 0000000..2f43b5d --- /dev/null +++ b/frontend/src/components/common/ActivityFeed.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/frontend/src/pages/relay/DrivePage.vue b/frontend/src/pages/relay/DrivePage.vue index 79323e4..7e4c4bd 100644 --- a/frontend/src/pages/relay/DrivePage.vue +++ b/frontend/src/pages/relay/DrivePage.vue @@ -8,6 +8,7 @@ import AppShell from '@/layouts/AppShell.vue' import EmptyState from '@/components/common/EmptyState.vue' import BaseModal from '@/components/common/BaseModal.vue' import LoadMore from '@/components/common/LoadMore.vue' +import ActivityFeed from '@/components/common/ActivityFeed.vue' import DriveNameModal from '@/components/drive/DriveNameModal.vue' import { useDriveStore } from '@/stores/drive.store' import { useDialog } from '@/composables/useDialog' @@ -18,7 +19,7 @@ import { isAllowedDriveMime, resolveMimeType, } from '@/shared/utils/drive' -import type { DriveActivity, DriveFile, DriveFolder } from '@/types/drive' +import type { DriveFile, DriveFolder } from '@/types/drive' const route = useRoute() const router = useRouter() @@ -33,7 +34,7 @@ const { uploads, itemCount, isEmpty, - activities, + activityDays, activityLoading, hasMoreActivity, } = storeToRefs(store) @@ -188,56 +189,6 @@ function openActivity(): void { showActivity.value = true void store.fetchActivity() // 열 때마다 최신으로 갱신 } - -// payload(자유형) 안전 추출 -function ps(payload: Record, key: string): string { - const v = payload[key] - return typeof v === 'string' ? v : '' -} -// 행위자 표시명(탈퇴 시 대체 문구) -function actorName(a: DriveActivity): string { - return a.actor?.name ?? '(탈퇴한 사용자)' -} -// 행위 문구 조립 — 대상 이름을 강조하기 위해 앞/대상/뒤 3토막으로 반환 -function actionParts(a: DriveActivity): { pre: string; target: string; post: string } { - const p = a.payload - switch (a.type) { - case 'file_uploaded': - return { pre: '파일 ', target: ps(p, 'name'), post: '을(를) 업로드했습니다' } - case 'file_deleted': - return { pre: '파일 ', target: ps(p, 'name'), post: '을(를) 삭제했습니다' } - case 'file_renamed': - return { - pre: '파일 이름을 ', - target: `${ps(p, 'fromName')} → ${ps(p, 'toName')}`, - post: '(으)로 변경했습니다', - } - case 'folder_created': - return { pre: '폴더 ', target: ps(p, 'name'), post: '을(를) 만들었습니다' } - case 'folder_deleted': { - const cnt = typeof p.fileCount === 'number' ? p.fileCount : 0 - return { - pre: '폴더 ', - target: ps(p, 'name'), - post: `을(를) 삭제했습니다 (파일 ${cnt}개 포함)`, - } - } - case 'folder_renamed': - return { - pre: '폴더 이름을 ', - target: `${ps(p, 'fromName')} → ${ps(p, 'toName')}`, - post: '(으)로 변경했습니다', - } - default: - return { pre: '', target: '', post: '' } - } -} -// 타입별 아이콘 톤(업로드/생성=accent, 삭제=danger, 이름변경=muted) -function actionTone(type: DriveActivity['type']): 'up' | 'del' | 'edit' { - if (type === 'file_deleted' || type === 'folder_deleted') return 'del' - if (type === 'file_renamed' || type === 'folder_renamed') return 'edit' - return 'up' -} - + 불러오는 중… - + 아직 기록이 없습니다. -
    -
  • - - - {{ actorName(a) }}님이 {{ actionParts(a).pre }}{{ actionParts(a).target }}{{ actionParts(a).post }} - {{ relativeTimeKo(a.createdAt) }} - -
  • -
+ :days="activityDays" + bare + /> // 프로젝트 활동 — 날짜 그룹 단위 활동 피드 (API 연동) -import { computed, defineComponent, h, ref, watch } from 'vue' +import { computed, ref, watch } from 'vue' import { useRoute, RouterLink } from 'vue-router' import AppShell from '@/layouts/AppShell.vue' 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 LoadMore from '@/components/common/LoadMore.vue' import EmptyState from '@/components/common/EmptyState.vue' +import ActivityFeed from '@/components/common/ActivityFeed.vue' import { useProjectStore } from '@/stores/project.store' import { useActivityStore } from '@/stores/activity.store' -import type { Project, ActivityIcon } from '@/mock/relay.mock' +import type { Project } from '@/mock/relay.mock' type Filter = 'all' | 'task' | 'member' | 'setting' const FILTER_OPTIONS: { value: Filter; label: string }[] = [ @@ -82,32 +82,6 @@ const memberCount = computed(() => ) const loading = computed(() => activityStore.loading) const isEmpty = computed(() => !loading.value && days.value.length === 0) - -// 활동 아이콘 — name 으로 SVG path 분기 -const ACT_PATHS: Record = { - check: '', - pencil: '', - 'member-add': '', - 'member-check': '', - 'member-remove': '', - lock: '', - repo: '', -} -const ActIcon = defineComponent({ - props: { name: { type: String, required: true } }, - setup(props) { - return () => - h('svg', { - viewBox: '0 0 24 24', - fill: 'none', - stroke: 'currentColor', - 'stroke-width': '2', - 'stroke-linecap': 'round', - 'stroke-linejoin': 'round', - innerHTML: ACT_PATHS[props.name as ActivityIcon] ?? '', - }) - }, -})