Skip to content

Commit 4cf0f38

Browse files
authored
Merge pull request #295 from Woolegend/Next-우재현-sprint9
[우재현] Sprint9
2 parents 8d163dd + 7304d26 commit 4cf0f38

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1249
-473
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NEXT_PUBLIC_API_BASE_URL = "https://panda-market-api.vercel.app"

api/article.api.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ArticleList } from "@/types/Article.type";
2+
import axios from "./axios";
3+
4+
interface GetArticleListParams {
5+
page?: number;
6+
pageSize?: number;
7+
orderBy?: OrderBy;
8+
keyword?: string | undefined;
9+
}
10+
11+
type OrderBy = "recent" | "like";
12+
13+
async function getArticleList({
14+
page = 1,
15+
pageSize = 10,
16+
orderBy = "recent",
17+
keyword,
18+
}: GetArticleListParams = {}): Promise<ArticleList> {
19+
const response = await axios.get("/articles", {
20+
params: {
21+
page,
22+
pageSize,
23+
orderBy,
24+
keyword,
25+
},
26+
});
27+
return response.data;
28+
}
29+
30+
export { getArticleList };
31+
export type { GetArticleListParams, OrderBy };

api/axios.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import axios from "axios";
2+
3+
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL;
4+
5+
const instance = axios.create({
6+
baseURL: BASE_URL,
7+
});
8+
9+
export default instance;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
.Board {
2+
width: 100%;
3+
}
4+
5+
.BoardHeader {
6+
display: flex;
7+
margin-bottom: 24px;
8+
}
9+
10+
.BoardTitle {
11+
flex-grow: 1;
12+
font-size: 20px;
13+
line-height: 32px;
14+
color: var(--gray-800);
15+
}
16+
17+
.PostItemList {
18+
display: flex;
19+
gap: 24px;
20+
}
21+
22+
.Item {
23+
flex-grow: 1;
24+
padding: 0 24px 16px;
25+
background-color: var(--gray-100);
26+
}
27+
28+
.Item .badge {
29+
display: flex;
30+
justify-content: center;
31+
align-items: center;
32+
gap: 10px;
33+
width: fit-content;
34+
padding: 2px 24px;
35+
border-bottom-left-radius: 16px;
36+
border-bottom-right-radius: 16px;
37+
margin-bottom: 16px;
38+
background-color: var(--blue);
39+
color: #ffffff;
40+
font-weight: 600;
41+
}
42+
43+
.Item .medal {
44+
width: 16px;
45+
height: 16px;
46+
position: relative;
47+
}
48+
49+
.Item .main {
50+
display: flex;
51+
gap: 8px;
52+
margin-bottom: 16px;
53+
}
54+
55+
.Item .title {
56+
flex-grow: 1;
57+
font-size: 20px;
58+
font-weight: 600;
59+
line-height: 32px;
60+
color: var(--gray-800);
61+
}
62+
63+
.Item .preview {
64+
position: relative;
65+
width: 72px;
66+
height: 72px;
67+
border: 1px solid var(--gray-100);
68+
border-radius: 8px;
69+
}
70+
71+
.Item .util {
72+
display: flex;
73+
align-items: center;
74+
gap: 8px;
75+
76+
font-size: 14px;
77+
line-height: 24px;
78+
color: var(--gray-500);
79+
}
80+
81+
.Item .likeCount {
82+
flex-grow: 1;
83+
display: flex;
84+
align-items: center;
85+
gap: 4px;
86+
}
87+
88+
.Item .likeCount .heart {
89+
width: 16px;
90+
height: 16px;
91+
position: relative;
92+
}

components/BestPostBoard.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Article, ArticleList } from "@/types/Article.type";
2+
import styles from "./BestPostBoard.module.css";
3+
import Image from "next/image";
4+
import formatDate from "../lib/formatDate";
5+
import { useDeviceType } from "@/contexts/DeviceTypeContext";
6+
import { useState } from "react";
7+
8+
const PAGE_SIZE = {
9+
desktop: 3,
10+
tablet: 2,
11+
mobile: 1,
12+
};
13+
14+
const IMAGE_PLACEHOLDER = "/images/landscape-placeholder.svg";
15+
16+
export default function BestPostBoard({ articles }: { articles: ArticleList }) {
17+
const deviceType = useDeviceType();
18+
19+
return (
20+
<div className={styles.Board}>
21+
<header className={styles.BoardHeader}>
22+
<h2 className={styles.BoardTitle}>베스트 게시글</h2>
23+
</header>
24+
<div className={styles.PostItemList}>
25+
{articles.list.map((article, i) => {
26+
if (i < PAGE_SIZE[deviceType]) {
27+
return <PostItem key={article.id} article={article} />;
28+
}
29+
})}
30+
</div>
31+
</div>
32+
);
33+
}
34+
35+
function PostItem({ article }: { article: Article }) {
36+
const [imgSrc, setImgSrc] = useState(article.image || IMAGE_PLACEHOLDER);
37+
38+
const createdAt = formatDate(article.createdAt);
39+
40+
return (
41+
<div className={styles.Item}>
42+
<div className={styles.badge}>
43+
<div className={styles.medal}>
44+
<Image fill src="/images/ic_medal.svg" alt="베스트" />
45+
</div>
46+
<span>Best</span>
47+
</div>
48+
<div className={styles.main}>
49+
<h3 className={styles.title}>{article.title}</h3>
50+
<div className={styles.preview}>
51+
<Image
52+
fill
53+
src={article.image}
54+
alt={article.title}
55+
style={{
56+
objectFit: "cover",
57+
}}
58+
onError={() => setImgSrc(IMAGE_PLACEHOLDER)}
59+
/>
60+
</div>
61+
</div>
62+
<div className={styles.util}>
63+
<span>{article.writer.nickname}</span>
64+
<div className={styles.likeCount}>
65+
<div className={styles.heart}>
66+
<Image fill src="/images/ic_heart.svg" alt="베스트" />
67+
</div>
68+
<span>{article.likeCount < 10000 ? article.likeCount : "9999+"}</span>
69+
</div>
70+
<span>{createdAt}</span>
71+
</div>
72+
</div>
73+
);
74+
}

