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
19 changes: 9 additions & 10 deletions components/login-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useRouter } from "next/navigation"
import { adminLogin, LoginFailedError, NetworkError } from "@/lib/api/admin"
import { enterExam, AuthError } from "@/lib/api/auth"
import { isMasterAdmin, saveAuthInfo } from "@/lib/auth/utils"
import { setCookie } from "@/lib/auth/cookie-utils"
import { useExamSessionStore } from "@/lib/stores/exam-session-store"

type TabType = "user" | "admin"
Expand All @@ -28,7 +27,6 @@ export default function LoginCard() {
const [userError, setUserError] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const router = useRouter();
const setSession = useExamSessionStore((state: { setSession: (examId: number, participantId: number, tokenLimit?: number) => void }) => state.setSession);


const handleClick = async () => {
Expand All @@ -55,17 +53,18 @@ export default function LoginCard() {
phone: phoneNumber.trim(),
});

// examId와 participantId를 전역 상태에 저장
// examId, participantId, accessToken을 한 번에 업데이트 (중간 상태로 소켓 훅이 null 토큰으로 마운트되는 것을 방지)
// STOMP용 accessToken은 Zustand에 저장 (HttpOnly 쿠키는 JS 접근 불가)
const examId = response.exam?.id;
const participantId = response.participant?.id;

if (examId && participantId) {
setSession(examId, participantId, response.session?.tokenLimit);
}

// accessToken 저장
if (response.accessToken) {
setCookie('user_access_token', response.accessToken);
if (examId && participantId && response.accessToken) {
useExamSessionStore.setState({
examId,
participantId,
tokenLimit: response.session?.tokenLimit ?? 20000,
accessToken: response.accessToken,
});
}

// ✅ API 성공 시에만 대기 화면으로 이동
Expand Down
19 changes: 1 addition & 18 deletions components/logs-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,7 @@ export function LogsContent() {
const [statusFilter, setStatusFilter] = useState("전체 상태")
const [isDropdownOpen, setIsDropdownOpen] = useState(false)

// 추후 로그 조원을 위한 API 연동 가능 (현재는 API 부재로 빈 상태)
/*
useEffect(() => {
const fetchLogs = async () => {
setIsLoading(true);
try {
const response = await fetch('/api/admin/logs');
const data = await response.json();
if (data.result) setLogs(data.result);
} catch (e) {
console.error("Failed to fetch logs", e);
} finally {
setIsLoading(false);
}
};
fetchLogs();
}, []);
*/
// 로그 API가 준비되면 컴포넌트에서 직접 fetch하지 말고 lib/api 레이어로 연결한다.

const filteredLogs = logs.filter((log: LogEntry) => {
const matchesSearch =
Expand Down
70 changes: 29 additions & 41 deletions components/problems-content.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client"

import { useState, useMemo, useEffect } from "react"
import { getCookie } from "@/lib/auth/cookie-utils"
import { Search, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react"
import { getProblems, type AdminProblem } from "@/lib/api/admin"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Switch } from "@/components/ui/switch"

Expand Down Expand Up @@ -35,55 +35,43 @@ export function ProblemsContent() {
const pageSize = 8

const fetchProblems = async () => {
setIsLoading(true);
setIsLoading(true)
try {
const token = getCookie('admin_access_token');
const response = await fetch('/api/admin/problems', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();

if (data.code === 'COMMON200' && data.result) {
const mapped: Problem[] = data.result.map((p: any) => {
// Difficulty 매핑
let difficulty: "쉬움" | "중간" | "어려움" = "쉬움";
if (p.difficulty === 'MEDIUM') difficulty = "중간";
if (p.difficulty === 'HARD') difficulty = "어려움";
const adminProblems = await getProblems()
const mapped: Problem[] = adminProblems.map((problem: AdminProblem) => {
let difficulty: "쉬움" | "중간" | "어려움" = "쉬움"
if (problem.difficulty === "MEDIUM") difficulty = "중간"
if (problem.difficulty === "HARD") difficulty = "어려움"

// Tags 파싱 (JSON String)
let tags: string[] = [];
try {
if (p.tags) {
tags = typeof p.tags === 'string' ? JSON.parse(p.tags) : p.tags;
}
} catch (e) {
console.error("Failed to parse tags:", p.tags);
let tags: string[] = []
try {
if (problem.tags) {
tags = typeof problem.tags === "string" ? JSON.parse(problem.tags) : problem.tags
}
} catch {
console.error("Failed to parse tags:", problem.tags)
}

return {
id: String(p.id),
title: p.title,
version: "v1.0", // BE에 버전 정보가 없으면 기본값
difficulty,
available: p.status === 'ACTIVE',
tags
};
});
setProblems(mapped);
}
return {
id: String(problem.id),
title: problem.title,
version: "v1.0",
difficulty,
available: problem.status === "PUBLISHED" || problem.status === "ACTIVE",
tags,
}
})
setProblems(mapped)
} catch (error) {
console.error("Failed to fetch problems:", error);
console.error("Failed to fetch problems:", error)
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}

useEffect(() => {
fetchProblems();
}, []);
fetchProblems()
}, [])

const filteredProblems = useMemo(() => {
let result = problems
Expand Down
55 changes: 16 additions & 39 deletions components/results-content.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
"use client"

import { useState, useEffect } from "react"
import { getCookie } from "@/lib/auth/cookie-utils"
import Link from "next/link"
import { Search, Download, Eye, X, CheckCircle, ChevronLeft, ChevronRight } from "lucide-react"

interface ResultEntry {
id: string
entryCode: string
total: number
completed: number
}

interface Toast {
id: string
title: string
description: string
}
import { getExams, type Exam } from "@/lib/api/admin"

interface ResultEntry {
id: string
Expand Down Expand Up @@ -48,36 +35,26 @@ export function ResultsContent() {
}

const fetchResults = async () => {
setIsLoading(true);
setIsLoading(true)
try {
const token = getCookie('admin_access_token');
const response = await fetch('/api/admin/exams', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();

if (data.code === 'COMMON200' && data.result) {
const mapped: ResultEntry[] = data.result.map((exam: any) => ({
id: String(exam.id),
entryCode: exam.title,
total: exam.participantCount || 0,
completed: exam.status === 'COMPLETED' ? (exam.participantCount || 0) : 0 // 실제 완료 인원은 별도 API 필요
}));
setResults(mapped);
}
const exams = await getExams()
const mapped: ResultEntry[] = exams.map((exam: Exam) => ({
id: String(exam.id),
entryCode: exam.title,
total: exam.participantCount,
completed: exam.completedCount,
}))
setResults(mapped)
} catch (error) {
console.error("Failed to fetch results:", error);
console.error("Failed to fetch results:", error)
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}

useEffect(() => {
fetchResults();
}, []);
fetchResults()
}, [])

const removeToast = (id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id))
Expand All @@ -91,7 +68,7 @@ export function ResultsContent() {
}

const filteredResults = results.filter((result) => result.entryCode.toLowerCase().includes(searchQuery.toLowerCase()))

const totalPages = Math.ceil(filteredResults.length / pageSize)
const startIndex = (currentPage - 1) * pageSize
const endIndex = currentPage * pageSize
Expand Down
102 changes: 38 additions & 64 deletions components/test-sessions-content.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client"

import { useState, useEffect } from "react"
import { getCookie } from "@/lib/auth/cookie-utils"
import { Eye, Trash2, MoreHorizontal, ChevronLeft, ChevronRight } from "lucide-react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
Expand All @@ -19,7 +18,7 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet"
import { useRouter } from "next/navigation";
import { deleteExam, getBoard, getExams, type Exam, type ExamineeBoardEntry } from "@/lib/api/admin";

export interface TestSession {
id: number
Expand Down Expand Up @@ -61,69 +60,53 @@ export function TestSessionsContent({ onViewDetails }: TestSessionsContentProps)

// 1. 시험 목록 조회 (Exams)
const fetchExams = async () => {
setIsLoading(true);
setIsLoading(true)
try {
const token = getCookie('admin_access_token');
const response = await fetch('/api/admin/exams', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();

if (data.code === 'COMMON200' && data.result) {
const mapped: TestSession[] = data.result.map((exam: any) => ({
id: exam.id,
sessionId: exam.title, // 제목을 세션 ID로 표시
createdBy: "Admin", // BE 상에 생성자 이름 정보가 부족할 경우 고정값
createdAt: exam.startTime ? exam.startTime.split('T')[0] : "-",
status: exam.status === 'RUNNING' ? 'Active' : 'Completed',
participants: exam.participantCount || 0
}));
setTestSessions(mapped);
}
const exams = await getExams()
const mapped: TestSession[] = exams.map((exam: Exam) => ({
id: exam.id,
sessionId: exam.title,
createdBy: "Admin",
createdAt: exam.startsAt ? exam.startsAt.split("T")[0] : "-",
status: ["RUNNING", "IN_PROGRESS"].includes(exam.state) ? "Active" : "Completed",
participants: exam.participantCount,
}))
Comment on lines +66 to +73
setTestSessions(mapped)
} catch (error) {
console.error("Failed to fetch exams:", error);
console.error("Failed to fetch exams:", error)
} finally {
setIsLoading(false);
setIsLoading(false)
}
};
}

// 2. 특정 시험의 참가자 현황 조회 (Board)
const fetchParticipants = async (examId: number) => {
setIsParticipantsLoading(true);
setIsParticipantsLoading(true)
try {
const token = getCookie('admin_access_token');
const response = await fetch(`/api/admin/board?examId=${examId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();

if (data.code === 'COMMON200' && data.result) {
const mapped: Participant[] = data.result.map((p: any) => ({
id: p.examParticipantId,
name: p.name,
phoneNumber: p.phoneMasked,
connectionStatus: p.state === 'ENTRANCE' ? "Connected" : "Disconnected",
submissionStatus: p.submitted ? "Submitted" : (p.state === 'ENTRANCE' ? "In Progress" : "Not Started"),
tokenUsage: p.tokenUsed || 0
}));
setParticipants(mapped);
}
const board = await getBoard(examId)
const mapped: Participant[] = board.map((participant: ExamineeBoardEntry) => ({
id: participant.examParticipantId,
name: participant.name,
phoneNumber: participant.phoneMasked,
connectionStatus: participant.state === "ENTRANCE" ? "Connected" : "Disconnected",
submissionStatus: participant.submitted
? "Submitted"
: participant.state === "ENTRANCE"
? "In Progress"
: "Not Started",
tokenUsage: participant.tokenUsed || 0,
}))
setParticipants(mapped)
} catch (error) {
console.error("Failed to fetch participants:", error);
console.error("Failed to fetch participants:", error)
} finally {
setIsParticipantsLoading(false);
setIsParticipantsLoading(false)
}
};
}

useEffect(() => {
fetchExams();
}, []);
fetchExams()
}, [])

const filteredSessions =
statusFilter === "All" ? testSessions : testSessions.filter((session) => session.status === statusFilter)
Expand All @@ -148,18 +131,10 @@ export function TestSessionsContent({ onViewDetails }: TestSessionsContentProps)
if (!selectedSession) return

try {
const token = getCookie('admin_access_token');
const response = await fetch(`/api/admin/exams/${selectedSession.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
setTestSessions((prev) => prev.filter((session) => session.id !== selectedSession.id))
}
await deleteExam(selectedSession.id)
setTestSessions((prev) => prev.filter((session) => session.id !== selectedSession.id))
} catch (error) {
console.error("Failed to delete exam:", error);
console.error("Failed to delete exam:", error)
} finally {
setIsDeleteSessionOpen(false)
setSelectedSession(null)
Expand Down Expand Up @@ -787,4 +762,3 @@ export function TestSessionsContent({ onViewDetails }: TestSessionsContentProps)
)
}
export default TestSessionsContent;

Loading
Loading