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 #311

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
135 changes: 135 additions & 0 deletions components/ArticleCommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import FormatRelativeTime from "@/utils/FormatRelativeTime";
import { useEffect, useState } from "react";
import styled from "styled-components";
import axios from "@/lib/axios";
import Image from "next/image";

const StyledSection = styled.section`
margin-top: 100px;
border-bottom: 1px solid var(--gray-200);
background-color: var(--gray-20);
`;
const StyledList = styled.ul`
padding: 0;
`;
const StyledListItem = styled.li`
list-style-type: none;
`;
const StyledContent = styled.p`
font-size: 16px;
font-weight: 400;
margin-bottom: 24px;
`;
const StyledBottomSection = styled.section`
display: flex;
align-items: center;
gap: 12px;
`;
const StyledWriterImg = styled.img`
width: 50px;
height: 50px;
border-radius: 50%;
`;
const StyledInfo = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
`;
const StyledNickname = styled.p`
font-size: 14px;
font-weight: 400;
margin: 0;
`;
const StyledTime = styled.div`
font-size: 12px;
font-weight: 400;
color: var(--gray-400);
margin: 0;
`;

const StyledEmptyComment = styled.p`
font-size: 16px;
font-weight: 400;
text-align: center;
color: var(--gray-400);
`;

interface Comment {
writer: {
image: string;
nickname: string;
id: number;
};
updatedAt: string;
createdAt: string;
content: string;
id: number;
}

function ArticleCommentList({ id }: any) {
const [comments, setComments] = useState<Comment[]>([]);

async function getArticleComments() {
const query = {
limit: 10,
};
const res = await axios.get(
`/articles/${id}/comments?limit=${query.limit}`
);
const nextComments = res.data.list;
setComments(nextComments);
}

useEffect(() => {
getArticleComments();
}, [id]);

Comment on lines +70 to +86
Copy link
Collaborator

@arthurkimdev arthurkimdev Aug 27, 2024

Choose a reason for hiding this comment

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

이 부분은 아래처럼 useArticleComments 커스텀 훅 같은걸 만들어서 재사용 가능한 훅을 만들어 볼 수 있겠어요. 😸

function useArticleComments(id: string, limit: number) {
  const [comments, setComments] = useState<Comment[]>([]);
  const [error, setError] = useState<Error | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    async function fetchComments() {
      setLoading(true);
      try {
        const res = await axios.get(`/articles/${id}/comments?limit=${limit}`);
        setComments(res.data.list);
      } catch (err) {
        setError(err instanceof Error ? err : new Error('Unknown error occurred'));
      } finally {
        setLoading(false);
      }
    }

    if (id) {
      fetchComments();
    }
  }, [id, limit]);

  return { comments, error, loading };
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

그 이후에는 아래처럼 사용해보면 되겠죠?

function ArticleCommentList({ id, limit = 10 }: ArticleCommentListProps) {
  const { comments, error, loading } = useArticleComments(id, limit);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error.message} />;
  if (comments.length === 0) return <EmptyCommentState />;

  ...

if (comments.length === 0) {
return (
<div className="empty-comment" style={{ textAlign: "center" }}>
<Image
unoptimized={true}
width={140}
height={140}
src="/image/img_comment_empty.png"
alt="No comments"
/>
<StyledEmptyComment>
아직 댓글이 없어요,
<br /> 지금 댓글을 달아보세요!
</StyledEmptyComment>
</div>
);
}
return (
<div>
<StyledSection>
<StyledList>
{comments.map((comment) => (
<StyledListItem key={comment.id}>
<StyledContent>{comment.content}</StyledContent>
<StyledBottomSection>
<StyledWriterImg
src={
comment.writer.image
? comment.writer.image
: "/image/profile_img_none.png"
}
alt="프로필 이미지"
/>
Comment on lines +112 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

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

웹 접근성 관점에서 alt 속성 값에 이런 경우 사용자닉네임도 넣어서 값을 만들면 대체 텍스트를 보다 더 구체적으로 만들 수 있어요.

          alt={`${comment.writer.nickname}의 프로필 이미지`}

Comment on lines +108 to +119
Copy link
Collaborator

Choose a reason for hiding this comment

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

보통 이렇게 map을 사용할 수 있는 부분은 별도의 컴포넌트로 분리하는걸 추천드려요. UI가 분리되고, 가독성이 좋아지거든요.
다른 곳에 재사용도 가능해지구요.

function CommentItem({ comment }: { comment: Comment }) {
  return (
    <StyledListItem as="article">
      <StyledContent>{comment.content}</StyledContent>
      <StyledBottomSection>
        <StyledWriterImg
          src={comment.writer.image || "/image/profile_img_none.png"}
          alt={`${comment.writer.nickname}의 프로필 이미지`}
        />
        <StyledInfo>
          <StyledNickname>{comment.writer.nickname}</StyledNickname>
          <StyledTime>
            <FormatRelativeTime time={comment.updatedAt} />
          </StyledTime>
        </StyledInfo>
      </StyledBottomSection>
    </StyledListItem>
  );
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

최종적으로 아래처럼 바꿀 수 있어요.

return (
    <CommentSection>
      <CommentList>
        {comments.map((comment) => (
          <CommentItem key={comment.id} comment={comment} />
        ))}
      </CommentList>
    </CommentSection>
  );

<StyledInfo>
<StyledNickname>{comment.writer.nickname}</StyledNickname>
<StyledTime>
<FormatRelativeTime time={comment.updatedAt} />
</StyledTime>
</StyledInfo>
</StyledBottomSection>
</StyledListItem>
))}
</StyledList>
</StyledSection>
</div>
);
}

export default ArticleCommentList;
63 changes: 35 additions & 28 deletions components/BestPostList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import DateTrimmer from "@/utils/TimeTrimmer";
import styled from "styled-components";
import { useState, useEffect } from "react";
import axios from "@/lib/axios";
import Link from "next/link";

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -72,6 +73,10 @@ const StyledDate = styled.p`
color: var(--gray-400);
`;

const StyledLink = styled(Link)`
text-decoration: none;
`;

type Writer = {
nickname: string;
id: number;
Expand Down Expand Up @@ -112,36 +117,38 @@ function BestPostList() {
<StyledTitle>베스트 게시글</StyledTitle>
<Container>
{articles.map((article) => (
<StyledPostArea key={article.id}>
<Image
unoptimized={true}
width={102}
height={30}
src="/image/best_badge.png"
alt="베스트 게시글 뱃지"
/>
<StyledTopArea>
<StyledPostTitle>{article.title}</StyledPostTitle>
<StyledImageWrapper
src={article.image}
alt="게시글 첨부 이미지"
<StyledLink href={`/boards/${article.id}`} key={article.id}>
<StyledPostArea>
<Image
unoptimized={true}
width={102}
height={30}
src="/image/best_badge.png"
alt="베스트 게시글 뱃지"
/>
</StyledTopArea>
<StyledBottomArea>
<StyledBottomLeftArea>
<StyledNickname>{article.writer.nickname}</StyledNickname>
<Image
unoptimized={true}
width={15}
height={13}
src="/image/heart_inactive.png"
alt="좋아요 아이콘"
<StyledTopArea>
<StyledPostTitle>{article.title}</StyledPostTitle>
<StyledImageWrapper
src={article.image}
alt="게시글 첨부 이미지"
/>
<StyledLikeCount>{article.likeCount}</StyledLikeCount>
</StyledBottomLeftArea>
<StyledDate>{DateTrimmer(article.createdAt)}</StyledDate>
</StyledBottomArea>
</StyledPostArea>
</StyledTopArea>
<StyledBottomArea>
<StyledBottomLeftArea>
<StyledNickname>{article.writer.nickname}</StyledNickname>
<Image
unoptimized={true}
width={15}
height={13}
src="/image/heart_inactive.png"
alt="좋아요 아이콘"
/>
<StyledLikeCount>{article.likeCount}</StyledLikeCount>
</StyledBottomLeftArea>
<StyledDate>{DateTrimmer(article.createdAt)}</StyledDate>
</StyledBottomArea>
</StyledPostArea>
</StyledLink>
))}
</Container>
</>
Expand Down
5 changes: 5 additions & 0 deletions components/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const StyledCommonButton = styled.button`
align-items: center;
font-size: 18px;
font-weight: 600;

