Skip to content
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
ce5acd9
fix: 문자열 따옴표 스타일 통일
minimo-9 Jul 23, 2025
6fc10df
fix: SVG 속성의 따옴표 스타일 통일
minimo-9 Jul 23, 2025
ef7211b
feat: BrandMark 컴포넌트 추가 및 SVG 구현
minimo-9 Jul 23, 2025
e54aebd
feat: Kakao 아이콘 컴포넌트 추가
minimo-9 Jul 23, 2025
ea4f735
feat: 인풋 입력 값 검증 함수 추가
minimo-9 Jul 23, 2025
cd11842
feat: User 인터페이스 추가
minimo-9 Jul 23, 2025
4753224
feat: 사용자 전역 상태 저장소(authStore) 추가
minimo-9 Jul 23, 2025
56489d5
feat: 커스텀 에러 클래스 추가
minimo-9 Jul 23, 2025
6f09491
feat: 인증 관련 커스텀 에러 클래스 추가
minimo-9 Jul 23, 2025
5e2d128
feat: accessToken 갱신을 위한 POST API 구현
minimo-9 Jul 23, 2025
570a039
feat: 로그아웃 처리 API 핸들러 추가
minimo-9 Jul 23, 2025
294c340
feat: 카카오 소셜 회원가입 처리 API 라우트 추가
minimo-9 Jul 23, 2025
b66986d
feat: 카카오 소셜 로그인 처리 API 라우트 추가
minimo-9 Jul 23, 2025
139ebe6
feat: 사용자 회원가입 폼 컴포넌트 추가 및 카카오 소셜 가입 기능 구현
minimo-9 Jul 23, 2025
d6a6884
feat: 회원가입 페이지 컴포넌트 추가
minimo-9 Jul 23, 2025
ee54cce
feat: 사용자 회원가입 처리 서버 액션 함수 추가
minimo-9 Jul 23, 2025
c90b368
feat: 카카오 회원가입 콜백 처리 페이지 컴포넌트 추가
minimo-9 Jul 23, 2025
1463919
feat: 카카오 로그인 콜백 처리 페이지 컴포넌트 추가
minimo-9 Jul 23, 2025
64dd14d
feat: 로그인 폼 컴포넌트 추가 및 카카오 소셜 로그인 기능 구현
minimo-9 Jul 23, 2025
909f916
feat: 로그인 페이지 컴포넌트 추가
minimo-9 Jul 23, 2025
4c1322d
feat: 사용자 로그인 처리 서버 액션 함수 추가
minimo-9 Jul 23, 2025
b23e131
feat: Axios 인스턴스 추가 및 401 에러 처리 로직 구현
minimo-9 Jul 23, 2025
8337ff0
feat: Axios 인스턴스 및 오류 처리 로직 추가
minimo-9 Jul 23, 2025
629f15a
Merge branch 'develop' into feat/43
minimo-9 Jul 23, 2025
53fdf29
fix: 로그인 성공 시 디버깅 로그 제거
minimo-9 Jul 23, 2025
c59beb1
Merge branch 'develop' into feat/43
minimo-9 Jul 24, 2025
a98bb93
refactor: Axios 인스턴스 주석 개선 및 코드 정리
minimo-9 Jul 24, 2025
1e6c97e
feat: 서버 환경에서 인증이 필요한 Axios 인스턴스 생성 및 쿠키 처리 기능 추가
minimo-9 Jul 24, 2025
366ef69
refactor: API 문서 주석 개선 및 axios 요청 구조 정리
minimo-9 Jul 24, 2025
0ffe8ca
feat: 사용자 정보를 가져오는 GET API 엔드포인트 추가
minimo-9 Jul 24, 2025
17703fe
feat: 인증 흐름 테스트 페이지 추가
minimo-9 Jul 24, 2025
b14c642
feat: 서버 요청 테스트 컴포넌트 추가
minimo-9 Jul 24, 2025
9ec4875
fix: refreshAccessToken 함수에서 오류 처리 개선
minimo-9 Jul 24, 2025
05c204d
fix: 카카오 회원가입 실패 시 오류 처리 개선
minimo-9 Jul 24, 2025
3a92edb
fix: 액세스 토큰 갱신 실패 시 오류 처리 개선
minimo-9 Jul 24, 2025
76e0fbc
fix: 사용자 상태 및 오류 처리 타입 개선
minimo-9 Jul 24, 2025
3a3527c
fix: 오류 처리 타입을 AxiosError로 개선
minimo-9 Jul 24, 2025
8d14fd4
fix: 로그인 함수에서 이메일 및 비밀번호 유효성 검사 추가
minimo-9 Jul 24, 2025
042f128
fix: 카카오 로그인 리디렉션 URL 유효성 검사 추가
minimo-9 Jul 24, 2025
3434e7a
fix: useUserStore를 의존성 배열에 추가하여 상태 업데이트 보장
minimo-9 Jul 24, 2025
9bddc33
fix: useEffect의 의존성 배열에서 setUser 제거
minimo-9 Jul 24, 2025
9ff49eb
fix: useEffect의 의존성 배열에 nickname 추가
minimo-9 Jul 24, 2025
b62b8e4
fix: 회원가입 입력 형식 검증 추가
minimo-9 Jul 24, 2025
162b986
fix: 카카오 로그인 및 회원가입 버튼에 type='button' 속성 추가
minimo-9 Jul 24, 2025
9efa216
fix: 액세스 토큰이 없을 경우 401 에러 응답 추가
minimo-9 Jul 24, 2025
7f687bc
fix: 비밀번호 유효성 오류 메시지의 문법 수정
minimo-9 Jul 24, 2025
65c49c4
fix: 비밀번호 정규식 특수문자 제거, 영문 숫자만 가능
minimo-9 Jul 24, 2025
398395c
fix: 카카오 아이콘 불필요한 중괄호 제거
minimo-9 Jul 24, 2025
6f85f87
fix: 닉네임 생성 useEffect 내부로 이동
minimo-9 Jul 24, 2025
b2d8c91
fix: 카카오 회원가입 유효성 검증 추가
minimo-9 Jul 24, 2025
e80b9ff
fix: 닉네임 검증 추가
minimo-9 Jul 24, 2025
a8554d7
fix: 인풋 내부 패딩 수정
minimo-9 Jul 24, 2025
a84bc80
fix: 카카오 로그인, 회원가입 페이지 CSR 강제 적용을 위한 dynamic 설정 추가
minimo-9 Jul 24, 2025
ff8ee18
fix: 빌드 오류로 인해 예외 처리
minimo-9 Jul 25, 2025
3dddc72
fix: 모듈 오류로 인해 수정
minimo-9 Jul 25, 2025
9ca1cf7
feat: InputProps에 focusColor 속성 추가
minimo-9 Jul 25, 2025
d9d63d9
feat: focus 스타일 추가
minimo-9 Jul 25, 2025
6b7cd65
feat: 인풋에 focus 스타일 추가
minimo-9 Jul 25, 2025
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
10 changes: 5 additions & 5 deletions public/assets/svg/bell.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import React from 'react';

const IconBell = ({ size = 20, color = '#A1A1A1', ...props }) => (
const IconBell = ({ size = 20, color = '#A1A1A1', ...props }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox="0 0 20 20"
viewBox='0 0 20 20'
fill={color}
{...props}
>
<path
fill={color}
d="M6.96 16.868c.7.89 1.802 1.465 3.04 1.465s2.34-.574 3.04-1.465a22.6 22.6 0 0 1-6.08 0M15.624 7.5v.586c0 .704.201 1.393.578 1.979l.923 1.435c.843 1.312.2 3.094-1.267 3.509a21.5 21.5 0 0 1-11.716 0c-1.466-.415-2.11-2.197-1.267-3.509l.923-1.435a3.66 3.66 0 0 0 .578-1.979V7.5c0-3.221 2.518-5.833 5.624-5.833s5.624 2.612 5.624 5.833"
d='M6.96 16.868c.7.89 1.802 1.465 3.04 1.465s2.34-.574 3.04-1.465a22.6 22.6 0 0 1-6.08 0M15.624 7.5v.586c0 .704.201 1.393.578 1.979l.923 1.435c.843 1.312.2 3.094-1.267 3.509a21.5 21.5 0 0 1-11.716 0c-1.466-.415-2.11-2.197-1.267-3.509l.923-1.435a3.66 3.66 0 0 0 .578-1.979V7.5c0-3.221 2.518-5.833 5.624-5.833s5.624 2.612 5.624 5.833'
/>
</svg>
Comment on lines 4 to 16
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

접근성을 위해 SVG에 대체 텍스트를 추가해주세요.

정적 분석 도구에서 지적한 대로, 스크린 리더 사용자를 위해 SVG에 대체 텍스트가 필요합니다.

다음 중 한 가지 방법으로 해결할 수 있습니다:

 const IconBell = ({ size = 20, color = '#A1A1A1', ...props }) => (
   <svg
     xmlns='http://www.w3.org/2000/svg'
+    role='img'
+    aria-label='알림'
     width={size}
     height={size}
     viewBox='0 0 20 20'
     fill={color}
     {...props}
   >

또는 title 요소를 추가하는 방법:

   <svg
     xmlns='http://www.w3.org/2000/svg'
     width={size}
     height={size}
     viewBox='0 0 20 20'
     fill={color}
     {...props}
   >
+    <title>알림</title>
     <path
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
viewBox="0 0 20 20"
viewBox='0 0 20 20'
fill={color}
{...props}
>
<path
fill={color}
d="M6.96 16.868c.7.89 1.802 1.465 3.04 1.465s2.34-.574 3.04-1.465a22.6 22.6 0 0 1-6.08 0M15.624 7.5v.586c0 .704.201 1.393.578 1.979l.923 1.435c.843 1.312.2 3.094-1.267 3.509a21.5 21.5 0 0 1-11.716 0c-1.466-.415-2.11-2.197-1.267-3.509l.923-1.435a3.66 3.66 0 0 0 .578-1.979V7.5c0-3.221 2.518-5.833 5.624-5.833s5.624 2.612 5.624 5.833"
d='M6.96 16.868c.7.89 1.802 1.465 3.04 1.465s2.34-.574 3.04-1.465a22.6 22.6 0 0 1-6.08 0M15.624 7.5v.586c0 .704.201 1.393.578 1.979l.923 1.435c.843 1.312.2 3.094-1.267 3.509a21.5 21.5 0 0 1-11.716 0c-1.466-.415-2.11-2.197-1.267-3.509l.923-1.435a3.66 3.66 0 0 0 .578-1.979V7.5c0-3.221 2.518-5.833 5.624-5.833s5.624 2.612 5.624 5.833'
/>
</svg>
<svg
xmlns='http://www.w3.org/2000/svg'
role='img'
aria-label='알림'
width={size}
height={size}
viewBox='0 0 20 20'
fill={color}
{...props}
>
<path
fill={color}
d='M6.96 16.868c.7.89 1.802 1.465 3.04 1.465s2.34-.574 3.04-1.465a22.6 22.6 0 0 1-6.08 0M15.624 7.5v.586c0 .704.201 1.393.578 1.979l.923 1.435c.843 1.312.2 3.094-1.267 3.509a21.5 21.5 0 0 1-11.716 0c-1.466-.415-2.11-2.197-1.267-3.509l.923-1.435a3.66 3.66 0 0 0 .578-1.979V7.5c0-3.221 2.518-5.833 5.624-5.833s5.624 2.612 5.624 5.833'
/>
</svg>
🧰 Tools
🪛 Biome (2.1.2)

[error] 4-11: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In public/assets/svg/bell.tsx around lines 4 to 16, the SVG element lacks
alternative text for accessibility. To fix this, add a <title> element inside
the SVG with a descriptive text or include an aria-label attribute on the SVG
tag to provide screen readers with meaningful information about the icon. This
will improve accessibility for users relying on assistive technologies.

);

export default IconBell;
export default IconBell;
30 changes: 30 additions & 0 deletions public/assets/svg/brand-mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';

const BrandMark = ({
width = 340,
height = 192,
color = '#0B3B2D',
...props
}) => (
Comment on lines +3 to +8
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

타입스크립트 인터페이스를 추가하여 타입 안전성을 개선하세요.

props에 대한 타입 정의를 추가하면 타입 안전성이 향상됩니다.

+interface BrandMarkProps {
+  width?: number;
+  height?: number;
+  color?: string;
+  [key: string]: any;
+}
+
-const BrandMark = ({
+const BrandMark: React.FC<BrandMarkProps> = ({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const BrandMark = ({
width = 340,
height = 192,
color = '#0B3B2D',
...props
}) => (
interface BrandMarkProps {
width?: number;
height?: number;
color?: string;
[key: string]: any;
}
const BrandMark: React.FC<BrandMarkProps> = ({
width = 340,
height = 192,
color = '#0B3B2D',
...props
}) => (
🤖 Prompt for AI Agents
In public/assets/svg/brand-mark.tsx around lines 3 to 8, the BrandMark component
lacks a TypeScript interface for its props, reducing type safety. Define a
TypeScript interface specifying the types for width, height, color, and any
additional props, then apply this interface to the component's props parameter
to improve type safety and clarity.

<svg
xmlns='http://www.w3.org/2000/svg'
width={width}
height={height}
fill={color}
viewBox='0 0 340 192'
{...props}
>
Comment on lines +9 to +16
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

접근성을 위해 SVG에 대체 텍스트를 추가하세요.

스크린 리더 사용자를 위해 SVG에 title 요소나 aria-label 속성을 추가해야 합니다.

  <svg
    xmlns='http://www.w3.org/2000/svg'
    width={width}
    height={height}
    fill={color}
    viewBox='0 0 340 192'
+   role='img'
+   aria-label='GlobalNomad 브랜드 마크'
    {...props}
  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
xmlns='http://www.w3.org/2000/svg'
width={width}
height={height}
fill={color}
viewBox='0 0 340 192'
{...props}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width={width}
height={height}
fill={color}
viewBox='0 0 340 192'
role='img'
aria-label='GlobalNomad 브랜드 마크'
{...props}
>
🧰 Tools
🪛 Biome (2.1.2)

[error] 9-16: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In public/assets/svg/brand-mark.tsx around lines 9 to 16, the SVG element lacks
accessible alternative text for screen readers. To fix this, add a <title>
element inside the SVG with a descriptive text or include an aria-label
attribute on the SVG element to provide meaningful alternative text for
accessibility.

<path
fill={color}
d='M334.004 169.556c3.315 0 5.995-2.643 5.995-5.903s-2.68-5.903-5.995-5.903c-3.31 0-5.994 2.643-5.994 5.903s2.684 5.903 5.994 5.903M18.409 191.999q-4 0-7.373-1.234-3.325-1.281-5.831-3.605a16.9 16.9 0 0 1-3.856-5.457Q0 178.573 0 174.824q0-3.748 1.35-6.879a16.6 16.6 0 0 1 3.903-5.456q2.505-2.325 5.88-3.558 3.372-1.281 7.42-1.281 4.482 0 8.048 1.471 3.615 1.47 6.072 4.269l-5.012 4.555q-1.83-1.897-4-2.799-2.168-.95-4.722-.949-2.458 0-4.482.759a10.3 10.3 0 0 0-3.518 2.182 10.3 10.3 0 0 0-2.265 3.369q-.77 1.946-.77 4.317 0 2.325.77 4.27a10.8 10.8 0 0 0 2.265 3.416 10.4 10.4 0 0 0 3.47 2.182q2.024.76 4.434.76 2.313 0 4.481-.712 2.217-.759 4.29-2.515l4.433 5.551q-2.747 2.04-6.41 3.131-3.614 1.092-7.228 1.092m6.506-5.219v-12.478h7.132v13.474zm11.926 4.649v-35.202h7.518v35.202zm24.904.38q-4.144 0-7.373-1.708-3.18-1.708-5.06-4.649-1.831-2.989-1.831-6.785 0-3.843 1.831-6.784 1.88-2.989 5.06-4.65 3.229-1.707 7.373-1.707 4.096 0 7.325 1.707 3.229 1.661 5.06 4.602t1.831 6.832q0 3.796-1.831 6.785-1.831 2.941-5.06 4.649t-7.325 1.708m0-6.073q1.88 0 3.374-.854 1.493-.854 2.36-2.419.869-1.614.868-3.796 0-2.23-.867-3.795-.868-1.566-2.361-2.42t-3.374-.854-3.373.854q-1.495.854-2.41 2.42-.867 1.565-.867 3.795 0 2.182.867 3.796.916 1.565 2.41 2.419t3.373.854m32.64 6.073q-3.424 0-5.88-1.423-2.459-1.424-3.76-4.318-1.3-2.941-1.301-7.401 0-4.507 1.35-7.401 1.396-2.893 3.855-4.317 2.457-1.423 5.735-1.423 3.662 0 6.553 1.612 2.94 1.614 4.627 4.555 1.734 2.942 1.735 6.974 0 3.985-1.735 6.927a12.1 12.1 0 0 1-4.627 4.602q-2.891 1.613-6.553 1.613m-15.327-.38v-35.202h7.519v14.849l-.482 7.544.144 7.591v5.218zm14.025-5.693q1.878 0 3.325-.854 1.494-.854 2.36-2.419.917-1.614.917-3.796 0-2.23-.917-3.795-.866-1.566-2.36-2.42-1.447-.854-3.325-.854-1.88 0-3.374.854t-2.361 2.42q-.869 1.565-.868 3.795 0 2.182.868 3.796.867 1.565 2.36 2.419 1.495.854 3.375.854m32.799 5.693v-4.981l-.482-1.091v-8.92q.001-2.372-1.493-3.7-1.446-1.329-4.481-1.329-2.073 0-4.097.665-1.975.616-3.372 1.708l-2.699-5.172q2.12-1.47 5.107-2.277a23.2 23.2 0 0 1 6.073-.806q5.927 0 9.204 2.751t3.277 8.587v14.565zm-7.902.38q-3.037 0-5.205-.996-2.167-1.044-3.325-2.8-1.156-1.756-1.156-3.937 0-2.278 1.108-3.986 1.157-1.708 3.614-2.656 2.458-.997 6.41-.997h6.891v4.318h-6.073q-2.65 0-3.662.854-.964.854-.963 2.135 0 1.423 1.108 2.277 1.156.807 3.132.806 1.879 0 3.374-.854 1.493-.9 2.167-2.609l1.158 3.416q-.82 2.467-2.987 3.748-2.17 1.281-5.591 1.281m19.815-.38v-35.202h7.517v35.202zm13.098 0v-33.21h6.456l19.904 23.911h-3.133v-23.911h7.71v33.21h-6.408l-19.951-23.911h3.133v23.911zm48.798.38q-4.146 0-7.373-1.708-3.18-1.708-5.059-4.649-1.833-2.989-1.831-6.785-.002-3.843 1.831-6.784 1.88-2.989 5.059-4.65 3.227-1.707 7.373-1.707 4.096 0 7.326 1.707 3.227 1.661 5.059 4.602 1.831 2.942 1.831 6.832 0 3.796-1.831 6.785-1.832 2.941-5.059 4.649-3.23 1.708-7.326 1.708m0-6.073q1.88 0 3.374-.854 1.492-.854 2.361-2.419.868-1.614.868-3.796 0-2.23-.868-3.795-.87-1.566-2.361-2.42-1.493-.854-3.374-.854-1.88 0-3.374.854-1.491.854-2.408 2.42-.868 1.565-.868 3.795 0 2.182.868 3.796.917 1.565 2.408 2.419 1.494.854 3.374.854m50.518-20.21c2.056 0 3.869.411 5.447 1.233q2.405 1.186 3.757 3.701c.93 1.644 1.399 3.763 1.399 6.357v14.612h-7.519v-13.473q0-3.085-1.3-4.555-1.302-1.47-3.663-1.471-1.64 0-2.939.759-1.302.712-2.025 2.183t-.723 3.748v12.809h-7.517v-13.473q0-3.085-1.302-4.555-1.255-1.47-3.613-1.471c-1.096 0-2.073.253-2.94.759q-1.301.712-2.025 2.183-.723 1.47-.723 3.748v12.809h-7.517v-25.524h7.181v6.974l-1.349-2.04q1.347-2.609 3.807-3.938 2.505-1.375 5.685-1.375 3.564 0 6.217 1.802 2.697 1.755 3.566 5.409l-2.651-.712q1.302-2.99 4.145-4.744 2.892-1.755 6.602-1.755m31.316 25.903v-4.981l-.482-1.091v-8.92q0-2.372-1.493-3.7-1.446-1.329-4.483-1.329-2.073 0-4.095.665-1.979.616-3.374 1.708l-2.699-5.172q2.118-1.47 5.109-2.277a23.2 23.2 0 0 1 6.071-.806q5.928 0 9.204 2.751t3.277 8.587v14.565zm-7.904.38q-3.034 0-5.203-.996-2.17-1.044-3.325-2.8-1.159-1.756-1.158-3.937 0-2.278 1.108-3.986 1.156-1.708 3.616-2.656 2.455-.997 6.408-.997h6.891v4.318h-6.071q-2.653 0-3.663.854-.965.854-.964 2.135 0 1.423 1.109 2.277 1.156.807 3.133.806 1.877 0 3.372-.854 1.493-.9 2.169-2.609l1.156 3.416q-.819 2.467-2.987 3.748-2.17 1.281-5.591 1.281m31.134 0q-3.66 0-6.602-1.613a12.5 12.5 0 0 1-4.674-4.602q-1.686-2.942-1.687-6.927.001-4.032 1.687-6.974 1.737-2.941 4.674-4.555c1.963-1.075 4.161-1.612 6.602-1.612q3.28 0 5.735 1.423 2.458 1.424 3.807 4.317 1.351 2.893 1.35 7.401 0 4.46-1.302 7.401-1.3 2.894-3.758 4.318-2.408 1.423-5.832 1.423m1.302-6.073q1.833 0 3.325-.854 1.497-.854 2.361-2.419.918-1.614.917-3.796.001-2.23-.917-3.795-.864-1.566-2.361-2.42-1.492-.854-3.325-.854-1.877 0-3.374.854-1.492.854-2.408 2.42-.868 1.565-.868 3.795 0 2.182.868 3.796.916 1.565 2.408 2.419 1.497.854 3.374.854m6.794 5.693v-5.218l.145-7.591-.481-7.544v-14.849h7.517v35.202z'
/>
<path
fill={color}
fillRule='evenodd'
d='M170.29 0c35.142 0 63.63 28.11 63.63 62.784s-28.488 62.783-63.63 62.783c-35.141 0-63.629-28.109-63.629-62.783S135.149 0 170.29 0m-2.3 4.698c-7.258 1.008-14.17 6.763-19.536 16.39-1.556 2.791-2.961 5.876-4.184 9.207 7.295-1.81 15.31-2.876 23.72-3.041zm-29.098 27.105c1.522-4.713 3.383-9.049 5.532-12.903 2.66-4.772 5.816-8.892 9.373-12.057-19.34 5.542-34.584 20.583-40.201 39.666 3.207-3.51 7.384-6.624 12.22-9.248 3.906-2.12 8.3-3.957 13.076-5.459m-1.528 5.305c-1.834 7.2-2.915 15.107-3.082 23.406h-22.86c1.022-7.163 6.855-13.982 16.61-19.276 2.83-1.536 5.956-2.922 9.332-4.13m1.519 23.406c.19-9.057 1.511-17.56 3.697-25.072 7.614-2.157 16.232-3.461 25.41-3.649v9.804c-3.424 8.65-10.408 15.54-19.174 18.917zm-4.601 4.539h-22.86c1.022 7.163 6.855 13.982 16.61 19.277 2.83 1.535 5.956 2.92 9.332 4.129-1.834-7.2-2.915-15.107-3.082-23.406m8.298 25.072c-2.186-7.512-3.507-16.015-3.697-25.072h9.933c8.766 3.377 15.751 10.268 19.174 18.918v9.803c-9.178-.187-17.796-1.492-25.41-3.649m-3.688 3.64c-4.776-1.502-9.17-3.339-13.076-5.458-4.836-2.625-9.013-5.74-12.22-9.25 5.617 19.084 20.861 34.125 40.201 39.667-3.557-3.165-6.713-7.285-9.373-12.057-2.149-3.854-4.01-8.19-5.532-12.902m29.098 27.104c-7.258-1.008-14.17-6.763-19.536-16.389-1.556-2.792-2.961-5.877-4.184-9.207 7.295 1.809 15.31 2.875 23.72 3.04zm18.794-2.145c3.556-3.165 6.714-7.285 9.373-12.057 2.148-3.854 4.01-8.19 5.532-12.902 4.776-1.502 9.17-3.339 13.076-5.458 4.836-2.625 9.013-5.74 12.22-9.25-5.617 19.084-20.861 34.125-40.201 39.667m9.527-23.451c-1.224 3.33-2.628 6.415-4.184 9.207-5.365 9.626-12.278 15.381-19.537 16.389V98.313c8.411-.165 16.426-1.231 23.721-3.04m6.906-6.814c3.375-1.208 6.502-2.594 9.332-4.13 9.755-5.294 15.588-12.113 16.609-19.276h-22.859c-.168 8.299-1.248 16.207-3.082 23.406m-1.519-23.406c-.189 9.057-1.512 17.56-3.697 25.072-7.614 2.157-16.232 3.462-25.411 3.65v-9.797c3.423-8.653 10.409-15.547 19.178-18.925zm4.601-4.539h22.859c-1.021-7.163-6.854-13.982-16.609-19.276-2.83-1.536-5.957-2.922-9.332-4.13 1.834 7.2 2.914 15.107 3.082 23.406m-8.298-25.072c2.185 7.513 3.508 16.015 3.697 25.072h-9.93c-8.769-3.378-15.755-10.272-19.178-18.925v-9.796c9.179.188 17.797 1.492 25.411 3.649m3.688-3.64c4.776 1.502 9.17 3.339 13.076 5.459 4.836 2.624 9.013 5.739 12.22 9.248-5.617-19.083-20.861-34.124-40.201-39.666 3.556 3.165 6.714 7.285 9.373 12.057 2.148 3.854 4.01 8.19 5.532 12.903M172.59 4.698c7.259 1.008 14.172 6.763 19.537 16.39 1.556 2.791 2.96 5.876 4.184 9.207-7.295-1.81-15.31-2.876-23.721-3.041z'
clipRule='evenodd'
/>
</svg>
);

export default BrandMark;
25 changes: 25 additions & 0 deletions public/assets/svg/kakao.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';

const IconKakao = ({ size = 72, ...props }) => (
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 정의를 추가하여 타입 안전성을 개선하세요.

컴포넌트의 props에 대한 타입 정의가 없어 타입 안전성이 떨어집니다.

다음과 같이 타입을 정의하세요:

import React from 'react';

+interface IconKakaoProps {
+  size?: number;
+  className?: string;
+  onClick?: () => void;
+  [key: string]: any; // 추가 props를 위한 인덱스 시그니처
+}
+
-const IconKakao = ({ size = 72, ...props }) => (
+const IconKakao: React.FC<IconKakaoProps> = ({ size = 72, ...props }) => (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const IconKakao = ({ size = 72, ...props }) => (
import React from 'react';
interface IconKakaoProps {
size?: number;
className?: string;
onClick?: () => void;
[key: string]: any; // 추가 props를 위한 인덱스 시그니처
}
const IconKakao: React.FC<IconKakaoProps> = ({ size = 72, ...props }) => (
<svg
width={size}
height={size}
viewBox="0 0 72 72"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
{/* …existing SVG paths and elements… */}
</svg>
);
export default IconKakao;
🤖 Prompt for AI Agents
In public/assets/svg/kakao.tsx at line 3, the IconKakao component lacks explicit
prop type definitions, reducing type safety. Define a TypeScript interface or
type for the component props, including the size property and any other props,
then apply this type to the component's props parameter to improve type safety
and clarity.

<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 72 72'
{...props}
>
Comment on lines +4 to +11
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

접근성을 위해 title 또는 aria-label을 추가하세요.

정적 분석 도구에서 지적한 대로, 스크린 리더 사용자를 위해 SVG에 대체 텍스트를 제공해야 합니다.

다음 중 하나의 방법으로 접근성을 개선하세요:

방법 1: title 요소 추가

  <svg
    xmlns='http://www.w3.org/2000/svg'
    width={size}
    height={size}
    fill='none'
    viewBox='0 0 72 72'
    {...props}
  >
+    <title>카카오 로그인</title>
    <circle cx='36' cy='35.998' r='35.25' stroke='#F2F2F2' strokeWidth='1.5' />

방법 2: aria-label 추가

  <svg
    xmlns='http://www.w3.org/2000/svg'
    width={size}
    height={size}
    fill='none'
    viewBox='0 0 72 72'
+    aria-label='카카오 로그인'
+    role='img'
    {...props}
  >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 72 72'
{...props}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 72 72'
{...props}
>
<title>카카오 로그인</title>
<circle cx='36' cy='35.998' r='35.25' stroke='#F2F2F2' strokeWidth='1.5' />
<!-- …rest of SVG… -->
</svg>
Suggested change
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 72 72'
{...props}
>
<svg
xmlns='http://www.w3.org/2000/svg'
width={size}
height={size}
fill='none'
viewBox='0 0 72 72'
aria-label='카카오 로그인'
role='img'
{...props}
>
<circle cx='36' cy='35.998' r='35.25' stroke='#F2F2F2' strokeWidth='1.5' />
<!-- …rest of SVG… -->
</svg>
🧰 Tools
🪛 Biome (2.1.2)

[error] 4-11: Alternative text title element cannot be empty

For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.

(lint/a11y/noSvgWithoutTitle)

🤖 Prompt for AI Agents
In public/assets/svg/kakao.tsx around lines 4 to 11, the SVG element lacks
accessibility features for screen readers. To fix this, add either a <title>
element inside the SVG with descriptive text or include an aria-label attribute
on the SVG element with a meaningful label. This will provide alternative text
for screen readers and improve accessibility.

<circle cx='36' cy='35.998' r='35.25' stroke='#F2F2F2' strokeWidth='1.5' />
<g fill='#331D1E' clipPath='url(#clip0_33762_8396)'>
<path d='M31.963 35.086h1.728l-.864-2.395z'></path>
<path d='M36 22.498c-8.285 0-15 5.164-15 11.535 0 4.119 2.808 7.737 7.031 9.774-.23.772-1.477 4.97-1.526 5.301 0 0-.03.248.134.342a.46.46 0 0 0 .358.021c.473-.064 5.48-3.49 6.346-4.08q1.322.18 2.657.177c8.285 0 15-5.164 15-11.535s-6.715-11.535-15-11.535m-7.424 9.744c-.018 1.632.015 3.348-.012 4.955-.01.513-.312.666-.722.813a.3.3 0 0 1-.144.01c-.469-.09-.842-.254-.854-.822-.033-1.605.01-3.324-.013-4.956-.396-.015-.962.016-1.33 0-.51-.032-.865-.349-.843-.82.021-.471.28-.81.852-.819 1.353-.02 3.029-.02 4.382 0 .577.009.834.35.85.82.018.469-.33.787-.84.82-.364.015-.928-.016-1.326 0m7.27 5.69a1.4 1.4 0 0 1-.551.117c-.36 0-.636-.14-.721-.373l-.433-1.094h-2.634l-.433 1.094c-.083.23-.359.373-.72.373-.19 0-.378-.04-.55-.117-.24-.107-.469-.402-.206-1.198l2.075-5.315c.087-.234.243-.438.448-.586a1.3 1.3 0 0 1 .706-.246c.255.014.5.1.705.248.205.149.36.352.449.586l2.068 5.311c.264.798.035 1.099-.203 1.2m4.373 0h-2.777a.83.83 0 0 1-.578-.22.79.79 0 0 1-.251-.553V31.43a.85.85 0 0 1 .271-.588.896.896 0 0 1 1.225 0 .85.85 0 0 1 .27.587v4.96h1.84a.81.81 0 0 1 .592.209.78.78 0 0 1 .25.564.76.76 0 0 1-.25.564.8.8 0 0 1-.592.21zm6.779-.636a.83.83 0 0 1-.21.449.886.886 0 0 1-.935.237.86.86 0 0 1-.407-.292l-2.03-2.62-.3.292v1.842a.83.83 0 0 1-.253.596.88.88 0 0 1-.612.248.88.88 0 0 1-.612-.248.83.83 0 0 1-.253-.596v-5.768c0-.224.09-.438.253-.597a.877.877 0 0 1 1.224 0 .83.83 0 0 1 .253.597v1.81l2.415-2.356a.67.67 0 0 1 .48-.188.9.9 0 0 1 .58.233.85.85 0 0 1 .271.552.65.65 0 0 1-.189.513l-1.976 1.923 2.13 2.753a.83.83 0 0 1 .166.625z'></path>
</g>
<defs>
<clipPath id='clip0_33762_8396'>
<path fill='#fff' d='M21 22.498h30v27H21z'></path>
</clipPath>
</defs>
</svg>
);

export default IconKakao;
100 changes: 100 additions & 0 deletions src/apis/instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import axios from 'axios';

/**
* Axios 인스턴스
*
* - 기본 `baseURL`은 환경 변수에서 설정
* - 요청 시간 초과는 5000ms (5초)
* - 모든 요청의 기본 Content-Type은 `application/json`
*/
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
timeout: 5000,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
});
Comment on lines +10 to +14
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

타임아웃 설정을 환경에 따라 조정 가능하도록 개선하세요.

5초 타임아웃이 일부 작업에는 짧을 수 있습니다. 환경 변수로 설정 가능하도록 하는 것을 고려해보세요.

const instance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
-  timeout: 5000,
+  timeout: Number(process.env.NEXT_PUBLIC_API_TIMEOUT) || 5000,
  headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
timeout: 5000,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
});
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
timeout: Number(process.env.NEXT_PUBLIC_API_TIMEOUT) || 5000,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
});
🤖 Prompt for AI Agents
In src/apis/instance.ts around lines 10 to 14, the axios instance timeout is
hardcoded to 5000ms, which may be too short for some tasks. Modify the timeout
setting to read from an environment variable, for example
process.env.API_TIMEOUT, and provide a default value of 5000ms if the variable
is not set. This allows adjusting the timeout dynamically based on the
deployment environment.


/**
* 오류 메시지 생성 함수
*
* Axios 또는 일반 에러 객체에서 사용자 친화적인 에러 메시지를 추출합니다.
*
* @param {unknown} error - Axios 요청 중 발생한 에러 객체
* @returns {string} - 사용자에게 표시할 수 있는 에러 메시지
*/
const getErrorMessage = (error: unknown): string => {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
const message = error.response?.data?.message;

if (typeof message === 'string') return message;
if (status) {
switch (status) {
case 400:
return '🚨 잘못된 요청입니다. (400)';
case 401:
return '🚨 인증이 필요합니다. (401)';
case 403:
return '🚨 권한이 없습니다. (403)';
case 404:
return '🚨 요청한 리소스를 찾을 수 없습니다. (404)';
case 429:
return '🚨 요청이 너무 많습니다. 잠시 후 다시 시도해주세요. (429)';
case 500:
return '🚨 서버 내부 오류가 발생했습니다. (500)';
default:
return `🚨 요청에 실패했습니다. (Status: ${status})`;
}
}
}

if (error instanceof Error && error.message === 'Network Error') {
return '🚨 네트워크 오류가 발생했습니다. 인터넷 연결을 확인해주세요.';
}

return '🚨 알 수 없는 오류가 발생했습니다.';
};

/** 최대 재시도 횟수 */
const MAX_RETRY = 3;

/**
* URL별 재시도 횟수를 추적하기 위한 Map
*
* 키: 요청 URL
* 값: 현재까지의 재시도 횟수
*/
const retryCounts = new Map<string, number>();

/**
* Axios 응답 인터셉터
*
* - 네트워크 오류 또는 5xx 서버 오류 발생 시 자동으로 재시도
* - 요청 URL 기준으로 재시도 횟수를 제한
* - 최대 재시도 횟수(`MAX_RETRY`) 초과 시 오류 메시지를 반환
*/
instance.interceptors.response.use(
(res) => res,
async (err) => {
const config = err.config;

if (!config || !config.url) {
return Promise.reject(new Error(getErrorMessage(err)));
}

const currentRetry = retryCounts.get(config.url) || 0;

if (
(err.message === 'Network Error' ||
(err.response && err.response.status >= 500)) &&
currentRetry < MAX_RETRY
) {
retryCounts.set(config.url, currentRetry + 1);
return instance(config); // 재시도
}

retryCounts.delete(config.url); // 메모리 누수 방지
return Promise.reject(new Error(getErrorMessage(err)));
},
);
Comment on lines +75 to +98
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

재시도 로직에 지수 백오프를 추가하는 것을 고려해보세요.

현재 재시도 로직은 즉시 재시도를 수행합니다. 서버 부하를 줄이기 위해 지수 백오프(exponential backoff)를 구현하는 것이 좋습니다.

      currentRetry < MAX_RETRY
    ) {
      retryCounts.set(config.url, currentRetry + 1);
+     // 지수 백오프: 1초, 2초, 4초 대기
+     await new Promise(resolve => setTimeout(resolve, Math.pow(2, currentRetry) * 1000));
      return instance(config); // 재시도
    }
🤖 Prompt for AI Agents
In src/apis/instance.ts between lines 75 and 98, the retry logic immediately
retries failed requests without delay, which can increase server load. Modify
the retry mechanism to include exponential backoff by adding a delay before each
retry attempt. Calculate the delay time exponentially based on the current retry
count (e.g., using a base delay multiplied by 2 to the power of the retry
count), and use an async wait or timeout function to pause before retrying the
request.


export { instance };
53 changes: 53 additions & 0 deletions src/apis/privateInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import axios from 'axios';

/**
* 인증이 필요한 클라이언트 요청을 처리하기 위한 Axios 인스턴스입니다.
*
* - 기본 baseURL은 `/api`입니다.
* - 모든 요청에 `application/json` 헤더가 포함됩니다.
* - 응답으로 401 Unauthorized가 반환되면, `/api/auth/refresh`를 호출하여 accessToken을 재발급받습니다.
* - 재발급에 성공하면, 실패했던 원래 요청을 한 번만 재시도합니다.
* - 재시도 여부는 `_retry` 플래그로 제어합니다.
*
* @module privateInstance
*/

const privateInstance = axios.create({
baseURL: '/api',
timeout: 5000,
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
});

privateInstance.interceptors.response.use(
(res) => res,
/**
* 응답 인터셉터
*
* 401 Unauthorized 응답이 발생한 경우:
* - accessToken 재발급을 위해 `/api/auth/refresh` 요청을 보냅니다.
* - 재발급 성공 시, 원래 요청에 새로운 토큰을 추가하여 재시도합니다.
* - 같은 요청이 반복되지 않도록 `originalRequest._retry` 플래그로 제어합니다.
*/
async (error) => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
const { data } = await axios.post('/api/auth/refresh');
const newAccessToken = data.accessToken;

originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;

return privateInstance(originalRequest);
Comment on lines +38 to +43
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

토큰 갱신 실패 시 보안 고려사항을 검토하세요.

현재 구현에서는 토큰 갱신 실패 시 단순히 에러를 반환하는데, 사용자를 로그인 페이지로 리디렉션하는 등의 추가 처리가 필요할 수 있습니다.

다른 파일에서 토큰 갱신 실패 시 어떻게 처리하는지 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# 토큰 갱신 실패 시 처리 로직 확인
rg -A 5 -B 5 "refresh.*fail|갱신.*실패|logout|로그아웃" --type ts --type tsx

Length of output: 98


토큰 갱신 실패 처리 로직이 다른 파일에 구현되어 있는지 다시 확인하기 위해 .ts.tsx 파일을 대상으로 확장자 필터를 사용해 검색을 실행합니다.


🏁 Script executed:

#!/bin/bash
# 토큰 갱신 실패 시 처리 로직 확인 (확장자 필터 적용)
rg -g '*.ts' -g '*.tsx' -A5 -B5 "refresh.*fail|갱신.*실패|logout|로그아웃"

Length of output: 3653


토큰 갱신 실패 시 사용자 로그아웃 및 로그인 페이지 리디렉션 처리 필요

현재 privateInstance의 리프레시 실패 구간(38–43행)은 단순히 에러를 반환만 하고 있어, 클라이언트에 남아 있는 사용자 인증 상태를 초기화하거나 로그인 페이지로 이동시키는 보안 처리가 빠져 있습니다.

  • 수정 대상:
    • 파일: src/apis/privateInstance.ts
    • 위치: 약 38–43행 (토큰 갱신 후 재시도 로직)

예시 변경안:

--- src/apis/privateInstance.ts
+++ src/apis/privateInstance.ts
@@ -1,6 +1,7 @@
 import axios from 'axios';
+import { useUserStore } from '../stores/authStore';

 const privateInstance = axios.create({ /* ... */ });

 privateInstance.interceptors.response.use(
   response => response,
   async error => {
     const originalRequest = error.config;
     if (
       error.response?.status === 401 &&
       !originalRequest._retry
     ) {
       originalRequest._retry = true;
       try {
         const { data } = await axios.post('/api/auth/refresh');
         const newAccessToken = data.accessToken;
         originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
         return privateInstance(originalRequest);
       } catch (refreshError) {
+        // 토큰 갱신 실패 시 사용자 상태 초기화 및 로그인 페이지 리디렉션
+        useUserStore.getState().clearUser();
+        window.location.href = '/login';
         return Promise.reject(refreshError);
       }
     }
     return Promise.reject(error);
   }
 );

위와 같이 실패 시 사용자 상태를 clearUser()로 초기화하고, 로그인 페이지로 이동하도록 처리해 주세요.

🤖 Prompt for AI Agents
In src/apis/privateInstance.ts around lines 38 to 43, the token refresh failure
handling only returns an error without clearing user authentication state or
redirecting to the login page. Modify the catch block for the token refresh
request to call clearUser() to reset user state and then redirect the client to
the login page to ensure proper logout and security.

} catch (refreshError) {
return Promise.reject(refreshError);
}
}

return Promise.reject(error);
},
);

