From 20b595b59898a71b7d99db3501b47a5adb0cb45b Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sun, 1 Feb 2026 17:38:10 +0900 Subject: [PATCH 01/15] =?UTF-8?q?fix:=20=EB=91=90=EA=B0=80=EC=A7=80=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=ED=94=BD=EC=8A=A4=20=EB=AA=85=EC=98=88?= =?UTF-8?q?=EC=9D=98=20=EC=A0=84=EB=8B=B9=EC=97=90=EC=84=9C=20=ED=94=84?= =?UTF-8?q?=EC=82=AC=20=EC=95=88=EB=B3=B4=EC=9E=84.=20=EC=95=84=EC=B9=B4?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C,=20=EC=8A=A4=ED=84=B0=EB=94=94=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=EC=9D=B4=20=EC=95=88=EB=9C=B8.=20=EC=A7=81=EB=AC=B4?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=84=A4=EC=A0=95=ED=95=9C=20?= =?UTF-8?q?=EA=B7=B8=EB=8C=80=EB=A1=9C=20=ED=91=9C=EA=B8=B0=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study-history/study-history-row.tsx | 52 ++++++++----- src/components/ui/profile-avatar.tsx | 76 +++++++++++++------ src/features/auth/model/types.ts | 4 +- .../history/ui/study-history-tab-client.tsx | 13 ++-- src/types/study-history.ts | 4 +- 5 files changed, 96 insertions(+), 53 deletions(-) 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; } From d943df1422aa67720856b884575a6ac457b8c4ab Mon Sep 17 00:00:00 2001 From: Hyeonjun0527 Date: Sun, 1 Feb 2026 21:03:05 +0900 Subject: [PATCH 02/15] =?UTF-8?q?fix:=20=EA=B0=81=EC=A2=85=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit . --- next.config.ts | 14 ++- src/app/(service)/home/home-content.tsx | 10 +- src/components/card/voting-card.tsx | 2 +- src/components/home/tab-navigation.tsx | 10 +- .../study-history/study-history-row.tsx | 14 +-- src/components/ui/avatar/index.tsx | 2 + src/components/ui/profile-avatar.tsx | 102 ------------------ src/components/voting/voting-detail-view.tsx | 60 ++++++++--- .../user/ui/user-phone-number-copy-modal.tsx | 34 ++++-- .../study/interview/api/interview-types.ts | 2 + .../archive/model/use-archive-actions.ts | 12 ++- .../one-to-one/archive/ui/archive-filters.tsx | 4 + .../archive/ui/archive-tab-client.tsx | 6 +- .../ui/hall-of-fame-tab-client.tsx | 20 ++-- .../schedule/ui/today-study-card.tsx | 10 +- .../study/one-to-one/ui/one-on-one-page.tsx | 14 +-- src/types/hall-of-fame.ts | 6 +- 17 files changed, 161 insertions(+), 161 deletions(-) delete mode 100644 src/components/ui/profile-avatar.tsx diff --git a/next.config.ts b/next.config.ts index fa0942f5..51ef58b3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,4 +1,7 @@ import type { NextConfig } from 'next'; +import type { RemotePattern } from 'next/dist/shared/lib/image-config'; + +const isProd = process.env.NODE_ENV === 'production'; const nextConfig: NextConfig = { // output: 'standalone', @@ -31,7 +34,16 @@ const nextConfig: NextConfig = { hostname: 'www.zeroone.it.kr', pathname: '/**', }, - // CMS 개발 환경에서 사용하는 이미지 도메인 허용 설정 + ...(isProd + ? ([] as RemotePattern[]) + : ([ + { + protocol: 'http', + hostname: 'localhost', + port: '8080', + pathname: '/**', + }, + ] as RemotePattern[])), { protocol: 'http', hostname: 'localhost', diff --git a/src/app/(service)/home/home-content.tsx b/src/app/(service)/home/home-content.tsx index b20f2849..47f639eb 100644 --- a/src/app/(service)/home/home-content.tsx +++ b/src/app/(service)/home/home-content.tsx @@ -5,18 +5,24 @@ import CommunityTab from '@/features/study/one-to-one/balance-game/ui/community- import HallOfFameTab from '@/features/study/one-to-one/hall-of-fame/ui/hall-of-fame-tab'; import StudyHistoryTab from '@/features/study/one-to-one/history/ui/study-history-tab'; import StudyTab from '@/features/study/one-to-one/schedule/ui/home-study-tab'; +import { getServerCookie } from '@/utils/server-cookie'; +import { isNumeric } from '@/utils/validation'; interface HomeContentProps { activeTab: string; } -export default function HomeContent({ activeTab }: HomeContentProps) { +export default async function HomeContent({ activeTab }: HomeContentProps) { + const isHistoryTab = activeTab === 'history'; + const memberIdStr = isHistoryTab ? await getServerCookie('memberId') : null; + const isLoggedIn = !!memberIdStr && isNumeric(memberIdStr); + const renderTabContent = () => { switch (activeTab) { case 'study': return ; case 'history': - return ; + return isLoggedIn ? : ; case 'ranking': return ; case 'archive': diff --git a/src/components/card/voting-card.tsx b/src/components/card/voting-card.tsx index 81b2b836..365e2eb1 100644 --- a/src/components/card/voting-card.tsx +++ b/src/components/card/voting-card.tsx @@ -90,7 +90,7 @@ export default function VotingCard({ voting, onClick }: VotingCardProps) {

)} - {/* 간단한 투표 결과 미리보기 (투표했을 때만) */} + {/* 간단한 투표 결과 미리보기 */} {hasVoted && (
diff --git a/src/components/home/tab-navigation.tsx b/src/components/home/tab-navigation.tsx index 13a09619..5f791d13 100644 --- a/src/components/home/tab-navigation.tsx +++ b/src/components/home/tab-navigation.tsx @@ -8,7 +8,9 @@ import { History, } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; +import { getCookie } from '@/api/client/cookie'; import { cn } from '@/components/ui/(shadcn)/lib/utils'; +import { useAuth } from '@/hooks/common/use-auth'; interface TabNavigationProps { activeTab: string; @@ -50,6 +52,12 @@ const TABS = [ export default function TabNavigation({ activeTab }: TabNavigationProps) { const router = useRouter(); const searchParams = useSearchParams(); + const { isAuthenticated } = useAuth(); + const hasMemberId = !!getCookie('memberId'); + const canViewHistory = isAuthenticated && hasMemberId; + const visibleTabs = canViewHistory + ? TABS + : TABS.filter((tab) => tab.id !== 'history'); const handleTabChange = (tabId: string) => { const params = new URLSearchParams(searchParams.toString()); @@ -64,7 +72,7 @@ export default function TabNavigation({ activeTab }: TabNavigationProps) {