Skip to content

Conversation

@grimza99
Copy link
Collaborator

@grimza99 grimza99 commented Apr 6, 2025

요구사항

기본

  • 상품 등록 페이지 주소는 "/addboard" 입니다.
  • 게시판 이미지는 최대 한개 업로드가 가능합니다.
  • 각 input의 placeholder 값을 정확히 입력해주세요.
  • 이미지를 제외하고 input 에 모든 값을 입력하면 '등록' 버튼이 활성화 됩니다.
  • 상품 상세 페이지 주소는 "/board/{id}" 입니다.
  • 댓글 input 값을 입력하면 '등록' 버튼이 활성화 됩니다.
  • 활성화된 '등록' 버튼을 누르면 댓글이 등록됩니다

심화

  • 상품 등록 페이지
  • 회원가입, 로그인 api를 사용하여 받은accessToken을 사용하여 게시물 등록을 합니다.
  • '등록' 버튼을 누르면 게시물 상세 페이지로 이동합니다.

주요 변경사항

스크린샷

멘토에게

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

@grimza99 grimza99 self-assigned this Apr 6, 2025
@grimza99 grimza99 added the 매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다. label Apr 6, 2025
@kiJu2
Copy link
Collaborator

kiJu2 commented Apr 8, 2025

스프리트 미션 하시느라 수고 많으셨어요.
학습에 도움 되실 수 있게 꼼꼼히 리뷰 하도록 해보겠습니다. 😊

