Skip to content

Commit dfa49a4

Browse files
authored
Merge pull request #69 from codeit-2team/feat/43
Feat/43 로그인 / 회원가입 페이지 구현
2 parents 5ebfbad + 6b7cd65 commit dfa49a4

File tree

28 files changed

+1670
-6
lines changed

28 files changed

+1670
-6
lines changed

public/assets/svg/bell.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import React from 'react';
22

3-
const IconBell = ({ size = 20, color = '#A1A1A1', ...props }) => (
3+
const IconBell = ({ size = 20, color = '#A1A1A1', ...props }) => (
44
<svg
5-
xmlns="http://www.w3.org/2000/svg"
5+
xmlns='http://www.w3.org/2000/svg'
66
width={size}
77
height={size}
8-
viewBox="0 0 20 20"
8+
viewBox='0 0 20 20'
99
fill={color}
1010
{...props}
1111
>
1212
<path
1313
fill={color}
14-
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"
14+
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'
1515
/>
1616
</svg>
1717
);
1818

19-
export default IconBell;
19+
export default IconBell;

public/assets/svg/brand-mark.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
3+
const BrandMark = ({
4+
width = 340,
5+
height = 192,
6+
color = '#0B3B2D',
7+
...props
8+
}) => (
9+
<svg
10+
xmlns='http://www.w3.org/2000/svg'
11+
width={width}
12+
height={height}
13+
fill={color}
14+
viewBox='0 0 340 192'
15+
{...props}
16+
>
17+
<path
18+
fill={color}
19+
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'
20+
/>
21+
<path
22+
fill={color}
23+
fillRule='evenodd'
24+
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'
25+
clipRule='evenodd'
26+
/>
27+
</svg>
28+
);
29+
30+
export default BrandMark;

public/assets/svg/kakao.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
3+
const IconKakao = ({ size = 72, ...props }) => (
4+
<svg
5+
xmlns='http://www.w3.org/2000/svg'
6+
width={size}
7+
height={size}
8+
fill='none'
9+
viewBox='0 0 72 72'
10+
{...props}
11+
>
12+
<circle cx='36' cy='35.998' r='35.25' stroke='#F2F2F2' strokeWidth='1.5' />
13+
<g fill='#331D1E' clipPath='url(#clip0_33762_8396)'>
14+
<path d='M31.963 35.086h1.728l-.864-2.395z'></path>
15+
<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>
16+
</g>
17+
<defs>
18+
<clipPath id='clip0_33762_8396'>
19+
<path fill='#fff' d='M21 22.498h30v27H21z'></path>
20+
</clipPath>
21+
</defs>
22+
</svg>
23+
);
24+
25+
export default IconKakao;

src/apis/instance.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import axios from 'axios';
2+
3+
/**
4+
* Axios 인스턴스
5+
*
6+
* - 기본 `baseURL`은 환경 변수에서 설정
7+
* - 요청 시간 초과는 5000ms (5초)
8+
* - 모든 요청의 기본 Content-Type은 `application/json`
9+
*/
10+
const instance = axios.create({
11+
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
12+
timeout: 5000,
13+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
14+
});
15+
16+
/**
17+
* 오류 메시지 생성 함수
18+
*
19+
* Axios 또는 일반 에러 객체에서 사용자 친화적인 에러 메시지를 추출합니다.
20+
*
21+
* @param {unknown} error - Axios 요청 중 발생한 에러 객체
22+
* @returns {string} - 사용자에게 표시할 수 있는 에러 메시지
23+
*/
24+
const getErrorMessage = (error: unknown): string => {
25+
if (axios.isAxiosError(error)) {
26+
const status = error.response?.status;
27+
const message = error.response?.data?.message;
28+
29+
if (typeof message === 'string') return message;
30+
if (status) {
31+
switch (status) {
32+
case 400:
33+
return '🚨 잘못된 요청입니다. (400)';
34+
case 401:
35+
return '🚨 인증이 필요합니다. (401)';
36+
case 403:
37+
return '🚨 권한이 없습니다. (403)';
38+
case 404:
39+
return '🚨 요청한 리소스를 찾을 수 없습니다. (404)';
40+
case 429:
41+
return '🚨 요청이 너무 많습니다. 잠시 후 다시 시도해주세요. (429)';
42+
case 500:
43+
return '🚨 서버 내부 오류가 발생했습니다. (500)';
44+
default:
45+
return `🚨 요청에 실패했습니다. (Status: ${status})`;
46+
}
47+
}
48+
}
49+
50+
if (error instanceof Error && error.message === 'Network Error') {
51+
return '🚨 네트워크 오류가 발생했습니다. 인터넷 연결을 확인해주세요.';
52+
}
53+
54+
return '🚨 알 수 없는 오류가 발생했습니다.';
55+
};
56+
57+
/** 최대 재시도 횟수 */
58+
const MAX_RETRY = 3;
59+
60+
/**
61+
* URL별 재시도 횟수를 추적하기 위한 Map
62+
*
63+
* 키: 요청 URL
64+
* 값: 현재까지의 재시도 횟수
65+
*/
66+
const retryCounts = new Map<string, number>();
67+
68+
/**
69+
* Axios 응답 인터셉터
70+
*
71+
* - 네트워크 오류 또는 5xx 서버 오류 발생 시 자동으로 재시도
72+
* - 요청 URL 기준으로 재시도 횟수를 제한
73+
* - 최대 재시도 횟수(`MAX_RETRY`) 초과 시 오류 메시지를 반환
74+
*/
75+
instance.interceptors.response.use(
76+
(res) => res,
77+
async (err) => {
78+
const config = err.config;
79+
80+
if (!config || !config.url) {
81+
return Promise.reject(new Error(getErrorMessage(err)));
82+
}
83+
84+
const currentRetry = retryCounts.get(config.url) || 0;
85+
86+
if (
87+
(err.message === 'Network Error' ||
88+
(err.response && err.response.status >= 500)) &&
89+
currentRetry < MAX_RETRY
90+
) {
91+
retryCounts.set(config.url, currentRetry + 1);
92+
return instance(config); // 재시도
93+
}
94+
95+
retryCounts.delete(config.url); // 메모리 누수 방지
96+
return Promise.reject(new Error(getErrorMessage(err)));
97+
},
98+
);
99+
100+
export { instance };

src/apis/privateInstance.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import axios from 'axios';
2+
3+
/**
4+
* 인증이 필요한 클라이언트 요청을 처리하기 위한 Axios 인스턴스입니다.
5+
*
6+
* - 기본 baseURL은 `/api`입니다.
7+
* - 모든 요청에 `application/json` 헤더가 포함됩니다.
8+
* - 응답으로 401 Unauthorized가 반환되면, `/api/auth/refresh`를 호출하여 accessToken을 재발급받습니다.
9+
* - 재발급에 성공하면, 실패했던 원래 요청을 한 번만 재시도합니다.
10+
* - 재시도 여부는 `_retry` 플래그로 제어합니다.
11+
*
12+
* @module privateInstance
13+
*/
14+
15+
const privateInstance = axios.create({
16+
baseURL: '/api',
17+
timeout: 5000,
18+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
19+
});
20+
21+
privateInstance.interceptors.response.use(
22+
(res) => res,
23+
/**
24+
* 응답 인터셉터
25+
*
26+
* 401 Unauthorized 응답이 발생한 경우:
27+
* - accessToken 재발급을 위해 `/api/auth/refresh` 요청을 보냅니다.
28+
* - 재발급 성공 시, 원래 요청에 새로운 토큰을 추가하여 재시도합니다.
29+
* - 같은 요청이 반복되지 않도록 `originalRequest._retry` 플래그로 제어합니다.
30+
*/
31+
async (error) => {
32+
const originalRequest = error.config;
33+
34+
if (error.response?.status === 401 && !originalRequest._retry) {
35+
originalRequest._retry = true;
36+
37+
try {
38+
const { data } = await axios.post('/api/auth/refresh');
39+
const newAccessToken = data.accessToken;
40+
41+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
42+
43+
return privateInstance(originalRequest);
44+
} catch (refreshError) {
45+
return Promise.reject(refreshError);
46+
}
47+
}
48+
49+
return Promise.reject(error);
50+
},
51+
);
52+
53+
export { privateInstance };

src/apis/privateServerInstance.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import axios, { AxiosInstance } from 'axios';
2+
import { cookies } from 'next/headers';
3+
4+
/**
5+
* 서버 환경에서 쿠키를 문자열로 변환하여 Authorization 요청 시 사용할 수 있도록 반환합니다.
6+
*
7+
* @returns {Promise<string>} - `name=value` 형식의 쿠키 문자열
8+
*/
9+
const getCookieHeader = async () => {
10+
const cookieStore = await cookies();
11+
return cookieStore
12+
.getAll()
13+
.map((token) => `${token.name}=${token.value}`)
14+
.join(';');
15+
};
16+
17+
/**
18+
* 서버에서 accessToken이 만료되었을 때, refreshToken을 사용하여 새로운 accessToken을 발급받습니다.
19+
*
20+
* @returns {Promise<string | null>} - 재발급된 accessToken (실패 시 null 반환)
21+
*/
22+
const refreshAccessToken = async (): Promise<string | null> => {
23+
try {
24+
const res = await axios.post(
25+
`${process.env.NEXT_PUBLIC_SITE_URL}/api/auth/refresh`,
26+
{},
27+
{ headers: { Cookie: await getCookieHeader() } },
28+
);
29+
30+
return res.data.accessToken;
31+
} catch {
32+
return null;
33+
}
34+
};
35+
36+
/**
37+
* 서버 환경에서 사용할 인증이 필요한 Axios 인스턴스를 생성합니다.
38+
*
39+
* - `accessToken`과 `refreshToken`은 Next.js 서버의 `cookies()`로부터 가져옵니다.
40+
* - 기본 baseURL은 `NEXT_PUBLIC_API_SERVER_URL`입니다.
41+
* - 응답에서 401 Unauthorized가 발생하면 `/api/auth/refresh`를 통해 accessToken을 갱신하고,
42+
* 실패했던 원래 요청을 한 번만 재시도합니다.
43+
* - 재시도 여부는 `originalRequest._retry` 플래그로 판단합니다.
44+
*
45+
* @returns {Promise<AxiosInstance>} - 인증이 설정된 Axios 인스턴스
46+
*/
47+
const privateServerInstance = async (): Promise<AxiosInstance> => {
48+
const cookieStore = await cookies();
49+
const accessToken = cookieStore.get('accessToken')?.value;
50+
const refreshToken = cookieStore.get('refreshToken')?.value;
51+
52+
const instance = axios.create({
53+
baseURL: process.env.NEXT_PUBLIC_API_SERVER_URL,
54+
timeout: 5000,
55+
headers: {
56+
'Content-Type': 'application/json',
57+
Accept: 'application/json',
58+
...(accessToken && { Authorization: `Bearer ${accessToken}` }),
59+
},
60+
});
61+
62+
instance.interceptors.response.use(
63+
(res) => res,
64+
65+
/**
66+
* 응답 인터셉터:
67+
* - 401 에러가 발생하면, accessToken을 새로 발급받고 원래 요청을 한 번만 재시도합니다.
68+
* - `_retry` 플래그를 사용하여 무한 루프를 방지합니다.
69+
*/
70+
async (err) => {
71+
const originalRequest = err.config;
72+
73+
if (
74+
err.response?.status === 401 &&
75+
!originalRequest._retry &&
76+
refreshToken
77+
) {
78+
originalRequest._retry = true;
79+
80+
const newAccessToken = await refreshAccessToken();
81+
82+
if (newAccessToken) {
83+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
84+
return instance(originalRequest);
85+
}
86+
}
87+
88+
return Promise.reject(err);
89+
},
90+
);
91+
92+
return instance;
93+
};
94+
95+
export { privateServerInstance };

0 commit comments

Comments
 (0)