feat: 프로필 이미지(avatarUrl) 필드 + UserAvatar 도입, 색상·이니셜 아바타 제거
- 백엔드: 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 }}</span>
|
||||
><UserAvatar
|
||||
:url="a.avatarUrl"
|
||||
:name="a.name"
|
||||
/></span>
|
||||
</span>
|
||||
{{ row.metaText }}
|
||||
</span>
|
||||
@@ -271,8 +274,10 @@ function loadMore() {
|
||||
v-for="a in row.assignees"
|
||||
:key="a.id"
|
||||
class="mini-av"
|
||||
:class="a.color"
|
||||
>{{ a.initial }}</span>
|
||||
><UserAvatar
|
||||
:url="a.avatarUrl"
|
||||
:name="a.name"
|
||||
/></span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="meta-sep" />
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
><ActIcon :name="item.icon" /></span>
|
||||
<div class="act-main">
|
||||
<span
|
||||
class="act-av"
|
||||
:class="item.color"
|
||||
>{{ item.initial }}</span>
|
||||
<span class="act-av"><UserAvatar :url="item.avatarUrl" /></span>
|
||||
<span
|
||||
class="act-text"
|
||||
v-html="item.html"
|
||||
@@ -341,24 +339,6 @@ const ActIcon = defineComponent({
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.act-av.av-a {
|
||||
background: #e0518d;
|
||||
}
|
||||
.act-av.av-b {
|
||||
background: #3d8bf2;
|
||||
}
|
||||
.act-av.av-c {
|
||||
background: #16a37b;
|
||||
}
|
||||
.act-av.av-d {
|
||||
background: #b3631f;
|
||||
}
|
||||
.act-av.av-e {
|
||||
background: #7c5cf0;
|
||||
}
|
||||
.act-av.av-f {
|
||||
background: #d0982a;
|
||||
}
|
||||
.act-text {
|
||||
font-size: 0.844rem;
|
||||
color: var(--text-2);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { storeToRefs } from 'pinia'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import { useRepoStore } from '@/stores/repo.store'
|
||||
import { initialOf } from '@/shared/utils/format'
|
||||
import type { Visibility } from '@/mock/relay.mock'
|
||||
|
||||
const router = useRouter()
|
||||
@@ -13,7 +12,7 @@ const repoStore = useRepoStore()
|
||||
|
||||
// 생성 폼 메타 — 소유자(GITEA_ORG) 고정 표시 + 템플릿 가용 정보
|
||||
const { owner, templateAvailable, templateSlug } = storeToRefs(repoStore)
|
||||
const ownerInitial = computed(() => initialOf(owner.value || '?').toUpperCase())
|
||||
const ownerInitial = computed(() => (owner.value.trim().charAt(0) || '?').toUpperCase())
|
||||
onMounted(() => {
|
||||
void repoStore.fetchCreateMeta()
|
||||
})
|
||||
|
||||
@@ -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<TaskStatus, string> = {
|
||||
v-for="a in task.assignees"
|
||||
:key="a.id"
|
||||
class="avatar"
|
||||
:class="a.color"
|
||||
>{{ a.initial }}</span>
|
||||
><UserAvatar
|
||||
:url="a.avatarUrl"
|
||||
:name="a.name"
|
||||
/></span>
|
||||
</span>
|
||||
<span
|
||||
class="st-badge"
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import AppShell from '@/layouts/AppShell.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { useRepoStore } from '@/stores/repo.store'
|
||||
import type { Visibility } from '@/mock/relay.mock'
|
||||
|
||||
@@ -265,8 +266,10 @@ const filteredRepos = computed(() =>
|
||||
v-for="m in repo.members"
|
||||
:key="m.id"
|
||||
class="avatar"
|
||||
:class="m.color"
|
||||
>{{ m.initial }}</span>
|
||||
><UserAvatar
|
||||
:url="m.avatarUrl"
|
||||
:name="m.name"
|
||||
/></span>
|
||||
<span
|
||||
v-if="repo.moreCount > 0"
|
||||
class="avatar av-more"
|
||||
|
||||
@@ -5,10 +5,10 @@ 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 { useMemberStore } from '@/stores/member.store'
|
||||
import { useUser } from '@/composables/useUser'
|
||||
import { avatarColor, initialOf } from '@/shared/utils/format'
|
||||
import type { Repo } from '@/mock/relay.mock'
|
||||
import type { ApiMember } from '@/types/repo'
|
||||
import type { MemberRoleType } from '@/types/member'
|
||||
@@ -339,10 +339,10 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||
:key="m.user.id"
|
||||
class="mem-row"
|
||||
>
|
||||
<span
|
||||
class="mem-av"
|
||||
:class="m.user.color"
|
||||
>{{ m.user.initial }}</span>
|
||||
<span class="mem-av"><UserAvatar
|
||||
:url="m.user.avatarUrl"
|
||||
:name="m.user.name"
|
||||
/></span>
|
||||
<div class="mem-info">
|
||||
<div class="mem-name">
|
||||
{{ m.user.name }} <span
|
||||
@@ -522,10 +522,10 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown))
|
||||
class="user-dd-item"
|
||||
@mousedown.prevent="selectUser(u)"
|
||||
>
|
||||
<span
|
||||
class="avatar user-av"
|
||||
:class="avatarColor(u.id)"
|
||||
>{{ initialOf(u.name) }}</span>
|
||||
<span class="avatar user-av"><UserAvatar
|
||||
:url="u.avatarUrl"
|
||||
:name="u.name"
|
||||
/></span>
|
||||
<span class="user-nm">{{ u.name }}</span>
|
||||
<span class="user-em">{{ u.email }}</span>
|
||||
</button>
|
||||
@@ -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;
|
||||
|
||||
@@ -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<User>(() => {
|
||||
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)"
|
||||
>
|
||||
<span
|
||||
class="avatar"
|
||||
:class="m.user.color"
|
||||
>{{ m.user.initial }}</span>
|
||||
<span class="avatar"><UserAvatar
|
||||
:url="m.user.avatarUrl"
|
||||
:name="m.user.name"
|
||||
/></span>
|
||||
<span class="ad-nm">{{ m.user.name }}</span>
|
||||
<span class="ad-em">{{ m.email }}</span>
|
||||
</button>
|
||||
@@ -495,10 +493,10 @@ function goBack() {
|
||||
:key="a.id"
|
||||
class="chip"
|
||||
>
|
||||
<span
|
||||
class="avatar"
|
||||
:class="a.color"
|
||||
>{{ a.initial }}</span>
|
||||
<span class="avatar"><UserAvatar
|
||||
:url="a.avatarUrl"
|
||||
:name="a.name"
|
||||
/></span>
|
||||
{{ a.name }}
|
||||
<span
|
||||
class="x"
|
||||
@@ -537,10 +535,10 @@ function goBack() {
|
||||
지시자
|
||||
</div>
|
||||
<div class="issuer">
|
||||
<span
|
||||
class="avatar"
|
||||
:class="issuer.color"
|
||||
>{{ issuer.initial }}</span>
|
||||
<span class="avatar"><UserAvatar
|
||||
:url="issuer.avatarUrl"
|
||||
:name="issuer.name"
|
||||
/></span>
|
||||
<span>
|
||||
<div class="nm">{{ issuer.name }}</div>
|
||||
</span>
|
||||
|
||||
@@ -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<User>(() => {
|
||||
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"
|
||||
>
|
||||
<span
|
||||
class="c-av"
|
||||
:class="c.author.color"
|
||||
>{{ c.author.initial }}</span>
|
||||
<span class="c-av"><UserAvatar
|
||||
:url="c.author.avatarUrl"
|
||||
:name="c.author.name"
|
||||
/></span>
|
||||
<div class="c-body">
|
||||
<div class="c-head">
|
||||
<span class="c-name">{{ c.author.name }}</span>
|
||||
@@ -804,10 +804,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
:key="ri"
|
||||
class="c-reply"
|
||||
>
|
||||
<span
|
||||
class="c-av"
|
||||
:class="r.author.color"
|
||||
>{{ r.author.initial }}</span>
|
||||
<span class="c-av"><UserAvatar
|
||||
:url="r.author.avatarUrl"
|
||||
:name="r.author.name"
|
||||
/></span>
|
||||
<div class="c-body">
|
||||
<div class="c-head">
|
||||
<span class="c-name">{{ r.author.name }}</span><span class="c-time">{{ r.time }}</span>
|
||||
@@ -841,10 +841,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
class="reply-form"
|
||||
:class="{ open: replyOpen === i }"
|
||||
>
|
||||
<span
|
||||
class="c-av"
|
||||
:class="me.color"
|
||||
>{{ me.initial }}</span>
|
||||
<span class="c-av"><UserAvatar
|
||||
:url="me.avatarUrl"
|
||||
:name="me.name"
|
||||
/></span>
|
||||
<div class="field">
|
||||
<textarea
|
||||
v-model="replyText"
|
||||
@@ -872,10 +872,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
</div>
|
||||
|
||||
<div class="composer">
|
||||
<span
|
||||
class="c-av"
|
||||
style="background: #0ea5a3"
|
||||
>정</span>
|
||||
<span class="c-av"><UserAvatar
|
||||
:url="me.avatarUrl"
|
||||
:name="me.name"
|
||||
/></span>
|
||||
<div class="field">
|
||||
<textarea
|
||||
v-model="commentText"
|
||||
@@ -1217,10 +1217,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
:key="a.id"
|
||||
class="ass"
|
||||
>
|
||||
<span
|
||||
class="avatar"
|
||||
:class="a.color"
|
||||
>{{ a.initial }}</span>
|
||||
<span class="avatar"><UserAvatar
|
||||
:url="a.avatarUrl"
|
||||
:name="a.name"
|
||||
/></span>
|
||||
<span><div class="nm">{{ a.name }}</div><div class="rl">{{ a.role }}</div></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1335,10 +1335,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
지시자
|
||||
</div>
|
||||
<div class="issuer">
|
||||
<span
|
||||
class="avatar"
|
||||
:class="task.issuer.color"
|
||||
>{{ task.issuer.initial }}</span>
|
||||
<span class="avatar"><UserAvatar
|
||||
:url="task.issuer.avatarUrl"
|
||||
:name="task.issuer.name"
|
||||
/></span>
|
||||
<span><div class="nm">{{ task.issuer.name }}</div><div class="rl">{{ task.issuer.role }}</div></span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
@@ -1606,7 +1606,10 @@ function badgeFor(name: string | undefined): { label: string; cls: string } {
|
||||
v-else-if="item.type === 'user'"
|
||||
class="ag-msg user"
|
||||
>
|
||||
<span class="ava">정</span>
|
||||
<span class="ava"><UserAvatar
|
||||
:url="me.avatarUrl"
|
||||
:name="me.name"
|
||||
/></span>
|
||||
<span class="bubble">{{ item.text }}</span>
|
||||
</div>
|
||||
|
||||
@@ -3121,7 +3124,7 @@ a.file-info {
|
||||
}
|
||||
.ag-msg.user .ava {
|
||||
order: 2;
|
||||
background: #0ea5a3;
|
||||
/* 내부 UserAvatar 가 이미지/placeholder 로 배경을 채운다 */
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user