Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 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 };
45 changes: 45 additions & 0 deletions src/apis/privateInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import axios from 'axios';

/**
* Axios 인스턴스를 생성하여 인증이 필요한 클라이언트 요청을 처리합니다.
*
* 이 인스턴스는 기본적으로 `/api`를 baseURL로 사용하며,
* 서버로부터 401 Unauthorized 응답을 받을 경우 `/api/auth/refresh` 엔드포인트를 통해
* accessToken을 재발급받고, 실패했던 원래 요청을 한 번만 재시도합니다.
*/

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

privateInstance.interceptors.response.use(
(res) => res,
/**
* 응답 인터셉터: 401 에러 발생 시 refresh 토큰을 사용하여 accessToken을 재발급하고,
* 실패했던 요청을 재시도합니다. 단, 동일 요청이 여러 번 재시도되지 않도록 `_retry` 플래그를 설정합니다.
*
* @param {import('axios').AxiosError} error - Axios 오류 객체
* @returns {Promise} - 성공 시 원래 요청 재시도, 실패 시 에러 반환
*/
async (error) => {
const originalRequest = error.config;

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

try {
await axios.post('/api/auth/refresh', null, {});
console.log('리프레시 토큰 전송');
return privateInstance(originalRequest);
} catch (refreshError) {
return Promise.reject(refreshError);
}
}

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

export { privateInstance };
96 changes: 96 additions & 0 deletions src/app/(non-header)/login/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use server';

import axios, { AxiosError } from 'axios';
import { cookies } from 'next/headers';

import {
EmailNotFoundError,
InternalServerError,
PasswordMismatchError,
PasswordValidateError,
UserNotFoundError,
} from '@/lib/errors/authErrors';
import { User } from '@/types/user';

type ServerErrorResponse = {
message: string;
};

interface LoginResponse {
user?: User;
error?: string;
}

/**
* 사용자 로그인 요청을 처리하는 서버 액션 함수입니다.
*
* 클라이언트로부터 전달된 이메일과 비밀번호를 사용해 백엔드 인증 API (`/auth/login`)에 요청을 보내고,
* 응답으로 받은 사용자 정보와 토큰(accessToken, refreshToken)을 쿠키에 저장합니다.
* 에러 상황에 따라 명확한 에러 메시지를 반환합니다.
*
* @param {unknown} prevState - 이전 상태 값 (React useActionState와 호환)
* @param {FormData} formData - 클라이언트에서 전송된 로그인 정보 (email, password 포함)
* @returns {Promise<LoginResponse>} 로그인 성공 시 사용자 정보, 실패 시 에러 메시지를 포함한 응답 객체
*
* @throws {UserNotFoundError} 응답에 사용자 정보가 포함되어 있지 않은 경우
* @throws {PasswordValidateError} 유효성 검사 실패 (`Validation Failed`)
* @throws {PasswordMismatchError} 비밀번호 불일치
* @throws {EmailNotFoundError} 존재하지 않는 이메일
* @throws {InternalServerError} 위 케이스 외의 서버 내부 오류
*/
export default async function Login(
prevState: unknown,
formData: FormData,
): Promise<LoginResponse> {
const email = formData.get('email');
const password = formData.get('password');
Copy link
Contributor

@BokyungCodes BokyungCodes Jul 24, 2025

Choose a reason for hiding this comment

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

찾아보니까 formData.get('email')과 formData.get('password')는 반환 타입이 FormDataEntryValue | null이라서 예상과 다르게 null이거나 File일 수도 있대요!

지금 코드처럼 바로 axios.post 요청에 넘기면 타입 불일치나 런타임 에러가 발생할 가능성이 있어서, 아래처럼 타입 체크를 한번 해주는 것도 고려하면 좋을 것 같습니닷 🙌

const email = formData.get('email');
const password = formData.get('password');

if (typeof email !== 'string' || typeof password !== 'string') {
return { error: '이메일 또는 비밀번호가 올바르지 않습니다.' };
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오 감사합니다! 몰랐던 정보인데 하나 배워갑니다!


try {
const res = await axios.post(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/auth/login`,
{ email, password },
{
headers: { 'Content-Type': 'application/json' },
},
);
const { user, accessToken, refreshToken } = res.data;

if (!user) throw new UserNotFoundError();

const cookieStore = await cookies();
cookieStore.set('accessToken', accessToken, {
httpOnly: true,
path: '/',
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 30,
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

쿠키 만료 시간을 상수로 관리하세요.

액세스 토큰(30분)과 리프레시 토큰(7일)의 만료 시간이 하드코딩되어 있습니다.

파일 상단에 상수 추가:

const ACCESS_TOKEN_MAX_AGE = 60 * 30; // 30분
const REFRESH_TOKEN_MAX_AGE = 60 * 60 * 24 * 7; // 7일

Also applies to: 73-73

🤖 Prompt for AI Agents
In src/app/(non-header)/login/action.ts at lines 66 and 73, the cookie
expiration times for access token and refresh token are hardcoded. Define
constants at the top of the file named ACCESS_TOKEN_MAX_AGE and
REFRESH_TOKEN_MAX_AGE with values 60 * 30 and 60 * 60 * 24 * 7 respectively,
then replace the hardcoded values at lines 66 and 73 with these constants to
manage expiration times consistently.

});
cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
path: '/',
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
});

return { user };
} catch (err) {
if (axios.isAxiosError(err)) {
const axiosError = err as AxiosError<ServerErrorResponse>;
const serverMsg = axiosError.response?.data?.message;

switch (serverMsg) {
case 'Validation Failed':
return { error: new PasswordValidateError().message };
case '비밀번호가 일치하지 않습니다.':
return { error: new PasswordMismatchError().message };
case '존재하지 않는 유저입니다.':
return { error: new EmailNotFoundError().message };
default:
break;
}
}

return { error: new InternalServerError().message };
}
}
Loading
Loading