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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,5 @@ coverage/
.vscode/
.vscode/settings.json

#monorepo issues
monorepo-docs/
2 changes: 1 addition & 1 deletion apps/web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const nextConfig = {
protocol: 'https',
hostname:
'dreampaste-soso-image-storage.s3.ap-northeast-2.amazonaws.com',
pathname: '/freeboard/**',
pathname: '/**', // S3 버킷의 모든 경로 허용 (freeboard, voteboard 등)
},
],
},
Expand Down
5 changes: 3 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"dev:https": "node server.mjs",
"dev": "node server.mjs",
"dev:http": "next dev",
"build": "next build",
"build:analyze": "node analyze.mjs",
"start": "next start",
Expand All @@ -29,6 +29,7 @@
"axios": "^1.10.0",
"chart.js": "^4.5.0",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"js-cookie": "^3.0.5",
"keen-slider": "^6.8.6",
"lucide-react": "^0.525.0",
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/main/community/constants/sortOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const SORT_OPTIONS: SortOption[] = [
{ label: '최신순', value: 'LATEST' },
{ label: '인기순', value: 'LIKE' },
{ label: '댓글순', value: 'COMMENT' },
{ label: '조회순', value: 'VIEW' },
];
9 changes: 9 additions & 0 deletions apps/web/src/app/main/community/constants/votesOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { GetVotePostListStatus } from '@/generated/api/models';
import { TabItem } from '@/types/tab.types';

export type VoteState = GetVotePostListStatus | null;

