-
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 25 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,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 }; |
| 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'); | ||
|
Contributor
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. 찾아보니까 formData.get('email')과 formData.get('password')는 반환 타입이 FormDataEntryValue | null이라서 예상과 다르게 null이거나 File일 수도 있대요! 지금 코드처럼 바로 axios.post 요청에 넘기면 타입 불일치나 런타임 에러가 발생할 가능성이 있어서, 아래처럼 타입 체크를 한번 해주는 것도 고려하면 좋을 것 같습니닷 🙌 const email = formData.get('email'); if (typeof email !== 'string' || typeof password !== 'string') {
Contributor
Author
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. 오 감사합니다! 몰랐던 정보인데 하나 배워갑니다! |
||
|
|
||
| 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, | ||
|
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) 쿠키 만료 시간을 상수로 관리하세요. 액세스 토큰(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 |
||
| }); | ||
| 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 }; | ||
| } | ||
| } | ||
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