[이유진] sprint mission 9#76
Conversation
rl0425
left a comment
There was a problem hiding this comment.
고생하셨습니다.
전반적으로 코드가 깔끔하고 체계적으로 작성되어있습니다.
다만 디테일한 에러나 예외처리, 기본값 설정에 주의를 기울이시면 더 좋은 코드가 될 것 같습니다.
|
|
||
| export default function Footer() { | ||
| return ( | ||
| <div className={styles.footerContainer}> |
There was a problem hiding this comment.
footer 컴포넌트면 태그를 <footer>로 바꾸시면 될 것 같습니다.
| </li> | ||
| </ul> | ||
| <div className={styles.socialGroup}> | ||
| <a href="https://www.facebook.com/" target="_blank"> |
There was a problem hiding this comment.
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 }) { | |||
There was a problem hiding this comment.
공통 컴포넌트의 경우에는 ARIA 속성을 사용하셔서 접근성을 향상시키는걸 추천드립니다.
<input
id={id}
{...props}
className={`${styles.input} ${error ? styles.inputError : ""}`}
aria-invalid={error ? "true" : "false"}
aria-describedby={error ? `${id}-error` : undefined}
/>There was a problem hiding this comment.
props로 받으시는 데이터들의 기본값을 설정해주시면 좋을 것 같습니다. 또 id나 label이 없을 경우의 예외처리도 필요합니다.
| priority | ||
| /> | ||
| </div> | ||
| <h1 className={`${styles.logoTitle} ${sizeClass}`}>판다마켓</h1> |
There was a problem hiding this comment.
<h1> 태그는 페이지당 하나만 있는게 원칙인데, 이 Logo 컴포넌트를 페이지당 하나만 사용하거나, 레이아웃 용도로 사용하시는게 아니라면 <span>으로 바꾸시면 될 것 같습니다.
| /> | ||
| <button | ||
| type="button" | ||
| onClick={() => setVisible(!visible)} |
There was a problem hiding this comment.
상태의 기존값으로 변경을 하지마시구, setVisible(prev => !prev) 처럼 함수형 업데이트를 하시는걸 추천드립니다.
추후 클로저 문제가 있을 수 있습니다.
| try { | ||
| const res = await axios.get("/boardPosts", { | ||
| params: { | ||
| search: search, |
There was a problem hiding this comment.
search의 스트링값을 그대로 사용하는데, .trim()을 사용하셔서 빈 공백을 제거하시거나, 정규화 패턴을 통해서 필터링을 추후에 추가하셔야 할 것 같습니다.
There was a problem hiding this comment.
setPost에 res.data.data를 넣는데, res가 다른 값이 올 경우의 예외처리도 해줘야합니다.
ext)
setPosts(res.data.data || []);There was a problem hiding this comment.
지금 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([]); |
There was a problem hiding this comment.
error일 경우에는 빈 값보다는, error 관련 jsx를 작성하시는게 더 명확합니다. 실제로 검색을 이상하게 해서, 정말 검색에 해당하는 내용이 없을 경우와 에러가 떴을 때의 사용자 경험이 일치하는 문제가 있습니다.
| // 접속 처음 로그인 시도(accessToken으로) | ||
| console.log("accessToken으로 유저정보를 가져오는 중입니다."); | ||
| const res = await axios.get( | ||
| "https://panda-market-api.vercel.app/users/me", |
There was a problem hiding this comment.
매직넘버는 따로 상수로 관리하시는걸 추천드립니다.
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"); |
There was a problem hiding this comment.
토큰을 전부 clear 하는 함수를 만드시고, 그 함수 하나만 호출하는 형태의 플로우가 더 깔끔할 것 같습니다.
| } else if (!emailRegex.test(email)) { | ||
| newErrors.email = "유효한 이메일 형식을 입력해 주세요."; | ||
| } | ||
| if (!password.trim()) newErrors.password = "비밀번호를 확인해 주세요."; |
There was a problem hiding this comment.
비밀번호 최소 길이 검증도 추가하시면 좋을 것 같습니다.
요구사항
로그인/회원가입 페이지
[ ] 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를 사용합니다.
심화
주요 변경사항
스크린샷
멘토에게