diff --git a/src/components/study-history/study-history-row.tsx b/src/components/study-history/study-history-row.tsx
index 851ccef4..7f92e9fe 100644
--- a/src/components/study-history/study-history-row.tsx
+++ b/src/components/study-history/study-history-row.tsx
@@ -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 (
{/* 날짜 */}
@@ -33,25 +35,37 @@ export const StudyHistoryRow = ({ item }: { item: StudyHistoryItem }) => {
{/* 상대방 */}
-
e.stopPropagation()} // 행 클릭 이벤트(링크 이동) 방지
- >
-
-
- {item.partner.name}
-
-
- }
- />
+ {partner ? (
+
e.stopPropagation()} // 행 클릭 이벤트(링크 이동) 방지
+ >
+
+
+ {partner.name}
+
+
+ }
+ />
+ ) : (
+
+ )}
{/* 역할 */}
diff --git a/src/components/ui/profile-avatar.tsx b/src/components/ui/profile-avatar.tsx
index aff38286..d7bc122c 100644
--- a/src/components/ui/profile-avatar.tsx
+++ b/src/components/ui/profile-avatar.tsx
@@ -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 {
@@ -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 (
setBroken(true)}
+ onError={() => {
+ setCandidateIndex((prev) =>
+ prev < imageCandidates.length ? prev + 1 : prev,
+ );
+ }}
/>
);
};
diff --git a/src/features/auth/model/types.ts b/src/features/auth/model/types.ts
index f891d496..747847d2 100644
--- a/src/features/auth/model/types.ts
+++ b/src/features/auth/model/types.ts
@@ -2,8 +2,8 @@
export interface SignUpResponse {
content: {
generatedMemberId: string;
- accessToken: string;
- refreshToken: string;
+ accessToken?: string;
+ refreshToken?: string;
};
status: number;
message: string;
diff --git a/src/features/study/one-to-one/history/ui/study-history-tab-client.tsx b/src/features/study/one-to-one/history/ui/study-history-tab-client.tsx
index 77fbf653..13d653d2 100644
--- a/src/features/study/one-to-one/history/ui/study-history-tab-client.tsx
+++ b/src/features/study/one-to-one/history/ui/study-history-tab-client.tsx
@@ -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,
@@ -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,
};
};
diff --git a/src/types/study-history.ts b/src/types/study-history.ts
index fcae31f5..6dd07878 100644
--- a/src/types/study-history.ts
+++ b/src/types/study-history.ts
@@ -20,7 +20,7 @@ export interface StudyHistoryContent {
memberId: number;
nickname: string;
profileImageUrl: string | null;
- };
+ } | null;
}
export interface PageableResponse {
@@ -70,5 +70,5 @@ export interface StudyHistoryItem {
id: number;
name: string;
profileImage: string | null;
- };
+ } | null;
}