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
68 changes: 68 additions & 0 deletions apps/service/src/apis/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';
import { Middleware } from 'openapi-fetch';

import { getAccessToken, setAccessToken } from '@/contexts/AuthContext';

const UNPROTECTED_ROUTES = ['/api/v1/auth/admin/login'];

const reissueToken = async () => {
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/v1/auth/reissue`, {
method: 'GET',
credentials: 'include',
});

if (!response.ok) throw new Error('Token reissue failed');

const data = await response.json();
const accessToken = data.data.accessToken;
setAccessToken(accessToken);
return accessToken;
} catch (error) {
console.error('Reissue failed:', error);
setAccessToken(null);
window.location.href = '/login';
return null;
}
};

const authMiddleware: Middleware = {
async onRequest({ schemaPath, request }: { schemaPath: string; request: Request }) {
if (UNPROTECTED_ROUTES.some((pathname) => schemaPath.startsWith(pathname))) {
return undefined;
}

let accessToken = getAccessToken();

if (!accessToken) {
accessToken = await reissueToken();

if (!accessToken) {
console.error('Access token reissue failed. Logging out...');
return request;
}
}

request.headers.set('Authorization', `Bearer ${accessToken}`);
return request;
},

async onResponse({ request, response }) {
if (response.status === 401) {
console.warn('Access token expired. Attempting reissue...');

const newAccessToken = await reissueToken();

if (!newAccessToken) {
console.error('Reissue failed. Logging out...');
return response;
}

request.headers.set('Authorization', `Bearer ${newAccessToken}`);
return fetch(request);
}
return response;
},
};

export default authMiddleware;
3 changes: 0 additions & 3 deletions apps/service/src/apis/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@ import createClient from 'openapi-react-query';

export const client = createFetchClient<paths>({
baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL,
headers: {
Authorization: `Bearer ${process.env.NEXT_PUBLIC_ACCESS_TOKEN}`,
},
});

export const TanstackQueryClient = createClient(client);
3 changes: 3 additions & 0 deletions apps/service/src/apis/controller/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import postLogin from './postLogin';

export { postLogin };
13 changes: 13 additions & 0 deletions apps/service/src/apis/controller/auth/postLogin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { client } from '@apis';

const postLogin = async (email: string, password: string) => {
return await client.POST('/api/v1/auth/admin/login', {
body: {
email,
password,
},
credentials: 'include',
});
};

export default postLogin;
20 changes: 14 additions & 6 deletions apps/service/src/apis/controller/commentary/getCommentary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ type GetCommentaryProps = {
};

const getCommentary = ({ publishId, problemId }: GetCommentaryProps) => {
return TanstackQueryClient.useQuery('get', '/api/v1/client/commentary', {
params: {
query: {
publishId: Number(publishId),
problemId: Number(problemId),
return TanstackQueryClient.useQuery(
'get',
'/api/v1/client/commentary',
{
params: {
query: {
publishId: Number(publishId),
problemId: Number(problemId),
},
},
},
});
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};

export default getCommentary;
15 changes: 11 additions & 4 deletions apps/service/src/apis/controller/home/getHomeFeed.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { client } from '@/apis/client';
import { TanstackQueryClient } from '@apis';

const getHomeFeed = async () => {
const { data } = await client.GET('/api/v1/client/home-feed');
return data?.data;
const getHomeFeed = () => {
return TanstackQueryClient.useQuery(
'get',
'/api/v1/client/home-feed',
{},
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};

export default getHomeFeed;
4 changes: 4 additions & 0 deletions apps/service/src/apis/controller/problem/getChildData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const getChildData = (publishId: string, problemId: string) => {
problemId: Number(problemId),
},
},
},
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const getChildProblemById = (publishId: string, problemId: string, childProblemI
childProblemId: Number(childProblemId),
},
},
},
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};
Expand Down
20 changes: 14 additions & 6 deletions apps/service/src/apis/controller/problem/getProblemAll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ type GetProblemAllProps = {
};

const getProblemAll = ({ year, month }: GetProblemAllProps) => {
return TanstackQueryClient.useQuery('get', '/api/v1/client/problem/all/{year}/{month}', {
params: {
path: {
year,
month,
return TanstackQueryClient.useQuery(
'get',
'/api/v1/client/problem/all/{year}/{month}',
{
params: {
path: {
year,
month,
},
},
},
});
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};

export default getProblemAll;
20 changes: 14 additions & 6 deletions apps/service/src/apis/controller/problem/getProblemById.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { TanstackQueryClient } from '@apis';

const getProblemById = (publishId: string, problemId: string) => {
return TanstackQueryClient.useQuery('get', '/api/v1/client/problem/{publishId}/{problemId}', {
params: {
path: {
publishId: Number(publishId),
problemId: Number(problemId),
return TanstackQueryClient.useQuery(
'get',
'/api/v1/client/problem/{publishId}/{problemId}',
{
params: {
path: {
publishId: Number(publishId),
problemId: Number(problemId),
},
},
},
});
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};

export default getProblemById;
25 changes: 16 additions & 9 deletions apps/service/src/apis/controller/problem/getProblemThumbnail.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { client } from '@apis';
import { TanstackQueryClient } from '@apis';

const getProblemThumbnail = async (publishId: string, problemId: string) => {
const { data } = await client.GET('/api/v1/client/problem/thumbnail/{publishId}/{problemId}', {
params: {
path: {
publishId: Number(publishId),
problemId: Number(problemId),
const getProblemThumbnail = (publishId: string, problemId: string) => {
return TanstackQueryClient.useQuery(
'get',
'/api/v1/client/problem/thumbnail/{publishId}/{problemId}',
{
params: {
path: {
publishId: Number(publishId),
problemId: Number(problemId),
},
},
},
});
return data?.data ?? {};
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};

export default getProblemThumbnail;
23 changes: 15 additions & 8 deletions apps/service/src/apis/controller/problem/getProblemsByPublishId.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { client } from '@apis';
import { TanstackQueryClient } from '@apis';

const getProblemsByPublishId = async (publishId: string) => {
const { data } = await client.GET('/api/v1/client/problem/{publishId}', {
params: {
path: {
publishId: Number(publishId),
const getProblemsByPublishId = (publishId: string) => {
return TanstackQueryClient.useQuery(
'get',
'/api/v1/client/problem/{publishId}',
{
params: {
path: {
publishId: Number(publishId),
},
},
},
});
return data?.data ?? {};
{
staleTime: Infinity,
gcTime: Infinity,
}
);
};

export default getProblemsByPublishId;
1 change: 1 addition & 0 deletions apps/service/src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { client, TanstackQueryClient } from './client';
export { client, TanstackQueryClient };

// controllers
export * from './controller/auth';
export * from './controller/home';
export * from './controller/problem';
export * from './controller/commentary';
Expand Down
6 changes: 4 additions & 2 deletions apps/service/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use client';
import Link from 'next/link';
import { Button } from '@components';
import { IcSearch } from '@svg';
Expand All @@ -13,8 +14,9 @@ import {
WeekProgress,
} from '@/components/home';

const Page = async () => {
const homeFeedData = await getHomeFeed();
const Page = () => {
const { data } = getHomeFeed();
const homeFeedData = data?.data;

const dailyProgresses = homeFeedData?.dailyProgresses;
const problemSets = homeFeedData?.problemSets;
Expand Down
65 changes: 61 additions & 4 deletions apps/service/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,35 @@
'use client';
import { postLogin } from '@apis';
import { Button, Input } from '@components';
import { SubmitHandler, useForm } from 'react-hook-form';
import { useRouter } from 'next/navigation';

import { LogoLogin } from '@/assets/svg/logo';
import { AppleButton } from '@/components/login';
import { KakaoButton } from '@/components/login';
import { setAccessToken } from '@/contexts/AuthContext';

interface LoginType {
email: string;
password: string;
}

const Page = () => {
const router = useRouter();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginType>();

const onSubmitLogin: SubmitHandler<LoginType> = async (formData) => {
const { data } = await postLogin(formData.email, formData.password);

const { accessToken } = data?.data || {};
if (accessToken) {
setAccessToken(accessToken);
router.push('/');
}
};

return (
<div className='mx-auto flex h-[100dvh] w-[33.5rem] flex-col pb-[2rem]'>
<div className='flex flex-col items-center py-[9.6rem]'>
Expand All @@ -14,11 +41,41 @@ const Page = () => {
<LogoLogin width={250} height={57} className='mt-[2.4rem]' />
<h1 className='text-main font-bold-24 mt-[1.6rem]'>포인터</h1>
</div>
<div className='mt-auto flex flex-col items-center gap-[1.6rem]'>
{/* <div className='mt-auto flex flex-col items-center gap-[1.6rem]'>
<p className='font-medium-12 text-lightgray500'>포인터는 태블릿의 스플릿뷰를 권장해요</p>
<KakaoButton />
<AppleButton />
</div>
</div> */}
<form
onSubmit={handleSubmit(onSubmitLogin)}
className='mt-[4.8rem] flex flex-col items-start justify-center gap-[2.4rem]'>
<Input
type='email'
placeholder='이메일을 입력해주세요'
autoComplete='username'
{...register('email', {
required: true,
})}
/>
<Input
type='password'
placeholder='비밀번호를 입력해주세요'
autoComplete='current-password'
{...register('password', {
required: true,
pattern: {
value: /^[A-Za-z0-9]*$/,
message: '비밀번호는 영문자와 숫자만 입력 가능합니다.',
},
})}
/>
{errors.password && (
<p className='font-medium-16 text-red mt-[1.2rem]' role='alert'>
{errors.password.message}
</p>
)}
<Button variant='blue'>로그인</Button>
</form>
</div>
);
};
Expand Down
Loading