Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
95d96aa
feat: Avatar 컴포넌트 및 기본 프로필 이미지 추가
ysw789 Jun 10, 2026
e345945
feat: 타입 및 API에 profileImageUrl 필드 추가
ysw789 Jun 10, 2026
2542637
feat: Manager 기능에 Avatar 컴포넌트 적용
ysw789 Jun 10, 2026
60703d1
feat: User 기능에 Avatar 컴포넌트 적용
ysw789 Jun 10, 2026
d62812f
feat: Shared UI와 프로필에 Avatar 컴포넌트 적용
ysw789 Jun 10, 2026
9117533
refactor: 남은 코드에서 이미지 처리 정리
ysw789 Jun 10, 2026
2e5fb9f
refactor: 불필요한 SubstituteProfileAvatar 제거
ysw789 Jun 10, 2026
26c8167
feat: 대타요청 DTO/타입에 profileImageUrl 필드 추가
ysw789 Jun 10, 2026
f095824
feat: 대타요청 어댑터에 프로필 이미지 매핑 연결
ysw789 Jun 10, 2026
bb3e7ab
fix: 대타요청 상세 Avatar에 profileImageUrl 전달
ysw789 Jun 10, 2026
d2c2ef6
test: 대타요청 어댑터 프로필 이미지 데이터 흐름 검증
ysw789 Jun 10, 2026
b0c4360
refactor: 근무자 선택 컴포넌트를 공유 Avatar로 마이그레이션
ysw789 Jun 10, 2026
cde9c06
fix: Avatar neutral 폴백에 onError 처리 추가 및 컬러 토큰 적용
ysw789 Jun 11, 2026
37afaab
refactor: 프로필 이미지 해석 로직 단순화 — workerName 체크 제거
ysw789 Jun 11, 2026
00c9f73
refactor: WorkerListItem 부모 div에서 중복 size 클래스 제거
ysw789 Jun 11, 2026
c9dcb02
fix: 보낸 대타요청에서 프로필 이미지 미노출, 받은 요청은 요청자 이미지만 표시
ysw789 Jun 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마지막으로 순수 궁금증,,, 저는 클로드는 글로벌(~/.gitignore_global)로 따로 빼두는 편인데 요거 보통 공유해서 쓰시나요?

아님 루트에 놓고 쓰는게 편한데 깃헙에 올리기는 좀 그래서 추가하셨나여?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

별다른 이유는 없고 공용으로 만들어서 팀에서 공유하는 경우도 있고 스킬은 자기 입맛에 따라 바꿔서 쓰는 경우도 있어서 내부적으로 정해서 쓰면 될듯용~

Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,7 @@ coverage

*storybook.log
storybook-static
.serena
.serena

CLAUDE.md
.claude
5 changes: 5 additions & 0 deletions src/assets/default-profile.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/features/manager/home/lib/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ export function adaptWorkerDto(dto: WorkerDto): ManagerWorkerItem {
position: dto.position.description || dto.position.type,
colorCode: dto.colorCode,
nextWorkDate: formatNextShiftDate(dto.nextShiftDateTime),
profileImageUrl: dto.user.profileImageUrl ?? undefined,
}
}
2 changes: 2 additions & 0 deletions src/features/manager/home/types/substitute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface SubstituteRequesterDto {
workerId: number
workerName: string
workerRole?: string
profileImageUrl?: string | null
}

