Skip to content

Commit 2a28fdf

Browse files
authored
Merge pull request #310 from Sookyeong02/Next-장수경-sprint10
[장수경] sprint10
2 parents 89f5ba4 + 7fb5e33 commit 2a28fdf

35 files changed

+3959
-421
lines changed

api/api.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,50 @@ export async function getProductComments({
7171
throw error;
7272
}
7373
}
74+
75+
// Article에 대한 API
76+
export async function getArticleDetail(articleId: number) {
77+
if (!articleId) {
78+
throw new Error("Invalid article ID");
79+
}
80+
81+
try {
82+
const response = await fetch(
83+
`https://panda-market-api.vercel.app/articles/${articleId}`
84+
);
85+
if (!response.ok) {
86+
throw new Error(`HTTP error: ${response.status}`);
87+
}
88+
const body = await response.json();
89+
return body;
90+
} catch (error) {
91+
console.error("실패:", error);
92+
throw error;
93+
}
94+
}
95+
96+
export async function getArticleComments({
97+
articleId,
98+
limit = 10,
99+
}: {
100+
articleId: number;
101+
limit?: number;
102+
}) {
103+
if (!articleId) {
104+
throw new Error("Invalid article ID");
105+
}
106+
107+
try {
108+
const response = await fetch(
109+
`https://panda-market-api.vercel.app/articles/${articleId}/comments?limit=${limit}`
110+
);
111+
if (!response.ok) {
112+
throw new Error(`HTTP error: ${response.status}`);
113+
}
114+
const body = await response.json();
115+
return body;
116+
} catch (error) {
117+
console.error("실패:", error);
118+
throw error;
119+
}
120+
}

components/board/AllArticle.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Article, ArticleSortOption } from "@/types/Types";
2+
import { useRouter } from "next/router";
3+
import { useEffect, useState } from "react";
4+
import ArticleItem from "./ArticleItem";
5+
import Search from "@/components/ui/Search";
6+
import Dropdown from "@/components/ui/Dropdown";
7+
8+
interface AllArticlesProps {
9+
initialArticles: Article[];
10+
}
11+
12+
const AllArticle = ({ initialArticles }: AllArticlesProps) => {
13+
const [orderBy, setOrderBy] = useState<ArticleSortOption>("recent");
14+
const [articles, setArticles] = useState(initialArticles);
15+
16+
const router = useRouter();
17+
const keyword = (router.query.q as string) || "";
18+
19+
const handleSort = (sortOption: ArticleSortOption) => {
20+
setOrderBy(sortOption);
21+
};
22+
23+
//
24+
const handleSearch = (searchKeyword: string) => {
25+
const query = { ...router.query };
26+
if (searchKeyword.trim()) {
27+
query.q = searchKeyword;
28+
} else {
29+
delete query.q;
30+
}
31+
router.replace({
32+
pathname: router.pathname,
33+
query,
34+
});
35+
};
36+
37+
useEffect(() => {
38+
const fetchArticles = async () => {
39+
let url = `https://panda-market-api.vercel.app/articles?orderBy=${orderBy}`;
40+
if (keyword.trim()) {
41+
url += `&keyword=${encodeURIComponent(keyword)}`;
42+
}
43+
const response = await fetch(url);
44+
const data = await response.json();
45+
setArticles(data.list);
46+
};
47+
48+
fetchArticles();
49+
}, [orderBy, keyword]);
50+
51+
return (
52+
<div>
53+
<div>
54+
<h2>게시글</h2>
55+
<button>글쓰기</button>
56+
</div>
57+
58+
<div>
59+
<Search onSearch={handleSearch} />
60+
<Dropdown
61+
onSortSelection={handleSort}
62+
sortOptions={[
63+
{ key: "recent", label: "최신순" },
64+
{ key: "like", label: "인기순" },
65+
]}
66+
/>
67+
</div>
68+
69+
{articles.length > 0
70+
? articles.map((article) => (
71+
<ArticleItem key={`article-${article.id}`} article={article} />
72+
))
73+
: // 키워드가 입력된 경우에만 결과가 없다는 메시지 표시
74+
keyword && (
75+
<div>
76+
<p>{`'${keyword}'로 검색된 결과가 없어요.`}</p>
77+
</div>
78+
)}
79+
</div>
80+
);
81+
};
82+
83+
export default AllArticle;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Article } from "@/types/Types";
2+
import Kebob from "@/public/images/ic_kebab.svg";
3+
import ArticleInfo from "./ArticleInfo";
4+
5+
interface ArticleContentProps {
6+
article: Article;
7+
}
8+
9+
const ArticleContent = ({ article }: ArticleContentProps) => {
10+
return (
11+
<div>
12+
<div>
13+
<h3>{article.title}</h3>
14+
15+
<button>
16+
<Kebob />
17+
</button>
18+
19+
<div>
20+
<ArticleInfo article={article} />
21+
{/* 하트 */}
22+
</div>
23+
</div>
24+
25+
<div>{article.content}</div>
26+
</div>
27+
);
28+
};
29+
30+
export default ArticleContent;

