Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 33 additions & 19 deletions src/components/study-history/study-history-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import UserProfileModal from '@/entities/user/ui/user-profile-modal';
import { StudyHistoryItem } from '@/types/study-history';

export const StudyHistoryRow = ({ item }: { item: StudyHistoryItem }) => {
const partner = item.partner;

return (
<div className="border-border-subtlest text-designer-14m flex items-center gap-400 border-b px-400 py-300 transition-colors last:border-0">
{/* 날짜 */}
Expand All @@ -33,25 +35,37 @@ export const StudyHistoryRow = ({ item }: { item: StudyHistoryItem }) => {

{/* 상대방 */}
<div className="flex w-[150px] shrink-0 items-center gap-100">
<UserProfileModal
memberId={item.partner.id}
trigger={
<div
className="flex cursor-pointer items-center gap-100 transition-opacity hover:opacity-80"
onClick={(e) => e.stopPropagation()} // 행 클릭 이벤트(링크 이동) 방지
>
<ProfileAvatar
src={item.partner.profileImage || undefined}
alt={item.partner.name}
size="sm"
className="h-8 w-8"
/>
<span className="text-text-default truncate font-medium">
{item.partner.name}
</span>
</div>
}
/>
{partner ? (
<UserProfileModal
memberId={partner.id}
trigger={
<div
className="flex cursor-pointer items-center gap-100 transition-opacity hover:opacity-80"
onClick={(e) => e.stopPropagation()} // 행 클릭 이벤트(링크 이동) 방지
>
<ProfileAvatar
src={partner.profileImage || undefined}
alt={partner.name}
size="sm"
className="h-8 w-8"
/>
<span className="text-text-default truncate font-medium">
{partner.name}
</span>
</div>
}
/>
) : (
<div className="text-text-subtlest flex items-center gap-100">
<ProfileAvatar
src={undefined}
alt="상대방 정보 없음"
size="sm"
className="h-8 w-8 opacity-60"
/>
<span className="truncate font-medium">상대방 정보 없음</span>
</div>
)}
</div>

{/* 역할 */}
Expand Down
76 changes: 51 additions & 25 deletions src/components/ui/profile-avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import Image from 'next/image';
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { cn } from '@/components/ui/(shadcn)/lib/utils';

interface ProfileAvatarProps {
Expand All @@ -22,41 +22,63 @@ export const ProfileAvatar = ({
const px = { sm: 32, md: 48, lg: 80, xl: 120 }[size]; // 기존 명예의 전당 사이즈와 최대한 비슷하게 매핑 (sm:32px는 기존 row에 맞춤)
const effectiveAlt = alt || name || 'profile';

// src 정리(공백/이상한 상대경로 방지)
const normalizedSrc = useMemo(() => {
if (!src || typeof src !== 'string') return null;
const imageCandidates = useMemo(() => {
if (!src || typeof src !== 'string') return [] as string[];
const s = src.trim();
if (!s) return null;
// 유효하지 않은 값 필터링 (LOCAL, null 등)
if (!s) return [];
if (s.toUpperCase() === 'LOCAL' || s === 'null' || s === 'undefined')
return null;
// LOCAL/로 시작하는 경우 처리 (예: LOCAL/https:/picsum.photos/202)
return [];
if (s.toUpperCase().startsWith('LOCAL/')) {
const afterLocal = s.substring(6); // 'LOCAL/'.length = 6
// LOCAL/ 뒤에 실제 URL이 있는 경우
const afterLocal = s.substring(6);
if (
afterLocal.startsWith('http://') ||
afterLocal.startsWith('https://')
) {
return afterLocal;
return [afterLocal];
}

// LOCAL/ 뒤에 유효하지 않은 값인 경우
return null;
return [];
}
if (
s.startsWith('http://') ||
s.startsWith('https://') ||
s.startsWith('/')
)
return s;

return `/${s}`;

if (s.startsWith('http://') || s.startsWith('https://')) return [s];

const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, '');
const candidates: string[] = [];

const addCandidate = (path: string) => {
if (apiBase) candidates.push(`${apiBase}${path}`);
candidates.push(path);
};

if (s.startsWith('/')) {
addCandidate(s);

return candidates;
}

if (s.includes('/')) {
addCandidate(`/${s}`);

return candidates;
}

const filename = s;
addCandidate(`/${filename}`);
addCandidate(`/images/profile-image/${filename}`);
addCandidate(`/profile-image/${filename}`);
addCandidate(`/files/images/profile-image/${filename}`);
addCandidate(`/MEMBER_PROFILE_IMAGE/images/profile-image/${filename}`);

return candidates;
}, [src]);

const [broken, setBroken] = useState(false);
const finalSrc =
!broken && normalizedSrc ? normalizedSrc : '/profile-default.svg';
const [candidateIndex, setCandidateIndex] = useState(0);

useEffect(() => {
setCandidateIndex(0);
}, [imageCandidates]);

const finalSrc = imageCandidates[candidateIndex] ?? '/profile-default.svg';

return (
<Image
Expand All @@ -70,7 +92,11 @@ export const ProfileAvatar = ({
)}
loading="eager"
unoptimized
onError={() => setBroken(true)}
onError={() => {
setCandidateIndex((prev) =>
prev < imageCandidates.length ? prev + 1 : prev,
);
}}
/>
);
};
4 changes: 2 additions & 2 deletions src/features/auth/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
export interface SignUpResponse {
content: {
generatedMemberId: string;
accessToken: string;
refreshToken: string;
accessToken?: string;
refreshToken?: string;
};
status: number;
message: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ const mapHistoryItem = (data: StudyHistoryContent): StudyHistoryItem => {
const dateObj = new Date(data.scheduledAt);
const dateStr = `${dateObj.getFullYear()}.${String(dateObj.getMonth() + 1).padStart(2, '0')}.${String(dateObj.getDate()).padStart(2, '0')}`;
const dayName = ['일', '월', '화', '수', '목', '금', '토'][dateObj.getDay()];
const partner = data.partner
? {
id: data.partner.memberId,
name: data.partner.nickname,
profileImage: data.partner.profileImageUrl,
}
: null;

return {
id: data.studyId,
Expand All @@ -29,11 +36,7 @@ const mapHistoryItem = (data: StudyHistoryContent): StudyHistoryItem => {
data.participation.attendance === 'PRESENT' ? 'ATTENDED' : 'NOT_STARTED',
link: data.studyLink,
status: data.status === 'COMPLETE' ? 'COMPLETED' : 'IN_PROGRESS',
partner: {
id: data.partner.memberId,
name: data.partner.nickname,
profileImage: data.partner.profileImageUrl,
},
partner,
};
};

Expand Down
4 changes: 2 additions & 2 deletions src/types/study-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface StudyHistoryContent {
memberId: number;
nickname: string;
profileImageUrl: string | null;
};
} | null;
}

export interface PageableResponse<T> {
Expand Down Expand Up @@ -70,5 +70,5 @@ export interface StudyHistoryItem {
id: number;
name: string;
profileImage: string | null;
};
} | null;
}
Loading