&:disabled {
background-color: var(--gray-400);
cursor: auto;
}
`;

export default StyledCommonButton;
3 changes: 3 additions & 0 deletions components/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ function FileInput({ name, value, onChange }: Props) {
<StyledLabel>이미지</StyledLabel>
<StyledFileArea>
<StyledFileInputBox>
<label htmlFor="imgFile" />
<StyledFileInput
id="imgFile"
name="imgFile"
type="file"
accept="image/png, image/jpeg"
onChange={handleChange}
Expand Down
81 changes: 45 additions & 36 deletions components/PostList.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Image from "next/image";
import SearchInput from "./SearchInput";
import DateTrimmer from "@/utils/TimeTrimmer";
import styled from "styled-components";
import DropDown from "./DropDown";
import Button from "./Button";
import Link from "next/link";

const StyledArea = styled.div`
display: flex;
Expand Down Expand Up @@ -66,6 +64,10 @@ const StyledDate = styled.p`
color: var(--gray-400);
`;

const StyledLink = styled(Link)`
text-decoration: none;
`;

type Writer = {
nickname: string;
id: number;
Expand All @@ -89,40 +91,47 @@ function PostList({ articles }: Props) {
return (
<>
{articles.map((article) => (
<StyledPostArea key={article.id}>
<StyledArea>
<StyledPostTitle>{article.title}</StyledPostTitle>
<StyledImageWrapper
src={
article.image ? article.image : "/image/product_img_default.png"
}
alt="게시글 첨부 이미지"
/>
</StyledArea>
<StyledArea>
<StyledBottomLeftArea>
<Image
unoptimized={true}
width={24}
height={24}
src="/image/profile_img_none.png"
alt="가짜 프로필 이미지"
/>
<StyledNickname>{article.writer.nickname}</StyledNickname>
<StyledDate>{DateTrimmer(article.createdAt)}</StyledDate>
</StyledBottomLeftArea>
<StyledBottomRightArea>
<Image
unoptimized={true}
width={20}
height={17}
src="/image/heart_inactive.png"
alt="좋아요 아이콘"
<StyledLink href={`/boards/${article.id}`} key={article.id}>
<StyledPostArea>
<StyledArea>
<StyledPostTitle>{article.title}</StyledPostTitle>
<StyledImageWrapper
src={
article.image
? encodeURI(article.image)
: "/image/product_img_default.png"
}
onError={(e) => {
e.currentTarget.src = "/image/product_img_default.png";
}}
alt="게시글 첨부 이미지"
/>
<StyledLikeCount>{article.likeCount}</StyledLikeCount>
</StyledBottomRightArea>
</StyledArea>
</StyledPostArea>
</StyledArea>
<StyledArea>
<StyledBottomLeftArea>
<Image
unoptimized={true}
width={24}
height={24}
src="/image/profile_img_none.png"
alt="가짜 프로필 이미지"
/>
<StyledNickname>{article.writer.nickname}</StyledNickname>
<StyledDate>{DateTrimmer(article.createdAt)}</StyledDate>
</StyledBottomLeftArea>
<StyledBottomRightArea>
<Image
unoptimized={true}
width={20}
height={17}
src="/image/heart_inactive.png"
alt="좋아요 아이콘"
/>
<StyledLikeCount>{article.likeCount}</StyledLikeCount>
</StyledBottomRightArea>
</StyledArea>
</StyledPostArea>
</StyledLink>
))}
</>
);
Expand Down
4 changes: 4 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
const nextConfig = {
reactStrictMode: true,

compiler: {
styledComponents: true,
},

images: {
remotePatterns: [
{
Expand Down
Loading
Loading