export { privateInstance };
95 changes: 95 additions & 0 deletions src/apis/privateServerInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import axios, { AxiosInstance } from 'axios';
import { cookies } from 'next/headers';

/**
* 서버 환경에서 쿠키를 문자열로 변환하여 Authorization 요청 시 사용할 수 있도록 반환합니다.
*
* @returns {Promise<string>} - `name=value` 형식의 쿠키 문자열
*/
Comment on lines +4 to +8
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

JSDoc 설명 수정 필요

JSDoc 설명에서 "Authorization 요청 시"라고 되어 있지만, 실제로는 Cookie 헤더에 사용됩니다.

 /**
- * 서버 환경에서 쿠키를 문자열로 변환하여 Authorization 요청 시 사용할 수 있도록 반환합니다.
+ * 서버 환경에서 쿠키를 문자열로 변환하여 Cookie 헤더에 사용할 수 있도록 반환합니다.
  *
  * @returns {Promise<string>} - `name=value` 형식의 쿠키 문자열
  */
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* 서버 환경에서 쿠키를 문자열로 변환하여 Authorization 요청 사용할 있도록 반환합니다.
*
* @returns {Promise<string>} - `name=value` 형식의 쿠키 문자열
*/
/**
* 서버 환경에서 쿠키를 문자열로 변환하여 Cookie 헤더에 사용할 있도록 반환합니다.
*
* @returns {Promise<string>} - `name=value` 형식의 쿠키 문자열
*/
🤖 Prompt for AI Agents
In src/apis/privateServerInstance.ts around lines 4 to 8, update the JSDoc
comment to correctly describe that the returned cookie string is used in the
Cookie header, not specifically for Authorization requests. Modify the
description to clarify that the function returns a cookie string formatted as
"name=value" for use in HTTP Cookie headers.