@kiJu2 kiJu2 closed this Apr 8, 2025
@kiJu2 kiJu2 reopened this Apr 8, 2025
//
interface Props {
onClick: onClick;
onClick?: onClick;
Copy link
Collaborator

Choose a reason for hiding this comment

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

(선택) 타입은 보통 파스칼 케이스로 사용하는게 일반적이예요 !

Suggested change
onClick?: onClick;
onClick?: OnClick;

global.d.ts에서 위와 같이 명칭을 바꾸는건 어떨까요?

//
interface Props {
onClick: onClick;
onClick?: onClick;
Copy link
Collaborator

Choose a reason for hiding this comment

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

또한, 해당 타입은 필요 없을 수도 있겠네요 !

Suggested change
onClick?: onClick;

(이어서)

Comment on lines +10 to +25
export default function Button({
onClick,
children,
className,
disabled,
...props
}: Props) {
return (
<>
<button
onClick={onClick}
className="flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer"
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer",
disabled ? "bg-gray-400" : "",
className
)}
Copy link
Collaborator

Choose a reason for hiding this comment

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

(이어서) onClick을 지우고 다음과 같이 작성해볼 수 있겠어요

Suggested change
export default function Button({
onClick,
children,
className,
disabled,
...props
}: Props) {
return (
<>
<button
onClick={onClick}
className="flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer"
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer",
disabled ? "bg-gray-400" : "",
className
)}
export default function Button({
children,
className,
disabled,
...props
}: Props) {
return (
<>
<button
className={clsx(
"flex bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer",
disabled ? "bg-gray-400" : "",
className
)}

Copy link
Collaborator

Choose a reason for hiding this comment

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

추가로 disabled도 지워볼 수 있을 것 같네요 !:

export default function Button({
  children,
  className,
  ...props
}: Props) {
  return (
    <>
      <button
        disabled={disabled}
        className={clsx(
          "flex  bg-blue-500 justify-center items-center w-full h-full rounded-[8px] border-none font-Pretendard text-H5Bold text-gray-100 bg-light-blue cursor-pointer disabled:bg-gray-400", className
        )}

disabled를 버튼의 속성으로 넣게 되면 덩달아 접근성도 좋아지겠어요 !
또한, disabled를 상태 선택자를 통해서 스타일도 입힐 수 있구요 😊😊

<input
className="relative w-full h-full bg-gray-100 font-Pretendard text-H6Regular border-none rounded-xl text-gray-800 px-4 py-6
<div className="flex flex-col gap-6 mobile:gap-[10px] ">
<p className="text-[18px] font-Pretendard text-H4Bold">{label}</p>
Copy link
Collaborator

Choose a reason for hiding this comment

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

<p> 태그는 단락을 나타냅니다 !

HTML <p> 요소는 하나의 문단을 나타냅니다. 시각적인 매체에서, 문단은 보통 인접 블록과의 여백과 첫 줄의 들여쓰기로 구분하지만, HTML에서 문단은 이미지나 입력 폼 등 서로 관련있는 콘텐츠 무엇이나 될 수 있습니다.

예를 들어 다음과 같은 값이 <p>태그에 적절할 수 있어요 😊:

Geckos are a group of usually small, usually nocturnal lizards. They are found on every continent except Antarctica.

✨ 대체 제안: <label> 태그로 변경해보세요!

Suggested change
<p className="text-[18px] font-Pretendard text-H4Bold">{label}</p>
<label htmlFor="fileUpload" className="text-[18px] font-Pretendard text-H4Bold">
{label}
</label>

혹은 이 텍스트가 진짜 단순한 스타일용 텍스트라면 도 괜찮아요. 하지만 입력 폼의 설명이나 타이틀 역할을 하고 있다면 <label>을 쓰는 걸 추천드려요!

Comment on lines +12 to +20
useEffect(() => {
const hasAccessToken = Boolean(localStorage.getItem("accessToken"));
setIsLogin(hasAccessToken);
}, []);

const handleClickLogin = async () => {
await signIn();
setIsLogin(true);
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

isLogin이라는 상태는 nav 뿐 아니라 여러 곳에서 사용될 수 있겠네요 !

이럴 때, Context API와 훅을 하나 만들어두는건 어떨까요? 추가로 isLoggin 보다 userData 상태를 가지고 있어도 되겠어요. 😊

const AuthContext = createContext(null);

export const AuthProvider = ({ children }) => {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    const token = localStorage.getItem("accessToken");
    if (token) {
      const userData = await getMy(); // 예시 입니다 !
      setUserData({ name: userData.name, email: userData.email }); // 예시 입니다 !
    }
  }, []);

  const signIn = async () => {
    const token = await login(); // 기존 코드에 `signIn` 함수와 같음
    localStorage.setItem("accessToken", token);
    const userData = await getMy(); // 예시 입니다 !
    setUserData({ name: userData.name, email: userData.email }); // 예시 입니다 !
  };

  // 추가로 signOut 함수도 필요하겠어요 !

  return (
    <AuthContext.Provider value={{ userData, signIn }}>
      {children}
    </AuthContext.Provider>
  );
};

export const useAuth = () => useContext(AuthContext);

Comment on lines +14 to +15
? useFormatUpDate(value.createdAt)
: useFormatDate(value.createdAt);
Copy link
Collaborator

Choose a reason for hiding this comment

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

두 함수는 리액트의 훅이라고 보기는 어려운 것 같아요.

유틸 함수로 사용하시는건 어떠세요?:

Suggested change
? useFormatUpDate(value.createdAt)
: useFormatDate(value.createdAt);
? formatUpDate(value.createdAt)
: formatDate(value.createdAt);

Comment on lines +15 to +33
instance.interceptors.request.use(
(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
if (typeof window !== "undefined") {
const accessToken = localStorage.getItem("accessToken");
if (!accessToken) return config;
config.headers.set("Authorization", `Bearer ${accessToken}`);
}
return config;
}
);

instance.interceptors.response.use((response: AxiosResponse) => {
const accessToken = response.data.accessToken;
if (typeof window !== "undefined" && accessToken) {
localStorage.setItem("accessToken", accessToken);
localStorage.setItem("refreshToken", response.data.refreshToken);
}
return response;
});
Copy link
Collaborator

Choose a reason for hiding this comment

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

크으 ~ 인터셉터를 적절히 사용하셨군요 👍👍👍

@kiJu2
Copy link
Collaborator

kiJu2 commented Apr 8, 2025

선향님 ~! 너무 수고 많으셨습니다.
놀랐어요 ㅋㅋㅋ 월요일 되자마자 바로 푸시하시다니.. 주말 푸욱 쉬시고 바로 제출하셨나보군요 !
프로젝트가 끝났음에도 불구하고 지치지 않고 꾸준히 노력하시는 모습이 너무 보기 좋습니다.
이번 미션 정말 수고 많으셨습니다 😊😊

@kiJu2 kiJu2 merged commit 1d793f6 into codeit-bootcamp-frontend:Next-유선향 Apr 8, 2025
1 check failed
Comment on lines +28 to +65
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const cookies = parseCookies(ctx);
const accessToken = cookies.accessToken;
const refreshToken = cookies.refreshToken;
if (!accessToken) {
return {
redirect: {
destination: "/",
},
props: {},
};
}
try {
await instance.get(`/user/me`);
} catch (err: any) {
if (err.response?.status === 401) {
try {
const refreshRes = await instance.post(
`/auth/refresh-token`,
{},
{ headers: { Cookie: `refreshToken=${refreshToken}` } }
);
const newAccessToken = refreshRes.data.accessToken;
setCookie(ctx, "accessToken", newAccessToken, {
path: "/",
httpOnly: true,
sameSite: "lax",
});
} catch {
return {
redirect: { destination: "/" },
props: {},
};
}
}
}
return { props: {} };
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

서버사이드에서 인가처리도 훌륭히 하셨네요 ! 👍

NextJs에서 서버 자원을 사용하는 방법도 훌륭히 익히신 것 같아요 😆😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

매운맛🔥 뒤는 없습니다. 그냥 필터 없이 말해주세요. 책임은 제가 집니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants