-
Notifications
You must be signed in to change notification settings - Fork 79
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
[오영택] sprint 5,9 #704
base: main
Are you sure you want to change the base?
The head ref may contain hidden characters: "Next.js-\uC624\uC601\uD0DD-sprint11"
[오영택] sprint 5,9 #704
Changes from all commits
3ec39b7
15cdb38
0e9a86e
dc40da8
1a14863
518a2c8
e2429ac
02c3e00
beafdbc
89953f3
6e604fa
c9b7e3d
7c235ce
ebc3c6b
8018ef6
8a2ed47
12f448c
c4feec8
8c22e77
460e01a
29035f1
eb680bf
3c5d256
7888704
79003dd
ed8c0d8
7304135
8d9c477
6e384fc
e6d72ad
775bc4b
0bccfbd
a6ecbc6
03cf374
fe83896
1204eff
242a5ea
887f471
8e788b1
9ba33d2
eed23d2
6f61a9a
5e394fe
21ccd78
c671874
34bed25
a2e2184
21325e0
5bcacd2
37e3f55
d79dfcc
46280b8
6d90898
982fe4c
3b4268a
ca3a67b
1611e0b
7223aa9
bc4dec6
a1794e5
fecbded
2d71a6b
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,3 @@ | ||
{ | ||
"extends": ["next/core-web-vitals", "prettier"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"arrowParens": "always", | ||
"endOfLine": "lf", | ||
"printWidth": 80, | ||
"quoteProps": "as-needed", | ||
"semi": true, | ||
"singleQuote": true, | ||
"tabWidth": 2, | ||
"useTabs": false, | ||
"trailingComma": "es5" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { ProductListFetcherParams } from '@/types/productTypes'; | ||
|
||
export async function getProducts({ | ||
orderBy, | ||
pageSize, | ||
page = 1, | ||
}: ProductListFetcherParams) { | ||
const params = new URLSearchParams({ | ||
orderBy, | ||
pageSize: String(pageSize), | ||
page: String(page), | ||
}) | ||
|
||
try { | ||
const res = await fetch( | ||
`https://panda-market-api.vercel.app/products?${params}` | ||
); | ||
|
||
if (!res.ok) { | ||
throw new Error(`HTTP error: ${res.status}`) | ||
} | ||
const body = await res.json(); | ||
return body; | ||
} catch (error) { | ||
console.error('상품을 불러오는데 실패했습니다', error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function getProductDetail(productId: number) { | ||
if (!productId) { | ||
throw new Error("Invalid product ID"); | ||
} | ||
|
||
try { | ||
const res = await fetch( | ||
`https://panda-market-api.vercel.app/products/${productId}` | ||
); | ||
if (!res.ok) { | ||
throw new Error(`HTTP error: ${res.status}`); | ||
} | ||
const body = await res.json(); | ||
return body; | ||
} catch (error) { | ||
console.error('상품정보를 불러오는데 실패했습니다', error); | ||
throw error; | ||
}; | ||
} | ||
|
||
export async function getProductComments({ | ||
productId, | ||
limit = 10, | ||
}: { | ||
productId: number; | ||
limit?: number; | ||
}) { | ||
if (!productId) { | ||
throw new Error('Invalid product ID'); | ||
} | ||
|
||
const params = { | ||
limit: String(limit), | ||
}; | ||
|
||
try { | ||
const query = new URLSearchParams(params).toString(); | ||
const res = await fetch( | ||
`https://panda-market-api.vercel.app/products/${productId}/comments?${query}` | ||
); | ||
if (!res.ok) { | ||
throw new Error(`HTTP error: ${res.status}`); | ||
} | ||
const body = await res.json(); | ||
return body; | ||
} catch (error) { | ||
console.error('상품 댓글을 불러오는데 실패했습니다', error); | ||
throw error; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { Article, ArticleSortOption } from '@/types/articleTypes'; | ||
import Button from '../ui/Button'; | ||
import SearchBar from '../ui/SearchBar'; | ||
import { useEffect, useState } from 'react'; | ||
import { useRouter } from 'next/router'; | ||
import Empty from '../ui/Empty'; | ||
import ArticleItem from './ArticleItem'; | ||
import Dropdown from '../ui/Dropdown'; | ||
|
||
interface AllArticlesSectionProps { | ||
initialArticles: Article[]; | ||
} | ||
|
||
const AllArticlesSection: React.FC<AllArticlesSectionProps> = ({ | ||
initialArticles, | ||
}) => { | ||
const [articles, setArticles] = useState(initialArticles); | ||
const [orderBy, setOrderBy] = useState('recent'); | ||
|
||
const router = useRouter(); | ||
const keyword = (router.query.q as string) || ''; | ||
|
||
const handleSortSelection = (sortOption: ArticleSortOption) => { | ||
setOrderBy(sortOption); | ||
}; | ||
|
||
const handleInputKeyword = (inputKeyword: string) => { | ||
const query = { ...router.query }; | ||
if (inputKeyword.trim()) { | ||
query.q = inputKeyword; | ||
} 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 res = await fetch(url); | ||
const data = await res.json(); | ||
setArticles(data.list); | ||
}; | ||
|
||
fetchArticles(); | ||
}, [orderBy, keyword]); | ||
|
||
return ( | ||
<div className="flex flex-col gap-6 w-[1200px] m-auto"> | ||
<div className="flex justify-between"> | ||
<header>게시글</header> | ||
<Button>글쓰기</Button> | ||
</div> | ||
|
||
<div className="flex justify-between"> | ||
<SearchBar onSearch={handleInputKeyword} /> | ||
<Dropdown | ||
onSortSelection={handleSortSelection} | ||
sortOptions={[ | ||
{ key: 'recent', label: '최신순' }, | ||
{ key: 'like', label: '인기순' }, | ||
]} | ||
/> | ||
</div> | ||
|
||
{articles.length | ||
? articles.map((article) => ( | ||
<ArticleItem key={`article-${article.id}`} article={article} /> | ||
)) | ||
: keyword && <Empty text={`'${keyword}'로 검색된 결과가 없습니다.`} />} | ||
</div> | ||
); | ||
}; | ||
|
||
export default AllArticlesSection; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { Article } from '@/types/articleTypes'; | ||
import Image from 'next/image'; | ||
import Link from 'next/link'; | ||
import LikeCount from '../ui/LikeCount'; | ||
import { format } from 'date-fns'; | ||
|
||
// Todo : LikeCount gap 동적으로 입력하기 | ||
|
||
interface ArticleProps { | ||
article: Article; | ||
} | ||
|
||
const ArticleItem: React.FC<ArticleProps> = ({ article }) => { | ||
const dateString = format(article.createdAt, 'yyyy. MM. dd'); | ||
return ( | ||
<> | ||
<Link | ||
href={`/boards/${article.id}`} | ||
className="flex flex-col gap-6 h-[136px] border-b border-[#e5e7eb]" | ||
> | ||
<div className="flex flex-col gap-4 justify-between"> | ||
<div className="flex justify-between gap-8 h-[72px]"> | ||
<h2 className="text-xl font-semibold text-[#1f2937]"> | ||
{article.title} | ||
</h2> | ||
{article.image && ( | ||
<div className="w-[72px] h-[72px] rounded-md border border-[#e5e7eb] relative"> | ||
<Image | ||
fill | ||
src={article.image} | ||
alt={`${article.id}번 게시글 이미지`} | ||
className="object-contain w-12 h-11 absolute" | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
<div className="flex gap-2 justify-between"> | ||
<div className="flex gap-2 items-center"> | ||
<span>아바타</span> | ||
<div className="flex gap-2 h-[17px] text-[#4b5563] text-sm"> | ||
{article.writer.nickname} | ||
<span className="text-[#9ca3af] text-sm">{dateString}</span> | ||
</div> | ||
</div> | ||
<LikeCount | ||
count={article.likeCount} | ||
width={24} | ||
height={24} | ||
gap={8} | ||
leading={24} | ||
fontSize={16} | ||
/> | ||
</div> | ||
</div> | ||
</Link> | ||
</> | ||
); | ||
}; | ||
|
||
export default ArticleItem; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import Link from 'next/link'; | ||
import MedalIcon from '@/public/images/icons/ic_medal.svg'; | ||
import Image from 'next/image'; | ||
import { Article } from '@/types/articleTypes'; | ||
import { format } from 'date-fns'; | ||
import LikeCount from '../ui/LikeCount'; | ||
|
||
const BestArticleCard = ({ article }: { article: Article }) => { | ||
const dateString = format(article.createdAt, 'yyyy. MM. dd'); | ||
return ( | ||
<Link | ||
href={`/boards/${article.id}`} | ||
className="w-[384px] h-[169px] bg-[#F9FAFB] rounded-lg" | ||
> | ||
<div className="flex flex-col gap-3 w-[336px] h-[153px] m-auto"> | ||
<div className="flex gap-1 items-center w-[102px] h-[30px] bg-[#3692FF] justify-center text-white rounded-b-3xl text-base font-semibold"> | ||
<MedalIcon width={16} height={16} /> | ||
Best | ||
</div> | ||
|
||
<div className="flex w-[336px] h-[72px] gap-3"> | ||
<h2 className="w-[256px] font-semibold text-xl text-[#1f2937]"> | ||
{article.title} | ||
</h2> | ||
{article.image && ( | ||
<div className="w-[72px] h-[72px] rounded-md border border-[#e5e7eb] relative"> | ||
<Image | ||
fill | ||
src={article.image} | ||
alt={`${article.id}번 게시글 이미지`} | ||
className="object-contain w-12 h-11 absolute" | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
|
||
<div className="flex justify-between"> | ||
<div className="flex gap-2 items-center text-[#4b5563] font-normal h-4 text-sm"> | ||
{article.writer.nickname} | ||
<LikeCount count={article.likeCount} fontSize={14} /> | ||
</div> | ||
<span>{dateString}</span> | ||
</div> | ||
</div> | ||
</Link> | ||
); | ||
}; | ||
|
||
export default BestArticleCard; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { useEffect, useState } from 'react'; | ||
import BestArticleCard from './BestArticleCard'; | ||
import { Article, ArticleListData } from '@/types/articleTypes'; | ||
import useViewport from '@/hooks/useViewport'; | ||
|
||
const getPageSize = (width: number): number => { | ||
if (width < 768) { | ||
return 1; // Mobile | ||
} else if (width < 1280) { | ||
return 2; // Tablet | ||
} else { | ||
return 3; // Desktop | ||
} | ||
}; | ||
|
||
const BestArticlesSection = () => { | ||
const [bestArticles, setBestArticles] = useState<Article[]>([]); | ||
const [pageSize, setPageSize] = useState<number | null>(null); | ||
|
||
const viewportWidth = useViewport(); | ||
|
||
useEffect(() => { | ||
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.
그래서 내부에서 추가적으로 호출해줘야되는 함수들이 늘어날 수 있기 때문에, 함수의 구현부( 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. 전반적으로 기본적인 기능 구현사항이라 크게 커멘트를 남길 필요는 없을 듯 합니다. 그리고 현재 각 컴포넌트에서 api 호출 후, 응답 데이터를 state로 관리하여 ui 렌더링 시키는 구조가 많은 데, type UseApiArgs {
// .....
}
// useApi 내부에서 실제 api 콜을 하는 함수
type Fetcher {
// ....
}
const useApi = <T>(fetcher: Fetcher) => {
const [data, setData] = useState<T>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const trigger = async () => {
try{
setIsLoading(true);
const result = await fetcher();
setIsLoading(false;
setData(result)
}
catch(err) {
setError(err);
}
}
return {
data, isLoading, error, trigger
}
}
// ./page or ./component
const fetcher = () => {
return fetch("", {});
}
const DummyComponent = () => {
const {data, isLoading, error, trigger} = useApi(fetcher);
return (
<>
{isLoading && <Loading />}
{!isLoading && <div>{JSON.stringify(data)}</div>
</>
)
} |
||
if (viewportWidth === 0) return; | ||
|
||
const newPageSize = getPageSize(viewportWidth); | ||
|
||
if (newPageSize !== pageSize) { | ||
setPageSize(newPageSize); | ||
|
||
const fetchBestArticles = async (size: number) => { | ||
try { | ||
const res = await fetch( | ||
`https://panda-market-api.vercel.app/articles?orderBy=like&pageSize=${size}` | ||
); | ||
const data: ArticleListData = await res.json(); | ||
setBestArticles(data.list); | ||
} catch (error) { | ||
console.error('베스트 게시물을 받아오는 데 실패했습니다.', error); | ||
} | ||
}; | ||
|
||
fetchBestArticles(newPageSize); | ||
} | ||
}, [viewportWidth, pageSize]); | ||
|
||
return ( | ||
<div className="flex flex-col m-auto gap-6 w-[1200px]"> | ||
<header className="font-bold text-xl text-[#111827]"> | ||
베스트 게시글 | ||
</header> | ||
|
||
<div className="flex justify-between"> | ||
{bestArticles.map((article) => ( | ||
<BestArticleCard | ||
key={`best-article-${article.id}`} | ||
article={article} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default BestArticlesSection; |
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.
공통 api 함수가 하나 있으면 좋겠네요.
아래 baseApi 함수를 확인해주시면 됩니다.