-
Notifications
You must be signed in to change notification settings - Fork 1
Feat/43 로그인 / 회원가입 페이지 구현 #69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 38 commits
ce5acd9
6fc10df
ef7211b
e54aebd
ea4f735
cd11842
4753224
56489d5
6f09491
5e2d128
570a039
294c340
b66986d
139ebe6
d6a6884
ee54cce
c90b368
1463919
64dd14d
909f916
4c1322d
b23e131
8337ff0
629f15a
53fdf29
c59beb1
a98bb93
1e6c97e
366ef69
0ffe8ca
17703fe
b14c642
9ec4875
05c204d
3a92edb
76e0fbc
3a3527c
8d14fd4
042f128
3434e7a
9bddc33
9ff49eb
b62b8e4
162b986
9efa216
7f687bc
65c49c4
398395c
6f85f87
b2d8c91
e80b9ff
a8554d7
a84bc80
ff8ee18
3dddc72
9ca1cf7
d9d63d9
6b7cd65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
|
|
||
| export default IconBell; | ||
| export default IconBell; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| <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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 접근성을 위해 SVG에 대체 텍스트를 추가하세요. 스크린 리더 사용자를 위해 SVG에 <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
Suggested change
🧰 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 |
||||||||||||||||||||||||||||||||||||||||
| <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; | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const IconKakao = ({ size = 72, ...props }) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 접근성을 위해 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
Suggested change
Suggested change
🧰 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <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; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * 오류 메시지 생성 함수 | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| * 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export { instance }; | ||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 tsxLength of output: 98 토큰 갱신 실패 처리 로직이 다른 파일에 구현되어 있는지 다시 확인하기 위해 🏁 Script executed: #!/bin/bash
# 토큰 갱신 실패 시 처리 로직 확인 (확장자 필터 적용)
rg -g '*.ts' -g '*.tsx' -A5 -B5 "refresh.*fail|갱신.*실패|logout|로그아웃"Length of output: 3653 토큰 갱신 실패 시 사용자 로그아웃 및 로그인 페이지 리디렉션 처리 필요 현재
예시 변경안: --- 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);
}
);위와 같이 실패 시 사용자 상태를 🤖 Prompt for AI Agents |
||
| } catch (refreshError) { | ||
| return Promise.reject(refreshError); | ||
| } | ||
| } | ||
|
|
||
| return Promise.reject(error); | ||
| }, | ||
| ); | ||
|
|
||
| export { privateInstance }; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 서버 환경에서 사용할 인증이 필요한 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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
🧰 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