Skip to content

[정남영] 스프린트미션9 [merge안해서 미션8이랑 겹쳐진상태]#71

Merged
rl0425 merged 15 commits intocodeit-sprint-fullstack:next-정남영from
dudska12:next-정남영
Aug 5, 2025

Hidden character warning

The head ref may contain hidden characters: "next-\uc815\ub0a8\uc601"
Merged

[정남영] 스프린트미션9 [merge안해서 미션8이랑 겹쳐진상태]#71
rl0425 merged 15 commits intocodeit-sprint-fullstack:next-정남영from
dudska12:next-정남영

Conversation

@dudska12
Copy link
Collaborator

@dudska12 dudska12 commented Jul 13, 2025

✅ 기본 요구사항

🔗 공통

🔐 로그인/회원가입 페이지

로그인 페이지

  • "회원 가입하기"를 클릭하면 회원가입 페이지로 이동해 주세요.
  • 로그인 실패하는 경우, 이메일 input 아래에 "이메일을 확인해 주세요.", 비밀번호 input 아래에 "비밀번호를 확인해 주세요." 에러 메시지를 표시해 주세요.
  • 로그인 버튼이 활성화된 후, 로그인 버튼 클릭 또는 Enter키 입력으로 로그인 실행합니다.
  • "/auth/signIn"으로 POST 요청해서 성공 응답을 받으면 중고 마켓 페이지로 이동합니다. 참고로 JWT로 구현되어 있습니다.
  • 실패할 경우, 실패 메시지를 모달을 통해 표시합니다.

회원가입 페이지

  • "회원 가입하기"를 클릭하면 '/signin' 페이지로 이동합니다.
  • 회원가입 버튼 클릭 또는 Enter키 입력으로 회원가입을 실행합니다.
  • 비밀번호 input과 비밀번호 확인 input의 값이 다른 경우, 비밀번호 확인 input 아래에 "비밀번호가 일치하지 않아요." 에러 메시지를 표시해 주세요.
  • 버튼이 활성화된 후, 회원가입은 "/auth/signUp" POST 요청해서 진행합니다. 참고로 JWT로 구현되어 있습니다.
  • 회원가입 성공 응답을 받으면 중고마켓 페이지로 이동합니다.
  • 실패할 경우, 실패 메시지를 모달을 통해 표시합니다.