components/board/ArticleInfo.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Profile from "@/public/images/profile.svg";
2+
import { Article } from "@/types/Types";
3+
import { formatDate } from "date-fns";
4+
5+
interface ArticleInfoProps {
6+
article: Article;
7+
}
8+
9+
const ArticleInfo = ({ article }: ArticleInfoProps) => {
10+
const formetDate = formatDate(article.createdAt, "yyyy. MM. dd");
11+
12+
return (
13+
<div>
14+
<Profile width={24} heigt={24} />
15+
{article.writer.nickname} {formetDate}
16+
</div>
17+
);
18+
};
19+
20+
export default ArticleInfo;

components/board/ArticleItem.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Article } from "@/types/Types";
2+
import Image from "next/image";
3+
import Link from "next/link";
4+
import ArticleInfo from "./ArticleInfo";
5+
6+
interface ArticleItemProps {
7+
article: Article;
8+
}
9+
10+
const ArticleItem = ({ article }: ArticleItemProps) => {
11+
return (
12+
<>
13+
<Link href={`/board/${article.id}`}>
14+
<div>
15+
<h3>{article.title}</h3>
16+
{article.image && (
17+
<div>
18+
<div>
19+
<Image
20+
fill
21+
src={article.image}
22+
alt={`${article.id}번 게시글 이미지`}
23+
style={{ objectFit: "contain" }}
24+
/>
25+
</div>
26+
</div>
27+
)}
28+
</div>
29+
30+
<div>
31+
<ArticleInfo article={article} />
32+
</div>
33+
</Link>
34+
</>
35+
);
36+
};
37+
38+
export default ArticleItem;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

components/board/BestArticle.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { useEffect, useState } from "react";
2+
import Image from "next/image";
3+
import Link from "next/link";
4+
import { format } from "date-fns";
5+
import MedalIcon from "@/public/images/ic_medal.svg";
6+
import { Article, ArticleList } from "@/types/Types";
7+
8+
const BestArticleCard = ({ article }: { article: Article }) => {
9+
const formatDate = format(article.createdAt, "yyyy. MM. dd");
10+
11+
return (
12+
<>
13+
<Link href={`/board/${article.id}`}>
14+
<div>
15+
<MedalIcon alt="베스트 게시글" />
16+
Best
17+
</div>
18+
19+
<div>
20+
<div>
21+
<h3>{article.title}</h3>
22+
{article.image && (
23+
<div>
24+
<Image
25+
fill
26+
src={article.image}
27+
alt={`${article.id}번 게시글 이미지`}
28+
style={{ objectFit: "contain" }}
29+
/>
30+
</div>
31+
)}
32+
</div>
33+
34+
<div>
35+
<div>{article.writer.nickname}</div>
36+
{/* 하트 */}
37+
</div>
38+
<div>{formatDate}</div>
39+
</div>
40+
</Link>
41+
</>
42+
);
43+
};
44+
45+
const getPageSize = (width: number): number => {
46+
if (width < 768) {
47+
// 모바일
48+
return 1;
49+
} else if (width < 1280) {
50+
// 태블릿
51+
return 2;
52+
} else {
53+
// PC
54+
return 3;
55+
}
56+
};
57+
58+
// 너비 추적
59+
const useViewport = () => {
60+
const [width, setWidth] = useState(0);
61+
62+
useEffect(() => {
63+
const handleWindowResize = () => setWidth(window.innerWidth);
64+
handleWindowResize();
65+
window.addEventListener("resize", handleWindowResize);
66+
return () => window.removeEventListener("resize", handleWindowResize);
67+
}, []);
68+
69+
return width;
70+
};
71+
72+
const BestArticle = () => {
73+
const [articles, setArticles] = useState<Article[]>([]);
74+
const [pageSize, setPageSize] = useState<number | null>(null);
75+
76+
const viewportWidth = useViewport();
77+
78+
useEffect(() => {
79+
if (viewportWidth === 0) return;
80+
81+
const newPageSize = getPageSize(viewportWidth);
82+
83+
if (newPageSize !== pageSize) {
84+
setPageSize(newPageSize);
85+
86+
const fetchArticles = async (size: number) => {
87+
try {
88+
const response = await fetch(
89+
`https://panda-market-api.vercel.app/articles?orderBy=like&pageSize=${size}`
90+
);
91+
const data: ArticleList = await response.json();
92+
setArticles(data.list);
93+
} catch (error) {
94+
console.error("실패:", error);
95+
}
96+
};
97+
98+
fetchArticles(newPageSize);
99+
}
100+
}, [viewportWidth, pageSize]);
101+
102+
return (
103+
<div>
104+
<div>
105+
<h2>베스트 게시글</h2>
106+
</div>
107+
108+
<div>
109+
{articles.map((article) => (
110+
<BestArticleCard
111+
key={`best-article-${article.id}`}
112+
article={article}
113+
/>
114+
))}
115+
</div>
116+
</div>
117+
);
118+
};
119+
120+
export default BestArticle;

components/boards/AllArticle.tsx

Whitespace-only changes.

components/boards/BestArticle.tsx

Whitespace-only changes.

0 commit comments

Comments
 (0)