-
Notifications
You must be signed in to change notification settings - Fork 40
[장수경] sprint10 #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The head ref may contain hidden characters: "Next-\uC7A5\uC218\uACBD-sprint10"
[장수경] sprint10 #310
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| import { Article, ArticleSortOption } from "@/types/Types"; | ||
| import { useRouter } from "next/router"; | ||
| import { useEffect, useState } from "react"; | ||
| import ArticleItem from "./ArticleItem"; | ||
| import Search from "@/components/ui/Search"; | ||
| import Dropdown from "@/components/ui/Dropdown"; | ||
|
|
||
| interface AllArticlesProps { | ||
| initialArticles: Article[]; | ||
| } | ||
|
|
||
| const AllArticle = ({ initialArticles }: AllArticlesProps) => { | ||
| const [orderBy, setOrderBy] = useState<ArticleSortOption>("recent"); | ||
| const [articles, setArticles] = useState(initialArticles); | ||
|
|
||
| const router = useRouter(); | ||
| const keyword = (router.query.q as string) || ""; | ||
|
|
||
| const handleSort = (sortOption: ArticleSortOption) => { | ||
| setOrderBy(sortOption); | ||
| }; | ||
|
|
||
| // | ||
| const handleSearch = (searchKeyword: string) => { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. searchParam 을 이용한 검색이 페이지 기반 라우트에서도 잘 동작하는군용? |
||
| 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 = `https://panda-market-api.vercel.app/articles?orderBy=${orderBy}`; | ||
| if (keyword.trim()) { | ||
| url += `&keyword=${encodeURIComponent(keyword)}`; | ||
| } | ||
| const response = await fetch(url); | ||
| const data = await response.json(); | ||
| setArticles(data.list); | ||
| }; | ||
|
|
||
| fetchArticles(); | ||
| }, [orderBy, keyword]); | ||
|
|
||
| return ( | ||
| <div> | ||
| <div> | ||
| <h2>게시글</h2> | ||
| <button>글쓰기</button> | ||
| </div> | ||
|
|
||
| <div> | ||
| <Search onSearch={handleSearch} /> | ||
| <Dropdown | ||
| onSortSelection={handleSort} | ||
| sortOptions={[ | ||
| { key: "recent", label: "최신순" }, | ||
| { key: "like", label: "인기순" }, | ||
| ]} | ||
| /> | ||
| </div> | ||
|
|
||
| {articles.length > 0 | ||
| ? articles.map((article) => ( | ||
| <ArticleItem key={`article-${article.id}`} article={article} /> | ||
| )) | ||
| : // 키워드가 입력된 경우에만 결과가 없다는 메시지 표시 | ||
| keyword && ( | ||
| <div> | ||
| <p>{`'${keyword}'로 검색된 결과가 없어요.`}</p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default AllArticle; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { Article } from "@/types/Types"; | ||
| import Kebob from "@/public/images/ic_kebab.svg"; | ||
| import ArticleInfo from "./ArticleInfo"; | ||
|
|
||
| interface ArticleContentProps { | ||
| article: Article; | ||
| } | ||
|
|
||
| const ArticleContent = ({ article }: ArticleContentProps) => { | ||
| return ( | ||
| <div> | ||
| <div> | ||
| <h3>{article.title}</h3> | ||
|
|
||
| <button> | ||
| <Kebob /> | ||
| </button> | ||
|
|
||
| <div> | ||
| <ArticleInfo article={article} /> | ||
| {/* 하트 */} | ||
| </div> | ||
| </div> | ||
|
|
||
| <div>{article.content}</div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ArticleContent; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import Profile from "@/public/images/profile.svg"; | ||
| import { Article } from "@/types/Types"; | ||
| import { formatDate } from "date-fns"; | ||
|
|
||
| interface ArticleInfoProps { | ||
| article: Article; | ||
| } | ||
|
|
||
| const ArticleInfo = ({ article }: ArticleInfoProps) => { | ||
| const formetDate = formatDate(article.createdAt, "yyyy. MM. dd"); | ||
|
|
||
| return ( | ||
| <div> | ||
| <Profile width={24} heigt={24} /> | ||
| {article.writer.nickname} {formetDate} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default ArticleInfo; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import { Article } from "@/types/Types"; | ||
| import Image from "next/image"; | ||
| import Link from "next/link"; | ||
| import ArticleInfo from "./ArticleInfo"; | ||
|
|
||
| interface ArticleItemProps { | ||
| article: Article; | ||
| } | ||
|
|
||
| const ArticleItem = ({ article }: ArticleItemProps) => { | ||
| return ( | ||
| <> | ||
| <Link href={`/board/${article.id}`}> | ||
| <div> | ||
| <h3>{article.title}</h3> | ||
| {article.image && ( | ||
| <div> | ||
| <div> | ||
| <Image | ||
| fill | ||
| src={article.image} | ||
| alt={`${article.id}번 게시글 이미지`} | ||
| style={{ objectFit: "contain" }} | ||
| /> | ||
| </div> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <ArticleInfo article={article} /> | ||
| </div> | ||
| </Link> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default ArticleItem; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 한 파일 당 하나의 컴포넌트만 존재하게 수정해주세용~ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { useEffect, useState } from "react"; | ||
| import Image from "next/image"; | ||
| import Link from "next/link"; | ||
| import { format } from "date-fns"; | ||
| import MedalIcon from "@/public/images/ic_medal.svg"; | ||
| import { Article, ArticleList } from "@/types/Types"; | ||
|
|
||
| const BestArticleCard = ({ article }: { article: Article }) => { | ||
| const formatDate = format(article.createdAt, "yyyy. MM. dd"); | ||
|
|
||
| return ( | ||
| <> | ||
| <Link href={`/board/${article.id}`}> | ||
| <div> | ||
| <MedalIcon alt="베스트 게시글" /> | ||
| Best | ||
| </div> | ||
|
|
||
| <div> | ||
| <div> | ||
| <h3>{article.title}</h3> | ||
| {article.image && ( | ||
| <div> | ||
| <Image | ||
| fill | ||
| src={article.image} | ||
| alt={`${article.id}번 게시글 이미지`} | ||
| style={{ objectFit: "contain" }} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <div>{article.writer.nickname}</div> | ||
| {/* 하트 */} | ||
| </div> | ||
| <div>{formatDate}</div> | ||
| </div> | ||
| </Link> | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| const getPageSize = (width: number): number => { | ||
| if (width < 768) { | ||
| // 모바일 | ||
| return 1; | ||
| } else if (width < 1280) { | ||
| // 태블릿 | ||
| return 2; | ||
| } else { | ||
| // PC | ||
| return 3; | ||
| } | ||
| }; | ||
|
|
||
| // 너비 추적 | ||
| const useViewport = () => { | ||
| const [width, setWidth] = useState(0); | ||
|
|
||
| useEffect(() => { | ||
| const handleWindowResize = () => setWidth(window.innerWidth); | ||
| handleWindowResize(); | ||
| window.addEventListener("resize", handleWindowResize); | ||
| return () => window.removeEventListener("resize", handleWindowResize); | ||
| }, []); | ||
|
|
||
| return width; | ||
| }; | ||
|
|
||
| const BestArticle = () => { | ||
| const [articles, setArticles] = useState<Article[]>([]); | ||
| const [pageSize, setPageSize] = useState<number | null>(null); | ||
|
|
||
| const viewportWidth = useViewport(); | ||
|
|
||
| useEffect(() => { | ||
| if (viewportWidth === 0) return; | ||
|
|
||
| const newPageSize = getPageSize(viewportWidth); | ||
|
|
||
| if (newPageSize !== pageSize) { | ||
| setPageSize(newPageSize); | ||
|
|
||
| const fetchArticles = async (size: number) => { | ||
| try { | ||
| const response = await fetch( | ||
| `https://panda-market-api.vercel.app/articles?orderBy=like&pageSize=${size}` | ||
| ); | ||
| const data: ArticleList = await response.json(); | ||
| setArticles(data.list); | ||
| } catch (error) { | ||
| console.error("실패:", error); | ||
| } | ||
| }; | ||
|
|
||
| fetchArticles(newPageSize); | ||
| } | ||
| }, [viewportWidth, pageSize]); | ||
|
|
||
| return ( | ||
| <div> | ||
| <div> | ||
| <h2>베스트 게시글</h2> | ||
| </div> | ||
|
|
||
| <div> | ||
| {articles.map((article) => ( | ||
| <BestArticleCard | ||
| key={`best-article-${article.id}`} | ||
| article={article} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default BestArticle; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
props 로 받아오는 initialArticles과 useState로 관리하는 articles 가 동일한 데이터로 보이는데요, 꼭 두 개의 state로 관리할 필요가 있을까요? 하나의 state 로도 통일할 수 있을 것 같습니다!