Skip to content

[이유진] sprint mission 9#76

Merged
rl0425 merged 6 commits intocodeit-sprint-fullstack:next-이유진from
nijuuy:feat/login-logout-signup
Aug 5, 2025
Merged

[이유진] sprint mission 9#76
rl0425 merged 6 commits intocodeit-sprint-fullstack:next-이유진from
nijuuy:feat/login-logout-signup

Conversation

@nijuuy
Copy link
Collaborator

@nijuuy nijuuy commented Jul 20, 2025

요구사항

로그인/회원가입 페이지

[ ] JavaScript로 구현한 로그인/회원가입 페이지를 React.js 혹은 Next.js로 마이그레이션해 주세요.
로그인 페이지

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

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

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

[x ] 상단 내비게이션 바에 프로필 영역은 인가된 경우, 유저 정보 API를 활용해 주세요.
[x ] 인가되지 않았을 경우 "로그인" 버튼이 보이게 해 주세요.
상품 상세 페이지

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

심화

  • [x]
  • []

주요 변경사항

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@nijuuy nijuuy marked this pull request as draft July 20, 2025 14:26
@nijuuy nijuuy requested a review from rl0425 July 20, 2025 14:26
@nijuuy nijuuy marked this pull request as ready for review July 20, 2025 14:27
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.

고생하셨습니다.

전반적으로 코드가 깔끔하고 체계적으로 작성되어있습니다.
다만 디테일한 에러나 예외처리, 기본값 설정에 주의를 기울이시면 더 좋은 코드가 될 것 같습니다.


export default function Footer() {
return (
<div className={styles.footerContainer}>
Copy link
Collaborator

Choose a reason for hiding this comment

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

footer 컴포넌트면 태그를 <footer>로 바꾸시면 될 것 같습니다.

</li>
</ul>
<div className={styles.socialGroup}>
<a href="https://www.facebook.com/" target="_blank">
Copy link
Collaborator

@rl0425 rl0425 Jul 22, 2025

Choose a reason for hiding this comment

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

target="_blank"를 쓸때는 항상 rel="noopener noreferrer" 를 함께 쓰셔야 안전합니다.

noopener  -> window.opener를 null로 만들어, 새 탭이 원본 페이지를 조작할 수 없게 함
noreferrer -> HTTP Referer 헤더를 전송하지 않아, 새 사이트가 어디서 왔는지 모르게 함

최신 브라우저들은 자동으로 noopener를 지원하지만, 명시적으로 작성하시는게 좋습니다.

@@ -0,0 +1,19 @@
import styles from "./LabeledInput.module.css";

export default function LabeledInput({ label, id, error, ...props }) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

공통 컴포넌트의 경우에는 ARIA 속성을 사용하셔서 접근성을 향상시키는걸 추천드립니다.

<input
  id={id}
  {...props}
  className={`${styles.input} ${error ? styles.inputError : ""}`}
  aria-invalid={error ? "true" : "false"}
  aria-describedby={error ? `${id}-error` : undefined}
/>

Copy link
Collaborator

Choose a reason for hiding this comment

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

props로 받으시는 데이터들의 기본값을 설정해주시면 좋을 것 같습니다. 또 id나 label이 없을 경우의 예외처리도 필요합니다.

priority
/>
</div>
<h1 className={`${styles.logoTitle} ${sizeClass}`}>판다마켓</h1>
Copy link
Collaborator

Choose a reason for hiding this comment

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

<h1> 태그는 페이지당 하나만 있는게 원칙인데, 이 Logo 컴포넌트를 페이지당 하나만 사용하거나, 레이아웃 용도로 사용하시는게 아니라면 <span>으로 바꾸시면 될 것 같습니다.

/>
<button
type="button"
onClick={() => setVisible(!visible)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

상태의 기존값으로 변경을 하지마시구, setVisible(prev => !prev) 처럼 함수형 업데이트를 하시는걸 추천드립니다.
추후 클로저 문제가 있을 수 있습니다.

try {
const res = await axios.get("/boardPosts", {
params: {
search: search,
Copy link
Collaborator

Choose a reason for hiding this comment

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

search의 스트링값을 그대로 사용하는데, .trim()을 사용하셔서 빈 공백을 제거하시거나, 정규화 패턴을 통해서 필터링을 추후에 추가하셔야 할 것 같습니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

setPost에 res.data.data를 넣는데, res가 다른 값이 올 경우의 예외처리도 해줘야합니다.
ext)

setPosts(res.data.data || []);

Copy link
Collaborator

Choose a reason for hiding this comment

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

지금 input의 값이 변경될때마다 API를 호출하는데 이럴 경우 너무 많은 호출이 있어, 디바운싱이나 쓰로틀을 사용하여 특정 시간 내의 인풋만 받도록 처리하는게 일반적입니다.

useEffect(() => {
  const fetchPosts  = () => {}
  ...
  const timeoutId = setTimeout(() => {
    fetchPosts();
  }, 300); // 디바운싱

  return () => clearTimeout(timeoutId);
}, [fetchPosts]);

setPosts(res.data.data);
} catch (err) {
console.error("게시글 로딩 실패:", err.message);
setPosts([]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

error일 경우에는 빈 값보다는, error 관련 jsx를 작성하시는게 더 명확합니다. 실제로 검색을 이상하게 해서, 정말 검색에 해당하는 내용이 없을 경우와 에러가 떴을 때의 사용자 경험이 일치하는 문제가 있습니다.

// 접속 처음 로그인 시도(accessToken으로)
console.log("accessToken으로 유저정보를 가져오는 중입니다.");
const res = await axios.get(
"https://panda-market-api.vercel.app/users/me",
Copy link
Collaborator

Choose a reason for hiding this comment

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

매직넘버는 따로 상수로 관리하시는걸 추천드립니다.

const API_BASE_URL = "https://panda-market-api.vercel.app";
const ENDPOINTS = {
  USER_ME: `${API_BASE_URL}/users/me`,
  REFRESH_TOKEN: `${API_BASE_URL}/auth/refresh-token`,
};

}

function handleLogout() {
localStorage.removeItem("accessToken");
Copy link
Collaborator

Choose a reason for hiding this comment

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

토큰을 전부 clear 하는 함수를 만드시고, 그 함수 하나만 호출하는 형태의 플로우가 더 깔끔할 것 같습니다.

} else if (!emailRegex.test(email)) {
newErrors.email = "유효한 이메일 형식을 입력해 주세요.";
}
if (!password.trim()) newErrors.password = "비밀번호를 확인해 주세요.";
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 9c218f9 into codeit-sprint-fullstack:next-이유진 Aug 5, 2025
1 check passed
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