From f7b8a4c42de5cd292f61ec34f7fb431fc1ffa194 Mon Sep 17 00:00:00 2001 From: ttipo Date: Tue, 30 Jun 2026 23:03:43 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20=ED=99=9C=EB=8F=99=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EA=B3=B5=EC=9A=A9=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EC=B6=9C=20=EB=B0=8F=20=EB=93=9C=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=EA=B8=B0=EB=A1=9D=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로젝트 활동 페이지에 인라인 중복 구현돼 있던 활동 피드(원형 아이콘+ 아바타+본문+날짜그룹) 마크업·아이콘맵·스타일을 공용 ActivityFeed로 추출한다. 드라이브 '기록' 패널도 단색 점 목록에서 이 공용 컴포넌트로 교체해 다른 화면과 동일한 디자인 시스템을 쓰도록 통일한다. - 신규 components/common/ActivityFeed.vue (모달 임베드용 bare 옵션 포함) - ProjectActivityPage: 인라인 피드 제거 → ActivityFeed 사용 - drive.store: 감사 로그를 ProjectActivityDay[]로 변환(activityDays), tone setting·repo/pencil 아이콘, escapeHtml로 XSS 방지 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/common/ActivityFeed.vue | 210 ++++++++++++++++++ frontend/src/pages/relay/DrivePage.vue | 128 +---------- .../src/pages/relay/ProjectActivityPage.vue | 186 +--------------- frontend/src/stores/drive.store.ts | 67 ++++++ 4 files changed, 292 insertions(+), 299 deletions(-) create mode 100644 frontend/src/components/common/ActivityFeed.vue 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] ?? '', - }) - }, -})