diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index f7a2a99..a5a79b3 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, Suspense } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { API_BASE_URL, getCookie, setTokens } from '../../utils/api'; +import { API_BASE_URL, setTokens } from '../../utils/api'; function AuthCallbackContent() { const router = useRouter(); @@ -55,35 +55,40 @@ function AuthCallbackContent() { }), }); + console.log('=== Backend Response Debug ==='); + console.log('Response status:', response.status); + console.log('Response ok:', response.ok); + console.log('Response headers:', Object.fromEntries(response.headers.entries())); + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); + console.log('Response data:', data); if (data.success) { - // 백엔드에서 쿠키로 토큰을 설정했는지 확인 - const accessToken = getCookie('access_social'); - const refreshToken = getCookie('refresh_social'); - - console.log('=== Cookie Check After API Call ==='); - console.log('Access Token from cookie:', accessToken ? 'Found' : 'Not found'); - console.log('Refresh Token from cookie:', refreshToken ? 'Found' : 'Not found'); - - if (accessToken && refreshToken) { - console.log('✅ JWT tokens found in cookies - authentication successful'); - setTokens(accessToken, refreshToken); - setStatus('success'); - setMessage('로그인 성공! 홈으로 이동합니다.'); - notifyAuthStateChange(); - setTimeout(() => { - router.push('/home'); - }, 1500); - } else { - console.log('❌ No JWT tokens in cookies after API call'); - setStatus('error'); - setMessage('인증 토큰을 받지 못했습니다. 다시 시도해주세요.'); - } + console.log('✅ Backend API call successful - authentication successful'); + console.log('Response data:', data); + + // 로컬스토리지 저장 전 확인 + console.log('=== LocalStorage Save Debug ==='); + console.log('Before saving - localStorage:', localStorage.getItem('isAuthenticated')); + + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('authTimestamp', Date.now().toString()); + + // 로컬스토리지 저장 후 확인 + console.log('After saving - isAuthenticated:', localStorage.getItem('isAuthenticated')); + console.log('After saving - authTimestamp:', localStorage.getItem('authTimestamp')); + console.log('All localStorage keys:', Object.keys(localStorage)); + + setStatus('success'); + setMessage('로그인 성공! 홈으로 이동합니다.'); + notifyAuthStateChange(); + setTimeout(() => { + router.push('/home'); + }, 1500); } else { throw new Error(data.message || '로그인 처리 중 오류가 발생했습니다.'); } diff --git a/app/components/ProtectedRoute.tsx b/app/components/ProtectedRoute.tsx index ddb9a17..52fae44 100644 --- a/app/components/ProtectedRoute.tsx +++ b/app/components/ProtectedRoute.tsx @@ -14,7 +14,9 @@ export default function ProtectedRoute({ children, fallback }: ProtectedRoutePro const router = useRouter(); useEffect(() => { + console.log('ProtectedRoute - Auth state:', { isLoggedIn, loading }); if (!loading && !isLoggedIn) { + console.log('ProtectedRoute - Redirecting to / due to not logged in'); router.push('/'); } }, [isLoggedIn, loading, router]); diff --git a/app/contexts/AuthContext.tsx b/app/contexts/AuthContext.tsx index 008af78..fa3d699 100644 --- a/app/contexts/AuthContext.tsx +++ b/app/contexts/AuthContext.tsx @@ -29,13 +29,48 @@ export const AuthProvider: React.FC = ({ children }) => { const [loading, setLoading] = useState(true); const checkAuth = () => { - // JWT 토큰만 확인 (세션 기반 인증 제거) - const token = getAccessToken(); - const newLoginState = !!token; - setIsLoggedIn(newLoginState); - setLoading(false); + console.log('=== AuthContext checkAuth ==='); + console.log('Document available:', typeof document !== 'undefined'); - console.log('Auth check:', { token: !!token, isLoggedIn: newLoginState }); + // 백엔드 API를 호출해서 인증 상태 확인 + const verifyAuth = async () => { + try { + console.log('Checking auth with backend...'); + console.log('API_BASE_URL:', process.env.NEXT_PUBLIC_API_BASE_URL); + console.log('Full URL:', `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/auth/check`); + + const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/auth/check`, { + method: 'GET', + credentials: 'include', // HttpOnly 쿠키 포함 + }); + + if (response.ok) { + // 백엔드에서 인증 성공 + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('authTimestamp', Date.now().toString()); + console.log('✅ Backend auth successful - setting localStorage'); + } else { + // 백엔드에서 인증 실패 + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('authTimestamp'); + console.log('❌ Backend auth failed - clearing localStorage'); + } + + const newLoginState = response.ok; + console.log('Setting isLoggedIn to:', newLoginState); + + setIsLoggedIn(newLoginState); + setLoading(false); + } catch (error) { + console.error('Auth verification failed:', error); + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('authTimestamp'); + setIsLoggedIn(false); + setLoading(false); + } + }; + + verifyAuth(); }; const logout = () => { @@ -51,6 +86,7 @@ export const AuthProvider: React.FC = ({ children }) => { // localStorage 변경 감지 useEffect(() => { const handleStorageChange = () => { + console.log('=== Storage change detected ==='); checkAuth(); }; @@ -59,12 +95,22 @@ export const AuthProvider: React.FC = ({ children }) => { // 현재 탭에서의 변경 감지를 위한 커스텀 이벤트 window.addEventListener('authStateChanged', handleStorageChange); + + // 주기적으로 인증 상태 확인 (개발용) + const interval = setInterval(() => { + const currentAuth = isAuthenticated(); + if (currentAuth !== isLoggedIn) { + console.log('Auth state changed from', isLoggedIn, 'to', currentAuth); + checkAuth(); + } + }, 1000); // 1초마다 확인 return () => { window.removeEventListener('storage', handleStorageChange); window.removeEventListener('authStateChanged', handleStorageChange); + clearInterval(interval); }; - }, []); + }, [isLoggedIn]); const value: AuthContextType = { isLoggedIn, diff --git a/app/home/page.tsx b/app/home/page.tsx index 4598a31..30cc115 100644 --- a/app/home/page.tsx +++ b/app/home/page.tsx @@ -4,7 +4,6 @@ import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import Container from "../components/Container" import NavigationBar from "../components/NavigationBar" -import ProtectedRoute from "../components/ProtectedRoute" const EMOTION_COLORS = { 즐거움: '#3DC8EF', @@ -209,151 +208,149 @@ export default function Register() { ]; return ( - - -
-
- {childName} + +
+
+ {childName} +
+ +
+
+ +

+ {currentMonth.getFullYear()}년 {currentMonth.getMonth() + 1}월 +

+
-
-
- -

- {currentMonth.getFullYear()}년 {currentMonth.getMonth() + 1}월 -

- -
+
+ {['일', '월', '화', '수', '목', '금', '토'].map(day => ( +
+ {day} +
+ ))} +
-
- {['일', '월', '화', '수', '목', '금', '토'].map(day => ( -
- {day} -
- ))} -
+
+ {calendarDays.map(date => ( + + ))} +
+
-
- {calendarDays.map(date => ( + {selectedDate && selectedDiary && ( +
+
+ {timeSlots.map((timeSlot) => ( ))}
+ )} - {selectedDate && selectedDiary && ( -
-
- {timeSlots.map((timeSlot) => ( - - ))} -
+ {selectedDate && selectedDiary && ( +
+
+ {new Date(selectedDate).toLocaleDateString('ko-KR', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'long' + })}
- )} - {selectedDate && selectedDiary && ( -
-
- {new Date(selectedDate).toLocaleDateString('ko-KR', { - year: 'numeric', - month: 'long', - day: 'numeric', - weekday: 'long' - })} +
+
+ {timeSlots.find(t => t.key === selectedTimeSlot)?.label} 기록
- -
-
- {timeSlots.find(t => t.key === selectedTimeSlot)?.label} 기록 -
+ + {(() => { + const timeData = selectedDiary[selectedTimeSlot]; - {(() => { - const timeData = selectedDiary[selectedTimeSlot]; - - return ( -
-
-
- - {timeData.predictedEmotions.map(e => e.emotion).join(', ')} - -
-

- {timeData.predictedText} -

+ return ( +
+
+
+ + {timeData.predictedEmotions.map(e => e.emotion).join(', ')} +
+

+ {timeData.predictedText} +

+
-
-
- - {timeData.actualEmotions.map(e => e.emotion).join(', ')} - -
-

- {timeData.actualText} -

+
+
+ + {timeData.actualEmotions.map(e => e.emotion).join(', ')} +
+

+ {timeData.actualText} +

- ); - })()} -
+
+ ); + })()}
- )} +
+ )} - {!selectedDate && ( -
-
- - - -
-

- 과거 날짜를 클릭해서 {childName}의 감정 일기를 확인해보세요 -

-

- 예측과 실제 감정을 비교하여 분석해드립니다 -

+ {!selectedDate && ( +
+
+ + +
- )} -
- - - - +

+ 과거 날짜를 클릭해서 {childName}의 감정 일기를 확인해보세요 +

+

+ 예측과 실제 감정을 비교하여 분석해드립니다 +

+
+ )} +
+ + + ) } \ No newline at end of file diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 6b650d8..95523ee 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import Container from "../components/Container" import NavigationBar from "../components/NavigationBar" diff --git a/app/utils/api.ts b/app/utils/api.ts index 50a8ce1..5a53243 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -9,49 +9,106 @@ export const handleKakaoLogin = () => { window.location.href = KAKAO_LOGIN_URL!; }; -// 쿠키에서 토큰 읽기 함수 -export const getCookie = (name: string): string | null => { - if (typeof document === 'undefined') return null; - - const value = `; ${document.cookie}`; - const parts = value.split(`; ${name}=`); - if (parts.length === 2) { - return parts.pop()?.split(';').shift() || null; - } - return null; -}; - -// 토큰 관리 함수들 - 백엔드 쿠키 이름 그대로 사용 +// 토큰 관리 함수들 - HttpOnly 쿠키 사용으로 인증 상태만 확인 export const getAccessToken = (): string | null => { if (typeof window === 'undefined') return null; - // 백엔드에서 설정한 쿠키 이름 그대로 사용 - return getCookie('access_social'); + // HttpOnly 쿠키는 JavaScript에서 읽을 수 없으므로 + // 로컬스토리지의 인증 상태만 확인 + const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'; + const authTimestamp = localStorage.getItem('authTimestamp'); + + if (!isAuthenticated || !authTimestamp) { + return null; + } + + // 인증 상태가 24시간 이내인지 확인 + const authAge = Date.now() - parseInt(authTimestamp); + const isRecentAuth = authAge < 24 * 60 * 60 * 1000; // 24시간 + + if (!isRecentAuth) { + // 만료된 인증 상태 삭제 + clearTokens(); + return null; + } + + // HttpOnly 쿠키는 백엔드에서만 읽을 수 있으므로 + // 인증 상태만 반환 (실제 토큰은 백엔드에서 처리) + return 'authenticated'; }; export const getRefreshToken = (): string | null => { if (typeof window === 'undefined') return null; - // 백엔드에서 설정한 쿠키 이름 그대로 사용 - return getCookie('refresh_social'); + // HttpOnly 쿠키는 JavaScript에서 읽을 수 없으므로 + // 로컬스토리지의 인증 상태만 확인 + const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'; + const authTimestamp = localStorage.getItem('authTimestamp'); + + if (!isAuthenticated || !authTimestamp) { + return null; + } + + // 인증 상태가 24시간 이내인지 확인 + const authAge = Date.now() - parseInt(authTimestamp); + const isRecentAuth = authAge < 24 * 60 * 60 * 1000; // 24시간 + + if (!isRecentAuth) { + // 만료된 인증 상태 삭제 + clearTokens(); + return null; + } + + // HttpOnly 쿠키는 백엔드에서만 읽을 수 있으므로 + // 인증 상태만 반환 (실제 토큰은 백엔드에서 처리) + return 'authenticated'; }; -// 토큰 설정 함수 - 백엔드에서 쿠키로 관리하므로 빈 함수 +// 토큰 설정 함수 - HttpOnly 쿠키 사용으로 인증 상태만 저장 export const setTokens = (accessToken: string, refreshToken?: string) => { - // 백엔드에서 쿠키로 설정하므로 프론트엔드에서는 설정하지 않음 - console.log('Tokens are managed by backend cookies'); + if (typeof window === 'undefined') return; + + // HttpOnly 쿠키는 백엔드에서 설정하므로 + // 프론트엔드에서는 인증 상태만 저장 + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('authTimestamp', Date.now().toString()); + + console.log('Authentication state saved to localStorage'); }; export const clearTokens = () => { if (typeof window === 'undefined') return; - // 백엔드 쿠키 이름 그대로 사용하여 삭제 + // 백엔드 쿠키 삭제 (가능한 경우) document.cookie = 'access_social=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; document.cookie = 'refresh_social=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + + // 로컬스토리지의 인증 상태 삭제 + localStorage.removeItem('isAuthenticated'); + localStorage.removeItem('authTimestamp'); }; export const isAuthenticated = (): boolean => { - return getAccessToken() !== null; + if (typeof window === 'undefined') return false; + + const isAuthInStorage = localStorage.getItem('isAuthenticated') === 'true'; + const authTimestamp = localStorage.getItem('authTimestamp'); + + if (!isAuthInStorage || !authTimestamp) { + return false; + } + + // 인증 상태가 24시간 이내인지 확인 + const authAge = Date.now() - parseInt(authTimestamp); + const isRecentAuth = authAge < 24 * 60 * 60 * 1000; // 24시간 + + if (!isRecentAuth) { + // 만료된 인증 상태 삭제 + clearTokens(); + return false; + } + + return true; }; // API 응답 타입 정의 @@ -68,8 +125,8 @@ export const authenticatedApiRequest = async ( options: RequestInit = {} ): Promise> => { try { - const accessToken = getAccessToken(); - if (!accessToken) { + // HttpOnly 쿠키를 사용하므로 인증 상태만 확인 + if (!isAuthenticated()) { throw new Error('인증 토큰이 없습니다.'); } @@ -77,10 +134,9 @@ export const authenticatedApiRequest = async ( const response = await fetch(url, { headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, ...options.headers, }, - credentials: 'include', // 쿠키 포함 + credentials: 'include', // HttpOnly 쿠키 자동 포함 ...options, });