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
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import CommentInput from './components/CommentInput';
import FreeboardDetailSkeleton from './components/FreeboardDetailSkeleton';
import { useGetFreeboardPost } from '@/generated/api/endpoints/freeboard/freeboard';
import { useAuthRestore } from '@/hooks/useAuth';
import ErrorFallback from '@/components/ErrorFallback';
import { ErrorBoundary } from 'react-error-boundary';

/**
* 자유 게시판 게시글 상세 클라이언트 화면
Expand All @@ -18,6 +20,7 @@ export default function ClientPage({ postId }: { postId: number }) {
data: post,
isPending,
error,
refetch,
} = useGetFreeboardPost(postId, {
query: {
staleTime: 0, // 언제나 신선하지 않은 것으로 간주
Expand All @@ -28,11 +31,37 @@ export default function ClientPage({ postId }: { postId: number }) {
},
});

// TODO: 로딩/에러 처리 구체화 필요
// postId가 유효하지 않은 경우 처리 필요
if (!postId) return <FreeboardDetailSkeleton />;
if (isPending) return <FreeboardDetailSkeleton />;
if (error || !post) return <div>에러가 발생했습니다.</div>;
if (error || !post) {
return (
<main className="space-y-6 pt-12">
<Header className="fixed top-0 left-0 right-0 z-50 bg-white">
<Header.Left>
<Header.BackButton /> {/* router.back() 내부 처리 */}
</Header.Left>
<Header.Center>자유게시판</Header.Center>
<Header.Right>
{/* TODO: onClick 핸들러 추가 */}
<Header.MenuButton
onClick={() => {
/* 바텀시트 열기 */
}}
/>
</Header.Right>
</Header>

<div className="px-5 pt-16">
<ErrorFallback
message="게시글을 불러오는 중 오류가 발생했습니다."
onRetry={() => {
refetch();
}}
/>
</div>
</main>
);
}

return (
<main className="space-y-6 pt-12">
Expand All @@ -51,7 +80,16 @@ export default function ClientPage({ postId }: { postId: number }) {
</Header.Right>
</Header>

<FreeboardDetail post={post} />
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<ErrorFallback
message={error?.message}
onRetry={resetErrorBoundary}
/>
)}
>
<FreeboardDetail post={post} />
</ErrorBoundary>

<section className="px-5 pb-6">
<CommentList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ export default function CommentList({
);
}, [data]);

const latestTotal =
const firstPageTotal =
data?.pages && data.pages.length > 0
? data.pages[data.pages.length - 1]?.total
? data.pages[0]?.total
: undefined;

const headerCount = latestTotal ?? initialCount ?? comments.length;
const headerCount = firstPageTotal ?? initialCount ?? 0;

return (
<section aria-label="댓글 섹션" className="flex-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import { useEffect, useState } from 'react';
import { ThumbsUp } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuthGuard, useAuthRestore } from '@/hooks/useAuth';
import { useAuthGuard } from '@/hooks/useAuth';
import { useToast } from '@/hooks/ui/useToast';
import { formatCappedCount } from '@/utils/formatCount';
import { getGetFreeboardCommentsByCursorQueryKey } from '@/generated/api/endpoints/freeboard-comment/freeboard-comment';
import { useToggleFreeboardCommentLike } from '@/generated/api/endpoints/freeboard-comment-like/freeboard-comment-like';
import { clampCount } from '@/utils/clampCount';