const getCookieHeader = async () => {
const cookieStore = await cookies();
return cookieStore
.getAll()
.map((token) => `${token.name}=${token.value}`)
.join(';');
};

/**
* 서버에서 accessToken이 만료되었을 때, refreshToken을 사용하여 새로운 accessToken을 발급받습니다.
*
* @returns {Promise<string | null>} - 재발급된 accessToken (실패 시 null 반환)
*/
const refreshAccessToken = async (): Promise<string | null> => {
try {
const res = await axios.post(
`${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/refresh`,
{},
{ headers: { Cookie: await getCookieHeader() } },
);

return res.data.accessToken;
} catch {
return null;
}
};
Comment on lines +22 to +34
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

에러 로깅 추가 고려

토큰 갱신 실패 시 디버깅을 위해 에러 로깅을 추가하는 것이 좋습니다.

 const refreshAccessToken = async (): Promise<string | null> => {
   try {
     const res = await axios.post(
       `${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/refresh`,
       {},
       { headers: { Cookie: await getCookieHeader() } },
     );
 
     return res.data.accessToken;
-  } catch {
+  } catch (error) {
+    console.error('[서버] 토큰 갱신 실패:', error);
     return null;
   }
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const refreshAccessToken = async (): Promise<string | null> => {
try {
const res = await axios.post(
`${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/refresh`,
{},
{ headers: { Cookie: await getCookieHeader() } },
);
return res.data.accessToken;
} catch {
return null;
}
};
const refreshAccessToken = async (): Promise<string | null> => {
try {
const res = await axios.post(
`${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/refresh`,
{},
{ headers: { Cookie: await getCookieHeader() } },
);
return res.data.accessToken;
} catch (error) {
console.error('[서버] 토큰 갱신 실패:', error);
return null;
}
};
🤖 Prompt for AI Agents
In src/apis/privateServerInstance.ts around lines 22 to 34, the
refreshAccessToken function catches errors but does not log them, which hinders
debugging. Modify the catch block to capture the error object and log the error
details before returning null, ensuring that any token refresh failures are
recorded for easier troubleshooting.


/**
* 서버 환경에서 사용할 인증이 필요한 Axios 인스턴스를 생성합니다.
*
* - `accessToken`과 `refreshToken`은 Next.js 서버의 `cookies()`로부터 가져옵니다.
* - 기본 baseURL은 `NEXT_PUBLIC_API_SERVER_URL`입니다.
* - 응답에서 401 Unauthorized가 발생하면 `/api/auth/refresh`를 통해 accessToken을 갱신하고,
* 실패했던 원래 요청을 한 번만 재시도합니다.
* - 재시도 여부는 `originalRequest._retry` 플래그로 판단합니다.
*
* @returns {Promise<AxiosInstance>} - 인증이 설정된 Axios 인스턴스
*/
const privateServerInstance = async (): Promise<AxiosInstance> => {
const cookieStore = await cookies();
const accessToken = cookieStore.get('accessToken')?.value;
const refreshToken = cookieStore.get('refreshToken')?.value;

const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
timeout: 5000,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

타임아웃 값을 설정 가능하게 만드세요.

타임아웃이 5초로 하드코딩되어 있습니다. 환경에 따라 조정이 필요할 수 있습니다.

+const AXIOS_TIMEOUT = Number(process.env.NEXT_PUBLIC_API_TIMEOUT) || 5000;

  const instance = axios.create({
    baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
-    timeout: 5000,
+    timeout: AXIOS_TIMEOUT,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
timeout: 5000,
const AXIOS_TIMEOUT = Number(process.env.NEXT_PUBLIC_API_TIMEOUT) || 5000;
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
timeout: AXIOS_TIMEOUT,
// …other config options
});
🤖 Prompt for AI Agents
In src/apis/privateServerInstance.ts at line 54, the timeout value is hardcoded
to 5000 milliseconds. Modify the code to make the timeout configurable by
accepting it as a parameter or reading it from an environment variable, allowing
adjustment based on different environments.

headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
},
});

instance.interceptors.response.use(
(res) => res,

/**
* 응답 인터셉터:
* - 401 에러가 발생하면, accessToken을 새로 발급받고 원래 요청을 한 번만 재시도합니다.
* - `_retry` 플래그를 사용하여 무한 루프를 방지합니다.
*/
async (err) => {
const originalRequest = err.config;

if (
err.response?.status === 401 &&
!originalRequest._retry &&
refreshToken
) {
originalRequest._retry = true;

const newAccessToken = await refreshAccessToken();

if (newAccessToken) {
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return instance(originalRequest);
}
}

return Promise.reject(err);
},
);

return instance;
};

export { privateServerInstance };
Loading
Loading