Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,50 @@ export async function getProductComments({
throw error;
}
}

// Article에 대한 API
export async function getArticleDetail(articleId: number) {
if (!articleId) {
throw new Error("Invalid article ID");
}

try {
const response = await fetch(
`https://panda-market-api.vercel.app/articles/${articleId}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("실패:", error);
throw error;
}
}

export async function getArticleComments({
articleId,
limit = 10,
}: {
articleId: number;
limit?: number;
}) {
if (!articleId) {
throw new Error("Invalid article ID");
}

try {
const response = await fetch(
`https://panda-market-api.vercel.app/articles/${articleId}/comments?limit=${limit}`
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const body = await response.json();
return body;
} catch (error) {
console.error("실패:", error);
throw error;
}
}
83 changes: 83 additions & 0 deletions components/board/AllArticle.tsx
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) => {
Copy link
Collaborator

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 로도 통일할 수 있을 것 같습니다!

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) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
1 change: 1 addition & 0 deletions components/board/ArticleComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

30 changes: 30 additions & 0 deletions components/board/ArticleContent.tsx
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;
20 changes: 20 additions & 0 deletions components/board/ArticleInfo.tsx
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;
38 changes: 38 additions & 0 deletions components/board/ArticleItem.tsx
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;
1 change: 1 addition & 0 deletions components/board/ArticlePageComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

120 changes: 120 additions & 0 deletions components/board/BestArticle.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 파일 당 하나의 컴포넌트만 존재하게 수정해주세용~
그리고 custom hook 도 별도의 파일로 분리해주시는게 좋습니다!

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;
Empty file removed components/boards/AllArticle.tsx
Empty file.
Empty file removed components/boards/BestArticle.tsx
Empty file.
20 changes: 10 additions & 10 deletions components/layout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@ import Link from "next/link";
import Image from "next/image";
import Logo from "../../public/images/logo.svg";
import Profile from "../../public/images/profile.svg";
import "../../styles/Header.module.css";
import styles from "../../styles/Header.module.css"; // CSS Modules import

const Header = () => {
return (
<header className="header">
<div className="headerSection">
<Link href="/" className="homeLogo">
<Image src={Logo} alt="판다마켓 로고" className="logoImg" />
<header className={styles.header}>
<div className={styles.headerSection}>
<Link href="/" className={styles.homeLogo}>
<Image src={Logo} alt="판다마켓 로고" className={styles.logoImg} />
</Link>

<nav>
<div className="listSection">
<Link href="/community" className="communityLink">
<div className={styles.listSection}>
<Link href="/CommunityPage" className={styles.communityLink}>
자유게시판
</Link>
<Link href="/items" className="itemLink">
<Link href="/MarketPage" className={styles.itemLink}>
중고마켓
</Link>
</div>
</nav>
</div>

<Link href="/login" className="progileSection">
<Image src={Profile} alt="프로필" className="profileImg" />
<Link href="/LoginPage" className={styles.profileSection}>
<Image src={Profile} alt="프로필" className={styles.profileImg} />
</Link>
</header>
);
Expand Down
Loading
Loading