-
Notifications
You must be signed in to change notification settings - Fork 21
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
[김주동] sprint10 #124
The head ref may contain hidden characters: "Next-\uAE40\uC8FC\uB3D9-sprint10"
[김주동] sprint10 #124
Changes from all commits
93a0e93
17be37c
7df9c2b
66f3ba6
3c68d4e
d48b01a
675f9b0
20fd6da
6ebe241
e111b83
4bfa053
3164f59
2fdcae3
10b590a
2032cc0
1437689
4f9c678
0b131e7
0c5bb5c
1fe9f12
a5ac8a6
85b532b
b7bd99e
bee7f20
a9f37e5
41f70e7
e4208c1
3312289
11218fa
531f8ea
35eb2cf
06d2176
c708ff7
fe773d5
fb2af31
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,50 @@ | ||
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("Failed to fetch article detail:", error); | ||
throw error; | ||
} | ||
} | ||
|
||
export async function getArticleComments({ | ||
articleId, | ||
limit = 10, | ||
}: { | ||
articleId: number; | ||
limit?: number; | ||
}) { | ||
if (!articleId) { | ||
throw new Error("Invalid article ID"); | ||
} | ||
|
||
const params = { | ||
limit: String(limit), | ||
}; | ||
|
||
try { | ||
const query = new URLSearchParams(params).toString(); | ||
const response = await fetch( | ||
`https://panda-market-api.vercel.app/articles/${articleId}/comments?${query}` | ||
); | ||
if (!response.ok) { | ||
throw new Error(`HTTP error: ${response.status}`); | ||
} | ||
const body = await response.json(); | ||
return body; | ||
} catch (error) { | ||
console.error("Failed to fetch article comments:", error); | ||
throw error; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
GET https://panda-market-api.vercel.app/articles | ||
|
||
### | ||
|
||
GET https://panda-market-api.vercel.app/articles?orderBy=like&pageSize=10&page=1 | ||
|
||
### | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,38 @@ | ||
import styled from "styled-components"; | ||
import { | ||
FlexRowCentered, | ||
LineDivider, | ||
SectionHeader, | ||
SectionTitle, | ||
StyledLink, | ||
} from "@/styles/CommonStyles"; | ||
import { Article, ArticleSortOption } from "@/types/articleTypes"; | ||
import { | ||
ArticleInfo, | ||
ArticleInfoWrapper, | ||
ArticleThumbnail, | ||
ArticleTitle, | ||
ImageWrapper, | ||
MainContent, | ||
Timestamp, | ||
} from "@/styles/BoardsStyles"; | ||
} from "@/styles/BoardStyles"; | ||
import Image from "next/image"; | ||
import { format } from "date-fns"; | ||
import Link from "next/link"; | ||
import SearchBar from "@/components/ui/SearchBar"; | ||
import DropdownMenu from "@/components/ui/DropdownMenu"; | ||
import { useEffect, useState } from "react"; | ||
import LikeCountDisplay from "@/components/ui/LikeCountDisplay"; | ||
import EmptyState from "@/components/ui/EmptyState"; | ||
import { useRouter } from "next/router"; | ||
import ArticleInfo from "@/components/board/ArticleInfo"; | ||
|
||
import { ItemContainer, ArticleInfoDiv, AddArticleLink } from "./AllArticlesSection.styles"; | ||
const ItemContainer = styled(Link)``; | ||
|
||
type ArticleItemProps = { | ||
interface ArticleItemProps { | ||
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. P3: |
||
article: Article; | ||
} | ||
|
||
const ArticleItem = ({ article }: ArticleItemProps) => { | ||
const dateString = format(article.createdAt, "yyyy. MM. dd"); | ||
|
||
const ArticleItem: React.FC<ArticleItemProps> = ({ article }) => { | ||
return ( | ||
<> | ||
<ItemContainer href={`/boards/${article.id}`}> | ||
<ItemContainer href={`/board/${article.id}`}> | ||
<MainContent> | ||
<ArticleTitle>{article.title}</ArticleTitle> | ||
{article.image && ( | ||
|
@@ -51,25 +49,27 @@ const ArticleItem = ({ article }: ArticleItemProps) => { | |
)} | ||
</MainContent> | ||
|
||
<ArticleInfo> | ||
<ArticleInfoDiv> | ||
{article.writer.nickname} <Timestamp>{dateString}</Timestamp> | ||
</ArticleInfoDiv> | ||
<ArticleInfoWrapper> | ||
<ArticleInfo article={article} /> | ||
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. P3: |
||
|
||
<LikeCountDisplay count={article.likeCount} iconWidth={24} gap={8} /> | ||
</ArticleInfo> | ||
</ArticleInfoWrapper> | ||
</ItemContainer> | ||
|
||
<LineDivider $margin="24px 0" /> | ||
</> | ||
); | ||
}; | ||
|
||
type AllArticlesSectionProps = { | ||
const AddArticleLink = styled(StyledLink)``; | ||
|
||
interface AllArticlesSectionProps { | ||
initialArticles: Article[]; | ||
} | ||
|
||
const AllArticlesSection = ({initialArticles}: AllArticlesSectionProps) => { | ||
const AllArticlesSection: React.FC<AllArticlesSectionProps> = ({ | ||
initialArticles, | ||
}) => { | ||
const [orderBy, setOrderBy] = useState<ArticleSortOption>("recent"); | ||
const [articles, setArticles] = useState(initialArticles); | ||
|
||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,49 @@ | ||||||
import { ChangeEvent, useState } from "react"; | ||||||
import { TextArea } from "@/styles/CommonStyles"; | ||||||
import ArticlePageCommentThread from "@/components/board/ArticlePageCommentThread"; | ||||||
import { | ||||||
CommentInputSection, | ||||||
CommentSectionTitle, | ||||||
PostCommentButton, | ||||||
} from "@/styles/CommentStyles"; | ||||||
|
||||||
interface ArticleCommentSectionProps { | ||||||
articleId: number; | ||||||
} | ||||||
|
||||||
const ArticleCommentSection: React.FC<ArticleCommentSectionProps> = ({ | ||||||
articleId, | ||||||
}) => { | ||||||
const [comment, setComment] = useState(""); | ||||||
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => { | ||||||
setComment(e.target.value); | ||||||
}; | ||||||
Comment on lines
+19
to
+21
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. P2: |
||||||
|
||||||
const handlePostComment = () => {}; | ||||||
|
||||||
return ( | ||||||
<> | ||||||
<CommentInputSection> | ||||||
<CommentSectionTitle>댓글 달기</CommentSectionTitle> | ||||||
|
||||||
<TextArea | ||||||
placeholder={"댓글을 입력해 주세요."} | ||||||
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. P3:
Suggested change
|
||||||
value={comment} | ||||||
onChange={handleInputChange} | ||||||
/> | ||||||
|
||||||
<PostCommentButton | ||||||
onClick={handlePostComment} | ||||||
disabled={!comment.trim()} | ||||||
> | ||||||
등록 | ||||||
</PostCommentButton> | ||||||
</CommentInputSection> | ||||||
|
||||||
<ArticlePageCommentThread articleId={articleId} /> | ||||||
</> | ||||||
); | ||||||
}; | ||||||
|
||||||
export default ArticleCommentSection; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import styled from "styled-components"; | ||
import { FlexRowCentered, LineDivider } from "@/styles/CommonStyles"; | ||
import { Article } from "@/types/articleTypes"; | ||
import SeeMoreIcon from "@/public/images/icons/ic_kebab.svg"; | ||
import ArticleInfo from "@/components/board/ArticleInfo"; | ||
import LikeCountDisplay from "@/components/ui/LikeCountDisplay"; | ||
|
||
const SectionContainer = styled.div` | ||
margin-bottom: 40px; | ||
|
||
@media ${({ theme }) => theme.mediaQuery.tablet} { | ||
margin-bottom: 64px; | ||
} | ||
`; | ||
|
||
const ArticleHeaderContainer = styled.div` | ||
position: relative; | ||
`; | ||
|
||
const SeeMoreButton = styled.button` | ||
position: absolute; | ||
top: 0; | ||
right: 0; | ||
`; | ||
|
||
const Title = styled.h1` | ||
font-size: 20px; | ||
font-weight: 700; | ||
margin-bottom: 16px; | ||
`; | ||
|
||
const ArticleInfoWrapper = styled(FlexRowCentered)` | ||
gap: 16px; | ||
`; | ||
|
||
const VerticalDivider = styled.div` | ||
border-left: 1px solid var(--gray-200); | ||
height: 24px; | ||
`; | ||
|
||
const Content = styled.p` | ||
font-size: 16px; | ||
`; | ||
|
||
interface ArticleContentSectionProps { | ||
article: Article; | ||
} | ||
|
||
const ArticleContentSection: React.FC<ArticleContentSectionProps> = ({ | ||
article, | ||
}) => { | ||
return ( | ||
<SectionContainer> | ||
<ArticleHeaderContainer> | ||
<Title>{article.title}</Title> | ||
|
||
<SeeMoreButton> | ||
<SeeMoreIcon /> | ||
</SeeMoreButton> | ||
|
||
<ArticleInfoWrapper> | ||
<ArticleInfo article={article} /> | ||
<VerticalDivider /> | ||
<LikeCountDisplay count={article.likeCount} iconWidth={24} gap={8} /> | ||
</ArticleInfoWrapper> | ||
</ArticleHeaderContainer> | ||
|
||
<LineDivider /> | ||
|
||
<Content>{article.content}</Content> | ||
</SectionContainer> | ||
); | ||
}; | ||
|
||
export default ArticleContentSection; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import styled from "styled-components"; | ||
import { FlexRowCentered } from "@/styles/CommonStyles"; | ||
import ProfilePlaceholder from "@/public/images/ui/ic_profile.svg"; | ||
import { Article } from "@/types/articleTypes"; | ||
import { formatDate } from "date-fns"; | ||
import { Timestamp } from "@/styles/BoardStyles"; | ||
|
||
const Container = styled(FlexRowCentered)` | ||
gap: 8px; | ||
color: var(--gray-600); | ||
font-size: 14px; | ||
`; | ||
|
||
interface ArticleInfoProps { | ||
article: Article; | ||
} | ||
|
||
const ArticleInfo: React.FC<ArticleInfoProps> = ({ article }) => { | ||
const dateString = formatDate(article.createdAt, "yyyy. MM. dd"); | ||
|
||
return ( | ||
<Container> | ||
<ProfilePlaceholder width={24} height={24} /> | ||
{article.writer.nickname} <Timestamp>{dateString}</Timestamp> | ||
</Container> | ||
); | ||
}; | ||
|
||
export default ArticleInfo; |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,69 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { useEffect, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import styled from "styled-components"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Comment, CommentListResponse } from "@/types/commentTypes"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import EmptyState from "@/components/ui/EmptyState"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import CommentItem from "@/components/thread/CommentItem"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { getArticleComments } from "@/api/articleApi"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const ThreadContainer = styled.div` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
margin-bottom: 40px; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
interface ArticlePageCommentThreadProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
articleId: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const ArticlePageCommentThread: React.FC<ArticlePageCommentThreadProps> = ({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
articleId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [comments, setComments] = useState<Comment[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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. P3: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [isLoading, setIsLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!articleId) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const fetchComments = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setIsLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const response: CommentListResponse = await getArticleComments({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
articleId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setComments(response.list); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setError(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error("Error fetching comments:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setError("게시글의 댓글을 불러오지 못했어요."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setIsLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fetchComments(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, [articleId]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+23
to
+44
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. P3: const fetchComments = useCallback(async () => {
setIsLoading(true);
try {
const response: CommentListResponse = await getArticleComments({
articleId,
});
setComments(response.list);
setError(null);
} catch (error) {
console.error("Error fetching comments:", error);
setError("게시글의 댓글을 불러오지 못했어요.");
} finally {
setIsLoading(false);
}
});
useEffect(() => {
if (!articleId) return;
fetchComments();
}, [articleId, fetchComments]); |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (isLoading) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return <div>게시글 댓글 로딩중...</div>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return <div>오류: {error}</div>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (comments && !comments.length) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<EmptyState text={`아직 댓글이 없어요,\n지금 댓글을 달아 보세요!`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<ThreadContainer> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{comments.map((item) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<CommentItem item={item} key={`comment-${item.id}`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</ThreadContainer> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+54
to
+67
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. P2:
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export default ArticlePageCommentThread; |
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.
P3:
공통으로 사용되는 기본 주소는 변수에 저장해두고 쓰셔도 좋을 것 같아요