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
3 changes: 2 additions & 1 deletion apps/service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"openapi": "pnpm dlx openapi-typescript https://dev.math-pointer.com/v3/api-docs --output ./src/types/api/schema.d.ts && prettier --write ./src/types/api/schema.d.ts"
"openapi": "pnpm dlx openapi-typescript https://api.math-pointer.com/v3/api-docs --output ./src/types/api/schema.d.ts && prettier --write ./src/types/api/schema.d.ts"
},
"dependencies": {
"@next/third-parties": "^15.2.4",
"@repo/pointer-design-system": "workspace:*",
"@tanstack/react-query": "^5.66.0",
"@tanstack/react-query-devtools": "^5.66.0",
"dayjs": "^1.11.13",
"clsx": "^2.1.1",
"next": "15.1.4",
"openapi-fetch": "^0.13.4",
"openapi-react-query": "^0.3.0",
Expand Down
50 changes: 27 additions & 23 deletions apps/service/src/apis/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
'use client';
import { Middleware } from 'openapi-fetch';

import { getAccessToken, setAccessToken } from '@utils';
import { getAccessToken, setAccessToken, setName, setRefreshToken } from '@utils';
import { postRefreshToken } from '@/apis/controller/auth';

const UNPROTECTED_ROUTES = ['/api/v1/auth/admin/login', '/api/v1/auth/oauth/social-login'];
const UNPROTECTED_ROUTES = ['/api/student/auth/social/login', '/api/common/auth/refresh'];
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The route '/api/student/auth/social/login' doesn't match the actual API endpoint '/api/student/auth/login/social' used in postKakaoLogin.ts. This could cause authentication issues.

Suggested change
const UNPROTECTED_ROUTES = ['/api/student/auth/social/login', '/api/common/auth/refresh'];
const UNPROTECTED_ROUTES = ['/api/student/auth/login/social', '/api/common/auth/refresh'];

Copilot uses AI. Check for mistakes.

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

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

const data = await response.json();
const accessToken = data.data.accessToken;
setAccessToken(accessToken);
if (accessToken) {
return accessToken;
} catch (error) {
console.error('Reissue failed:', error);
}

const result = await postRefreshToken();

if (!result.isSuccess || !result.data) {
console.error('액세스토큰 갱신 실패:', result.error);
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.error should be removed as indicated in the PR checklist. Consider using proper error handling or logging service instead.

Copilot uses AI. Check for mistakes.
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return null;
}

if (result.data?.token.accessToken) {
setAccessToken(result.data.token.accessToken);
accessToken = result.data.token.accessToken;
}
if (result.data?.token.refreshToken) {
setRefreshToken(result.data.token.refreshToken);
}
if (result.data?.name) {
setName(result.data.name);
}
return accessToken;
};

const authMiddleware: Middleware = {
Expand All @@ -32,18 +42,12 @@ const authMiddleware: Middleware = {
return undefined;
}

let accessToken = getAccessToken();
const accessToken = await reissueToken();

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

if (!accessToken) {
console.error('Access token reissue failed. Logging out...');
return request;
}
if (accessToken) {
request.headers.set('Authorization', `Bearer ${accessToken}`);
}

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

Expand Down
5 changes: 3 additions & 2 deletions apps/service/src/apis/controller/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import postLogin from './postLogin';
import postKakaoLogin from './postKakaoLogin';
import postUserInfo from './postUserInfo';
import postRefreshToken from './postRefreshToken';

export { postLogin, postKakaoLogin };
export { postKakaoLogin, postUserInfo, postRefreshToken };
48 changes: 9 additions & 39 deletions apps/service/src/apis/controller/auth/postKakaoLogin.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,24 @@
'use client';

import { setAccessToken, setName } from '@utils';
import { client } from '@/apis/client';

const postKakaoAccessToken = async (code: string) => {
const response = await fetch(`https://kauth.kakao.com/oauth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
body: `grant_type=authorization_code&client_id=${process.env.NEXT_PUBLIC_REST_API_KEY}&redirect_uri=${process.env.NEXT_PUBLIC_REDIRECT_URI}&code=${code}`,
});
const jsonData = await response.json();
return jsonData.access_token;
};

const postKakaoLogin = async (code: string) => {
const accessToken = await postKakaoAccessToken(code);
const response = await client.POST('/api/v1/auth/oauth/social-login', {
params: {
header: {
social_access_token: accessToken,
},
query: {
provider: 'KAKAO',
},
const postKakaoLogin = async () => {
const response = await client.POST('/api/student/auth/login/social', {
body: {
provider: 'KAKAO',
redirectUri: process.env.NEXT_PUBLIC_REDIRECT_URI ?? '',
},
});

try {
if (
response &&
response.data &&
response.data.data &&
response.data.data.name &&
response.data.data.accessToken
) {
const { accessToken, name } = response.data.data;
setAccessToken(accessToken);
setName(name);

window.location.href = '/';
if (response && response.data) {
return { isSuccess: true, loginUrl: response.data.loginUrl };
} else {
console.error('accessToken을 찾을 수 없습니다:', response);
return { isSuccess: false, error: '데이터를 찾을 수 없습니다.' };
}
} catch (error) {
console.error('소셜 로그인 요청 오류:', error);
return { isSuccess: false, error: error };
}

return response;
};

export default postKakaoLogin;
13 changes: 0 additions & 13 deletions apps/service/src/apis/controller/auth/postLogin.ts

This file was deleted.

17 changes: 17 additions & 0 deletions apps/service/src/apis/controller/auth/postRefreshToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

const postRefreshToken = async () => {
try {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/common/auth/refresh`, {
method: 'GET',
});
if (!res.ok) throw new Error('Refresh failed');

const data = await res.json();
return { isSuccess: true, data };
} catch (e) {
return { isSuccess: false, error: e };
}
};

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

