From 5351373694fa474bbb959b6b62a9c898d74ef39d Mon Sep 17 00:00:00 2001 From: ttipo Date: Fri, 19 Jun 2026 14:04:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80(avatarUrl)=20=ED=95=84=EB=93=9C=20+=20UserAv?= =?UTF-8?q?atar=20=EB=8F=84=EC=9E=85,=20=EC=83=89=EC=83=81=C2=B7=EC=9D=B4?= =?UTF-8?q?=EB=8B=88=EC=85=9C=20=EC=95=84=EB=B0=94=ED=83=80=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 백엔드: User.avatarUrl(nullable) 추가 → PublicUser/toPublic 로 모든 유저 응답에 노출. - 프론트: 공용 UserAvatar(이미지 있으면 img, 없으면 사람 실루엣 placeholder)로 전 화면 아바타 통일(상단바·저장소·멤버·담당자·활동·내업무·댓글·AI패널). 화면마다 달랐던 색상+이니셜/하드코딩 청록 아바타 제거. - 죽은 코드 정리: 색상·이니셜 시스템(avatarColor/initialOf/AvatarColor 유틸·타입, User/RepoActivityItem 의 initial/color, 전역·컴포넌트 .av-a~.av-f CSS) + 미사용 정적 목업 데이터(USERS/REPOS/REPO_TASKS/TASK_DETAIL/MY_TASKS_*/REPO_MEMBERS/ REPO_ACTIVITY 등) 전부 삭제. relay.mock.ts 는 뷰 타입 + countActive 만 유지. avatar_url 컬럼은 dev synchronize 가 추가. 업로드(실제 사진 설정)는 추후 단계. 검증: 백엔드 build 0, 프론트 type-check/lint/build 0. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/modules/user/entities/user.entity.ts | 4 + backend/src/modules/user/user.service.ts | 2 + frontend/src/assets/styles/relay.css | 18 - frontend/src/components/RepoHeader.vue | 7 +- frontend/src/components/UserAvatar.vue | 55 ++ frontend/src/layouts/AppShell.vue | 11 +- frontend/src/mock/relay.mock.ts | 704 +----------------- frontend/src/pages/relay/MyTasksPage.vue | 31 +- frontend/src/pages/relay/RepoActivityPage.vue | 24 +- frontend/src/pages/relay/RepoCreatePage.vue | 3 +- frontend/src/pages/relay/RepoDetailPage.vue | 7 +- frontend/src/pages/relay/RepoListPage.vue | 7 +- frontend/src/pages/relay/RepoMembersPage.vue | 36 +- frontend/src/pages/relay/TaskCreatePage.vue | 32 +- frontend/src/pages/relay/TaskDetailPage.vue | 61 +- frontend/src/shared/utils/format.ts | 19 - frontend/src/stores/activity.store.ts | 9 +- frontend/src/stores/member.store.ts | 4 +- frontend/src/stores/my-task.store.ts | 5 +- frontend/src/stores/repo.store.ts | 5 +- frontend/src/stores/task.store.ts | 7 +- frontend/src/types/auth.ts | 1 + frontend/src/types/repo.ts | 1 + 23 files changed, 171 insertions(+), 882 deletions(-) create mode 100644 frontend/src/components/UserAvatar.vue diff --git a/backend/src/modules/user/entities/user.entity.ts b/backend/src/modules/user/entities/user.entity.ts index 44d8c4b..c61bbf6 100644 --- a/backend/src/modules/user/entities/user.entity.ts +++ b/backend/src/modules/user/entities/user.entity.ts @@ -29,6 +29,10 @@ export class User { @Column({ type: 'varchar', nullable: true }) role!: string | null; + // 프로필 이미지 URL — 미설정 시 null(프론트는 placeholder 표시). 업로드는 추후 단계. + @Column({ name: 'avatar_url', type: 'varchar', nullable: true }) + avatarUrl!: string | null; + @CreateDateColumn({ name: 'created_at' }) createdAt!: Date; diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts index 2a7b3a1..f3696d0 100644 --- a/backend/src/modules/user/user.service.ts +++ b/backend/src/modules/user/user.service.ts @@ -9,6 +9,7 @@ export interface PublicUser { email: string; name: string; role: string | null; + avatarUrl: string | null; } // 사용자 데이터 접근 계층 — 인증 모듈이 주입하여 사용 @@ -76,6 +77,7 @@ export class UserService { email: user.email, name: user.name, role: user.role, + avatarUrl: user.avatarUrl ?? null, }; } } diff --git a/frontend/src/assets/styles/relay.css b/frontend/src/assets/styles/relay.css index 1cba258..de5f9cb 100644 --- a/frontend/src/assets/styles/relay.css +++ b/frontend/src/assets/styles/relay.css @@ -189,24 +189,6 @@ body { color: #fff; flex-shrink: 0; } -.av-a { - background: #e0518d; -} -.av-b { - background: #3d8bf2; -} -.av-c { - background: #16a37b; -} -.av-d { - background: #b3631f; -} -.av-e { - background: #7c5cf0; -} -.av-f { - background: #d0982a; -} .av-more { background: #eceef1; color: var(--text-2); diff --git a/frontend/src/components/RepoHeader.vue b/frontend/src/components/RepoHeader.vue index f916e1c..1578df5 100644 --- a/frontend/src/components/RepoHeader.vue +++ b/frontend/src/components/RepoHeader.vue @@ -1,6 +1,7 @@ + + + + diff --git a/frontend/src/layouts/AppShell.vue b/frontend/src/layouts/AppShell.vue index f5051ba..4f24d26 100644 --- a/frontend/src/layouts/AppShell.vue +++ b/frontend/src/layouts/AppShell.vue @@ -6,6 +6,7 @@ import { storeToRefs } from 'pinia' import { useRoute, useRouter, RouterLink } from 'vue-router' import { useAuthStore } from '@/stores/auth.store' import { useNotificationStore } from '@/stores/notification.store' +import UserAvatar from '@/components/UserAvatar.vue' import type { NotificationView } from '@/types/notification' const route = useRoute() @@ -17,9 +18,8 @@ const activeNav = computed<'tasks' | 'repos'>(() => route.path.startsWith('/repos') ? 'repos' : 'tasks', ) -// 현재 로그인 사용자 표시(아바타 이니셜 = 이름 첫 글자) +// 현재 로그인 사용자(프로필 아바타는 UserAvatar 가 이미지/placeholder 로 표시) const me = computed(() => authStore.user) -const meInitial = computed(() => me.value?.name?.charAt(0) ?? '?') // 사용자 메뉴 토글 const menuOpen = ref(false) @@ -223,7 +223,10 @@ async function onLogout() { :aria-expanded="menuOpen" @click="menuOpen = !menuOpen" > - {{ meInitial }} + @@ -522,7 +525,7 @@ async function onLogout() { font-size: 0.719rem; font-weight: 700; color: #fff; - background: #0ea5a3; + /* 내부 UserAvatar 가 이미지/placeholder 로 배경을 채운다(버튼 자체 배경 없음) */ border: none; cursor: pointer; padding: 0; diff --git a/frontend/src/mock/relay.mock.ts b/frontend/src/mock/relay.mock.ts index e7bc977..3418d6a 100644 --- a/frontend/src/mock/relay.mock.ts +++ b/frontend/src/mock/relay.mock.ts @@ -1,11 +1,8 @@ /** - * Relay 목업 데이터 — 프론트 UI 우선 구성용 정적 데이터 - * 추후 backend API 연동 시 composable/store 에서 이 데이터를 실제 응답으로 교체한다. + * Relay 화면용 뷰 타입 정의. + * (구 목업 정적 데이터는 백엔드 연동 완료로 제거됨 — store/composable 가 실제 응답을 사용) */ -/** 아바타 색상 키 (relay.css 의 .av-* 클래스와 대응) */ -export type AvatarColor = 'av-a' | 'av-b' | 'av-c' | 'av-d' | 'av-e' | 'av-f' - /** 업무 상태 */ export type TaskStatus = 'todo' | 'prog' | 'review' | 'done' | 'changes' @@ -17,8 +14,8 @@ export interface User { id: string name: string role: string - initial: string - color: AvatarColor + /** 프로필 이미지 URL — 없으면 UserAvatar 가 placeholder 표시 */ + avatarUrl?: string | null } /** 첨부파일 */ @@ -152,322 +149,6 @@ export interface TaskDetail { issuedLabel: string } -/* ============================================================ - * 사용자 - * ============================================================ */ -// satisfies 로 선언해 각 키 접근이 `User | undefined` 로 넓어지지 않도록 한다 -// (tsconfig 의 noUncheckedIndexedAccess 대응) -export const USERS = { - kim: { id: 'kim', name: '김서연', role: '콘텐츠 기획', initial: '김', color: 'av-a' }, - park: { id: 'park', name: '박지훈', role: '퍼포먼스 마케팅', initial: '박', color: 'av-b' }, - lee: { id: 'lee', name: '이도윤', role: '카피라이팅', initial: '이', color: 'av-c' }, - choi: { id: 'choi', name: '최유진', role: 'UX 디자인', initial: '최', color: 'av-d' }, - han: { id: 'han', name: '한승우', role: '브랜드 전략', initial: '한', color: 'av-e' }, - jung: { id: 'jung', name: '정태경', role: '마케팅 그룹 리드', initial: '정', color: 'av-f' }, -} satisfies Record - -/* ============================================================ - * 저장소 목록 - * ============================================================ */ -export const REPOS: Repo[] = [ - { - id: 'q3-launch-campaign', - name: '3분기 신제품 런칭 캠페인', - owner: 'marketing-team', - slug: 'marketing-team/q3-launch-campaign', - desc: '3분기 신제품(라인 클렌저) 런칭 캠페인 소재 제작 및 검수', - visibility: 'private', - branch: 'main', - members: [USERS.kim, USERS.park, USERS.lee], - moreCount: 2, - updatedAgo: '2시간 전 업데이트', - doneCount: 8, - totalCount: 12, - progressPct: 67, - progressLevel: 'mid', - progressLabel: '67% 완료', - }, - { - id: 'brand-refresh-2026', - name: '브랜드 리뉴얼 2026', - owner: 'marketing-team', - slug: 'marketing-team/brand-refresh-2026', - desc: '브랜드 아이덴티티 리뉴얼 및 가이드라인 개편 프로젝트', - visibility: 'private', - branch: 'main', - members: [USERS.choi, USERS.jung, USERS.han], - moreCount: 1, - updatedAgo: '어제 업데이트', - doneCount: 5, - totalCount: 9, - progressPct: 56, - progressLevel: 'mid', - progressLabel: '56% 완료', - }, - { - id: 'monthly-newsletter', - name: '월간 뉴스레터 운영', - owner: 'marketing-team', - slug: 'marketing-team/monthly-newsletter', - desc: '월간 뉴스레터 기획 · 콘텐츠 작성 · 발송 정기 운영', - visibility: 'public', - branch: 'main', - members: [USERS.lee, USERS.park, USERS.kim], - moreCount: 0, - updatedAgo: '3일 전 업데이트', - doneCount: 14, - totalCount: 14, - progressPct: 100, - progressLevel: 'done', - progressLabel: '100% 완료', - }, - { - id: 'seo-content-hub', - name: 'SEO 콘텐츠 허브', - owner: 'marketing-team', - slug: 'marketing-team/seo-content-hub', - desc: '검색 유입 강화를 위한 SEO 콘텐츠 기획 및 제작 허브', - visibility: 'private', - branch: 'main', - members: [USERS.park, USERS.han, USERS.lee], - moreCount: 3, - updatedAgo: '5시간 전 업데이트', - doneCount: 2, - totalCount: 11, - progressPct: 18, - progressLevel: 'low', - progressLabel: '18% 완료', - }, - { - id: 'event-handbook', - name: '사내 행사 핸드북', - owner: 'marketing-team', - slug: 'marketing-team/event-handbook', - desc: '사내 행사 운영 가이드 및 준비 체크리스트 모음', - visibility: 'public', - branch: 'main', - members: [USERS.jung, USERS.kim], - moreCount: 0, - updatedAgo: '1주 전 업데이트', - doneCount: 0, - totalCount: 4, - progressPct: 0, - progressLevel: 'low', - progressLabel: '시작 전', - }, - { - id: 'design-system-assets', - name: '디자인 시스템 에셋', - owner: 'marketing-team', - slug: 'marketing-team/design-system-assets', - desc: '공통 디자인 시스템 에셋 및 컴포넌트 관리', - visibility: 'private', - branch: 'develop', - members: [USERS.kim, USERS.choi, USERS.lee], - moreCount: 1, - updatedAgo: '30분 전 업데이트', - doneCount: 9, - totalCount: 10, - progressPct: 90, - progressLevel: 'done', - progressLabel: '90% 완료', - }, -] - -/* ============================================================ - * 저장소 상세 — 업무 목록 (q3-launch-campaign 기준) - * ============================================================ */ -export const REPO_TASKS: Record = { - 'q3-launch-campaign': [ - { - id: 11, - title: '법무 검토 요청 및 회신 반영', - status: 'prog', - dueLabel: '2일 지남', - overdue: true, - checklist: [1, 3], - assignees: [USERS.lee], - }, - { - id: 9, - title: '메인 배너 이미지 2종 (가로/세로) 디자인', - status: 'review', - dueLabel: 'D-3', - checklist: [1, 3], - comments: 2, - assignees: [USERS.park, USERS.choi], - }, - { - id: 8, - title: '상세페이지 카피라이팅', - status: 'todo', - dueLabel: 'D-5', - checklist: [0, 3], - assignees: [USERS.lee], - }, - { - id: 7, - title: 'SNS 콘텐츠 1차 시안', - status: 'todo', - dueLabel: 'D-8', - checklist: [0, 4], - assignees: [USERS.han], - }, - { - id: 6, - title: '캠페인 핵심 메시지 시안 작성', - status: 'done', - dueLabel: '6월 9일 완료', - checklist: [3, 3], - assignees: [USERS.kim], - }, - { - id: 5, - title: '레퍼런스 리서치 및 무드보드 정리', - status: 'done', - dueLabel: '6월 6일 완료', - assignees: [USERS.kim], - }, - { - id: 4, - title: '퍼포먼스 광고 소재 세팅', - status: 'done', - dueLabel: '6월 5일 완료', - checklist: [2, 2], - assignees: [USERS.park], - }, - { - id: 3, - title: '타깃 오디언스 정의 및 페르소나 정리', - status: 'done', - dueLabel: '6월 4일 완료', - assignees: [USERS.park], - }, - { - id: 1, - title: '킥오프 미팅 및 일정 수립', - status: 'done', - dueLabel: '6월 2일 완료', - assignees: [USERS.jung], - }, - ], -} - -/* ============================================================ - * 업무 상세 (#9) - * ============================================================ */ -export const TASK_DETAIL: TaskDetail = { - id: 9, - repoId: 'q3-launch-campaign', - repoName: '3분기 신제품 런칭 캠페인', - title: '메인 배너 이미지 2종 (가로/세로) 디자인', - status: 'review', - statusLabel: '승인 대기', - content: [ - '9월 신제품 런칭 캠페인에 사용할 메인 배너를 가로형/세로형 2종으로 제작합니다. 톤앤매너는 @브랜드_가이드라인.pdf 기준을 따라주세요.', - '퍼포먼스 광고와 자사몰 상단에 동시 노출되므로, 두 비율 모두 핵심 메시지("민감 피부 저자극")가 명확히 보이도록 구성해주세요. 1차 시안 확정 후 모바일 적응형까지 진행합니다.', - ], - checklist: [ - { text: '가로형 메인 배너 1920×1080 디자인', done: true }, - { text: '세로형 메인 배너 1080×1920 디자인', done: false }, - { text: '모바일 적응형 600×600 제작', done: false }, - ], - comments: [ - { - author: USERS.jung, - roleBadge: '지시자', - time: '2일 전', - text: '가로형부터 확정하고 세로형 진행해주세요. 카피 위치는 좌측 정렬 기준입니다.', - replies: [ - { author: USERS.park, time: '2일 전', text: '네, 가로형 정렬 기준으로 먼저 잡겠습니다.' }, - ], - }, - { - author: USERS.park, - time: '1일 전', - text: '가로형 1차 시안 업로드했습니다. 검토 부탁드려요.', - file: 'main_banner_h_v1.png', - replies: [], - }, - { - author: USERS.choi, - time: '3시간 전', - text: '세로형은 내일 오전까지 공유하겠습니다.', - replies: [], - }, - ], - activities: [ - { - tone: 'amber', - icon: 'check-circle', - html: '박지훈님이 승인을 요청했습니다', - statusFrom: '진행 중', - statusTo: '승인 대기', - time: '2시간 전 · 오후 3:24', - }, - { - tone: 'green', - icon: 'check', - html: '박지훈님이 가로형 메인 배너 1920×1080 디자인 항목을 완료했습니다', - time: '1일 전 · 오후 6:10', - }, - { - tone: '', - icon: 'paperclip', - html: '박지훈님이 파일을 첨부했습니다', - file: 'main_banner_h_v1.png', - time: '1일 전 · 오후 6:08', - }, - { - tone: 'blue', - icon: 'calendar', - html: '정태경님이 마감기한을 6월 18일 (목)로 설정했습니다', - time: '6월 8일 · 오전 10:32', - }, - { - tone: 'blue', - icon: 'users', - html: '정태경님이 박지훈, 최유진님을 담당자로 지정했습니다', - time: '6월 8일 · 오전 10:31', - }, - { - tone: 'accent', - icon: 'flag', - html: '정태경님이 업무를 생성했습니다', - time: '6월 8일 · 오전 10:30', - }, - ], - assignees: [USERS.park, USERS.choi], - issuer: USERS.jung, - dueLabel: '6월 18일 (목)', - dday: 'D-3', - files: [ - { name: '레퍼런스_무드보드.png', size: '3.2 MB', kind: 'img' }, - { name: '배너_소스_파일.zip', size: '18.4 MB', kind: 'zip' }, - ], - approval: { - requester: '박지훈', - requestedAgo: '2시간 전', - note: '가로형/세로형 메인 배너 1차 제작을 완료했습니다. 모바일 적응형은 컨펌 후 진행 예정이며, 우선 두 비율 검토 부탁드립니다.', - }, - issuedLabel: '6월 8일 지시 · #9', -} - -/* ============================================================ - * 조회 헬퍼 - * ============================================================ */ -export function findRepo(id: string): Repo | undefined { - return REPOS.find((r) => r.id === id) -} - -export function findRepoTasks(id: string): RepoTask[] { - return REPO_TASKS[id] ?? [] -} - -/* ============================================================ - * 내 업무 (담당 / 지시) — 상태 그룹 단위 정적 데이터 - * ============================================================ */ - /** D-day 강조 단계 */ export type DdayLevel = 'urgent' | 'soon' | 'calm' @@ -512,257 +193,12 @@ export interface MyTaskGroup { rows: MyTaskRow[] } -/** 담당 업무 */ -export const MY_TASKS_ASSIGNED: MyTaskGroup[] = [ - { - key: 'changes', - label: '수정 요청', - rows: [ - { - id: 14, - title: '3분기 캠페인 예산안 재작성', - status: 'changes', - repoName: '3분기 신제품 런칭 캠페인', - metaText: '지시 · 한승우', - dueLabel: '6월 16일', - dday: 'D-1', - ddayLevel: 'urgent', - statusLabel: '수정 요청', - }, - ], - }, - { - key: 'prog', - label: '진행 중', - rows: [ - { - id: 12, - title: '브랜드 리뉴얼 킥오프 기획서 작성', - status: 'prog', - repoName: '브랜드 리뉴얼 2026', - checklist: [2, 5], - metaText: '지시 · 한승우', - dueLabel: '6월 18일', - dday: 'D-3', - ddayLevel: 'soon', - statusLabel: '진행 중', - }, - { - id: 10, - title: '9월 신제품 GTM 전략 수립', - status: 'prog', - repoName: '3분기 신제품 런칭 캠페인', - checklist: [1, 4], - metaText: '지시 · 한승우', - dueLabel: '6월 21일', - dday: 'D-6', - ddayLevel: 'calm', - statusLabel: '진행 중', - }, - ], - }, - { - key: 'todo', - label: '할 일', - rows: [ - { - id: 15, - title: '월간 뉴스레터 8월호 주제 선정', - status: 'todo', - repoName: '월간 뉴스레터', - metaText: '지시 · 한승우', - dueLabel: '6월 24일', - dday: 'D-9', - ddayLevel: 'calm', - statusLabel: '할 일', - }, - { - id: 13, - title: 'SEO 키워드 전략 분기 리뷰', - status: 'todo', - repoName: 'SEO 콘텐츠 허브', - metaText: '지시 · 한승우', - dueLabel: '6월 27일', - dday: 'D-12', - ddayLevel: 'calm', - statusLabel: '할 일', - }, - ], - }, - { - key: 'review', - label: '승인 대기', - rows: [ - { - id: 2, - title: '브랜드 무드보드 방향 정리', - status: 'review', - repoName: '브랜드 리뉴얼 2026', - metaText: '한승우님 검토 중 · 1일 전 요청', - dueLabel: '6월 17일', - dday: 'D-2', - ddayLevel: 'soon', - statusLabel: '승인 대기', - }, - ], - }, - { - key: 'done', - label: '완료', - rows: [ - { - id: 6, - title: '2분기 캠페인 성과 회고 정리', - status: 'done', - repoName: '월간 뉴스레터', - metaText: '지시 · 한승우', - dueLabel: '6월 11일 승인됨', - statusLabel: '완료', - done: true, - }, - { - id: 5, - title: '신제품 네이밍 후보 정리', - status: 'done', - repoName: '3분기 신제품 런칭 캠페인', - metaText: '지시 · 한승우', - dueLabel: '6월 5일 승인됨', - statusLabel: '완료', - done: true, - }, - ], - }, -] - -/** 지시한 업무 */ -export const MY_TASKS_ISSUED: MyTaskGroup[] = [ - { - key: 'review', - label: '승인 대기', - action: '· 내 승인 필요', - attn: true, - rows: [ - { - id: 9, - title: '메인 배너 이미지 2종 (가로/세로) 디자인', - status: 'review', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.park, USERS.choi], - metaText: '박지훈님이 2시간 전 승인 요청', - metaAttn: true, - dueLabel: '6월 18일', - dday: 'D-3', - ddayLevel: 'soon', - reviewBtn: true, - }, - ], - }, - { - key: 'prog', - label: '진행 중', - rows: [ - { - id: 11, - title: '법무 검토 요청 및 회신 반영', - status: 'prog', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.lee], - metaText: '이도윤', - dueLabel: '6월 12일', - dday: '3일 지남', - ddayLevel: 'urgent', - overdue: true, - statusLabel: '진행 중', - }, - { - id: 16, - title: 'SNS 콘텐츠 2차 시안 제작', - status: 'prog', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.han], - metaText: '정민호', - checklist: [3, 6], - dueLabel: '6월 19일', - dday: 'D-4', - ddayLevel: 'soon', - statusLabel: '진행 중', - }, - ], - }, - { - key: 'changes', - label: '수정 요청함', - rows: [ - { - id: 8, - title: '상세페이지 카피라이팅', - status: 'changes', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.lee], - metaText: '이도윤에게 수정 요청 · 1일 전', - dueLabel: '6월 20일', - dday: 'D-5', - ddayLevel: 'soon', - statusLabel: '수정 요청', - }, - ], - }, - { - key: 'todo', - label: '할 일', - rows: [ - { - id: 7, - title: 'SNS 콘텐츠 1차 시안', - status: 'todo', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.han], - metaText: '정민호', - dueLabel: '6월 23일', - dday: 'D-8', - ddayLevel: 'calm', - statusLabel: '할 일', - }, - ], - }, - { - key: 'done', - label: '완료', - rows: [ - { - id: 6, - title: '캠페인 핵심 메시지 시안 작성', - status: 'done', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.kim], - metaText: '김서연', - dueLabel: '6월 9일 승인됨', - statusLabel: '완료', - done: true, - }, - { - id: 4, - title: '퍼포먼스 광고 소재 세팅', - status: 'done', - repoName: '3분기 신제품 런칭 캠페인', - assignees: [USERS.park], - metaText: '박지훈', - dueLabel: '6월 5일 승인됨', - statusLabel: '완료', - done: true, - }, - ], - }, -] - -/** 비완료(활성) 업무 수 — 뷰 탭 카운트용 */ +/** 진행 중(완료 제외) 업무 수 — 탭 카운트 */ export function countActive(groups: MyTaskGroup[]): number { return groups.filter((g) => g.key !== 'done').reduce((sum, g) => sum + g.rows.length, 0) } -/* ============================================================ - * 저장소 멤버 - * ============================================================ */ +/** 저장소 멤버 */ export interface RepoMember { user: User email: string @@ -776,23 +212,6 @@ export interface RepoMember { owner?: boolean } -export const REPO_MEMBERS: Record = { - 'q3-launch-campaign': [ - { user: USERS.jung, email: 'tkjung@acme.co', taskCount: 1, roleType: 'admin', isYou: true, owner: true }, - { user: USERS.kim, email: 'sykim@acme.co', taskCount: 2, roleType: 'member' }, - { user: USERS.park, email: 'jhpark@acme.co', taskCount: 2, roleType: 'member' }, - { user: USERS.lee, email: 'dylee@acme.co', taskCount: 2, roleType: 'member' }, - { user: USERS.choi, email: 'yjchoi@acme.co', taskCount: 1, roleType: 'member' }, - ], -} - -export function findRepoMembers(id: string): RepoMember[] { - return REPO_MEMBERS[id] ?? [] -} - -/* ============================================================ - * 저장소 활동 피드 - * ============================================================ */ /** 활동 아이콘 종류(모양) */ export type ActivityIcon = | 'check' @@ -809,9 +228,8 @@ export interface RepoActivityItem { tone: 'task' | 'member' | 'setting' /** 아이콘 모양 */ icon: ActivityIcon - /** 행위자 아바타 */ - initial: string - color: AvatarColor + /** 행위자 프로필 이미지 URL — 없으면 placeholder */ + avatarUrl?: string | null /** 본문 HTML(내부 생성, 신뢰 가능 마크업) */ html: string time: string @@ -822,109 +240,3 @@ export interface RepoActivityDay { day: string items: RepoActivityItem[] } - -export const REPO_ACTIVITY: RepoActivityDay[] = [ - { - day: '오늘', - items: [ - { - tone: 'task', - icon: 'check', - initial: '이', - color: 'av-c', - html: '이도윤님이 법무 검토 요청 및 회신 반영 업무를 진행 중으로 변경했습니다', - time: '2시간 전', - }, - { - tone: 'task', - icon: 'check', - initial: '박', - color: 'av-b', - html: '박지훈님이 메인 배너 이미지 2종 디자인의 체크리스트 ‘가로형 메인 배너’를 완료했습니다', - time: '3시간 전', - }, - { - tone: 'member', - icon: 'member-add', - initial: '정', - color: 'av-f', - html: '정태경님이 한지민님을 멤버로 초대했습니다', - time: '5시간 전', - }, - { - tone: 'member', - icon: 'member-check', - initial: '한', - color: 'av-d', - html: '한지민님이 멤버로 합류했습니다', - time: '5시간 전', - }, - ], - }, - { - day: '어제', - items: [ - { - tone: 'task', - icon: 'check', - initial: '최', - color: 'av-d', - html: '최유진님이 메인 비주얼 컨셉 확정 업무를 완료했습니다', - time: '어제 18:40', - }, - { - tone: 'task', - icon: 'pencil', - initial: '정', - color: 'av-f', - html: '정태경님이 SNS 콘텐츠 1차 시안 업무를 지시했습니다 · 담당 정민호', - time: '어제 14:10', - }, - ], - }, - { - day: '이번 주', - items: [ - { - tone: 'setting', - icon: 'lock', - initial: '정', - color: 'av-f', - html: '정태경님이 저장소 공개 범위를 비공개로 변경했습니다', - time: '6월 11일', - }, - { - tone: 'member', - icon: 'member-remove', - initial: '박', - color: 'av-b', - html: '박지훈님의 역할이 멤버로 설정되었습니다', - time: '6월 11일', - }, - { - tone: 'setting', - icon: 'pencil', - initial: '정', - color: 'av-f', - html: '정태경님이 표시 제목을 ‘3분기 신제품 런칭 캠페인’으로 변경했습니다', - time: '6월 10일', - }, - { - tone: 'task', - icon: 'check', - initial: '김', - color: 'av-a', - html: '김서연님이 캠페인 핵심 메시지 시안 작성 업무를 완료했습니다', - time: '6월 9일', - }, - { - tone: 'setting', - icon: 'repo', - initial: '정', - color: 'av-f', - html: '정태경님이 저장소를 생성했습니다', - time: '6월 2일', - }, - ], - }, -] diff --git a/frontend/src/pages/relay/MyTasksPage.vue b/frontend/src/pages/relay/MyTasksPage.vue index d69f4fb..7fe9288 100644 --- a/frontend/src/pages/relay/MyTasksPage.vue +++ b/frontend/src/pages/relay/MyTasksPage.vue @@ -3,6 +3,7 @@ import { computed, onMounted, ref } from 'vue' import { RouterLink } from 'vue-router' import AppShell from '@/layouts/AppShell.vue' +import UserAvatar from '@/components/UserAvatar.vue' import { useMyTaskStore } from '@/stores/my-task.store' import { countActive, type MyTaskRow } from '@/mock/relay.mock' @@ -258,8 +259,10 @@ function loadMore() { v-for="a in row.assignees" :key="a.id" class="mini-av" - :class="a.color" - >{{ a.initial }} + > {{ row.metaText }} @@ -271,8 +274,10 @@ function loadMore() { v-for="a in row.assignees" :key="a.id" class="mini-av" - :class="a.color" - >{{ a.initial }} + > @@ -676,24 +681,6 @@ function loadMore() { box-shadow: 0 0 0 1.5px #fff; flex-shrink: 0; } -.mini-av.av-a { - background: #e0518d; -} -.mini-av.av-b { - background: #3d8bf2; -} -.mini-av.av-c { - background: #16a37b; -} -.mini-av.av-d { - background: #b3631f; -} -.mini-av.av-e { - background: #7c5cf0; -} -.mini-av.av-f { - background: #d0982a; -} .mini-stack .mini-av + .mini-av { margin-left: -0.3125rem; } diff --git a/frontend/src/pages/relay/RepoActivityPage.vue b/frontend/src/pages/relay/RepoActivityPage.vue index ca620dc..3996da3 100644 --- a/frontend/src/pages/relay/RepoActivityPage.vue +++ b/frontend/src/pages/relay/RepoActivityPage.vue @@ -5,6 +5,7 @@ import { useRoute, RouterLink } from 'vue-router' import AppShell from '@/layouts/AppShell.vue' import RepoHeader from '@/components/RepoHeader.vue' import RepoTabs from '@/components/RepoTabs.vue' +import UserAvatar from '@/components/UserAvatar.vue' import { useRepoStore } from '@/stores/repo.store' import { useActivityStore } from '@/stores/activity.store' import type { Repo, ActivityIcon } from '@/mock/relay.mock' @@ -209,10 +210,7 @@ const ActIcon = defineComponent({ :class="item.tone" >
- {{ item.initial }} + initialOf(owner.value || '?').toUpperCase()) +const ownerInitial = computed(() => (owner.value.trim().charAt(0) || '?').toUpperCase()) onMounted(() => { void repoStore.fetchCreateMeta() }) diff --git a/frontend/src/pages/relay/RepoDetailPage.vue b/frontend/src/pages/relay/RepoDetailPage.vue index 38ba2a4..9078864 100644 --- a/frontend/src/pages/relay/RepoDetailPage.vue +++ b/frontend/src/pages/relay/RepoDetailPage.vue @@ -8,6 +8,7 @@ import AppShell from '@/layouts/AppShell.vue' import RepoHeader from '@/components/RepoHeader.vue' import RepoTabs from '@/components/RepoTabs.vue' import GiteaRepoBar from '@/components/GiteaRepoBar.vue' +import UserAvatar from '@/components/UserAvatar.vue' import { useRepoStore } from '@/stores/repo.store' import { useTaskStore } from '@/stores/task.store' import type { TaskListQuery, TaskSegment } from '@/types/task' @@ -378,8 +379,10 @@ const STATUS_LABEL: Record = { v-for="a in task.assignees" :key="a.id" class="avatar" - :class="a.color" - >{{ a.initial }} + > v-for="m in repo.members" :key="m.id" class="avatar" - :class="m.color" - >{{ m.initial }} + > window.removeEventListener('keydown', onKeydown)) :key="m.user.id" class="mem-row" > - {{ m.user.initial }} +
{{ m.user.name }} window.removeEventListener('keydown', onKeydown)) class="user-dd-item" @mousedown.prevent="selectUser(u)" > - {{ initialOf(u.name) }} + {{ u.name }} {{ u.email }} @@ -659,24 +659,6 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown)) color: #fff; flex-shrink: 0; } -.mem-av.av-a { - background: #e0518d; -} -.mem-av.av-b { - background: #3d8bf2; -} -.mem-av.av-c { - background: #16a37b; -} -.mem-av.av-d { - background: #b3631f; -} -.mem-av.av-e { - background: #7c5cf0; -} -.mem-av.av-f { - background: #d0982a; -} .mem-info { flex: 1; min-width: 0; diff --git a/frontend/src/pages/relay/TaskCreatePage.vue b/frontend/src/pages/relay/TaskCreatePage.vue index 260def1..c74d7a2 100644 --- a/frontend/src/pages/relay/TaskCreatePage.vue +++ b/frontend/src/pages/relay/TaskCreatePage.vue @@ -4,12 +4,12 @@ import { computed, ref, watch } from 'vue' import { useRoute, useRouter, RouterLink } from 'vue-router' import AppShell from '@/layouts/AppShell.vue' +import UserAvatar from '@/components/UserAvatar.vue' import { useRepoStore } from '@/stores/repo.store' import { useMemberStore } from '@/stores/member.store' import { useTaskStore } from '@/stores/task.store' import { useAuthStore } from '@/stores/auth.store' import { useTask } from '@/composables/useTask' -import { avatarColor, initialOf } from '@/shared/utils/format' import type { Repo, ChecklistItem, User } from '@/mock/relay.mock' import type { ApiMember } from '@/types/repo' @@ -32,8 +32,7 @@ function toUserView(m: ApiMember): User { id: m.id, name: m.name, role: m.role ?? '', - initial: initialOf(m.name), - color: avatarColor(m.id), + avatarUrl: m.avatarUrl, } } @@ -100,8 +99,7 @@ const issuer = computed(() => { id: u?.id ?? '', name: u?.name ?? '나', role: u?.role ?? '', - initial: initialOf(u?.name ?? '나'), - color: avatarColor(u?.id ?? ''), + avatarUrl: u?.avatarUrl ?? null, } }) @@ -470,10 +468,10 @@ function goBack() { class="assignee-dd-item" @mousedown.prevent="addAssignee(m.user.id)" > - {{ m.user.initial }} + {{ m.user.name }} {{ m.email }} @@ -495,10 +493,10 @@ function goBack() { :key="a.id" class="chip" > - {{ a.initial }} + {{ a.name }}
- {{ issuer.initial }} +
{{ issuer.name }}
diff --git a/frontend/src/pages/relay/TaskDetailPage.vue b/frontend/src/pages/relay/TaskDetailPage.vue index 1f24387..620c167 100644 --- a/frontend/src/pages/relay/TaskDetailPage.vue +++ b/frontend/src/pages/relay/TaskDetailPage.vue @@ -23,7 +23,8 @@ import { } from '@/mock/relay.mock' import { useTaskStore } from '@/stores/task.store' import { useAuthStore } from '@/stores/auth.store' -import { attachmentKindOf, avatarColor, fileBadge, initialOf } from '@/shared/utils/format' +import UserAvatar from '@/components/UserAvatar.vue' +import { attachmentKindOf, fileBadge } from '@/shared/utils/format' const route = useRoute() const router = useRouter() @@ -188,8 +189,7 @@ const me = computed(() => { id: u?.id ?? '', name: u?.name ?? '나', role: u?.role ?? '', - initial: initialOf(u?.name ?? '나'), - color: avatarColor(u?.id ?? ''), + avatarUrl: u?.avatarUrl ?? null, } }) @@ -769,10 +769,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { :key="i" class="comment" > - {{ c.author.initial }} +
{{ c.author.name }} @@ -804,10 +804,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { :key="ri" class="c-reply" > - {{ r.author.initial }} +
{{ r.author.name }}{{ r.time }} @@ -841,10 +841,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } { class="reply-form" :class="{ open: replyOpen === i }" > - {{ me.initial }} +