Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
domains: [
'deving-bucket.s3.ap-northeast-2.amazonaws.com',
'helpx.adobe.com',
remotePatterns: [
{
protocol: 'https',
hostname: 'deving-bucket.s3.ap-northeast-2.amazonaws.com',
},
{
protocol: 'https',
hostname: 'helpx.adobe.com',
},
],
},
async redirects() {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "node server.js",
"dev": "next dev",
"dev:https": "node server.js",
"build": "next build",
"start": "next start",
"lint": "next lint",
Expand Down
12 changes: 6 additions & 6 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { readFileSync } from 'fs';
import { createServer } from 'https';
import next from 'next';
import { parse } from 'url';
const { createServer } = require('https');
const { parse } = require('url');
const next = require('next');
const fs = require('fs');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

const httpsOptions = {
key: readFileSync('./localhost-key.pem'),
cert: readFileSync('./localhost.pem'),
key: fs.readFileSync('./localhost-key.pem'),
cert: fs.readFileSync('./localhost.pem'),
};

app.prepare().then(() => {
Expand Down
7 changes: 4 additions & 3 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Header from '@/components/common/Header';
import { ToastProvider } from '@/components/common/ToastContext';
import ReactQueryProviders from '@/hooks/useReactQuery';
import { authAPI } from '@/lib/axios/authApi';
import axiosInstance from '@/lib/axios/axiosInstance';
import type { Metadata } from 'next';
import localFont from 'next/font/local';

Expand All @@ -24,7 +24,7 @@

async function getUserInfo() {
try {
const { data } = await authAPI.get('/api/v1/mypage/banner');
const { data } = await axiosInstance.get('/api/v1/mypage/banner');
return data.data;
} catch (error) {
return null;
Expand All @@ -36,13 +36,14 @@
children: React.ReactNode;
}>) {
const userInfo = await getUserInfo();
console.log('[layout] userInfo', userInfo);

Check warning on line 39 in src/app/layout.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected console statement

return (
<html lang="ko" className={pretendard.variable}>
<body className="bg-BG">
<ReactQueryProviders>
<ToastProvider>
<Header userInfo={userInfo} />
<Header />
<div className="m-auto max-w-[1340px]">{children}</div>
</ToastProvider>
</ReactQueryProviders>
Expand Down
3 changes: 1 addition & 2 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Button } from '@/components/ui/Button';
import { baseURL } from '@/lib/axios/defaultConfig';
import { baseURL } from '@/lib/axios/axiosInstance';

import DummyUser from './_features/DummyUser';

Check warning on line 3 in src/app/login/page.tsx

View workflow job for this annotation

GitHub Actions / check

'DummyUser' is defined but never used. Allowed unused vars must match /^_/u
import LoginForm from './_features/LoginForm';

export const metadata = {
Expand Down
8 changes: 4 additions & 4 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ export default function Home() {
members: 8,
maxMembers: 10,
location: '온라인',
imageUrl: '/api/placeholder/300/160',
// imageUrl: '/api/placeholder/300/160',
},
{
title: '주말 모각코 모임',
category: '모각코',
members: 12,
maxMembers: 20,
location: '서울 강남',
imageUrl: '/api/placeholder/300/160',
// imageUrl: '/api/placeholder/300/160',
},
{
title: 'AI 챗봇 프로젝트',
category: '사이드 프로젝트',
members: 4,
maxMembers: 6,
location: '온라인/오프라인',
imageUrl: '/api/placeholder/300/160',
// imageUrl: '/api/placeholder/300/160',
},
];

Expand Down Expand Up @@ -129,7 +129,7 @@ export default function Home() {
>
<div className="relative h-40 bg-Cgray300">
<Image
src={group.imageUrl}
src={''}
alt={group.title}
width={300}
height={160}
Expand Down
2 changes: 1 addition & 1 deletion src/app/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { baseURL } from '@/lib/axios/defaultConfig';
import { baseURL } from '@/lib/axios/axiosInstance';

import SignupForm from './_features/SignupForm';

Expand Down
15 changes: 13 additions & 2 deletions src/components/common/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
'use client';

import Logo from '@/assets/icon/logo.svg';
import { QUERY_KEYS, useBannerQueries } from '@/hooks/queries/useMyPageQueries';
import { removeAccessToken } from '@/lib/serverActions';
import { translateCategoryNameToKor } from '@/util/searchFilter';
import { useQueryClient } from '@tanstack/react-query';
import { MEETING_TYPES } from 'constants/category/category';
import { Menu } from 'lucide-react';
import Image from 'next/image';
import Link from 'next/link';
import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useEffect, useState } from 'react';

import Dropdown from './Dropdown';
import { useToast } from './ToastContext';
Expand Down Expand Up @@ -190,9 +192,18 @@ const NavLinks = ({ isMobile }: { isMobile?: boolean }) => {
);
};

const Header = ({ userInfo }: { userInfo: IUserInfo }) => {
const Header = () => {
const [isOpen, setIsOpen] = useState(false);
const { data: userInfo, isLoading } = useBannerQueries();
const userId = undefined;
const isLogIn = !!userInfo;

console.log('[Header] userInfo: ', userInfo, 'isLogIn: ', isLogIn);
// 유저 정보 꺼내기
// const queryClient = useQueryClient();

// const userInfo = queryClient.getQueryData<IUserInfo>(QUERY_KEYS.banner())

return (
<div>
{/* desktop */}
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/mutations/useMeetingFormMutation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { authAPI } from '@/lib/axios/authApi';
import axiosInstance from '@/lib/axios/axiosInstance';
import { useMutation } from '@tanstack/react-query';
import { meetingURL } from 'service/api/endpoints';
import { CreateMeetingPayload } from 'types/meetingForm';
Expand All @@ -16,7 +16,7 @@ const useMeetingFormMutation = ({
}) => {
const createMeeting = useMutation({
mutationFn: async (formData: CreateMeetingPayload) => {
const response = await authAPI.post<CreateMeetingResponse>(
const response = await axiosInstance.post<CreateMeetingResponse>(
meetingURL.create,
formData,
);
Expand Down
23 changes: 16 additions & 7 deletions src/hooks/mutations/useUserMutation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useToast } from '@/components/common/ToastContext';
import { setAccessToken } from '@/lib/serverActions';
import { useMutation } from '@tanstack/react-query';
import { setAccessToken, setRefreshToken } from '@/lib/serverActions';
import {
QueryClient,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import {
getEmailCheck,
getNameCheck,
Expand All @@ -9,21 +13,26 @@ import {
} from 'service/api/user';
import { ISignupFormData } from 'types/auth';

import { QUERY_KEYS } from '../queries/useMyPageQueries';

const useLoginMutation = ({
onSuccessCallback,
}: {
onSuccessCallback: () => void;
}) => {
const { showToast } = useToast();
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ email, password }: { email: string; password: string }) =>
postLogin({ email, password }),
onSuccess: async (res) => {
// 쿠키 저장
const accessToken = res.headers.token;
if (accessToken) {
await setAccessToken(accessToken);
}
// 유저 정보 불러오기
console.log('유저 정보 invalidate');
// queryClient.invalidateQueries({ queryKey: QUERY_KEYS.banner() });

// refreshToken 저장
await setRefreshToken(res.refreshToken);

showToast('로그인 성공', 'success');
// 메인페이지로 리다이렉트
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/queries/useMyPageQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const useProfileQuery = (options = {}) => {
export const useBannerQueries = (options = {}) => {
return useQuery({
queryKey: QUERY_KEYS.banner(),
queryFn: () => getBanner(),
queryFn: getBanner,
staleTime: 5 * 60 * 1000, // 5분 동안 fresh 상태 유지
...options,
});
Expand Down
43 changes: 0 additions & 43 deletions src/lib/axios/authApi.ts

This file was deleted.

108 changes: 108 additions & 0 deletions src/lib/axios/axiosInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import axios from 'axios';

import {
getRefreshToken,
removeAccessToken,
removeRefreshToken,
} from '../serverActions';

export const baseURL = process.env.NEXT_PUBLIC_API_URL;

const axiosInstance = axios.create({
baseURL,
withCredentials: true,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});

// 토큰 갱신 중인지 여부를 확인하는 플래그
let isRefreshing = false;
let refreshSubscribers: (() => void)[] = [];

// 모든 대기 중인 요청을 재시도하는 함수
const onAccessTokenFetched = () => {
refreshSubscribers.forEach((callback) => callback());
refreshSubscribers = []; // 모든 요청이 처리되었기에 배열 초기화
};
axiosInstance.interceptors.request.use(
async (config) => {
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
async (error) => {
return Promise.reject(error);
},
);

axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
/**
* TODO:(refresh 토큰 발급 이후)
* - 토근 재발급 로직
*/

/**
* - 401 에러로 실패하면, 로그인 페이지로 리다이렉트하는 로직
* - 리다이렉트 전에 사용자에게 경고 메시지
*/
if (error.response?.status === 401) {
// await removeAccessToken();
console.log('401 Unauthorized - 토큰 재발급 시도');

const refreshToken = await getRefreshToken();
console.log('refreshToken: ', refreshToken);

if (!refreshToken) {
console.log('Refresh Token 없음 -> 강제 로그아웃');
await removeAccessToken();
await removeRefreshToken();
window.location.href = '/login';
return Promise.reject(error);
}

if (!isRefreshing) {
isRefreshing = true;

try {
// Refresh Token으로 Access Token 재발급 시도
await axios.post(
'https://deving.shop/api/v1/auths/refresh',
{
refreshToken,
},
{ withCredentials: true },
);

isRefreshing = false;

// 대기중인 요청들을 새로운 access token으로 실행
onAccessTokenFetched();
// ✅ 기존 요청 다시 실행 (Access Token 갱신 후)
return axiosInstance(error.config);
} catch (refreshError) {
console.error('Refresh Token 만료 -> 강제 로그아웃');

// Refresh Token이 만료되었다면 강제 로그아웃 및 로그인 페이지 이동
await removeAccessToken();
await removeRefreshToken();
window.location.href = '/login';
return Promise.reject(refreshError);
}
} else {
// refresh token 요청이 진행 중이라면 대기 (Promise를 반환)

return new Promise((resolve) => {
refreshSubscribers.push(() => {
resolve(axiosInstance(error.config)); // 기존의 요청을 새로운 토큰으로 재시도
});
});
}
}
return Promise.reject(error);
},
);

export default axiosInstance;
5 changes: 0 additions & 5 deletions src/lib/axios/basicApi.ts

This file was deleted.

Loading
Loading