const postUserInfo = async (name: string, grade: number) => {
try {
const response = await client.POST('/api/student/auth/register/social', {
body: {
name: name,
grade: grade,
},
});
return { isSuccess: true, data: response.data };
} catch (error) {
return { isSuccess: false, error: error };
}
};

export default postUserInfo;
25 changes: 19 additions & 6 deletions apps/service/src/app/api/auth/callback/kakao/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
'use client';

import { useSearchParams } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

import { postKakaoLogin } from '@apis';
import { setAccessToken, setRefreshToken } from '@utils';

const Page = () => {
const searchParams = useSearchParams();
const code = searchParams.get('code');
const router = useRouter();
const { success, isFirstLogin, accessToken, refreshToken } = Object.fromEntries(
searchParams.entries()
);

useEffect(() => {
if (code) {
postKakaoLogin(code);
if (!success || !accessToken) {
router.replace('/login');
return;
}
}, [code]);

setAccessToken(accessToken);
setRefreshToken(refreshToken);

if (isFirstLogin) {
router.replace('/onboarding');
} else {
router.replace('/');
}
}, [searchParams]);

return <></>;
};
Expand Down
2 changes: 1 addition & 1 deletion apps/service/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default function RootLayout({
modal: React.ReactNode;
}>) {
return (
<html lang='ko'>
<html lang='ko' suppressHydrationWarning>
<head>
<link rel='preconnect' href='https://www.google-analytics.com' crossOrigin='anonymous' />
<link rel='preconnect' href='https://prod.math-pointer.com' crossOrigin='anonymous' />
Expand Down
13 changes: 8 additions & 5 deletions apps/service/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ import { useEffect } from 'react';
import { useRouter } from 'next/navigation';

import { getAccessToken, trackEvent } from '@utils';
import { postKakaoLogin } from '@apis';
import { LogoLogin } from '@/assets/svg/logo';
import { KakaoButton } from '@/components/login';

const Page = () => {
const router = useRouter();
const kakaoLoginUrl = `https://kauth.kakao.com/oauth/authorize?client_id=${
process.env.NEXT_PUBLIC_REST_API_KEY
}&redirect_uri=${process.env.NEXT_PUBLIC_REDIRECT_URI}&response_type=code`;

const handleLoginClick = () => {
const handleLoginClick = async () => {
trackEvent('kakao_login_click');
window.location.replace(kakaoLoginUrl);
const result = await postKakaoLogin();
if (result.isSuccess && result.loginUrl) {
router.push(result.loginUrl);
} else {
console.error('로그인 URL을 가져오는 데 실패했습니다.');
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.error should be removed as indicated in the PR checklist. Consider using proper error handling or logging service instead.

Copilot uses AI. Check for mistakes.
}
};

useEffect(() => {
Expand Down
75 changes: 75 additions & 0 deletions apps/service/src/app/onboarding/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client';

import { useForm, FormProvider } from 'react-hook-form';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';

import { Button, Header } from '@components';
import { postUserInfo } from '@/apis/controller/auth';
import { setName } from '@utils';
import UserInfoForm from '@/components/onboarding/UserInfoForm';

const Page = () => {
type FormValues = { name: string; grade: string };
const router = useRouter();
const [isFormFilled, setIsFormFilled] = useState(false);

const methods = useForm<FormValues>({
Comment on lines +13 to +17
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FormValues type is defined locally but duplicates the existing UserInfoFormData type from @/types. Use the imported type to avoid duplication.

Suggested change
type FormValues = { name: string; grade: string };
const router = useRouter();
const [isFormFilled, setIsFormFilled] = useState(false);
const methods = useForm<FormValues>({
import { UserInfoFormData } from '@/types';
const router = useRouter();
const [isFormFilled, setIsFormFilled] = useState(false);
const methods = useForm<UserInfoFormData>({

Copilot uses AI. Check for mistakes.
mode: 'onChange',
});
const { register, formState, getFieldState } = methods;
const handleSubmitForm = async () => {
const result = await postUserInfo(
methods.getValues('name'),
Number(methods.getValues('grade'))
);
if (result.isSuccess) {
if (result.data) {
setName(result.data.name);
}
router.push('/');
} else {
console.error('회원 정보 입력 실패:', result.error);
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Console.error should be removed as indicated in the PR checklist. Consider using proper error handling or logging service instead.

Copilot uses AI. Check for mistakes.
router.push('/login');
}
};

useEffect(() => {
const isNameValid = getFieldState('name', formState);
const isGradeValid = getFieldState('grade', formState);

if (
!isNameValid.invalid &&
!isGradeValid.invalid &&
isNameValid.isDirty &&
isGradeValid.isDirty
) {
setIsFormFilled(true);
} else {
setIsFormFilled(false);
}
}, [formState, methods]);
Copy link

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including the entire 'methods' object in the dependency array will cause unnecessary re-renders. Only include 'getFieldState' which is the specific method being used.

Suggested change
}, [formState, methods]);
}, [formState, getFieldState]);

Copilot uses AI. Check for mistakes.

return (
<>
<Header title='회원 정보 입력' iconType='none' />
<main className='flex h-dvh flex-col items-center justify-between px-[2rem] pt-[8rem] pb-[1.5rem]'>
<FormProvider {...methods}>
<div className='flex w-full flex-col items-start justify-start gap-[3.2rem]'>
<p className='font-bold-24 text-black'>
<span className='text-main'>포인터</span>
에 오신 걸 <br />
환영합니다!
</p>
<UserInfoForm formState={formState} register={register} />
</div>
<Button type='submit' variant='blue' disabled={!isFormFilled} onClick={handleSubmitForm}>
수정 완료
</Button>
</FormProvider>
</main>
</>
);
};

export default Page;
3 changes: 2 additions & 1 deletion apps/service/src/components/common/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { IcHome, IcLeft } from '@svg';

interface HeaderProps {
title: string;
iconType?: 'home' | 'back';
iconType?: 'home' | 'back' | 'none';
}

const Header = ({ title, iconType = 'home' }: HeaderProps) => {
Expand All @@ -22,6 +22,7 @@ const Header = ({ title, iconType = 'home' }: HeaderProps) => {
<div className='flex w-1/6 cursor-pointer items-center'>
{iconType === 'home' && <IcHome width={24} height={24} onClick={handleClickHome} />}
{iconType === 'back' && <IcLeft width={24} height={24} onClick={() => router.back()} />}
{iconType === 'none' && <div className='h-6 w-6' />}
</div>

<h1 className='font-bold-16 flex w-2/3 items-center justify-center text-black'>{title}</h1>
Expand Down
2 changes: 1 addition & 1 deletion apps/service/src/components/common/Inputs/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const Input = forwardRef<HTMLInputElement, InputHTMLAttributes<HTMLInputElement>
return (
<input
ref={ref}
className='placeholder:text-lightgray500 disabled:text-lightgray500 font-medium-16 h-[5.6rem] w-full rounded-[16px] bg-white px-[1.6rem] text-black'
className='placeholder:text-lightgray500 disabled:text-lightgray500 font-medium-16 h-[5.6rem] w-full rounded-[16px] bg-white px-[1.6rem] text-black focus:outline-none'
{...props}
/>
);
Expand Down
Loading
Loading