-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #285 from naynara87/Next-나윤주-sprint9
[나윤주] Sprint9
- Loading branch information
Showing
101 changed files
with
1,888 additions
and
487 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,20 @@ | ||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | ||
# 스프린트 미션 9 | ||
|
||
## Getting Started | ||
## 기본 요구사항 | ||
|
||
First, run the development server: | ||
- [x] Github에 PR(Pull Request)을 만들어서 미션을 제출합니다. | ||
- [x] 피그마 디자인에 맞게 페이지를 만들어 주세요. | ||
- [x] 기존의 React, Typescript로 구현한 프로젝트와 별도로 진행합니다. | ||
- [x] Next.js를 사용합니다 | ||
|
||
```bash | ||
npm run dev | ||
# or | ||
yarn dev | ||
# or | ||
pnpm dev | ||
# or | ||
bun dev | ||
``` | ||
## 체크리스트 [기본] | ||
|
||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | ||
- [x] 자유 게시판 페이지 주소는 “/boards” 입니다. | ||
- [x] 전체 게시글에서 드롭 다운으로 “최신 순” 또는 “좋아요 순”을 선택해서 정렬을 할 수 있습니다. | ||
- [x] 게시글 목록 조회 api를 사용하여 베스트 게시글, 게시글을 구현합니다. | ||
- [x] 게시글 title에 검색어가 일부 포함되면 검색이 됩니다. | ||
|
||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. | ||
## 체크리스트 [심화] | ||
|
||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. | ||
|
||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | ||
|
||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | ||
|
||
## Learn More | ||
|
||
To learn more about Next.js, take a look at the following resources: | ||
|
||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | ||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | ||
|
||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | ||
|
||
## Deploy on Vercel | ||
|
||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | ||
|
||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. | ||
- [x] 반응형으로 보여지는 베스트 게시판 개수를 다르게 설정할때 서버에 보내는 pageSize값을 적절하게 설정합니다. | ||
- [x] next의 prefetch 기능을 사용해봅니다. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
.all-board-wrap { | ||
width: 100%; | ||
max-width: 1200px; | ||
margin: 26px auto; | ||
padding-bottom: 20px; | ||
@media (max-width: 1199px) { | ||
padding: 0 24px; | ||
} | ||
@media (max-width: 767px) { | ||
padding: 0 16px; | ||
} | ||
.section-header { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
margin-bottom: 24px; | ||
.title { | ||
font-size: 20px; | ||
font-weight: 600; | ||
color: var(--gray900); | ||
@media (max-width: 767px) { | ||
font-size: 18px; | ||
} | ||
} | ||
} | ||
.board-list { | ||
display: flex; | ||
flex-direction: column; | ||
gap: 24px; | ||
li { | ||
padding: 24px 0; | ||
border-bottom: 1px solid var(--gray200); | ||
.title-info { | ||
display: flex; | ||
.title { | ||
font-weight: 500; | ||
font-size: 20px; | ||
line-height: 1.6; | ||
&:hover { | ||
text-decoration: underline; | ||
} | ||
@media (max-width: 767px) { | ||
font-size: 18px; | ||
} | ||
} | ||
.thumb-wrap { | ||
flex: none; | ||
width: 72px; | ||
height: 72px; | ||
margin-left: auto; | ||
background-color: #fff; | ||
border: 1px solid var(--gray200); | ||
border-radius: 6px; | ||
overflow: hidden; | ||
} | ||
} | ||
} | ||
} | ||
.input-search-wrap { | ||
display: flex; | ||
gap: 16px; | ||
} | ||
.title-info { | ||
margin-bottom: 20px; | ||
.title { | ||
@media (max-width: 767px) { | ||
font-size: 18px; | ||
} | ||
} | ||
} | ||
.writer-info { | ||
display: flex; | ||
align-items: center; | ||
gap: 8px; | ||
|
||
.profile-info { | ||
display: flex; | ||
align-items: center; | ||
gap: 8px; | ||
|
||
.profile-wrap { | ||
position: relative; | ||
width: 24px; | ||
height: 24px; | ||
border-radius: 50%; | ||
overflow: hidden; | ||
img { | ||
width: 100%; | ||
height: 100%; | ||
} | ||
} | ||
.nick-name { | ||
font-size: 14px; | ||
color: var(--gray600); | ||
font-weight: 200; | ||
} | ||
} | ||
.favorite { | ||
font-size: 16px; | ||
color: var(--gray500); | ||
margin-left: auto; | ||
} | ||
} | ||
} | ||
.favorite { | ||
display: flex; | ||
align-items: center; | ||
gap: 0.2em; | ||
} | ||
.date { | ||
font-size: 14px; | ||
color: var(--gray400); | ||
font-weight: 200; | ||
} | ||
.thumb-wrap { | ||
position: relative; | ||
flex: none; | ||
width: 72px; | ||
height: 72px; | ||
margin-left: auto; | ||
background-color: #fff; | ||
border: 1px solid var(--gray200); | ||
border-radius: 6px; | ||
overflow: hidden; | ||
} | ||
.search-none { | ||
color: var(--gray400); | ||
font-size: 16px; | ||
line-height: 24px; | ||
padding: 50px 0; | ||
text-align: center; | ||
.img-wrap { | ||
position: relative; | ||
width: 140px; | ||
height: 140px; | ||
margin: 0 auto; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import { useEffect, useState } from "react"; | ||
import Image from "next/image"; | ||
import styles from "./AllBoard.module.scss"; | ||
import Icon from "@/components/ui/Icon"; | ||
import LinkButton from "@/components/ui/LinkButton"; | ||
import SearchForm from "@/components/form/SearchForm"; | ||
import Dropdown from "@/components/ui/Dropdown"; | ||
import { useRouter } from "next/router"; | ||
import axios from "@/lib/axios"; | ||
import Link from "next/link"; | ||
import { formatDate } from "@/lib/utils/formatDate"; | ||
import { Article } from "@/types/article"; | ||
|
||
interface AllBoardProps { | ||
initialArticles: Article[]; | ||
} | ||
|
||
function AllBoard({ initialArticles }: AllBoardProps) { | ||
const [orderBy, setOrderBy] = useState<string>("recent"); | ||
const [articles, setArticles] = useState<Article[]>(initialArticles); | ||
|
||
const router = useRouter(); | ||
const keyword = (router.query.q as string) || ""; | ||
|
||
const [selectedOption, setSelectedOption] = useState({ | ||
value: "recent", | ||
label: "최신순", | ||
}); | ||
const sortOptions = [ | ||
{ value: "recent", label: "최신순" }, | ||
{ value: "like", label: "인기순" }, | ||
]; | ||
|
||
const handleSortSelection = (sortOption: string) => { | ||
const selected = sortOptions.find( | ||
(option) => option.value === sortOption | ||
) || { value: "recent", label: "최신순" }; | ||
setSelectedOption(selected); | ||
setOrderBy(sortOption); | ||
}; | ||
|
||
function handleSearch(searchKeyword: string) { | ||
const query = { ...router.query }; | ||
if (searchKeyword.trim()) { | ||
query.q = searchKeyword; | ||
} else { | ||
delete query.q; | ||
} | ||
router.replace({ | ||
pathname: router.pathname, | ||
query, | ||
}); | ||
} | ||
|
||
useEffect(() => { | ||
const fetchArticles = async () => { | ||
let url = `/articles?orderBy=${orderBy}`; | ||
if (keyword.trim()) { | ||
url += `&keyword=${encodeURIComponent(keyword)}`; | ||
} | ||
const response = await axios.get(url); | ||
setArticles(response.data.list); | ||
}; | ||
|
||
fetchArticles(); | ||
}, [orderBy, keyword]); | ||
|
||
return ( | ||
<section className={styles["all-board-wrap"]}> | ||
<div className={styles["section-header"]}> | ||
<h2 className={styles.title}>게시글</h2> | ||
<LinkButton href="#" size="sm" color="primary"> | ||
글쓰기 | ||
</LinkButton> | ||
</div> | ||
<div className={styles["input-search-wrap"]}> | ||
<SearchForm onSearch={handleSearch} /> | ||
<Dropdown | ||
onSortSelection={handleSortSelection} | ||
sortOptions={sortOptions} | ||
selectedOption={selectedOption} | ||
/> | ||
</div> | ||
<ul className={styles["board-list"]}> | ||
{articles.length | ||
? articles.map((article) => ( | ||
<li key={article.id}> | ||
<Link href={`/boards/${article.id}`}> | ||
<div className={styles["title-info"]}> | ||
<p className={styles.title}>{article.title}</p> | ||
<div className={styles["thumb-wrap"]}> | ||
<Image | ||
fill | ||
src={article.image || "/img/sample.png"} | ||
alt={`${article.id}번 게시글 이미지`} | ||
/> | ||
</div> | ||
</div> | ||
<div className={styles["writer-info"]}> | ||
<div className={styles["profile-info"]}> | ||
<div className={styles["profile-wrap"]}> | ||
<Image | ||
src="/img/profile.png" | ||
alt="프로필 이미지" | ||
fill | ||
/> | ||
</div> | ||
<div className={styles["nick-name"]}> | ||
{article.writer.nickname} | ||
</div> | ||
</div> | ||
<div className={styles.date}> | ||
{formatDate(article.createdAt)} | ||
</div> | ||
<div className={styles.favorite}> | ||
<Icon type="heart" size="md" /> | ||
<span className={styles.num}>{article.likeCount}</span> | ||
</div> | ||
</div> | ||
</Link> | ||
</li> | ||
)) | ||
: keyword && ( | ||
<div className={styles["search-none"]}> | ||
<div className={styles["img-wrap"]}> | ||
<Image | ||
src="/img/Img_reply_empty.png" | ||
alt="검색 결과 없음" | ||
fill | ||
/> | ||
</div> | ||
<p> | ||
<b>"{keyword}"</b>로 검색된 결과가 없어요. | ||
</p> | ||
</div> | ||
)} | ||
</ul> | ||
</section> | ||
); | ||
} | ||
|
||
export default AllBoard; |
Oops, something went wrong.