Skip to content
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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
93a0e93
chore: 머지 후 브랜치 삭제 github action 추가
withyj-codeit Sep 3, 2023
17be37c
Initial commit from Create Next App
withyj-codeit Sep 8, 2023
7df9c2b
fix: 머지 후 브랜치 삭제 github action 수정
hanseulhee Oct 10, 2023
66f3ba6
env: workflows 폴더로 이동
hanseulhee Oct 10, 2023
3c68d4e
First commit Sprint Mission5 on React
joodongkim Sep 7, 2024
d48b01a
First commit Sprint Mission5 on React (#78)
joodongkim Sep 9, 2024
675f9b0
Sprint mission 6 commit.
joodongkim Sep 12, 2024
20fd6da
Sprint Mission 6 Update 1
joodongkim Sep 12, 2024
6ebe241
update sprint mission 6
joodongkim Sep 14, 2024
e111b83
Merge pull request #1 from joodongkim/React-김주동-sprint6
joodongkim Sep 14, 2024
4bfa053
Sprint mission 6 update commit.
joodongkim Sep 20, 2024
3164f59
Merge branch 'React-김주동' into React-김주동-sprint6
joodongkim Sep 20, 2024
2fdcae3
Apply using Sprint mission 6 update commit.
joodongkim Sep 21, 2024
10b590a
delete .bak files
joodongkim Sep 21, 2024
2032cc0
Update Comments
joodongkim Sep 21, 2024
1437689
modify ItemComment
joodongkim Sep 21, 2024
4f9c678
React 김주동 sprint7 (#97)
joodongkim Sep 23, 2024
0b131e7
Update pull conflict
joodongkim Sep 24, 2024
0c5bb5c
refactoring to typescript
joodongkim Oct 18, 2024
1fe9f12
refactoring to typescript
joodongkim Oct 18, 2024
a5ac8a6
add LoginPage
joodongkim Oct 18, 2024
85b532b
Change to Next-김주동
joodongkim Oct 25, 2024
b7bd99e
First build using upstrem Next-김주동
joodongkim Oct 25, 2024
bee7f20
First commit
joodongkim Oct 25, 2024
a9f37e5
First commit using Next-김주동-sprint9
joodongkim Oct 25, 2024
41f70e7
REFACTOR: styled-components
joodongkim Oct 26, 2024
e4208c1
Merge branch 'Next-김주동' of https://github.com/joodongkim/10-Sprint-Mi…
joodongkim Oct 28, 2024
3312289
change from React.FC<type> to ({}:type)
joodongkim Oct 28, 2024
11218fa
feat: replace interface to type
joodongkim Oct 29, 2024
531f8ea
refactoring for nextjs
joodongkim Oct 29, 2024
35eb2cf
refactor: for nextjs
joodongkim Oct 29, 2024
06d2176
Merge branch 'Next-김주동' into Next-김주동-sprint9
joodongkim Oct 29, 2024
c708ff7
Merge branch 'Next-김주동-sprint9' of https://github.com/joodongkim/10-S…
joodongkim Oct 29, 2024
fe773d5
feat: sprint#10 first commit
joodongkim Nov 2, 2024
fb2af31
review: sprint#10
joodongkim Nov 2, 2024
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
3 changes: 0 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
Expand Down
50 changes: 50 additions & 0 deletions api/articleApi.ts
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}`
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
공통으로 사용되는 기본 주소는 변수에 저장해두고 쓰셔도 좋을 것 같아요

Suggested change
`https://panda-market-api.vercel.app/articles/${articleId}/comments?${query}`
`${BASE_URL}/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;
}
}
8 changes: 8 additions & 0 deletions api/example.http
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 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
interface로 바꿔주신것 좋습니다 👍

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 && (
Expand All @@ -51,25 +49,27 @@ const ArticleItem = ({ article }: ArticleItemProps) => {
)}
</MainContent>

<ArticleInfo>
<ArticleInfoDiv>
{article.writer.nickname} <Timestamp>{dateString}</Timestamp>
</ArticleInfoDiv>
<ArticleInfoWrapper>
<ArticleInfo article={article} />
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
기존 ArticleInfoDiv에서 ArticleInfo 태그명을 빼주신게 좋네요.


<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);

Expand Down
49 changes: 49 additions & 0 deletions components/board/ArticleCommentSection.tsx
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2:
이렇게 인풋값이 입력될때마다 state를 바꿔주는 경우 debounce를 통해 이벤트를 묶어 실행해주시는 것이 좋습니다.
그렇게 되면 리렌더링되는 횟수를 줄일 수 있습니다.


const handlePostComment = () => {};

return (
<>
<CommentInputSection>
<CommentSectionTitle>댓글 달기</CommentSectionTitle>

<TextArea
placeholder={"댓글을 입력해 주세요."}
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:

Suggested change
placeholder={"댓글을 입력해 주세요."}
placeholder="댓글을 입력해 주세요."

value={comment}
onChange={handleInputChange}
/>

<PostCommentButton
onClick={handlePostComment}
disabled={!comment.trim()}
>
등록
</PostCommentButton>
</CommentInputSection>

<ArticlePageCommentThread articleId={articleId} />
</>
);
};

export default ArticleCommentSection;
75 changes: 75 additions & 0 deletions components/board/ArticleContentSection.tsx
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;
29 changes: 29 additions & 0 deletions components/board/ArticleInfo.tsx
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;
69 changes: 69 additions & 0 deletions components/board/ArticlePageCommentThread.tsx
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[]>([]);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

P3:
아래처럼 fetchComments 함수를 useEffect 외부로 빼셔도 됩니다.
이렇게 되면 로직이 분리되어 가독성에도 도움이 됩니다.

  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
Copy link
Collaborator

Choose a reason for hiding this comment

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

P2:
이미 comments && !comments.length 조건문에서 return을 하고 있으니 else문은 사용하지 않으셔도 됩니다.

Suggested change
if (comments && !comments.length) {
return (
<EmptyState text={`아직 댓글이 없어요,\n지금 댓글을 달아 보세요!`} />
);
} else {
return (
<ThreadContainer>
{comments.map((item) => (
<CommentItem item={item} key={`comment-${item.id}`} />
))}
</ThreadContainer>
);
}
};
if (comments && !comments.length) {
return (
<EmptyState text={`아직 댓글이 없어요,\n지금 댓글을 달아 보세요!`} />
);
}
return (
<ThreadContainer>
{comments.map((item) => (
<CommentItem item={item} key={`comment-${item.id}`} />
))}
</ThreadContainer>
);
};


export default ArticlePageCommentThread;
Loading
Loading