diff --git a/src/components/discussion/discussion-detail-modal.tsx b/src/components/discussion/discussion-detail-modal.tsx
index 66a84835..dcf48e13 100644
--- a/src/components/discussion/discussion-detail-modal.tsx
+++ b/src/components/discussion/discussion-detail-modal.tsx
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
import { Discussion, VoteType } from '@/types/discussion';
import { TOPIC_LABELS } from '@/mocks/discussion-mock-data';
import { X, ThumbsUp, ThumbsDown, Eye, Clock, MessageCircle } from 'lucide-react';
+import UserAvatar from '@/components/ui/avatar';
+import UserProfileModal from '@/entities/user/ui/user-profile-modal';
import { formatDistanceToNow } from 'date-fns';
import { ko } from 'date-fns/locale';
import { cn } from '@/components/ui/(shadcn)/lib/utils';
@@ -73,13 +75,24 @@ export default function DiscussionDetailModal({
{/* 메타 정보 */}
{/* 작성자 */}
-
-
- {discussion.author.nickname.charAt(0)}
-
-
- {discussion.author.nickname}
-
+
e.stopPropagation()}>
+
+
+
+
+
+ {discussion.author.nickname}
+
+
+ }
+ />
{/* 시간 */}
diff --git a/src/components/home/tab-navigation.tsx b/src/components/home/tab-navigation.tsx
new file mode 100644
index 00000000..36b36064
--- /dev/null
+++ b/src/components/home/tab-navigation.tsx
@@ -0,0 +1,84 @@
+'use client';
+
+import { useRouter, useSearchParams } from 'next/navigation';
+import { cn } from '@/components/ui/(shadcn)/lib/utils';
+import { Trophy, BookOpen, MessageSquareText, Calendar, History } from 'lucide-react';
+
+interface TabNavigationProps {
+ activeTab: string;
+}
+
+const TABS = [
+ {
+ id: 'study',
+ label: '스터디',
+ icon: Calendar,
+ description: '나의 스터디 일정'
+ },
+ {
+ id: 'ranking',
+ label: '명예의 전당',
+ icon: Trophy,
+ description: '랭킹 시스템'
+ },
+ {
+ id: 'archive',
+ label: '제로원 아카이브',
+ icon: BookOpen,
+ description: '학습 자료'
+ },
+ {
+ id: 'history',
+ label: '나의 스터디 기록',
+ icon: History,
+ description: '1:1 스터디 히스토리'
+ },
+ {
+ id: 'community',
+ label: '밸런스게임',
+ icon: MessageSquareText,
+ description: '커뮤니티'
+ }
+];
+
+export default function TabNavigation({ activeTab }: TabNavigationProps) {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+
+ const handleTabChange = (tabId: string) => {
+ const params = new URLSearchParams(searchParams.toString());
+ params.set('tab', tabId);
+ router.push(`/home?${params.toString()}`);
+ };
+
+ return (
+
+
+
제로원 홈
+
+
+
+
+ );
+}
diff --git a/src/components/home/tabs/archive-tab.tsx b/src/components/home/tabs/archive-tab.tsx
new file mode 100644
index 00000000..ae32571c
--- /dev/null
+++ b/src/components/home/tabs/archive-tab.tsx
@@ -0,0 +1,597 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import { cn } from '@/components/ui/(shadcn)/lib/utils';
+import {
+ LibraryBig,
+ LayoutGrid,
+ List,
+ ArrowUpDown,
+ Bookmark,
+ Search,
+ Eye,
+ Heart,
+ Loader2,
+ ChevronLeft,
+ ChevronRight,
+} from 'lucide-react';
+
+// ----------------------------------------------------------------------
+// Types & Mock Data (Library)
+// ----------------------------------------------------------------------
+
+type CurationLevel = 'BEGINNER' | 'INTERMEDIATE' | 'ADVANCED';
+
+interface LibraryItem {
+ id: number;
+ title: string;
+ description: string;
+ author: string;
+ date: string;
+ views: number;
+ likes: number;
+ link: string;
+ isLiked: boolean;
+ level: CurationLevel;
+ tags: string[];
+ isBookmarked: boolean;
+ isRecommended?: boolean;
+ isHidden?: boolean;
+}
+
+const MOCK_LIBRARY_DATA: LibraryItem[] = [
+ {
+ id: 1,
+ title: '2025년 상반기 백엔드 개발자 면접 질문 모음 (네카라쿠배)',
+ description: '네이버, 카카오, 라인, 쿠팡, 배민 등 주요 기업의 실제 면접 질문을 분석했습니다.',
+ author: '제로원 운영진',
+ date: '2025.01.10',
+ views: 1250,
+ likes: 342,
+ link: 'https://velog.io',
+ isLiked: true,
+ level: 'INTERMEDIATE',
+ tags: ['면접', '백엔드', '취업'],
+ isBookmarked: false,
+ isRecommended: true,
+ },
+ {
+ id: 2,
+ title: '프론트엔드 성능 최적화: React 19 도입 가이드',
+ description: '최신 React 19의 성능 개선 포인트와 마이그레이션 전략을 소개합니다.',
+ author: 'TechLead_Kim',
+ date: '2025.01.12',
+ views: 890,
+ likes: 120,
+ link: 'https://medium.com',
+ isLiked: false,
+ level: 'ADVANCED',
+ tags: ['React', '성능', '최적화'],
+ isBookmarked: true,
+ },
+ {
+ id: 3,
+ title: '비전공자가 6개월 만에 개발자로 취업한 현실적인 공부법',
+ description: '비전공 출신이 실제로 경험한 학습 로드맵과 포트폴리오 전략을 공유합니다.',
+ author: 'NewDeveloper',
+ date: '2025.01.05',
+ views: 2100,
+ likes: 560,
+ link: 'https://brunch.co.kr',
+ isLiked: true,
+ level: 'BEGINNER',
+ tags: ['비전공', '취업', '학습법'],
+ isBookmarked: false,
+ isRecommended: true,
+ },
+ {
+ id: 4,
+ title: 'CS 기초: 운영체제와 네트워크 핵심 요약 (PDF 다운로드)',
+ description: '면접 대비를 위한 운영체제와 네트워크 핵심 개념 정리 자료입니다.',
+ author: 'CS_Master',
+ date: '2024.12.28',
+ views: 1500,
+ likes: 410,
+ link: 'https://tistory.com',
+ isLiked: false,
+ level: 'INTERMEDIATE',
+ tags: ['CS', '운영체제', '네트워크'],
+ isBookmarked: false,
+ },
+ {
+ id: 5,
+ title: '주니어 개발자를 위한 이력서 첨삭 가이드 101',
+ description: '합격하는 개발자 이력서 작성법과 실제 첨삭 사례를 소개합니다.',
+ author: 'HR_Manager',
+ date: '2025.01.15',
+ views: 750,
+ likes: 230,
+ link: 'https://linkedin.com',
+ isLiked: false,
+ level: 'BEGINNER',
+ tags: ['이력서', '취업', '포트폴리오'],
+ isBookmarked: true,
+ },
+ ...Array.from({ length: 10 }, (_, i): LibraryItem => ({
+ id: 10 + i,
+ title: `개발자 면접 대비 - 자료구조 핵심 질문 ${i + 1}탄`,
+ description: '자주 출제되는 자료구조 면접 질문과 모범 답변을 정리했습니다.',
+ author: 'Admin',
+ date: `2024.12.${20 - i}`,
+ views: 100 + i * 10,
+ likes: 10 + i,
+ link: 'https://google.com',
+ isLiked: Math.random() > 0.5,
+ level: i % 3 === 0 ? 'BEGINNER' : i % 3 === 1 ? 'INTERMEDIATE' : 'ADVANCED',
+ tags: ['면접', '자료구조', 'CS'],
+ isBookmarked: Math.random() > 0.7,
+ })),
+];
+
+// ----------------------------------------------------------------------
+// Components
+// ----------------------------------------------------------------------
+
+const LibraryCard = ({
+ item,
+ onLike,
+ onView,
+ onBookmark,
+ onHide,
+ isAdmin,
+}: {
+ item: LibraryItem;
+ onLike: (e: React.MouseEvent, id: number) => void;
+ onView: (link: string) => void;
+ onBookmark: (e: React.MouseEvent, id: number) => void;
+ onHide?: (e: React.MouseEvent, id: number) => void;
+ isAdmin?: boolean;
+}) => {
+ return (
+
+
+
+ {item.isHidden && (
+
+ 숨김됨
+
+ )}
+
+
+ {isAdmin && onHide && (
+
+ )}
+
+
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+
+
+ by {item.author}
+
+
+
+
+ {item.views.toLocaleString()}
+
+
+
+
+
+ );
+};
+
+const LibraryRow = ({
+ item,
+ onLike,
+ onView,
+ onBookmark,
+ onHide,
+ isAdmin,
+}: {
+ item: LibraryItem;
+ onLike: (e: React.MouseEvent, id: number) => void;
+ onView: (link: string) => void;
+ onBookmark: (e: React.MouseEvent, id: number) => void;
+ onHide?: (e: React.MouseEvent, id: number) => void;
+ isAdmin?: boolean;
+}) => {
+ return (
+
onView(item.link)}
+ className={cn(
+ "group flex items-center gap-300 px-300 py-200 border-b border-border-subtlest hover:bg-fill-neutral-subtle-hover transition-colors cursor-pointer last:border-0",
+ item.isHidden && "opacity-50"
+ )}
+ >
+ {/* Title Area */}
+
+
+
+ {item.title}
+
+ {item.isHidden && (
+
+ 숨김됨
+
+ )}
+
+
+ {item.author}
+
+ {item.date}
+
+
+
+ {/* Stats Area */}
+
+ {isAdmin && onHide && (
+
+ )}
+
+
+
+
+ {item.views.toLocaleString()}
+
+
+
+
+
+ );
+};
+
+// ----------------------------------------------------------------------
+// Main Component
+// ----------------------------------------------------------------------
+
+export default function ArchiveTab() {
+ const [libraryItems, setLibraryItems] = useState
([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [librarySort, setLibrarySort] = useState<'LATEST' | 'VIEWS' | 'LIKES'>('LATEST');
+ const [viewMode, setViewMode] = useState<'GRID' | 'LIST'>('GRID');
+ const [currentPage, setCurrentPage] = useState(1);
+ const [searchTerm, setSearchTerm] = useState('');
+
+ // New States
+ const [showBookmarkedOnly, setShowBookmarkedOnly] = useState(false);
+ const [isAdmin, setIsAdmin] = useState(false); // Mock Admin Mode
+
+ const ITEMS_PER_PAGE = viewMode === 'LIST' ? 15 : 10;
+
+ // Data Loading Simulation
+ useEffect(() => {
+ setIsLoading(true);
+ setCurrentPage(1);
+
+ const timer = setTimeout(() => {
+ setLibraryItems(MOCK_LIBRARY_DATA);
+ setIsLoading(false);
+ }, 400);
+
+ return () => clearTimeout(timer);
+ }, []);
+
+ // Handler for Likes
+ const handleLike = (e: React.MouseEvent, id: number) => {
+ e.stopPropagation();
+ setLibraryItems(prev => prev.map(item => {
+ if (item.id === id) {
+ return {
+ ...item,
+ isLiked: !item.isLiked,
+ likes: item.isLiked ? item.likes - 1 : item.likes + 1
+ };
+ }
+ return item;
+ }));
+ };
+
+ const handleView = (link: string) => {
+ window.open(link, '_blank');
+ };
+
+ const handleLibraryBookmark = (e: React.MouseEvent, id: number) => {
+ e.stopPropagation();
+ setLibraryItems(prev =>
+ prev.map(item =>
+ item.id === id ? { ...item, isBookmarked: !item.isBookmarked } : item,
+ ),
+ );
+ };
+
+ const handleHide = (e: React.MouseEvent, id: number) => {
+ e.stopPropagation();
+ setLibraryItems(prev =>
+ prev.map(item =>
+ item.id === id ? { ...item, isHidden: !item.isHidden } : item,
+ ),
+ );
+ };
+
+ // Filtering Logic
+ const filteredLibrary = libraryItems.filter((item) => {
+ const matchesSearch = item.title.toLowerCase().includes(searchTerm.toLowerCase());
+ const matchesBookmark = showBookmarkedOnly ? item.isBookmarked : true;
+ const matchesHidden = isAdmin ? true : !item.isHidden;
+
+ return matchesSearch && matchesBookmark && matchesHidden;
+ });
+
+ const sortedLibrary = [...filteredLibrary].sort((a, b) => {
+ if (librarySort === 'VIEWS') return b.views - a.views;
+ if (librarySort === 'LIKES') return b.likes - a.likes;
+ return new Date(b.date).getTime() - new Date(a.date).getTime();
+ });
+
+ // Pagination Logic
+ const totalItems = sortedLibrary.length;
+ const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);
+
+ const currentLibrary = sortedLibrary.slice(
+ (currentPage - 1) * ITEMS_PER_PAGE,
+ currentPage * ITEMS_PER_PAGE,
+ );
+
+ return (
+
+ {/* Header Area */}
+
+
+ 제로원 아카이브
+
+
+
+ {/* Admin Toggle (Hidden/Dev feature) */}
+
+
+
+ {/* Filters & Search */}
+
+
+ {/* Left Side Filters */}
+
+ {/* Bookmark Filter */}
+
+
+
+ {/* Right Side Controls */}
+
+ {/* Search Bar */}
+
+ setSearchTerm(e.target.value)}
+ className="h-600 w-full rounded-100 border border-border-subtle bg-background-default pl-200 pr-500 font-designer-14m outline-none focus:border-border-default focus:ring-2 focus:ring-fill-neutral-default-default transition-all"
+ />
+
+
+
+
+
+ {/* Sort & View Toggle */}
+
+ {/* Sort Dropdown */}
+
+
+
+ {/* Dropdown */}
+
+
+
+
+
+
+
+
+
+
+
+ {/* View Mode Toggle */}
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ <>
+ {/* Library Content */}
+ {viewMode === 'GRID' ? (
+ /* Grid View */
+
+ {currentLibrary.length > 0 ? (
+ currentLibrary.map((item) => (
+
+ ))
+ ) : (
+
+ )}
+
+ ) : (
+ /* List View */
+
+ {currentLibrary.length > 0 ? (
+
+ {currentLibrary.map((item) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ )}
+ >
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+
+ {currentPage} / {totalPages}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/home/tabs/community-tab.tsx b/src/components/home/tabs/community-tab.tsx
new file mode 100644
index 00000000..51908306
--- /dev/null
+++ b/src/components/home/tabs/community-tab.tsx
@@ -0,0 +1,303 @@
+'use client';
+
+import React, { useState, useEffect, useCallback, useRef } from 'react';
+import { Loader2, Vote, SearchX, Plus, MessageSquareText } from 'lucide-react';
+import { Voting } from '@/types/voting';
+import { mockFetchVotings } from '@/mocks/voting-mock-data';
+import VotingCard from '@/components/cards/voting-card';
+import VotingCreateModal from '@/components/voting/voting-create-modal';
+import VotingDetailView from '@/components/voting/voting-detail-view';
+import { VotingCreateFormData } from '@/types/schemas/zod-schema';
+import { cn } from '@/components/ui/(shadcn)/lib/utils';
+
+export default function CommunityTab() {
+ // 상태 관리
+ const [votings, setVotings] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
+ const [hasMore, setHasMore] = useState(true);
+ const [page, setPage] = useState(1);
+ const [error, setError] = useState(null);
+ const [filterMode, setFilterMode] = useState<'active' | 'all' | 'popular'>('active');
+ const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
+ const [selectedVotingId, setSelectedVotingId] = useState(null);
+
+ // 무한 스크롤용 ref
+ const observerTarget = useRef(null);
+
+ // 데이터 로딩 함수
+ const loadVotings = useCallback(
+ async (pageNum: number, reset: boolean = false) => {
+ try {
+ if (reset) {
+ setIsLoading(true);
+ setError(null);
+ } else {
+ setIsLoadingMore(true);
+ }
+
+ const activeOnly = filterMode === 'active';
+ const sortBy = filterMode === 'popular' ? 'popular' : 'latest';
+
+ const result = await mockFetchVotings({
+ page: pageNum,
+ limit: 10,
+ activeOnly: activeOnly,
+ sortBy: sortBy,
+ });
+
+ setVotings((prev) => (reset ? result.items : [...prev, ...result.items]));
+ setHasMore(result.hasMore);
+ } catch (err) {
+ setError('데이터를 불러오는데 실패했습니다.');
+ console.error(err);
+ } finally {
+ setIsLoading(false);
+ setIsLoadingMore(false);
+ }
+ },
+ [filterMode],
+ );
+
+ // 투표 생성 핸들러
+ const handleCreateVoting = async (data: VotingCreateFormData) => {
+ try {
+ // Mock API 호출 (실제로는 서버에 요청)
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ // 새 투표 객체 생성
+ const newVoting: Voting = {
+ id: Date.now(), // 임시 ID
+ round: votings.length > 0 ? Math.max(...votings.map(v => v.round)) + 1 : 1,
+ title: data.title,
+ description: data.description || undefined,
+ options: data.options.map((opt, index) => ({
+ id: Date.now() + index,
+ label: opt.label,
+ voteCount: 0,
+ percentage: 0,
+ })),
+ totalVotes: 0,
+ myVote: undefined,
+ commentCount: 0,
+ comments: [],
+ createdAt: new Date().toISOString(),
+ endsAt: data.endsAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7일 후
+ isActive: true,
+ tags: data.tags || [],
+ author: { id: 999, nickname: '나' }, // TODO: 실제 사용자 정보로 교체
+ };
+
+ // localStorage에 저장 (상세 페이지에서 조회 가능하도록)
+ const customVotings = localStorage.getItem('customVotings');
+ const existingVotings = customVotings ? JSON.parse(customVotings) : [];
+ localStorage.setItem('customVotings', JSON.stringify([newVoting, ...existingVotings]));
+
+ // 목록 맨 앞에 추가
+ setVotings((prev) => [newVoting, ...prev]);
+
+ console.log('새 투표 생성 완료:', newVoting);
+ } catch (error) {
+ console.error('투표 생성 실패:', error);
+ throw error;
+ }
+ };
+
+ // 필터 변경 시 첫 페이지 로드
+ useEffect(() => {
+ setPage(1);
+ loadVotings(1, true);
+ }, [loadVotings]);
+
+ // 무한 스크롤 Intersection Observer
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting && hasMore && !isLoadingMore && !isLoading) {
+ const nextPage = page + 1;
+ setPage(nextPage);
+ loadVotings(nextPage, false);
+ }
+ },
+ { threshold: 0.1 },
+ );
+
+ const currentTarget = observerTarget.current;
+ if (currentTarget) {
+ observer.observe(currentTarget);
+ }
+
+ return () => {
+ if (currentTarget) {
+ observer.unobserve(currentTarget);
+ }
+ };
+ }, [hasMore, isLoadingMore, isLoading, page, loadVotings]);
+
+ // 상세 화면으로 전환
+ const handleVotingClick = (votingId: number) => {
+ setSelectedVotingId(votingId);
+ };
+
+ // 목록으로 돌아가기
+ const handleBackToList = () => {
+ setSelectedVotingId(null);
+ };
+
+ // 상세 화면이 열려있으면 상세 화면 표시
+ if (selectedVotingId) {
+ return (
+
+
+
+ );
+ }
+
+ // 로딩 상태
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ // 에러 상태
+ if (error) {
+ return (
+
+
+
+
{error}
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ {/* Header */}
+
+
+ 밸런스게임
+
+
+
+
+ {/* 헤더 설명 */}
+
+
+ 다양한 주제에 투표하고 댓글로 자유롭게 토론할 수 있습니다.
+
+
+
+ {/* 필터 + 주제 생성 버튼 */}
+
+ {/* 필터 버튼 */}
+
+
+
+
+
+
+ {/* 주제 생성 버튼 */}
+
+
+
+ {/* 투표 목록 */}
+ {votings.length === 0 ? (
+
+
+
+
투표가 없습니다
+
+ 곧 새로운 투표가 등록될 예정입니다
+
+
+
+ ) : (
+ <>
+
+ {votings.map((voting) => (
+ handleVotingClick(voting.id)}
+ />
+ ))}
+
+
+ {/* 무한 스크롤 트리거 */}
+
+ {isLoadingMore && (
+
+
+ 불러오는 중...
+
+ )}
+ {!hasMore && votings.length > 0 && (
+
모든 투표를 불러왔습니다
+ )}
+
+ >
+ )}
+
+
+ {/* 주제 생성 모달 */}
+ setIsCreateModalOpen(false)}
+ onSubmit={handleCreateVoting}
+ />
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/components/home/tabs/hall-of-fame-tab.tsx b/src/components/home/tabs/hall-of-fame-tab.tsx
new file mode 100644
index 00000000..fc91d13e
--- /dev/null
+++ b/src/components/home/tabs/hall-of-fame-tab.tsx
@@ -0,0 +1,497 @@
+'use client';
+
+import React, { useState, useEffect } from 'react';
+import Image from 'next/image';
+import { cn } from '@/components/ui/(shadcn)/lib/utils';
+import UserProfileModal from '@/entities/user/ui/user-profile-modal';
+import {
+ Trophy,
+ Flame,
+ FileText,
+ Thermometer,
+ Search,
+ Loader2,
+ ChevronLeft,
+ ChevronRight,
+} from 'lucide-react';
+
+// ----------------------------------------------------------------------
+// Types & Mock Data (Hall of Fame)
+// ----------------------------------------------------------------------
+
+type RankingType = 'ATTENDANCE' | 'STUDY_LOG' | 'SINCERITY';
+
+interface Ranker {
+ rank: number;
+ userId: number;
+ nickname: string;
+ profileImage: string | null;
+ score: number;
+ scoreLabel: string;
+ change?: 'up' | 'down' | 'same';
+ lastActive: string;
+ studyTime: string;
+ major?: string;
+ streak?: number;
+ changeValue?: number;
+}
+
+const JOBS = [
+ 'IT 노베이스 - 비지니스/창업',
+ 'IT 노베이스 - 업무 자동화',
+ 'IT 노베이스 - 내 서비스 개발',
+ 'IT 실무자 - PM/PO/기획',
+ 'IT 실무자 - 프론트엔드',
+ 'IT 실무자 - 백엔드',
+ 'IT 실무자 - AI/머신러닝',
+ 'IT 실무자 - iOS',
+ 'IT 실무자 - 안드로이드',
+ 'IT 실무자 - DevOps',
+ 'IT 실무자 - 데이터 분석',
+ 'IT 실무자 - QA',
+ 'IT 실무자 - 게임 개발',
+ 'IT 실무자 - 디자인',
+ 'IT 실무자 - 마케팅',
+ 'IT 실무자 - 기타',
+];
+
+const generateMockRankers = (type: RankingType): Ranker[] => {
+ return Array.from({ length: 20 }, (_, i) => {
+ const rank = i + 1;
+ let score = 0;
+ let scoreLabel = '';
+
+ if (type === 'ATTENDANCE') {
+ score = 150 - i * 3;
+ scoreLabel = `${score}회`;
+ } else if (type === 'STUDY_LOG') {
+ score = 80 - i * 2;
+ scoreLabel = `${score}건`;
+ } else {
+ score = parseFloat((99.9 - i * 1.5).toFixed(1));
+ scoreLabel = `${score}℃`;
+ }
+
+ const lastActive = i < 3 ? '방금 전' : `${i * 10 + 5}분 전`;
+ const studyTime = `${Math.floor(Math.random() * 40 + 10)}시간`;
+ const streak = Math.floor(Math.random() * 50) + 1;
+ const major = JOBS[Math.floor(Math.random() * JOBS.length)];
+
+ return {
+ rank,
+ userId: 100 + i,
+ nickname: `User_${100 + i}`,
+ profileImage: null as string | null,
+ score,
+ scoreLabel,
+ change: Math.random() > 0.7 ? 'up' : Math.random() > 0.8 ? 'down' : 'same',
+ lastActive,
+ studyTime,
+ major,
+ streak,
+ changeValue: Math.floor(Math.random() * 5),
+ };
+ });
+};
+
+const TAB_CONFIG: Record<
+ RankingType,
+ { label: string; desc: string; icon: React.ReactNode; colorClass: string }
+> = {
+ ATTENDANCE: {
+ label: '불꽃 출석왕',
+ desc: '비가 오나 눈이 오나 자리를 지킨, 제로원의 개근상',
+ icon: ,
+ colorClass: 'text-text-brand',
+ },
+ STUDY_LOG: {
+ label: '열정 기록왕',
+ desc: '제로원의 아카이브를 채운, 자료 공유의 신',
+ icon: ,
+ colorClass: 'text-text-information',
+ },
+ SINCERITY: {
+ label: '성실 온도왕',
+ desc: '가장 뜨거운 심장을 가진, 제로원의 신뢰 아이콘',
+ icon: ,
+ colorClass: 'text-text-warning',
+ },
+};
+
+// ----------------------------------------------------------------------
+// Components
+// ----------------------------------------------------------------------
+
+const RankBadge = ({ rank }: { rank: number }) => {
+ const iconPath =
+ rank === 1
+ ? '/icons/gold-rank.svg'
+ : rank === 2
+ ? '/icons/silver-rank.svg'
+ : '/icons/bronze-rank.svg';
+
+ return (
+
+
+
+ );
+};
+
+const ProfileAvatar = ({
+ src,
+ alt,
+ size = 'md',
+ className = '',
+}: {
+ src: string | null;
+ alt: string;
+ size?: 'sm' | 'md' | 'lg' | 'xl';
+ className?: string;
+}) => {
+ const sizeClass = {
+ sm: 'w-[40px] h-[40px]',
+ md: 'w-[48px] h-[48px]',
+ lg: 'w-[80px] h-[80px]',
+ xl: 'w-[120px] h-[120px]',
+ }[size];
+
+ return (
+
+ {src ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const TopRankerCard = ({ ranker }: { ranker: Ranker }) => {
+ const isFirst = ranker.rank === 1;
+
+ return (
+
+ {isFirst && (
+
+
+
+ )}
+
+
+
+
+ {!isFirst && (
+
+
+
+ )}
+
+ {ranker.nickname}
+
+
+
+
+ {parseInt(ranker.scoreLabel.replace(/[^0-9.]/g, ''))}
+
+
+ {ranker.scoreLabel.replace(/[0-9.]/g, '')}
+
+
+
+
+
+ 최근 활동
+ {ranker.lastActive}
+
+
+
+ 누적 학습
+ {ranker.studyTime}
+
+
+
+
+ }
+ />
+ );
+};
+
+// ----------------------------------------------------------------------
+// Main Component
+// ----------------------------------------------------------------------
+
+export default function HallOfFameTab() {
+ const [rankingType, setRankingType] = useState