로그인/회원가입 페이지 공통

  • 눈 모양 아이콘 클릭 시 비밀번호의 문자열이 보이기도 하고, 가려집니다.
  • 비밀번호의 문자열이 가려질 때는 눈 모양 아이콘에는 사선이 그어져 있고, 보일 때는 사선이 없는 눈 모양 아이콘이 보입니다.
  • 소셜 로그인에 구글 아이콘 클릭 시 'https://www.google.com/', 카카오 아이콘 클릭 시 '[https://www.kakaocorp.com/page'로](https://www.kakaocorp.com/page'로) 이동합니다.
  • 로그인/회원가입 시 성공 응답으로 받은 accessToken을 로컬 스토리지에 저장합니다.
  • 로그인/회원가입 페이지에 접근 시 로컬 스토리지에 accessToken이 있는 경우 '/items' 페이지로 이동합니다.

📌 GNB (상단 내비게이션 바)

  • 프로필 영역은 인가된 경우, 유저 정보 API를 활용해 주세요.
  • 인가되지 않았을 경우 "로그인" 버튼이 보이게 해 주세요.

🛒 상품 상세 페이지

  • PC, Tablet, Mobile 디자인에 해당하는 상품 상세 페이지를 만들어 주세요.
  • 상품 상세 페이지 url path는 "/items/{itemId}"로 설정하세요.
  • '목록으로 돌아가기' 버튼 클릭 시 중고마켓 페이지 "/items"로 이동합니다.
  • 상품 상세 데이터는 '/products/{productId}' GET 메서드 사용해 불러오세요. 이때, 상품 상세 조회는 인가된 사용자만 이용할 수 있도록 합니다.
  • 상품에 대한 댓글 조회도 가능합니다.
  • 상품 수정 및 삭제 기능을 API를 활용해 구현합니다. 이때, 인가된 사용자만 이용할 수 있도록 합니다.
  • 상품 수정은 '/products/{productId}' PATCH을 사용합니다.
  • 상품 삭제는 '/products/{productId}' DELETE를 사용합니다.
  • 상품 삭제 전, 확인 모달을 띄워주세요.
  • 상품에 대한 좋아요 및 좋아요 취소 기능을 '/products/{productId}/favorite' POST & DELETE로 구현합니다. 인가된 사용자만 사용할 수 있도록 합니다.
  • 댓글 생성 및 삭제 기능을 API를 활용해 구현합니다. 인가된 사용자만 이용할 수 있도록 합니다.
  • 댓글 수정은 '/comments/{commentId}' PATCH을 사용합니다.
  • 댓글 삭제는 '/comments/{commentId}' DELETE를 사용합니다.

🔥 심화 요구사항

🔐 로그인 및 회원가입 페이지 공통

  • 로그인, 회원가입 기능에 react-hook-form을 활용해 주세요.

  • 반응형 디자인 적용 (viewport 기준):

    • PC: 1200px 이상
    • Tablet: 744px 이상 ~ 1199px 이하
    • Mobile: 375px 이상 ~ 743px 이하
      ※ 375px 미만 사이즈는 고려하지 않음

🙋‍♂️ 유저 기능

  • 인증 토큰을 헤더에 첨부할 때 axios interceptors를 활용해 주세요. (axios를 사용하지 않는다면 유사한 기능 활용)

🔁 React Query 마이그레이션

  • 기존 fetch/axios 코드 → React Query로 마이그레이션

🔄 로딩 및 에러 핸들링

  • 로딩 인디케이터 구현
  • 에러 메시지 표시
  • 상품 목록 및 상세 데이터 prefetching

🧠 상품 데이터 캐싱 및 업데이트

  • React Query의 캐싱 기능으로 데이터 로딩 최소화
  • 실시간 업데이트를 위한 Query Refresh 적용

멘토님께

  • 실수로 merge를 안해서 기존 pr이랑 겹쳐져서 기존 pr를 수정해서 pr보냅니다.
  • 아직 진행중입니다 ㅠ 빠르게하고나서 디스코드로 코드리뷰요청하겠습니다.
  • 아직 배포안해서 배포하게되면 링크추가하겠습니다

@rl0425 rl0425 self-requested a review July 13, 2025 11:16
Copy link
Collaborator

@rl0425 rl0425 left a comment

Choose a reason for hiding this comment

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

고생많으셨습니다.

전체적으로 사용하지 않는 div 태그, Fragment와
디버깅 코드는 제거하시고 올리시면 좋을 것 같습니다.

또한, 변수명 설정에 있어서 카멜케이스를 사용하시지 않는 경우와 실제 데이터와 변수명의 이름이 서로 불일치하는 경우가 있어보여, 이 부분을 수정하시면 더 좋은 코드가 될 것 같습니다.
수고하셨습니다!

import axios from "axios";
import { postBoard } from "@/pages/api/product";

export default function usePostBoard() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 코드는 커스텀 훅이라기보다는 단순한 래핑 함수인 것 같습니다. 실제로 postBoard()가 필요하면 usePostBoard()를 사용하는 것과 postBoard()를 그대로 불러오는 것이 차이가 없습니다. 따라서, 현재로서는 커스텀 훅의 의미가 거의 없기 때문에 로딩/에러 상태를 추가하시는게 좋을 것 같습니다.

export default function usePostBoard() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const submitPost = async (title, content) => {
    setLoading(true);
    setError(null);
    
    try {
      const res = await postBoard(title, content);
      return res;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  };

  return { submitPost, loading, error };
}

@@ -0,0 +1,12 @@
import { getAricleList } from "@/pages/api/product";
Copy link
Collaborator

Choose a reason for hiding this comment

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

오타가 나신 것 같습니다.
getAricleList -> getArticleList

import { useEffect, useState } from "react";

export default function useArticleList() {
const [articleList, setarticleList] = useState([]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

카멜케이스를 사용하시는 거면
setarticleList -> setArticleList 로 바꾸시는게 좋을 것 같습니다.

const [articleList, setarticleList] = useState([]);

useEffect(() => {
getAricleList().then((data) => setarticleList(data));
Copy link
Collaborator

Choose a reason for hiding this comment

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

전반적으로 커스텀 훅이 단순히 Api를 불러오기만 하는 코드이기 때문에, 위에서 말씀드린 것 처럼 로딩과 에러 처리를 전반적으로 처리해주셔야 할 것 같습니다.

import style from "@/styles/component.module.css";
export default function Board() {
return (
<>
Copy link
Collaborator

Choose a reason for hiding this comment

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

불필요한 Fragment는 제거해주시면 될 것 같습니다.

</div>
</div>
))}
</>
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기도 사용하지 않는 Fragment는 제거해주시면 될 것 같습니다.

{/* 베스트이미지 */}
<Image
src={ic_medal}
style={{ width: 30, height: 30 }}
Copy link
Collaborator

Choose a reason for hiding this comment

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

<Image> 태그는 크기와 넓이를 정하실 때, style 대신 속성을 사용하시면 됩니다.

<Image 
  src={ic_medal} 
  width={30}
  height={30}
  alt="메달이미지" 
/>

<div className={style.BoardItemContainer}>
<div className={style.boardItemBetween}>
<div>
<p>{content}</p>
Copy link
Collaborator

Choose a reason for hiding this comment

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

content가 없을 경우의 예외처리도 해주시면 좋을 것 같습니다.

const router = useRouter();

const handleClick = () => {
setmodal(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

이렇게 작성하지 마시고,

const handleClick = () => {
  setModal(prev => !prev);
};

이렇게 바꾸셔야 할 것 같습니다.

</div>
<div className={style.detailCommentList}>
{/* 댓글리스트 */}
{commentList.map((comment) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

여기서 사용하시는 comment라는 변수명과 실제 위에 useState로 사용하시는 comment 변수명이 일치하기 때문에, 하나는 바꿔주셔야 할 것 같습니다. 저는

const [comment, setComment] = useState("");
이걸
const [commentText, setCommentText] = useState("");

이렇게 바꾸시는 걸 추천드립니다.

@dudska12 dudska12 changed the title [정남영] 스프린트미션8 [정남영] 스프린트미션9 [merge안해서 미션8이랑 겹쳐진상태] Jul 20, 2025
@dudska12 dudska12 self-assigned this Jul 20, 2025
Copy link
Collaborator

@rl0425 rl0425 left a comment

Choose a reason for hiding this comment

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

고생하셨습니다.

너무 잘해주셨는데, 몇 가지만 말씀을 드리자면

  1. 관심사를 훅으로 분리하시는 건 너무 좋은 패턴이지만, 훅 내부에 로딩이나 에러 처리 없습니다. 아직 미완성이시겠지만, 비동기 패칭을 위한 훅이라면 내부적으로 로딩과 에러는 제일 먼저 설계하시는걸 추천드립니다.

  2. 훅들간의 코드 통일성이 없습니다. 어떤 훅은 useEffect 안에서의 함수 선언과 실행을 하지만, 다른 훅은 외부의 펑션을 useEffect에서 가져다 쓰고 있습니다.

전체적으로 코드를 통일성있게 작성하시면 좋은 공부가 되실 것 같습니다.

사실 컴포넌트에서 함수나 변수는 거의 동일하게 설계되고 작성되기 때문에, 본인만의 훅 관리나 패턴을 익히셔서 함수나 변수의 선언과 사용에 있어서 통일감을 주시면 더 좋은 코드가 될 것 같습니다.

import { useRouter } from "next/router";
import { createContext, useState, useContext, useEffect } from "react";

const AuthContext = createContext(null);
Copy link
Collaborator

Choose a reason for hiding this comment

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

아직 타입스크립트를 안사용하시지만, 컨택스트를 사용하실 때는 기본값을 제공하시는게 좋습니다.

const AuthContext = createContext({
  user: null,
  accessToken: null,
  refreshToken: null,
  login: () => {},
  logout: () => {},
});

logout();
router.push("/login");
}
}, [accessToken, refreshToken]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 if (storedAccessToken && storedRefreshToken) 을 타게되면, 엑세스 토큰이 변경이 되고, 의존성에 accessToken, refreshToken를 둘다 넣어주셨으니 useEffect가 다시 실행이 됩니다. 그럴 경우 조건문만 계속 맞는다면, 무한 루프가 돌 수 있기 때문에, 조건문이 다시 돌때는 false로 처리가 되서 문제가 없는 상황이여도 무조건 고치셔야 합니다.

전체적인 코드 흐름을 알 수 없어서 현재 추천드릴 수 있는건, 의존성 배열에 두 아이템을 빼셔야 할 것 같습니다.

const [comments, setComments] = useState([]);

useEffect(() => {
fetchCommentList().then((data) => setComments(data));
Copy link
Collaborator

Choose a reason for hiding this comment

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

데이터 패칭 시 try..catch 문을 반 필수적으로 작성하셔서 예외/에러처리를 하셔야 합니다.

fetchCommentList().then((data) => setComments(data));
}, []);

return { comments, setComments };
Copy link
Collaborator

Choose a reason for hiding this comment

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

훅으로 다른곳에서 커멘트를 불러오는 함수라면, isLoading, error 데이터와 로직도 넣으셔서 패칭 중간에는 isLoading을 true로 반환하시고, 패칭이 완료될 경우 false로 변경하셔서 export 해주셔야할 것 같습니다.

const [commentList, setCommentList] = useState([]);

const fetchComments = async () => {
console.log("id값넘어감???????????", articleId);
Copy link
Collaborator

Choose a reason for hiding this comment

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

실무에서도 콘솔로그를 실수로 넣는 경우가 많습니다!

export default function BoardItem({ userInfo, item = {} }) {
const { title, createdAt, likeCount, writer, image } = item;
const user = userInfo || defaultUserInfo;
const nickName = writer.nickname;
Copy link
Collaborator

Choose a reason for hiding this comment

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

writer가 없을경우의 에러처리가 필요합니다.

const nickName = writer?.nickname || "익명";

export default function UserBoard() {
const router = useRouter();
const [searchKeyword, setSearchKeyWord] = useState("");
const [orderBy, setOrderBy] = useState();
Copy link
Collaborator

Choose a reason for hiding this comment

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

orderBy는 기본값을 설정해주시는게 좋을 것 같습니다.

const [orderBy, setOrderBy] = useState("recent");

</select>
</div>
<div className={style.userBoardByBoardItem}>
{searchKeyword === "" ? (
Copy link
Collaborator

Choose a reason for hiding this comment

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

현재 삼항연산자가 엄청 중첩되있는데, 제가 알기로는 요즘 개발 추세 자체가 삼항연산자 사용을 기피하고 있습니다. 대표적인 예시가 지금과 같은 상태일텐데, 처음 코드를 보는 사람이라면, 숙련된 개발자라도 어느 조건에 어느 부분이 렌더링되는지 한눈에 파악이 어렵습니다.

따라서, 삼항연산자를 중복해서 사용하시거나, 조건문이 너무 까다로운 경우에는 사용을 자제하시고, && 이나, 변수에 선언하시고, 변수를 불러오는 방식, 컴포넌트를 쪼개서 임포트해오는 방식 등을 고려해보시면 좋을 것 같습니다.

<p>정말로 상품을 삭제하시겠어요?</p>
<div className={style.DeleteModalButtonBox}>
<button className={style.DeleteModalButton1} onClick={onClose}>
취소
Copy link
Collaborator

Choose a reason for hiding this comment

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

네와 반대되는 버튼이기 때문에, '아니오'나 '아니요'가 더 적합하지 않을까요?
취소라는 문구를 사용하고 싶으시면 '네' 대신, '확인'이 나을 것 같습니다.

@rl0425 rl0425 merged commit e602d39 into codeit-sprint-fullstack:next-정남영 Aug 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants