Skip to content

Commit 44cddd9

Browse files
authored
Merge pull request #5 from Final-Project-Team-Temporary/feature/quiz-solve-log-api
Feat: 대시보드 개선, 로그인 연동 및 퀴즈 결과 저장 API 연동
2 parents ff2bbd3 + ff72f93 commit 44cddd9

6 files changed

Lines changed: 207 additions & 66 deletions

File tree

app/(auth)/login/page.tsx

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import { Button } from "@/components/ui/button"
99
import { Input } from "@/components/ui/input"
1010
import { Label } from "@/components/ui/label"
1111
import { Separator } from "@/components/ui/separator"
12-
import { TrendingUp, Eye, EyeOff, ArrowLeft } from "lucide-react"
12+
import { TrendingUp, Eye, EyeOff, ArrowLeft, Loader2 } from "lucide-react"
13+
import { useAuth } from "@/contexts/AuthContext"
14+
import { loginUser } from "@/services/auth"
1315

1416
export default function LoginPage() {
1517
const router = useRouter()
18+
const { login } = useAuth()
1619
const [showPassword, setShowPassword] = useState(false)
20+
const [isLoading, setIsLoading] = useState(false)
21+
const [error, setError] = useState<string | null>(null)
1722
const [formData, setFormData] = useState({
1823
email: "",
1924
password: "",
@@ -27,11 +32,36 @@ export default function LoginPage() {
2732
}))
2833
}
2934

30-
const handleLogin = (e: React.FormEvent) => {
35+
const handleLogin = async (e: React.FormEvent) => {
3136
e.preventDefault()
32-
// 로그인 로직 구현
33-
console.log("로그인 시도:", formData)
34-
router.push("/dashboard")
37+
setIsLoading(true)
38+
setError(null)
39+
40+
try {
41+
const response = await loginUser(formData)
42+
43+
if (response.success) {
44+
// 토큰 저장
45+
localStorage.setItem("authToken", response.data.accessToken)
46+
localStorage.setItem("refreshToken", response.data.refreshToken)
47+
48+
// AuthContext에 사용자 정보 저장
49+
login(response.data.accessToken, {
50+
id: "", // 백엔드에서 제공하지 않으면 빈 문자열
51+
email: formData.email,
52+
name: response.data.userName,
53+
})
54+
55+
// 홈으로 이동
56+
router.push("/")
57+
} else {
58+
setError(response.message || "로그인에 실패했습니다.")
59+
}
60+
} catch (err) {
61+
setError("로그인 중 오류가 발생했습니다.")
62+
} finally {
63+
setIsLoading(false)
64+
}
3565
}
3666

3767
const handleKakaoLogin = () => {
@@ -91,6 +121,13 @@ export default function LoginPage() {
91121
</div>
92122
</div>
93123

124+
{/* 에러 메시지 */}
125+
{error && (
126+
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
127+
<p className="text-sm text-red-600">{error}</p>
128+
</div>
129+
)}
130+
94131
{/* 이메일 로그인 폼 */}
95132
<form onSubmit={handleLogin} className="space-y-4">
96133
<div className="space-y-2">
@@ -140,8 +177,19 @@ export default function LoginPage() {
140177
</button>
141178
</div>
142179

143-
<Button type="submit" className="w-full bg-blue-900 hover:bg-blue-800 h-12">
144-
로그인
180+
<Button
181+
type="submit"
182+
className="w-full bg-blue-900 hover:bg-blue-800 h-12"
183+
disabled={isLoading}
184+
>
185+
{isLoading ? (
186+
<>
187+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
188+
로그인 중...
189+
</>
190+
) : (
191+
"로그인"
192+
)}
145193
</Button>
146194
</form>
147195

app/page.tsx

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { VideoRecommendation } from "@/types/video"
2424
import { fetchRecommendedVideos, getYoutubeThumbnail } from "@/services/videos"
2525
import { RecommendedArticle } from "@/types/article"
2626
import { fetchRecommendedArticles } from "@/services/articles"
27+
import { fetchLearningStreak, LearningStats } from "@/services/learning"
2728

2829
export default function FinancialLearningPlatform() {
2930
const router = useRouter()
@@ -44,27 +45,18 @@ export default function FinancialLearningPlatform() {
4445
const [articlesLoading, setArticlesLoading] = useState(false)
4546
const [articlesError, setArticlesError] = useState<string | null>(null)
4647

47-
// 사용자 통계
48-
const userStats = {
49-
articlesRead: 28,
50-
badges: 8,
51-
quizScore: 85,
52-
streak: 15,
53-
}
48+
// 학습 통계 상태
49+
const [learningStats, setLearningStats] = useState<LearningStats>({
50+
consecutiveDays: 0,
51+
quizCount: 0,
52+
articleCount: 0,
53+
})
5454

5555
const userPreferences = {
5656
keywords: ["ETF", "배당주", "부동산", "경제지표"],
5757
difficultyLevel: "intermediate",
5858
}
5959

60-
const learningProgress = {
61-
completedCourses: 12,
62-
totalCourses: 20,
63-
currentLevel: "Intermediate",
64-
badges: 8,
65-
streak: 15,
66-
}
67-
6860
// 유튜브 영상 추천 API 호출
6961
useEffect(() => {
7062
const loadRecommendedVideos = async () => {
@@ -117,6 +109,28 @@ export default function FinancialLearningPlatform() {
117109
loadRecommendedArticles()
118110
}, [])
119111

112+
// 학습 통계 API 호출
113+
useEffect(() => {
114+
const loadLearningStats = async () => {
115+
if (!isAuthenticated) return
116+
117+
try {
118+
const response = await fetchLearningStreak()
119+
if (response.success) {
120+
setLearningStats({
121+
consecutiveDays: response.data.consecutiveDays,
122+
quizCount: response.data.quizCount,
123+
articleCount: response.data.articleCount,
124+
})
125+
}
126+
} catch (error) {
127+
console.error("Failed to load learning stats:", error)
128+
}
129+
}
130+
131+
loadLearningStats()
132+
}, [isAuthenticated])
133+
120134
const formatArticleDate = (dateString: string) => {
121135
const date = new Date(dateString)
122136
const now = new Date()
@@ -163,7 +177,7 @@ export default function FinancialLearningPlatform() {
163177
</div>
164178
<div className="text-right">
165179
<div className="text-sm text-blue-200">연속 학습일</div>
166-
<div className="text-3xl font-bold">{learningProgress.streak}</div>
180+
<div className="text-3xl font-bold">{learningStats.consecutiveDays}</div>
167181
</div>
168182
</div>
169183
</div>
@@ -175,8 +189,14 @@ export default function FinancialLearningPlatform() {
175189
<div className="flex items-center justify-between">
176190
<div>
177191
<p className="text-sm text-gray-600">읽은 기사</p>
178-
<p className="text-2xl font-bold">{userStats.articlesRead}</p>
179-
<p className="text-sm text-gray-500">개 완독</p>
192+
{isAuthenticated ? (
193+
<>
194+
<p className="text-2xl font-bold">{learningStats.articleCount}</p>
195+
<p className="text-sm text-gray-500">개 완독</p>
196+
</>
197+
) : (
198+
<p className="text-sm text-gray-500 mt-2">로그인 후 EconoEasy와 함께 학습해봐요</p>
199+
)}
180200
</div>
181201
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
182202
<Newspaper className="w-6 h-6 text-blue-600" />
@@ -189,9 +209,15 @@ export default function FinancialLearningPlatform() {
189209
<CardContent className="p-6">
190210
<div className="flex items-center justify-between">
191211
<div>
192-
<p className="text-sm text-gray-600">퀴즈 점수</p>
193-
<p className="text-2xl font-bold">{userStats.quizScore}</p>
194-
<p className="text-sm text-gray-500">평균 점수</p>
212+
<p className="text-sm text-gray-600">푼 퀴즈</p>
213+
{isAuthenticated ? (
214+
<>
215+
<p className="text-2xl font-bold">{learningStats.quizCount}</p>
216+
<p className="text-sm text-gray-500">개 완료</p>
217+
</>
218+
) : (
219+
<p className="text-sm text-gray-500 mt-2">로그인 후 EconoEasy와 함께 학습해봐요</p>
220+
)}
195221
</div>
196222
<div className="w-12 h-12 bg-orange-100 rounded-lg flex items-center justify-center">
197223
<Target className="w-6 h-6 text-orange-600" />
@@ -205,8 +231,14 @@ export default function FinancialLearningPlatform() {
205231
<div className="flex items-center justify-between">
206232
<div>
207233
<p className="text-sm text-gray-600">연속 학습일</p>
208-
<p className="text-2xl font-bold">{learningProgress.streak}</p>
209-
<p className="text-sm text-gray-500">학습 중</p>
234+
{isAuthenticated ? (
235+
<>
236+
<p className="text-2xl font-bold">{learningStats.consecutiveDays}</p>
237+
<p className="text-sm text-gray-500">학습 중</p>
238+
</>
239+
) : (
240+
<p className="text-sm text-gray-500 mt-2">로그인 후 EconoEasy와 함께 학습해봐요</p>
241+
)}
210242
</div>
211243
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center">
212244
<Target className="w-6 h-6 text-purple-600" />

app/quiz/result/page.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"use client"
22

3-
import { useState, useEffect } from "react"
3+
import { useState, useEffect, useRef } from "react"
44
import { useRouter } from "next/navigation"
55
import { Card, CardContent } from "@/components/ui/card"
66
import { Button } from "@/components/ui/button"
77
import { Progress } from "@/components/ui/progress"
88
import Header from "@/components/layout/Header"
99
import { Trophy, Star, RotateCcw, Home } from "lucide-react"
10-
import { QuizData } from "@/lib/types"
10+
import { QuizData, QuizResultItem } from "@/lib/types"
11+
import { saveQuizResult } from "@/services/quiz"
1112

1213
interface QuizResult {
1314
score: number
@@ -27,6 +28,7 @@ export default function QuizResultPage() {
2728
const router = useRouter()
2829
const [result, setResult] = useState<QuizResult | null>(null)
2930
const [termStats, setTermStats] = useState<TermStat[]>([])
31+
const hasSubmittedRef = useRef(false)
3032

3133
useEffect(() => {
3234
const savedResult = sessionStorage.getItem("quizResult")
@@ -38,6 +40,12 @@ export default function QuizResultPage() {
3840
// 용어별 통계 계산
3941
if (data.quizData?.quizzes && Array.isArray(data.quizData.quizzes)) {
4042
calculateTermStats(data)
43+
44+
// 퀴즈 결과 API 저장 (한 번만 실행)
45+
if (!hasSubmittedRef.current) {
46+
hasSubmittedRef.current = true
47+
submitQuizResult(data)
48+
}
4149
}
4250
} catch (error) {
4351
console.error("Failed to parse quiz result:", error)
@@ -48,6 +56,23 @@ export default function QuizResultPage() {
4856
}
4957
}, [router])
5058

59+
const submitQuizResult = async (data: QuizResult) => {
60+
try {
61+
const results: QuizResultItem[] = data.quizData.quizzes.map((quiz, index) => ({
62+
question: quiz.question,
63+
options: quiz.options,
64+
answerIndex: quiz.answerIndex,
65+
userAnswerIndex: data.userAnswers[index],
66+
explanation: quiz.explanation,
67+
term: quiz.term || "기타",
68+
}))
69+
70+
await saveQuizResult({ results })
71+
} catch (error) {
72+
console.error("Failed to save quiz result:", error)
73+
}
74+
}
75+
5176
const calculateTermStats = (data: QuizResult) => {
5277
const statsMap = new Map<string, { correct: number; total: number }>()
5378

lib/types.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,20 @@ export interface LoginCredentials {
127127
password: string
128128
}
129129

130+
// 백엔드 로그인 응답 타입
131+
export interface LoginResponse {
132+
code: string
133+
message: string
134+
success: boolean
135+
data: {
136+
accessToken: string
137+
refreshToken: string
138+
userStatus: "ACTIVE" | "INACTIVE" | "SUSPENDED"
139+
loginStatus: "EXISTING_USER" | "NEW_USER"
140+
userName: string
141+
}
142+
}
143+
130144
export interface SignupData {
131145
email: string
132146
password: string
@@ -264,10 +278,17 @@ export interface ChallengeSubmitRequest {
264278
answers: number[]
265279
}
266280

267-
export interface QuizResultRequest {
281+
export interface QuizResultItem {
282+
question: string
283+
options: string[]
284+
answerIndex: number
285+
userAnswerIndex: number
286+
explanation?: string
268287
term: string
269-
score: number
270-
totalQuestions: number
288+
}
289+
290+
export interface QuizResultRequest {
291+
results: QuizResultItem[]
271292
}
272293

273294
export interface LearningStats {

services/auth.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
1-
import { LoginCredentials, SignupData, User, ApiResponse } from "@/lib/types"
1+
import { LoginCredentials, SignupData, User, ApiResponse, LoginResponse } from "@/lib/types"
2+
import apiClient from "@/lib/axios"
23

34
// 로그인 API 호출
4-
export async function loginUser(credentials: LoginCredentials): Promise<ApiResponse<{ user: User; token: string }>> {
5+
export async function loginUser(credentials: LoginCredentials): Promise<LoginResponse> {
56
try {
6-
// 실제 API 호출 대신 모의 응답
7-
await new Promise(resolve => setTimeout(resolve, 1000)) // 네트워크 지연 시뮬레이션
8-
9-
if (credentials.email === "[email protected]" && credentials.password === "password") {
10-
return {
11-
success: true,
12-
data: {
13-
user: {
14-
id: "1",
15-
email: credentials.email,
16-
name: "김투자",
17-
age: "30s",
18-
investmentExperience: "intermediate",
19-
riskTolerance: "moderate",
20-
investmentGoals: ["장기 자산 증식", "은퇴 자금 마련"],
21-
interests: ["ETF", "주식", "부동산"],
22-
createdAt: new Date(),
23-
updatedAt: new Date(),
24-
},
25-
token: "mock-jwt-token"
26-
}
27-
}
28-
} else {
29-
return {
30-
success: false,
31-
error: "이메일 또는 비밀번호가 올바르지 않습니다."
32-
}
7+
const response = await apiClient.post<LoginResponse>("/api/auth/login", credentials)
8+
return response.data
9+
} catch (error: any) {
10+
// axios 에러 처리
11+
if (error.response?.data) {
12+
return error.response.data as LoginResponse
3313
}
34-
} catch (error) {
3514
return {
15+
code: "E001",
16+
message: "로그인 중 오류가 발생했습니다.",
3617
success: false,
37-
error: "로그인 중 오류가 발생했습니다."
18+
data: {
19+
accessToken: "",
20+
refreshToken: "",
21+
userStatus: "INACTIVE",
22+
loginStatus: "EXISTING_USER",
23+
userName: "",
24+
},
3825
}
3926
}
4027
}

0 commit comments

Comments
 (0)