Skip to content

Commit

Permalink
Merge pull request #46 from CSID-DGU/frontend/feature/add-exam
Browse files Browse the repository at this point in the history
FE: [feat] 수험자 화면 API 연결
  • Loading branch information
hyeona01 authored Nov 19, 2024
2 parents 791225f + 66dadb7 commit 3e521f4
Show file tree
Hide file tree
Showing 18 changed files with 275 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/frontend/eyesee-admin/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { deleteAccessToken, getAccessToken } from "@/utils/auth";
import { getAccessToken } from "@/utils/auth";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactNode } from "react";

Expand Down
3 changes: 2 additions & 1 deletion src/frontend/eyesee-user/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"axios": "^1.7.7",
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028"
"react-dom": "19.0.0-rc-02c0e824-20241028",
"zustand": "^5.0.1"
},
"devDependencies": {
"@svgr/webpack": "^8.1.0",
Expand Down
26 changes: 26 additions & 0 deletions src/frontend/eyesee-user/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/frontend/eyesee-user/src/apis/examCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { apiWithoutAuth } from ".";
import { RESTYPE } from "@/types/common";
import { ExamRequest, ExamResponse } from "@/types/exam";

export const enterSession = async (
data: ExamRequest
): Promise<RESTYPE<ExamResponse>> => {
const response = await apiWithoutAuth.post(`/sessions/join`, data);
return response.data;
};
1 change: 0 additions & 1 deletion src/frontend/eyesee-user/src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ function createInstance() {
*
* @description
* 이 함수는 기본 URL만 설정된 Axios 인스턴스를 생성합니다.
* 인증 토큰이 필요하지 않은 API 요청(예: 로그인, 회원가입 등)에 사용됩니다.
*/
function createInstanceWithoutAuth() {
const instance = axios.create({
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/eyesee-user/src/apis/userInformation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { apiWithoutAuth } from ".";
import { RESTYPE } from "@/types/common";
import { UserInfoRequest, UserInfoResponse } from "@/types/exam";

export const userInformation = async (
data: UserInfoRequest
): Promise<RESTYPE<UserInfoResponse>> => {
const response = await apiWithoutAuth.post(`/sessions/student`, data);
return response.data;
};
22 changes: 20 additions & 2 deletions src/frontend/eyesee-user/src/app/enter/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
"use client";

import { enterSession } from "@/apis/examCode";
import NextButton from "@/components/common/NextButton";
import SubHeader from "@/components/common/SubHeader";
import InputTestCode from "@/components/enterTestCode/InputTestCode";
import { useExamStore } from "@/store/useExamStore";
import { useRouter } from "next/navigation";
import React, { useEffect, useState } from "react";

const EnterPage = () => {
const [code, setCode] = useState("");
const [isAvailable, setIsAvailable] = useState(false);
const { setExam } = useExamStore();
const router = useRouter();

// 시험 코드 제출 핸들러
const handleSubmit = async () => {
try {
const response = await enterSession({ examCode: code }); // API 호출
console.log("응답 데이터:", response); // 성공 시 데이터 처리
setExam(response.data);
router.push("/notice");
} catch (error) {
console.error("시험 세션 참여 실패:", error); // 실패 시 에러 처리
alert("시험 코드 확인에 실패했습니다. 다시 시도해주세요.");
}
};

useEffect(() => {
if (code != "") {
if (code.trim() != "") {
setIsAvailable(true);
}
}, [code]);
Expand All @@ -23,9 +41,9 @@ const EnterPage = () => {
</div>
<div className="fixed bottom-6 right-0">
<NextButton
action="/agreement"
title="NEXT"
isAvailable={isAvailable}
onSubmit={handleSubmit}
/>
</div>
</div>
Expand Down
38 changes: 32 additions & 6 deletions src/frontend/eyesee-user/src/app/information/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
"use client";

import { userInformation } from "@/apis/userInformation";
import NextButton from "@/components/common/NextButton";
import SubHeader from "@/components/common/SubHeader";
import InformationSection from "@/components/information/InformationSection";
import { Information } from "@/types/information";
import { useExamStore } from "@/store/useExamStore";
import { UserInfoRequest } from "@/types/exam";
import { setAccessToken, setRefreshToken } from "@/utils/auth";
import { informationValidation } from "@/utils/validation";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

const InformationPage = () => {
const router = useRouter();
const { exam } = useExamStore();
const [isAvailable, setIsAvailable] = useState(false);
const [information, setInformation] = useState<Information>({
const [information, setInformation] = useState<UserInfoRequest>({
// TODO: 서버 데이터 타입 통일 필요
examCode: exam.examRandomCode,
name: "",
major: "",
studentNumber: "",
seatNumber: 0,
department: "",
userNum: 0,
seatNum: 0,
});

// 수험자 정보 제출 핸들러
const handleSubmit = async () => {
try {
const response = await userInformation(information); // API 호출
console.log("응답 데이터:", response); // 성공 시 데이터 처리
setAccessToken(response.data.access_token);
setRefreshToken(response.data.refresh_token);
router.push("/camera");
} catch (error) {
console.error("수험 정보 입력 실패:", error); // 실패 시 에러 처리
alert("수험 정보 입력에 실패했습니다. 다시 시도해주세요.");
}
};

useEffect(() => {
if (informationValidation(information)) {
setIsAvailable(true);
Expand All @@ -32,7 +54,11 @@ const InformationPage = () => {
setInformation={setInformation}
/>
<div className="fixed bottom-6 right-0">
<NextButton action="/camera" title="NEXT" isAvailable={isAvailable} />
<NextButton
onSubmit={handleSubmit}
title="NEXT"
isAvailable={isAvailable}
/>
</div>
</div>
);
Expand Down
21 changes: 21 additions & 0 deletions src/frontend/eyesee-user/src/app/notice/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import NextButton from "@/components/common/NextButton";
import SubHeader from "@/components/common/SubHeader";
import ExamCard from "@/components/notice/ExamCard";
import NoticeCard from "@/components/notice/NoticeCard";

const NoticePage = () => {
return (
<div>
<SubHeader title="시험 유의사항" />
<div className="flex flex-col items-center">
<NoticeCard />
<ExamCard />
</div>
<div className="fixed bottom-6 right-0">
<NextButton title="NEXT" isAvailable={true} action="/agreement" />
</div>
</div>
);
};

export default NoticePage;
3 changes: 3 additions & 0 deletions src/frontend/eyesee-user/src/assets/icons/InfoIcon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ type NextButtonProps = {
isAvailable: boolean;
title: string;
noArrow?: boolean;
onSubmit?: () => void;
};

const NextButton = ({
action,
isAvailable,
title,
noArrow,
onSubmit,
}: NextButtonProps) => {
const route = useRouter();
const handleClick = () => {
Expand All @@ -23,7 +25,8 @@ const NextButton = ({
};
return (
<div
onClick={handleClick}
// onClick={handleClick}
onClick={onSubmit ? onSubmit : handleClick}
className={`z-50 flex justify-between items-center gap-14 text-white text-[14px] tracking-[4.2px] px-4 py-3 ${
isAvailable ? "bg-[rgb(14,29,60,0.8)]" : "bg-[rgb(146,146,146,0.8)]"
}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Information } from "@/types/information";
import { UserInfoRequest } from "@/types/exam";
import { Dispatch, SetStateAction } from "react";

type InformationSectionProps = {
information: Information;
setInformation: Dispatch<SetStateAction<Information>>;
information: UserInfoRequest;
setInformation: Dispatch<SetStateAction<UserInfoRequest>>;
};

const InformationSection = ({
information,
setInformation,
}: InformationSectionProps) => {
const handleChange = (key: keyof Information, value: string) => {
const handleChange = (key: keyof UserInfoRequest, value: string) => {
setInformation((prev) => ({
...prev,
[key]: value,
Expand Down Expand Up @@ -41,8 +41,8 @@ const InformationSection = ({
id="department"
type="text"
placeholder="학과"
value={information.major}
onChange={(e) => handleChange("major", e.target.value)}
value={information.department}
onChange={(e) => handleChange("department", e.target.value)}
className="w-full text-[#141412] text-[14px] px-3 py-3 border-b border-black"
/>
</div>
Expand All @@ -52,11 +52,11 @@ const InformationSection = ({
학번
</label>
<input
id="studentId"
id="userNum"
type="text"
placeholder="학번"
value={information.studentNumber}
onChange={(e) => handleChange("studentNumber", e.target.value)}
value={information.userNum == 0 ? "" : information.userNum}
onChange={(e) => handleChange("userNum", e.target.value)}
className="w-full text-[#141412] text-[14px] px-3 py-3 border-b border-black"
/>
</div>
Expand All @@ -66,11 +66,11 @@ const InformationSection = ({
좌석 번호
</label>
<input
id="seatNumber"
id="seatNum"
type="text"
placeholder="좌석 번호"
value={information.seatNumber}
onChange={(e) => handleChange("seatNumber", e.target.value)}
value={information.seatNum == 0 ? "" : information.seatNum}
onChange={(e) => handleChange("seatNum", e.target.value)}
className="w-full text-[#141412] text-[14px] px-3 py-3 border-b border-black"
/>
</div>
Expand Down
41 changes: 41 additions & 0 deletions src/frontend/eyesee-user/src/components/notice/ExamCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"use client";

import { useExamStore } from "@/store/useExamStore";

const ExamCard = () => {
const { exam } = useExamStore();

return (
<div className="mt-8 py-[26px] px-[20px] bg-[#0E1D3C] shadow-lg flex flex-col rounded-ss-2xl rounded-ee-2xl">
<div className="text-white flex items-center py-3 border-b border-white px-3">
<div className="text-[14px] w-[25vw]">강의명</div>
<div className="text-[12px] font-semibold">{exam.examName}</div>
</div>
<div className="text-white flex items-center py-3 border-b border-white px-3">
<div className="text-[14px] w-[25vw]">담당 교수</div>
<div className="text-[12px] font-semibold">확인중</div>
</div>
<div className="text-white flex items-center py-3 border-b border-white px-3">
<div className="text-[14px] w-[25vw]">시험 시작시간</div>
<div className="text-[12px] font-semibold">{exam.examStartTime}</div>
</div>
<div className="text-white flex items-center py-3 border-b border-white px-3">
<div className="text-[14px] w-[25vw]">진행 시간</div>
<div className="text-[12px] font-semibold">{exam.examDuration}</div>
</div>
<div className="text-white flex items-center py-3 border-b border-white px-3">
<div className="text-[14px] w-[25vw]">문제 수</div>
<div className="text-[12px] font-semibold">확인 중</div>
</div>
<div className="text-white flex items-center py-3 border-b border-white px-3">
<div className="text-[14px] w-[25vw]">총 점수</div>
<div className="text-[12px] font-semibold">확인 중</div>
</div>
<div className="text-white text-[10px] mt-3">
※ 카메라 권한을 허용해야 합니다.
</div>
</div>
);
};

export default ExamCard;
21 changes: 21 additions & 0 deletions src/frontend/eyesee-user/src/components/notice/NoticeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import InfoIcon from "@/assets/icons/InfoIcon.svg";

const NoticeCard = () => {
return (
<div className="w-[80vw] mt-8 py-5 rounded-xl bg-[rgba(14,29,60,0.1)]">
<div className="flex items-center justify-center gap-1 text-black font-semibold text-[14px] text-center mb-3">
<p>유의사항</p>
<InfoIcon />
</div>
<div className="text-[10px] font-light text-black text-center">
시험 정보는 시험 감독자(관리자)가 입력한 정보입니다.
<br />
아래 기재된 정보와 관련하여 궁금한 사항은
<br />
시험장에 상주하는 관리자를 통해 확인하시기 바랍니다.
</div>
</div>
);
};

export default NoticeCard;
Loading

0 comments on commit 3e521f4

Please sign in to comment.