Skip to content

Commit 08ae16c

Browse files
authored
[김희진] sprint10 (#125)
* refactor: add import type * refactor: code review * feat: add select box on board filter * feat: add board card css * feat: add infinite scroll * chore: add image domain * refactor: extract variable * fix: remove useApi * refactor: extract useBorad hook * refactor: rename file * chore: remove unused import
1 parent c4034c1 commit 08ae16c

20 files changed

+373
-39
lines changed

next.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
const nextConfig = {
33
reactStrictMode: true,
44
images: {
5-
domains: ['sprint-fe-project.s3.ap-northeast-2.amazonaws.com'],
5+
domains: [
6+
'sprint-fe-project.s3.ap-northeast-2.amazonaws.com',
7+
'example.com',
8+
'via.placeholder.com',
9+
'flexible.img.hani.co.kr',
10+
],
611
},
712
};
813

pages/boards/[id].tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { useRouter } from 'next/router';
2+
import styles from './[id].module.css';
3+
4+
export default function BoardDetail() {
5+
const router = useRouter();
6+
const { id } = router.query;
7+
8+
return <div>{id}</div>;
9+
}

pages/boards/components/BestBoardCard.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Image from 'next/image';
22
import styles from './BestBoardCard.module.css';
33
import medalSvg from '@/src/assets/ic_medal.svg';
4-
import heardSvg from '@/src/assets/ic_heart.svg';
5-
import { Board } from '@/src/apis/boardTypes';
4+
import heartSvg from '@/src/assets/ic_heart.svg';
5+
import type { Board } from '@/src/apis/boardTypes';
66

77
interface BestBoardCardProps
88
extends Pick<
@@ -26,13 +26,18 @@ export default function BestBoardCard({
2626
<div className={styles.contentContainer}>
2727
<h4 className={styles.contentTitle}>{title}</h4>
2828
<div className={styles.contentImgWrapper}>
29-
<Image src={image} alt="medal" fill style={{ objectFit: 'cover' }} />
29+
<Image
30+
src={image}
31+
alt="게시판 첨부이미지"
32+
fill
33+
style={{ objectFit: 'cover' }}
34+
/>
3035
</div>
3136
</div>
3237
<div className={styles.additionalInfo}>
3338
<span>{writer.nickname}</span>
3439
<div className={styles.likeCountWrapper}>
35-
<Image src={heardSvg} alt="heardIcon" width={16} height={16} />
40+
<Image src={heartSvg} alt="heartIcon" width={16} height={16} />
3641
<span>{likeCount}</span>
3742
</div>
3843
<span>{new Date(createdAt).toLocaleDateString()}</span>

pages/boards/components/BestBoards.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Board } from '@/src/apis/boardTypes';
1+
import type { Board } from '@/src/apis/boardTypes';
22
import BestBoardCard from './BestBoardCard';
33
import styles from './BestBoards.module.css';
44

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
.boardCard {
2+
background: #fcfcfc;
3+
padding: 20px;
4+
position: relative;
5+
display: flex;
6+
flex-direction: column;
7+
gap: 16px;
8+
}
9+
10+
.boardCard::after {
11+
content: '';
12+
position: absolute;
13+
left: 0;
14+
bottom: 0;
15+
width: 100%;
16+
height: 1px;
17+
background-color: var(--Secondary-200);
18+
}
19+
20+
.contentContainer {
21+
display: flex;
22+
gap: 8px;
23+
min-height: 72px;
24+
}
25+
26+
.title {
27+
color: var(--Secondary-800);
28+
font-size: 20px;
29+
font-weight: 600;
30+
31+
flex: 1;
32+
}
33+
34+
.imageWrapper {
35+
position: relative;
36+
width: 72px;
37+
height: 72px;
38+
39+
border-radius: 6px;
40+
border: 1px solid var(--Secondary-200, #e5e7eb);
41+
background: #fff;
42+
}
43+
44+
.additionalInfo {
45+
display: flex;
46+
justify-content: space-between;
47+
}
48+
49+
.infoWrapper {
50+
display: flex;
51+
align-items: center;
52+
gap: 8px;
53+
}
54+
55+
.nickname {
56+
color: var(--Secondary-600, #4b5563);
57+
font-size: 14px;
58+
font-weight: 400;
59+
}
60+
61+
.date {
62+
color: var(--Secondary-400, #9ca3af);
63+
font-size: 14px;
64+
font-weight: 400;
65+
}
66+
67+
.likeCountWrapper {
68+
display: flex;
69+
align-items: center;
70+
gap: 8px;
71+
color: var(--Secondary-500, #6b7280);
72+
font-size: 16px;
73+
font-weight: 400;
74+
}

pages/boards/components/BoardCard.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Image from 'next/image';
2+
import styles from './BoardCard.module.css';
3+
import type { Board } from '@/src/apis/boardTypes';
4+
import heartSvg from '@/src/assets/ic_heart.svg';
5+
import avatarSvg from '@/src/assets/avatar.svg';
6+
7+
export default function BoardCard({
8+
title,
9+
image,
10+
writer,
11+
createdAt,
12+
likeCount,
13+
}: Board) {
14+
return (
15+
<div className={styles.boardCard}>
16+
<div className={styles.contentContainer}>
17+
<p className={styles.title}>{title}</p>
18+
{image && (
19+
<div className={styles.imageWrapper}>
20+
<Image
21+
src={image}
22+
alt="게시판 첨부이미지"
23+
fill
24+
style={{ objectFit: 'cover' }}
25+
/>
26+
</div>
27+
)}
28+
</div>
29+
<div className={styles.additionalInfo}>
30+
<div className={styles.infoWrapper}>
31+
<Image src={avatarSvg} alt="avatar" width={24} height={24} />
32+
<span className={styles.nickname}>{writer.nickname}</span>
33+
<span className={styles.date}>
34+
{new Date(createdAt).toLocaleDateString()}
35+
</span>
36+
</div>
37+
<div className={styles.likeCountWrapper}>
38+
<Image src={heartSvg} alt="heardIcon" width={16} height={16} />
39+
<span>{likeCount}</span>
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
}

pages/boards/components/Boards.module.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111
font-weight: 700;
1212
}
1313

14+
.filter {
15+
display: flex;
16+
gap: 20px;
17+
}
18+
1419
.searchBar {
1520
padding: 9px 20px 9px 16px;
1621
border-radius: 12px;
1722
background: var(--Secondary-100, #f3f4f6);
23+
flex: 1;
1824

1925
display: flex;
2026
align-items: center;
@@ -31,6 +37,27 @@
3137
color: var(--Secondary-400, #9ca3af);
3238
}
3339

40+
.options {
41+
height: 100%;
42+
padding: 12px 20px;
43+
padding-right: 50px;
44+
45+
border-radius: 12px;
46+
border: 1px solid var(--Secondary-200, #e5e7eb);
47+
48+
appearance: none;
49+
background: url('../../../src/assets/ic_arrow_down.svg') no-repeat right 10px
50+
center;
51+
}
52+
53+
.boardsContainer {
54+
margin-top: 24px;
55+
display: flex;
56+
flex-direction: column;
57+
justify-content: center;
58+
gap: 24px;
59+
}
60+
3461
/* tablet */
3562
@media screen and (max-width: 1199px) {
3663
.boardsHeader {

pages/boards/components/Boards.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,59 @@ import Image from 'next/image';
22
import Button from '@/src/components/Button';
33
import styles from './Boards.module.css';
44
import searchSvg from '@/src/assets/ic_search.svg';
5+
import { useState } from 'react';
6+
import BoardCard from './BoardCard';
7+
import Link from 'next/link';
8+
import { useBoards } from '@/src/hooks/useBoards';
59

610
export default function Boards() {
11+
const [orderBy, setOrderBy] = useState('recent');
12+
const { boards, isLoading, error, observerRef, resetBoards } =
13+
useBoards(orderBy);
14+
15+
const handleOptionChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
16+
const newOrder = e.target.value;
17+
setOrderBy(newOrder);
18+
resetBoards();
19+
};
20+
721
return (
822
<div>
9-
<div className={styles.boardsHeader}>
23+
<section className={styles.boardsHeader}>
1024
<h3 className={styles.title}>게시글</h3>
1125
<div>
1226
<Button>글쓰기</Button>
1327
</div>
14-
</div>
15-
<div className={styles.filter}>
28+
</section>
29+
<section className={styles.filter}>
1630
<div className={styles.searchBar}>
1731
<Image src={searchSvg} alt="searchIcon" width={24} height={24} />
1832
<input
1933
className={styles.searchData}
2034
placeholder="검색할 상품을 입력해주세요"
2135
/>
2236
</div>
23-
</div>
37+
<div>
38+
<select
39+
className={styles.options}
40+
id="options"
41+
onChange={handleOptionChange}
42+
>
43+
<option value="recent">최신순</option>
44+
<option value="like">좋아요순</option>
45+
</select>
46+
</div>
47+
</section>
48+
<section className={styles.boardsContainer}>
49+
{boards.map((board) => (
50+
<Link key={board.id} href={`/boards/${board.id}`}>
51+
<BoardCard {...board} />
52+
</Link>
53+
))}
54+
{isLoading && <div>Loading...</div>}
55+
{error && <div>{error}</div>}
56+
</section>
57+
<div ref={observerRef} style={{ height: '1px' }}></div>
2458
</div>
2559
);
2660
}

pages/boards/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { GetStaticProps } from 'next';
22
import BestBoards from './components/BestBoards';
3-
import { Board } from '@/src/apis/boardTypes';
3+
import type { Board } from '@/src/apis/boardTypes';
44
import { getBoards } from '@/src/apis/boardsApi';
55
import Boards from './components/Boards';
66

@@ -28,6 +28,6 @@ export const getStaticProps: GetStaticProps = async () => {
2828
props: {
2929
boards: list || [],
3030
},
31-
revalidate: 600, // Re-generate the page every 600 seconds (ISR)
31+
revalidate: 600,
3232
};
3333
};

src/apis/boardTypes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ export interface GetBoardsResponse {
2222
export interface GetBoardsRequestParams {
2323
page?: number;
2424
pageSize?: number;
25-
orderBy?: 'recent' | 'like';
25+
orderBy?: string;
2626
keyword?: string;
2727
}

0 commit comments

Comments
 (0)