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; }