export const VOTE_STATES: TabItem<VoteState>[] = [
{ label: '진행중', value: 'IN_PROGRESS' },
{ label: '완료', value: 'COMPLETED' },
];
22 changes: 20 additions & 2 deletions apps/web/src/app/main/community/freeboard/ClientPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import React, { useState } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { useOverlay } from '@/hooks/ui/useOverlay';
import { PillChipsTab } from '@/components/tabs/PillChipsTab';
import { CATEGORIES, Category } from '../constants/categories';
import { SortHeader } from '../components/SortHeader';
import { SortValue } from '@/types/options.types';
import { SORT_OPTIONS } from '../constants/sortOptions';
import FloatingButton from '@/components/buttons/FloatingButton';
import { FreeBoardCard } from '../components/FreeboardCard';
import FloatingCategoryMenu from '@/components/buttons/FloatingCategoryMenu';
import CommunityPostList from '../components/CommunityPostList';
import { FreeboardSummary } from '@/generated/api/models';
import {
Expand All @@ -28,7 +30,7 @@ import {
export default function FreeboardClientPage() {
const [category, setCategory] = useState<Category | null>(null);
const [sortOption, setSortOption] = useState<SortValue>('LATEST');

const { open } = useOverlay();
// 무한스크롤 데이터 페칭
const {
data,
Expand Down Expand Up @@ -66,6 +68,22 @@ export default function FreeboardClientPage() {
// 총 게시글 개수
const totalCount = data?.pages[0]?.totalCount ?? 0;

const handleFloatingButtonClick = () => {
open(
({ close }) => (
<FloatingCategoryMenu
route="freeboard"
categories={CATEGORIES}
onClose={() => close(null, { duration: 200 })}
/>
),
{
backdrop: true,
closeOnBackdrop: true,
},
);
};

return (
<main className="w-full h-full flex flex-col">
<PillChipsTab<Category>
Expand Down Expand Up @@ -96,7 +114,7 @@ export default function FreeboardClientPage() {
)}
storageKey="freeboard-post-list-scroll"
/>
<FloatingButton categories={CATEGORIES} />
<FloatingButton onClick={handleFloatingButtonClick} />
</main>
);
}
118 changes: 118 additions & 0 deletions apps/web/src/app/main/community/votesboard/ClientPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use client';

import { useState } from 'react';
import { useOverlay } from '@/hooks/ui/useOverlay';
import { SortHeader } from '../components/SortHeader';
import { SORT_OPTIONS } from '../constants/sortOptions';
import { SortValue } from '@/types/options.types';
import { PillChipsTab } from '@/components/tabs/PillChipsTab';
import { CATEGORIES } from '../constants/categories';
import { VOTE_STATES, VoteState } from '../constants/votesOptions';
import { VoteboardSummary } from '@/generated/api/models';
import FloatingCategoryMenu from '@/components/buttons/FloatingCategoryMenu';
import CommunityPostList from '../components/CommunityPostList';
import { VoteBoardCard } from './components/VoteBoardCard';
import {
getVotePostsByCursor,
getGetVotePostsByCursorQueryKey,
} from '@/generated/api/endpoints/voteboard/voteboard';
import { useInfiniteQuery } from '@tanstack/react-query';
import FloatingButton from '@/components/buttons/FloatingButton';
/**
* 투표 게시판 클라이언트 메인 페이지
*
* @description
* - [전체/진행중/완료] 상태 탭 제공
* - 상태별 투표 게시글 목록 표시
* - 정렬 옵션 제공
*
*/

export default function VotesboardClientPage() {
const [sortOption, setSortOption] = useState<SortValue>('LATEST');
const [voteState, setVoteState] = useState<VoteState>(null);
const { open } = useOverlay();
// 무한스크롤 데이터 페칭
const {
data,
fetchNextPage,
hasNextPage,
isLoading,
isFetchingNextPage,
error,
refetch,
} = useInfiniteQuery({
queryKey: getGetVotePostsByCursorQueryKey({
status: voteState ?? undefined,
sort: sortOption,
}),
queryFn: ({ pageParam, signal }) =>
getVotePostsByCursor(
{
status: voteState ?? undefined,
sort: sortOption,
cursor: pageParam,
size: 10,
},
signal,
),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => {
return lastPage.hasNext ? lastPage.nextCursor : undefined;
},
});
const allVotePosts: VoteboardSummary[] =
data?.pages.flatMap((page) => page.posts ?? []) ?? [];
const totalCount = data?.pages[0]?.totalCount ?? 0;

const handleFloatingButtonClick = () => {
open(
({ close }) => (
<FloatingCategoryMenu
route="votesboard"
categories={CATEGORIES}
onClose={() => close(null, { duration: 200 })}
/>
),
{
backdrop: true,
closeOnBackdrop: true,
},
);
};

return (
<main className="w-full h-full flex flex-col">
<PillChipsTab<VoteState>
chips={VOTE_STATES}
showAll
activeValue={voteState}
onChange={setVoteState}
ariaLabel="투표 상태 선택 필터"
/>
{/* 필터 헤더 */}
<SortHeader
totalCount={totalCount}
sortOptions={SORT_OPTIONS}
currentValue={sortOption}
onFilterChange={setSortOption}
/>

<CommunityPostList<VoteboardSummary>
items={allVotePosts}
hasNextPage={hasNextPage || false}
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
initialLoading={isLoading}
error={error}
onRetry={() => refetch()}
storageKey="votesboard-post-list-scroll"
getItemKey={(post, index) => post.postId ?? `post-${index}`}
renderItem={(post) => <VoteBoardCard post={post} />}
/>

{/* TODO: FloatingButton 추가 */}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO 주석이 이미 완료된 작업을 가리키고 있습니다. FloatingButton이 이미 115번 라인에 구현되어 있으므로 이 TODO 주석을 제거해야 합니다.

Suggested change
{/* TODO: FloatingButton 추가 */}

Copilot uses AI. Check for mistakes.
<FloatingButton onClick={handleFloatingButtonClick} />
</main>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// src/components/CommunityCard.tsx
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일 경로 주석이 잘못되었습니다. 이 파일은 src/components/CommunityCard.tsx가 아니라 src/app/main/community/votesboard/components/VoteBoardCard.tsx입니다. 파일 위치를 정확히 반영하도록 주석을 수정해야 합니다.

Suggested change
// src/components/CommunityCard.tsx
// src/app/main/community/votesboard/components/VoteBoardCard.tsx

Copilot uses AI. Check for mistakes.
import Image from 'next/image';
import Card from '@/components/Card';
import { CategoryChip } from '@/components/chips/CategoryChip';
import { Category } from '../../constants/categories';
import { LaptopMinimalCheck, MessageSquareMore } from 'lucide-react';
//import { useRouter } from 'next/navigation';
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 처리된 import와 코드를 제거해야 합니다. 추후 구현 예정인 기능이라면, 구현 시점에 추가하는 것이 코드 가독성과 유지보수 측면에서 더 좋습니다.

Copilot uses AI. Check for mistakes.
import { formatCount } from '@/utils/formatCount';
import type { VoteboardSummary } from '@/generated/api/models';
import { formatVoteDeadline } from '@/utils/vote-deadline';
import { VoteStatusChip } from '@/components/chips/VoteStatusChip';
import { cn } from '@/utils/cn';
export interface VoteBoardCardProps {
post: VoteboardSummary;
}

export function VoteBoardCard({ post }: VoteBoardCardProps) {
const {
postId,
title,
contentPreview,
category,
thumbnailUrl,
commentCount,
totalVotes,
voteStatus,
endTime,
hasVoted,
} = post;

// const router = useRouter();

const handleOnClick = () => {
console.warn(
'추후 투표 상세 페이지로 이동할 예정입니다. => postId:',
postId,
);
//router.push(`/main/community/votesboard/${postId}`);
Comment on lines +34 to +38
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

주석 처리된 코드를 제거해야 합니다. 추후 구현 예정인 기능이라면, 구현 시점에 추가하는 것이 더 좋습니다. 또한 console.warn 대신 실제 동작을 구현하거나, 임시 구현이라면 주석으로만 남기는 것을 권장합니다.

Suggested change
console.warn(
'추후 투표 상세 페이지로 이동할 예정입니다. => postId:',
postId,
);
//router.push(`/main/community/votesboard/${postId}`);
// TODO: 투표 상세 페이지로 이동 기능은 추후 구현 예정입니다.

Copilot uses AI. Check for mistakes.
};

return (
<Card
className="w-full flex flex-col gap-2"
onClick={handleOnClick}
>
<div className="flex items-center gap-1">
<CategoryChip category={category as Category} />
<VoteStatusChip voteStatus={voteStatus} endTime={endTime} />
</div>

<div className="flex flex-row justify-between items-center gap-4">
<section className="flex flex-col gap-1">
<h3 className="text-title2 truncate" title={title}>
{title}
</h3>
<p className="text-body truncate" title={contentPreview}>
{contentPreview}
</p>
</section>
{thumbnailUrl && (
<Image
src={thumbnailUrl}
alt="게시글 썸네일 이미지"
width={55}
height={55}
className="w-[55px] h-[55px] object-cover rounded-md flex-shrink-0"
/>
)}
</div>

<section className="flex justify-between items-center">
{/* 남은시간 */}
<span className="text-neutral-500 text-xs">
{formatVoteDeadline(endTime)}
</span>
<div className="flex items-center gap-2">
{/* 득표수 */}
<div
className="flex items-center gap-1"
aria-label={`투표수 ${totalVotes ?? 0}개`}
>
<LaptopMinimalCheck
className={cn(
'w-4 h-4 ',
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

클래스명에 불필요한 공백이 있습니다. 'w-4 h-4 ' 대신 'w-4 h-4'로 수정해야 합니다.

Suggested change
'w-4 h-4 ',
'w-4 h-4',

Copilot uses AI. Check for mistakes.
hasVoted ? 'text-primary-500' : 'text-neutral-500',
)}
/>
<span className="text-xs">{formatCount(totalVotes)}</span>
</div>
{/* 댓글 */}
<div
className="flex items-center gap-1"
aria-label={`댓글 ${commentCount ?? 0}개`}
>
<MessageSquareMore className="w-4 h-4 text-neutral-500" />
<span className="text-xs">
{formatCount(commentCount)}
</span>
</div>
</div>
</section>
</Card>
);
}
Loading