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
14 changes: 13 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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',
Expand Down
10 changes: 8 additions & 2 deletions src/app/(service)/home/home-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <StudyTab />;
case 'history':
return <StudyHistoryTab />;
return isLoggedIn ? <StudyHistoryTab /> : <StudyTab />;
case 'ranking':
return <HallOfFameTab />;
case 'archive':
Expand Down
2 changes: 1 addition & 1 deletion src/components/card/voting-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default function VotingCard({ voting, onClick }: VotingCardProps) {
</p>
)}

{/* 간단한 투표 결과 미리보기 (투표했을 때만) */}
{/* 간단한 투표 결과 미리보기 */}
{hasVoted && (
<div className="rounded-100 border-border-subtle bg-background-alternative mb-300 border p-300">
<div className="font-designer-12b text-text-subtle mb-100">
Expand Down
10 changes: 9 additions & 1 deletion src/components/home/tab-navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -64,7 +72,7 @@ export default function TabNavigation({ activeTab }: TabNavigationProps) {
</div>

<nav className="border-border-subtle flex gap-100 border-b">
{TABS.map((tab) => {
{visibleTabs.map((tab) => {
const Icon = tab.icon;
const isActive = activeTab === tab.id;

Expand Down
14 changes: 7 additions & 7 deletions src/components/study-history/study-history-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
CheckCircle2,
} from 'lucide-react';
import { cn } from '@/components/ui/(shadcn)/lib/utils';
import { ProfileAvatar } from '@/components/ui/profile-avatar';
import UserAvatar from '@/components/ui/avatar';
import UserProfileModal from '@/entities/user/ui/user-profile-modal';
import { StudyHistoryItem } from '@/types/study-history';

Expand Down Expand Up @@ -43,10 +43,10 @@ export const StudyHistoryRow = ({ item }: { item: StudyHistoryItem }) => {
className="flex cursor-pointer items-center gap-100 transition-opacity hover:opacity-80"
onClick={(e) => e.stopPropagation()} // 행 클릭 이벤트(링크 이동) 방지
>
<ProfileAvatar
src={partner.profileImage || undefined}
<UserAvatar
image={partner.profileImage || undefined}
alt={partner.name}
size="sm"
size={32}
className="h-8 w-8"
/>
<span className="text-text-default truncate font-medium">
Expand All @@ -57,10 +57,10 @@ export const StudyHistoryRow = ({ item }: { item: StudyHistoryItem }) => {
/>
) : (
<div className="text-text-subtlest flex items-center gap-100">
<ProfileAvatar
src={undefined}
<UserAvatar
image={undefined}
alt="상대방 정보 없음"
size="sm"
size={32}
className="h-8 w-8 opacity-60"
/>
<span className="truncate font-medium">상대방 정보 없음</span>
Expand Down
2 changes: 2 additions & 0 deletions src/components/ui/avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export default function Avatar({
image.toUpperCase() !== 'LOCAL' &&
(image.startsWith('http://') ||
image.startsWith('https://') ||
image.startsWith('blob:') ||
image.startsWith('data:') ||
image.startsWith('/'));

const showImage = isValidImage && !isError;
Expand Down
102 changes: 0 additions & 102 deletions src/components/ui/profile-avatar.tsx

This file was deleted.

60 changes: 43 additions & 17 deletions src/components/voting/voting-detail-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
MoreVertical,
Edit,
Trash2,
Lock,
} from 'lucide-react';
import React, { useState, useEffect } from 'react';
import CommentForm from '@/components/discussion/comment-form';
Expand All @@ -20,6 +21,7 @@ import DailyStatsChart from '@/components/voting/daily-stats-chart';
import VoteResultsChart from '@/components/voting/vote-results-chart';
import VoteTimer from '@/components/voting/vote-timer';
import UserProfileModal from '@/entities/user/ui/user-profile-modal';
import LoginModal from '@/features/auth/ui/login-modal';
import {
useVoteBalanceGameMutation,
useCancelVoteBalanceGameMutation,
Expand All @@ -33,6 +35,7 @@ import {
useBalanceGameDetailQuery,
useBalanceGameCommentsQuery,
} from '@/features/study/one-to-one/balance-game/model/use-balance-game-query';
import { useAuth } from '@/hooks/common/use-auth';
import { useUserStore } from '@/stores/useUserStore';
import { BalanceGameComment } from '@/types/balance-game';
import {
Expand Down Expand Up @@ -61,6 +64,7 @@ export default function VotingDetailView({

// User Info
const memberId = useUserStore((state) => state.memberId);
const { isAuthenticated } = useAuth();

// Queries
const {
Expand Down Expand Up @@ -109,6 +113,9 @@ export default function VotingDetailView({
});
}, [commentsData]);

const commentTotalCount =
commentsData?.pages?.[0]?.totalElements ?? comments.length;

// isActive는 백엔드가 내려줄 수도 있고(권장), 없으면 endsAt 기준으로 프론트에서 계산
// VoteTimer도 endsAt으로 "종료"를 판단하므로, 두 로직이 어긋나지 않게 맞춘다.
const isActiveByEndsAt = React.useMemo(() => {
Expand Down Expand Up @@ -254,7 +261,7 @@ export default function VotingDetailView({
isActive,
});

const showVoteOptions = !hasVoted && isActive;
const showVoteOptions = isAuthenticated && !hasVoted && isActive;

return (
<div className="transition-all duration-300">
Expand Down Expand Up @@ -512,31 +519,50 @@ export default function VotingDetailView({
</button>
)}
</div>
<VoteResultsChart
options={votingOptions}
myVote={voting.myVote || undefined}
totalVotes={voting.totalVotes}
/>
<div className="relative">
<div className={cn(!isAuthenticated && 'blur-[6px]')}>
<VoteResultsChart
options={votingOptions}
myVote={voting.myVote || undefined}
totalVotes={voting.totalVotes}
/>
</div>
{!isAuthenticated && (
<div className="absolute inset-0 flex items-center justify-center">
<LoginModal
openTrigger={
<button className="rounded-100 bg-background-default/90 text-text-strong border-border-subtle flex items-center gap-100 border px-200 py-100 text-[12px] font-medium">
<Lock className="h-3 w-3" />
회원가입 후 결과 보기
</button>
}
/>
</div>
)}
</div>
</>
)}
</div>

{/* 일별 통계 (투표 후에만 표시) */}
{hasVoted && voting.dailyStats && voting.dailyStats.length > 0 && (
<div className="mb-500">
<DailyStatsChart
dailyStats={voting.dailyStats}
options={votingOptions}
myVote={voting.myVote || undefined}
/>
</div>
)}
{isAuthenticated &&
hasVoted &&
voting.dailyStats &&
voting.dailyStats.length > 0 && (
<div className="mb-500">
<DailyStatsChart
dailyStats={voting.dailyStats}
options={votingOptions}
myVote={voting.myVote || undefined}
/>
</div>
)}

{/* 댓글 섹션 */}
<div className="rounded-200 border-border-subtle bg-background-default shadow-1 border p-500">
<div className="font-designer-16b text-text-strong mb-400 flex items-center gap-100">
<MessageCircle className="h-5 w-5" />
<span>댓글 {voting.commentCount || 0}</span>
<span>댓글 {commentTotalCount}</span>
</div>

{/* 댓글 목록 (항상 표시) */}
Expand Down Expand Up @@ -564,7 +590,7 @@ export default function VotingDetailView({
</div>

{/* 댓글 작성 폼 */}
{isActive && (
{isAuthenticated && isActive && (
<>
{!hasVoted ? (
<div className="rounded-200 border-border-subtle bg-background-alternative border p-400 text-center">
Expand Down
Loading
Loading