export interface SubstituteStatusDto {
Expand Down Expand Up @@ -87,6 +88,7 @@ export function adaptSubstituteRequestDto(
name: dto.requester.workerName,
role: dto.schedule.position,
workerRole: mapApiWorkerRole(dto.requester.workerRole),
imageUrl: dto.requester.profileImageUrl ?? null,
dateRange: formatDateRange(
dto.schedule.startDateTime,
dto.schedule.endDateTime
Expand Down
1 change: 1 addition & 0 deletions src/features/manager/home/types/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface WorkerUserDto {
name: string
contact: string
gender: string
profileImageUrl?: string | null
}

export interface WorkerStatusDto {
Expand Down
11 changes: 2 additions & 9 deletions src/features/manager/home/ui/StoreWorkerListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState } from 'react'
import MoreVerticalIcon from '@/assets/icons/home/more-vertical.svg'
import { WorkerRoleBadge } from '@/shared/ui/home/WorkerRoleBadge'
import { ActionMenu, type ActionMenuItem } from '@/shared/ui/common/ActionMenu'
import { Avatar } from '@/shared/ui/common/Avatar'
import type { StoreWorkerRole } from '@/features/manager/home/types/storeWorkerRole'

interface StoreWorkerListItemProps {
Expand All @@ -28,15 +29,7 @@ export function StoreWorkerListItem({
className={`relative flex h-[60px] items-center justify-between rounded-lg bg-white px-3 py-1 ${className}`}
>
<div className="flex items-center gap-4">
<div className="h-[38px] w-[38px] overflow-hidden rounded-full bg-bg-dark">
{profileImageUrl ? (
<img
src={profileImageUrl}
alt={name}
className="h-full w-full object-cover"
/>
) : null}
</div>
<Avatar src={profileImageUrl} alt={name} size={38} />

<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
Expand Down
12 changes: 3 additions & 9 deletions src/features/manager/home/ui/TodayWorkerList.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Avatar } from '@/shared/ui/common/Avatar'

interface TodayWorkerItem {
id: number | string
name: string
Expand All @@ -13,15 +15,7 @@ interface TodayWorkerListProps {
function TodayWorkerCard({ worker }: { worker: TodayWorkerItem }) {
return (
<div className="h-[188px] w-[148px] rounded-2xl border border-line-1 bg-white px-[30px] py-6">
<div className="h-[88px] w-[88px] overflow-hidden rounded-full bg-bg-dark">
{worker.profileImageUrl ? (
<img
src={worker.profileImageUrl}
alt={worker.name}
className="h-full w-full object-cover"
/>
) : null}
</div>
<Avatar src={worker.profileImageUrl} alt={worker.name} size={88} />
<p className="mt-3 text-center typography-body02-semibold text-text-100">
{worker.name}
</p>
Expand Down
13 changes: 3 additions & 10 deletions src/features/manager/worker-list/ui/WorkerListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ScheduleColor } from '@/features/manager/worker-schedule/types/sch
import { SwipeActionButton } from '@/shared/ui/SwipeActionButton'
import { SwipeableRow } from '@/shared/ui/SwipeableRow'
import { WorkerRoleBadge } from '@/shared/ui/home/WorkerRoleBadge'
import { Avatar } from '@/shared/ui/common/Avatar'
import type { WorkerRole } from '@/shared/types/workerRole'

interface WorkerListItemProps {
Expand All @@ -30,16 +31,8 @@ export function WorkerListItem({
const content = (
<div className="flex h-[70px] min-w-[358px] items-center justify-between bg-white px-6 py-1">
<div className="flex items-center gap-4">
<div className="relative size-[38px] shrink-0">
{profileImageUrl ? (
<img
src={profileImageUrl}
alt={`${name} 프로필`}
className="size-full rounded-full object-cover"
/>
) : (
<div className="size-full rounded-full bg-bg-dark" />
)}
<div className="relative shrink-0">
<Avatar src={profileImageUrl} alt={`${name} 프로필`} size={38} />
<div
className="absolute inset-0 rounded-full"
style={{ boxShadow: `inset 0 0 0 3px ${scheduleColor}` }}
Expand Down
2 changes: 2 additions & 0 deletions src/features/user/home/workspace/api/workspaceMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function adaptWorkerDto(dto: WorkspaceWorkerDto): WorkspaceWorkerItem {
id: dto.id,
userId: dto.user.id,
name: dto.user.name,
profileImageUrl: dto.user.profileImageUrl ?? null,
positionType: dto.position.type,
positionDescription: dto.position.description,
positionEmoji: dto.position.emoji,
Expand All @@ -61,6 +62,7 @@ export function adaptManagerDto(
id: dto.id,
managerId: dto.manager.id,
name: dto.manager.name,
profileImageUrl: dto.manager.profileImageUrl ?? null,
positionType: dto.position.type,
positionDescription: dto.position.description,
positionEmoji: dto.position.emoji,
Expand Down
6 changes: 4 additions & 2 deletions src/features/user/home/workspace/types/workspaceMembers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ export interface WorkspacePositionDto {

export interface WorkspaceWorkerDto {
id: number
user: { id: number; name: string }
user: { id: number; name: string; profileImageUrl?: string | null }
position: WorkspacePositionDto
employedAt: string
nextShiftDateTime: string
}

export interface WorkspaceManagerDto {
id: number
manager: { id: number; name: string }
manager: { id: number; name: string; profileImageUrl?: string | null }
position: WorkspacePositionDto
}

Expand Down Expand Up @@ -48,6 +48,7 @@ export interface WorkspaceWorkerItem {
id: number
userId: number
name: string
profileImageUrl?: string | null
positionType: string
positionDescription: string
positionEmoji: string
Expand All @@ -59,6 +60,7 @@ export interface WorkspaceManagerItem {
id: number
managerId: number
name: string
profileImageUrl?: string | null
positionType: string
positionDescription: string
positionEmoji: string
Expand Down
174 changes: 174 additions & 0 deletions src/features/user/substitute/lib/adaptUserSubstituteRequest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import { describe, expect, it } from 'vitest'

import type {
ReceivedSubstituteRequestDto,
SentSubstituteRequestDetailApiDto,
SentSubstituteRequestDetailDto,
SentSubstituteRequestListItemDto,
SubstituteTargetDto,
} from '@/features/user/substitute/types'

import {
adaptReceivedSubstituteDetail,
adaptSentSubstituteDetail,
adaptUserSubstituteListItem,
normalizeSentSubstituteDetailDto,
} from './adaptUserSubstituteRequest'

const schedule = {
scheduleId: 1,
startDateTime: '2026-04-05T09:00:00',
endDateTime: '2026-04-05T13:00:00',
position: 'STAFF',
}
const workspace = { workspaceId: 10, workspaceName: '카페' }

function received(
profileImageUrl?: string | null
): ReceivedSubstituteRequestDto {
return {
id: 1,
schedule,
workspace,
requester: { workerId: 5, workerName: '요청자', profileImageUrl },
requestType: 'SPECIFIC',
status: 'PENDING',
createdAt: '2026-04-01T00:00:00',
}
}

function target(profileImageUrl?: string | null): SubstituteTargetDto {
return {
targetId: 7,
workerName: '대상자',
profileImageUrl,
status: 'PENDING',
}
}

function sentList(opts: {
acceptedWorker?: {
workerId: number
workerName: string
profileImageUrl?: string | null
} | null
targets?: SubstituteTargetDto[]
}): SentSubstituteRequestListItemDto {
return {
id: 2,
schedule,
workspace,
requestType: 'SPECIFIC',
status: 'PENDING',
createdAt: '2026-04-01T00:00:00',
targets: opts.targets,
acceptedWorker: opts.acceptedWorker ?? null,
}
}

function sentDetail(opts: {
acceptedWorker?: {
workerId: number
workerName: string
profileImageUrl?: string | null
} | null
targets?: SubstituteTargetDto[]
}): SentSubstituteRequestDetailDto {
return {
id: 3,
schedule,
workspace,
requester: { workerId: 5, workerName: '요청자' },
requestType: 'SPECIFIC',
targets: opts.targets ?? [],
acceptedWorker: opts.acceptedWorker ?? null,
status: 'PENDING',
createdAt: '2026-04-01T00:00:00',
}
}

describe('대타요청 프로필 이미지 데이터 흐름', () => {
it('RECEIVED 목록은 요청자의 profileImageUrl을 imageUrl로 매핑한다', () => {
const item = adaptUserSubstituteListItem(
received('https://img/requester.png'),
'RECEIVED'
)
expect(item.imageUrl).toBe('https://img/requester.png')
})

it('RECEIVED 상세는 요청자의 profileImageUrl을 imageUrl로 매핑한다', () => {
const detail = adaptReceivedSubstituteDetail(
received('https://img/requester.png')
)
expect(detail.imageUrl).toBe('https://img/requester.png')
})

it('SENT은 수락자가 있으면 수락자의 이미지를 사용한다', () => {
const accepted = {
workerId: 9,
workerName: '수락자',
profileImageUrl: 'https://img/accepted.png',
}
const targets = [target('https://img/target.png')]

expect(
adaptUserSubstituteListItem(
sentList({ acceptedWorker: accepted, targets }),
'SENT'
).imageUrl
).toBe('https://img/accepted.png')
expect(
adaptSentSubstituteDetail(
sentDetail({ acceptedWorker: accepted, targets })
).imageUrl
).toBe('https://img/accepted.png')
})

it('SENT은 수락자가 없으면 첫 대상자의 이미지를 사용한다', () => {
const targets = [target('https://img/target.png')]

expect(
adaptUserSubstituteListItem(sentList({ targets }), 'SENT').imageUrl
).toBe('https://img/target.png')
expect(adaptSentSubstituteDetail(sentDetail({ targets })).imageUrl).toBe(
'https://img/target.png'
)
})

it('이미지 필드가 없으면 null로 정규화한다', () => {
expect(
adaptUserSubstituteListItem(received(), 'RECEIVED').imageUrl
).toBeNull()
expect(adaptReceivedSubstituteDetail(received()).imageUrl).toBeNull()
expect(
adaptUserSubstituteListItem(sentList({ targets: [target()] }), 'SENT')
.imageUrl
).toBeNull()
})

it('normalize는 API target의 profileImageUrl을 정규화 target으로 전달한다', () => {
const api: SentSubstituteRequestDetailApiDto = {
id: 4,
schedule,
workspace,
requester: { workerId: 5, workerName: '요청자' },
requestType: 'SPECIFIC',
targets: [
{
target: {
workerId: 7,
workerName: '대상자',
profileImageUrl: 'https://img/target.png',
},
status: 'PENDING',
},
],
status: 'PENDING',
createdAt: '2026-04-01T00:00:00',
}

expect(
normalizeSentSubstituteDetailDto(api).targets[0].profileImageUrl
).toBe('https://img/target.png')
})
})
Loading
Loading