components/ImageSafe.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import getConfig from "next/config";
2+
import Image from "next/image";
3+
import { useState, useEffect } from "react";
4+
5+
const IMAGE_PLACEHOLDER = "/images/landscape-placeholder.svg";
6+
7+
export default function ImageSafe({ src, alt }: { src: string; alt: string }) {
8+
const [imageSrc, setImageSrc] = useState<string | null>(null);
9+
10+
useEffect(() => {
11+
const checkImageConfig = async () => {
12+
try {
13+
const res = await fetch(
14+
"/api/check-image?url=" + encodeURIComponent(src)
15+
);
16+
console.log(res);
17+
if (res.ok) {
18+
setImageSrc(src);
19+
} else {
20+
console.error("Image source not configured in next.config.js");
21+
console.log(res);
22+
setImageSrc(IMAGE_PLACEHOLDER);
23+
}
24+
} catch (error) {
25+
console.error("Error checking image configuration:", error);
26+
setImageSrc(IMAGE_PLACEHOLDER);
27+
}
28+
};
29+
30+
checkImageConfig();
31+
}, [src]);
32+
33+
if (!imageSrc) {
34+
return null; // 또는 로딩 표시기
35+
}
36+
37+
return (
38+
<Image
39+
fill
40+
src={imageSrc}
41+
alt={alt}
42+
style={{
43+
objectFit: "cover",
44+
}}
45+
/>
46+
);
47+
}

components/Navigation.module.css

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
.Navigation {
2+
width: 100%;
3+
background-color: #ffffff;
4+
border-bottom: 1px solid #dfdfdf;
5+
margin-bottom: 24px;
6+
}
7+
8+
.wrap {
9+
display: flex;
10+
justify-content: space-between;
11+
align-items: center;
12+
13+
max-width: 1200px;
14+
padding: 14px 0;
15+
margin: 0 auto;
16+
}
17+
18+
.logo {
19+
display: flex;
20+
justify-content: space-between;
21+
align-items: center;
22+
gap: 10px;
23+
}
24+
25+
.logo .icon {
26+
width: 40px;
27+
}
28+
29+
.logo .text {
30+
font-size: 26px;
31+
font-weight: 700;
32+
color: var(--blue);
33+
text-decoration: none;
34+
font-family: "ROKAF Sans";
35+
}
36+
37+
.tabList {
38+
display: flex;
39+
justify-content: flex-start;
40+
flex-grow: 1;
41+
margin: 0 32px;
42+
}
43+
44+
.tabList li {
45+
flex: 0 0 108px;
46+
text-align: center;
47+
}
48+
49+
.tabList .tab {
50+
font-size: 18px;
51+
font-weight: 700;
52+
line-height: 26px;
53+
color: var(--gray-600);
54+
}
55+
56+
.tabList .tab.current {
57+
color: var(--blue);
58+
}
59+
60+
@media screen and (min-width: 1600px) {
61+
.Navigation {
62+
padding: 0 200px;
63+
}
64+
.wrap {
65+
max-width: 100%;
66+
}
67+
}
68+
69+
@media screen and (max-width: 1199px) {
70+
.wrap {
71+
padding: 14px 24px;
72+
}
73+
}
74+
75+
@media screen and (max-width: 767px) {
76+
.logo .icon {
77+
display: none;
78+
}
79+
80+
.logo .text {
81+
font-size: 20px;
82+
line-height: 26px;
83+
}
84+
85+
.tabList {
86+
margin: 0 12px;
87+
}
88+
89+
.tabList li {
90+
flex: 0 0 70px;
91+
}
92+
93+
.tabList .tab {
94+
font-size: 16px;
95+
}
96+
}

0 commit comments

Comments
 (0)