11import { useEffect } from 'react' ;
2+ import { BrowserRouter , Routes , Route , useNavigate } from 'react-router-dom' ;
23import { MainPage } from './pages/MainPage' ;
4+ import { LoginPage } from './pages/LoginPage' ;
35import { validateEnv } from './config/env' ;
6+ import { isAuthenticated , setAuthToken , setRefreshToken , setUserInfo } from './utils/auth' ;
47import './App.css' ;
58
6- function App ( ) {
9+ // 로그인 콜백 처리 컴포넌트
10+ // 서버가 JWT 토큰을 URL Fragment(#)로 전달하여 프론트엔드로 리다이렉트합니다
11+ // Fragment는 서버로 전송되지 않고 브라우저 히스토리에도 남지 않아 보안상 더 안전합니다
12+ const KakaoCallbackHandler = ( ) => {
13+ const navigate = useNavigate ( ) ;
14+
15+ useEffect ( ( ) => {
16+ // URL Fragment에서 토큰 정보 추출
17+ // 예: http://localhost:5173/login/kakao#accessToken=xxx&refreshToken=yyy
18+ const hash = window . location . hash . substring ( 1 ) ; // # 제거
19+ const params = new URLSearchParams ( hash ) ;
20+
21+ const accessToken = params . get ( 'accessToken' ) ;
22+ const refreshToken = params . get ( 'refreshToken' ) ;
23+
24+ if ( accessToken ) {
25+ // 서버에서 전달받은 JWT 토큰 저장
26+ setAuthToken ( accessToken ) ;
27+
28+ // 리프레시 토큰이 있으면 저장
29+ if ( refreshToken ) {
30+ setRefreshToken ( refreshToken ) ;
31+ }
32+
33+ // 사용자 정보가 별도로 전달되는 경우 (선택사항)
34+ const userInfo = params . get ( 'userInfo' ) ;
35+ if ( userInfo ) {
36+ try {
37+ setUserInfo ( JSON . parse ( decodeURIComponent ( userInfo ) ) ) ;
38+ } catch ( error ) {
39+ console . warn ( '사용자 정보 파싱 실패:' , error ) ;
40+ }
41+ }
42+
43+ // Fragment 제거 (보안을 위해 URL에서 토큰 정보 제거)
44+ window . history . replaceState ( null , '' , window . location . pathname ) ;
45+
46+ // 로그인 성공 후 메인 페이지로 이동
47+ navigate ( '/' , { replace : true } ) ;
48+ } else {
49+ // 토큰이 없으면 로그인 페이지로 리다이렉트
50+ console . error ( '토큰이 전달되지 않았습니다.' ) ;
51+ alert ( '로그인에 실패했습니다. 다시 시도해주세요.' ) ;
52+ navigate ( '/login' , { replace : true } ) ;
53+ }
54+ } , [ navigate ] ) ;
55+
56+ return (
57+ < div style = { {
58+ display : 'flex' ,
59+ alignItems : 'center' ,
60+ justifyContent : 'center' ,
61+ minHeight : '100vh' ,
62+ backgroundColor : '#F2F2F7' ,
63+ } } >
64+ < p style = { { fontSize : '16px' , color : '#8E8E93' } } > 로그인 처리 중...</ p >
65+ </ div >
66+ ) ;
67+ } ;
68+
69+ // 인증이 필요한 라우트 보호 컴포넌트
70+ const ProtectedRoute = ( { children } : { children : React . ReactNode } ) => {
71+ const navigate = useNavigate ( ) ;
72+
73+ useEffect ( ( ) => {
74+ if ( ! isAuthenticated ( ) ) {
75+ navigate ( '/login' , { replace : true } ) ;
76+ }
77+ } , [ navigate ] ) ;
78+
79+ if ( ! isAuthenticated ( ) ) {
80+ return null ;
81+ }
82+
83+ return < > { children } </ > ;
84+ } ;
85+
86+ function AppContent ( ) {
787 useEffect ( ( ) => {
888 try {
989 validateEnv ( ) ;
@@ -13,9 +93,25 @@ function App() {
1393 } , [ ] ) ;
1494
1595 try {
16- return < MainPage /> ;
96+ return (
97+ < Routes >
98+ < Route path = "/login" element = { < LoginPage /> } />
99+ < Route
100+ path = "/login/kakao"
101+ element = { < KakaoCallbackHandler /> }
102+ />
103+ < Route
104+ path = "/"
105+ element = {
106+ < ProtectedRoute >
107+ < MainPage />
108+ </ ProtectedRoute >
109+ }
110+ />
111+ </ Routes >
112+ ) ;
17113 } catch ( error ) {
18- console . error ( 'MainPage render error:' , error ) ;
114+ console . error ( 'App render error:' , error ) ;
19115 return (
20116 < div style = { {
21117 padding : '20px' ,
@@ -28,4 +124,12 @@ function App() {
28124 }
29125}
30126
127+ function App ( ) {
128+ return (
129+ < BrowserRouter >
130+ < AppContent />
131+ </ BrowserRouter >
132+ ) ;
133+ }
134+
31135export default App ;
0 commit comments