interface LikeButtonCommentProps {
postId: number;
Expand All @@ -16,9 +17,6 @@ interface LikeButtonCommentProps {
initialLikeCount: number;
}

// 음수 방지(보정) 헬퍼
const clampMin0 = (n: number) => (n < 0 ? 0 : n);

/**
* 댓글 좋아요 버튼
*
Expand All @@ -32,7 +30,6 @@ export default function LikeButtonComment({
initialLiked,
initialLikeCount,
}: LikeButtonCommentProps) {
const { isRestoring, isAuthenticated } = useAuthRestore();
const queryClient = useQueryClient();
const toast = useToast();
const { guard } = useAuthGuard();
Expand Down Expand Up @@ -66,7 +63,7 @@ export default function LikeButtonComment({
setLiked((prev) => {
const next = !prev;
const delta = next ? 1 : -1;
setLikeCount((count) => clampMin0(count + delta));
setLikeCount((count) => clampCount(count + delta));
return next;
});

Expand Down Expand Up @@ -104,23 +101,6 @@ export default function LikeButtonComment({
toggleLike.mutate({ freeboardId: postId, commentId });
});

// 인증 복원 중임을 명시(시각적 피드백)
if (isRestoring) {
return (
<button
className="flex items-center gap-1.5 opacity-60 cursor-wait"
disabled
aria-label="좋아요 로딩 중"
type="button"
>
<ThumbsUp className="inline w-4 h-4 text-neutral-200" />
<span className="text-neutral-500 text-input2">
{likeCount}
</span>
</button>
);
}

return (
<button
type="button"
Expand All @@ -129,7 +109,6 @@ export default function LikeButtonComment({
className="flex items-center gap-1.5"
disabled={toggleLike.isPending}
aria-label={liked ? '좋아요 취소' : '좋아요'}
title={!isAuthenticated ? '로그인이 필요합니다' : undefined}
>
<ThumbsUp
className={`inline w-4 h-4 text-neutral-200 transition-colors ${
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,20 @@
import { useEffect, useState } from 'react';
import { Heart } from 'lucide-react';
import { useQueryClient } from '@tanstack/react-query';
import { useAuthGuard, useAuthRestore } from '@/hooks/useAuth';
import { useAuthGuard } from '@/hooks/useAuth';
import { formatCappedCount } from '@/utils/formatCount';
import { useToast } from '@/hooks/ui/useToast';
import { FreeboardDetailResponse } from '@/generated/api/models';
import { getGetFreeboardPostQueryKey } from '@/generated/api/endpoints/freeboard/freeboard';
import { useToggleFreeboardLike } from '@/generated/api/endpoints/freeboard-like/freeboard-like';
import { clampCount } from '@/utils/clampCount';

interface LikeButtonPostProps {
postId: number;
initialLiked: boolean;
initialLikeCount: number;
}

// 음수 방지(보정) 헬퍼
const clampMin0 = (n: number) => (n < 0 ? 0 : n);

/**
* 게시글 좋아요 버튼
*
Expand All @@ -31,7 +29,6 @@ export default function LikeButtonPost({
initialLiked,
initialLikeCount,
}: LikeButtonPostProps) {
const { isRestoring } = useAuthRestore();
const queryClient = useQueryClient();
const toast = useToast();
const { guard } = useAuthGuard();
Expand Down Expand Up @@ -69,7 +66,7 @@ export default function LikeButtonPost({
setLiked((prev) => {
const next = !prev;
const delta = next ? 1 : -1;
setLikeCount((count) => clampMin0(count + delta));
setLikeCount((count) => clampCount(count + delta));
return next;
});

Expand All @@ -79,7 +76,7 @@ export default function LikeButtonPost({
(old) => {
if (!old) return old;
const nextLiked = !(old.isLiked ?? false);
const nextCount = clampMin0(
const nextCount = clampCount(
old.likeCount + (nextLiked ? 1 : -1),
);
return {
Expand Down Expand Up @@ -125,22 +122,6 @@ export default function LikeButtonPost({
toggleLike.mutate({ freeboardId: postId });
});

// 인증 복원 중임을 명시(시각적 피드백)
if (isRestoring) {
return (
<button
className="flex items-center gap-1.5 opacity-60 cursor-wait"
disabled
aria-label="좋아요 로딩 중"
>
<Heart className="inline w-4 h-4 text-neutral-200" />
<span className="text-neutral-500 text-input2">
{formatCappedCount(likeCount)}
</span>
</button>
);
}

return (
<button
type="button"
Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/app/main/community/freeboard/[freeboardId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,15 @@ export default async function Page({
const queryOptions = getGetFreeboardPostQueryOptions(postId);
await queryClient.fetchQuery(queryOptions);
} catch (error: unknown) {
if (isAxiosError(error) && error.response?.status === 404)
notFound();
return <FreeboardDetailSkeleton />;
if (isAxiosError(error)) {
const status = error.response?.status;

if (status === 404) {
notFound();
}
}

throw error;
}

const dehydratedState = dehydrate(queryClient);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/ErrorFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function ErrorFallback({
onRetry,
}: ErrorFallbackProps) {
return (
<div className="p-6 text-center space-y-4">
<div className="p-12 text-center space-y-4">
<p className="text-red-500 font-medium">
{message ?? '문제가 발생했습니다.'}
</p>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/infiniteScrolls/VirtualList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function VirtualList<T>({
<div
key={row.key}
ref={virtualizer.measureElement}
data-index={row.index} // ✅ 경고 해결: 측정 요소에 data-index 추가
data-index={row.index}
style={{
position: 'absolute',
top: 0,
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/utils/clampCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* 카운트 값이 min(기본 0) 아래로 내려가지 않도록 보정합니다.
*/
export const clampCount = (n: number, min = 0) => {
return Math.max(n, min);
};