Skip to content

Commit 7fb56f2

Browse files
authored
Merge pull request #38 from CSID-DGU/frontend/feature/user
FE: [feat] 수험자 페이지 구축
2 parents cf7cbf5 + 90f7845 commit 7fb56f2

32 files changed

+2847
-173
lines changed

src/frontend/eyesee-admin/src/apis/index.ts

-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ function createInstance(type: string) {
5656
* 이 함수는 기본 URL만 설정된 Axios 인스턴스를 생성합니다.
5757
* 인증 토큰이 필요하지 않은 API 요청(예: 로그인, 회원가입 등)에 사용됩니다.
5858
*/
59-
// Todo: .env 파일에서 환경변수로 api 주소를 가져오고 있지만, 서버 주소가 나오면 env 파일 없이도 사용 가능하게 수정해야 함.
6059
function createInstanceWithoutAuth() {
6160
const instance = axios.create({
6261
baseURL: process.env.NEXT_PUBLIC_API_KEY,

src/frontend/eyesee-user/package.json

+7-5
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12+
"axios": "^1.7.7",
13+
"next": "15.0.2",
1214
"react": "19.0.0-rc-02c0e824-20241028",
13-
"react-dom": "19.0.0-rc-02c0e824-20241028",
14-
"next": "15.0.2"
15+
"react-dom": "19.0.0-rc-02c0e824-20241028"
1516
},
1617
"devDependencies": {
17-
"typescript": "^5",
18+
"@svgr/webpack": "^8.1.0",
1819
"@types/node": "^20",
1920
"@types/react": "^18",
2021
"@types/react-dom": "^18",
22+
"eslint": "^8",
23+
"eslint-config-next": "15.0.2",
2124
"postcss": "^8",
2225
"tailwindcss": "^3.4.1",
23-
"eslint": "^8",
24-
"eslint-config-next": "15.0.2"
26+
"typescript": "^5"
2527
}
2628
}

src/frontend/eyesee-user/pnpm-lock.yaml

+2,047-60
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { getAccessToken } from "@/utils/auth";
2+
import axios, { AxiosInstance } from "axios";
3+
4+
/**
5+
* Axios 인스턴스에 인터셉터를 설정하는 함수
6+
*
7+
* @function setInterceptors
8+
* @param {AxiosInstance} instance - 인터셉터를 설정할 Axios 인스턴스
9+
* @returns {AxiosInstance} 인터셉터가 설정된 Axios 인스턴스
10+
*
11+
* @description
12+
* 이 함수는 요청 인터셉터를 설정하여 각 API 요청에 인증 토큰을 추가합니다.
13+
* 클라이언트 사이드에서만 토큰을 추가하며, 서버 사이드 렌더링 시에는 토큰을 추가하지 않습니다.
14+
*/
15+
16+
function setInterceptors(instance: AxiosInstance) {
17+
instance.interceptors.request.use(
18+
(config) => {
19+
if (typeof window !== "undefined" && config.headers) {
20+
config.headers.Authorization = `Bearer ${getAccessToken()}`;
21+
}
22+
return config;
23+
},
24+
(error) => {
25+
return Promise.reject(error);
26+
}
27+
);
28+
29+
return instance;
30+
}
31+
32+
/**
33+
* 인증이 필요한 API 요청을 위한 Axios 인스턴스를 생성하는 함수
34+
*
35+
* @function createInstance
36+
* @returns {AxiosInstance} 인터셉터가 설정된 Axios 인스턴스
37+
*
38+
* @description
39+
* 이 함수는 기본 URL이 설정된 Axios 인스턴스를 생성하고,
40+
* setInterceptors 함수를 통해 인증 토큰을 자동으로 추가하는 인터셉터를 설정합니다.
41+
*/
42+
function createInstance() {
43+
const instance = axios.create({
44+
baseURL: process.env.NEXT_PUBLIC_API_KEY,
45+
});
46+
return setInterceptors(instance);
47+
}
48+
49+
/**
50+
* 인증이 필요하지 않은 API 요청을 위한 Axios 인스턴스를 생성하는 함수
51+
*
52+
* @function createInstanceWithoutAuth
53+
* @returns {AxiosInstance} 기본 설정만 된 Axios 인스턴스
54+
*
55+
* @description
56+
* 이 함수는 기본 URL만 설정된 Axios 인스턴스를 생성합니다.
57+
* 인증 토큰이 필요하지 않은 API 요청(예: 로그인, 회원가입 등)에 사용됩니다.
58+
*/
59+
function createInstanceWithoutAuth() {
60+
const instance = axios.create({
61+
baseURL: process.env.NEXT_PUBLIC_API_KEY,
62+
});
63+
return instance;
64+
}
65+
66+
/**
67+
* 인증이 필요한 API 요청에 사용할 Axios 인스턴스
68+
* @const {AxiosInstance}
69+
*/
70+
export const api = createInstance();
71+
72+
/**
73+
* 인증이 필요하지 않은 API 요청에 사용할 Axios 인스턴스
74+
* @const {AxiosInstance}
75+
*/
76+
export const apiWithoutAuth = createInstanceWithoutAuth();

src/frontend/eyesee-user/src/app/(home)/page.tsx

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
import HomeHeader from "@/components/home/HomeHeader";
2+
import Explain from "@/assets/icons/Explain.svg";
3+
import HomeCta from "@/components/home/HomeCta";
4+
15
const HomePage = () => {
26
return (
3-
<div>
4-
<div>유저 페이지</div>
7+
<div className="bg-bgGradient w-screen h-screen flex flex-col justify-between">
8+
<HomeHeader />
9+
<div className="mb-[10vh]">
10+
<Explain className="mx-[8vw] mb-[12vh]" />
11+
<HomeCta />
12+
</div>
513
</div>
614
);
715
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"use client";
2+
3+
import AgreementSection from "@/components/agreement/AgreementSection";
4+
import NextButton from "@/components/common/NextButton";
5+
import SubHeader from "@/components/common/SubHeader";
6+
import { Agreements } from "@/types/agreements";
7+
import { useEffect, useState } from "react";
8+
9+
const AgreementPage = () => {
10+
const [isAvailable, setIsAvailable] = useState(false);
11+
const [agreements, setAgreements] = useState<Agreements>({
12+
all: false,
13+
personalInfo: false,
14+
thirdParty: false,
15+
});
16+
17+
useEffect(() => {
18+
if (agreements.personalInfo) {
19+
setIsAvailable(true);
20+
} else {
21+
setIsAvailable(false);
22+
}
23+
}, [agreements.personalInfo]);
24+
25+
return (
26+
<div>
27+
<SubHeader title="시험 시작하기" />
28+
<AgreementSection agreements={agreements} setAgreements={setAgreements} />
29+
<div className="fixed bottom-6 right-0">
30+
<NextButton
31+
action="/information"
32+
title="NEXT"
33+
isAvailable={isAvailable}
34+
/>
35+
</div>
36+
</div>
37+
);
38+
};
39+
40+
export default AgreementPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import NextButton from "@/components/common/NextButton";
4+
import SubHeader from "@/components/common/SubHeader";
5+
import { useRef, useEffect } from "react";
6+
7+
const CameraPage = () => {
8+
const videoRef = useRef<HTMLVideoElement>(null);
9+
10+
// 비디오 스트림 가져오기
11+
const startStreaming = async () => {
12+
try {
13+
const constraints = { video: true };
14+
const stream = await navigator.mediaDevices.getUserMedia(constraints);
15+
if (videoRef.current) {
16+
videoRef.current.srcObject = stream;
17+
}
18+
} catch (error) {
19+
console.error("비디오 스트림 가져오기 오류:", error);
20+
alert("카메라 권한을 허용해주세요.");
21+
}
22+
};
23+
24+
// 컴포넌트 마운트 시 비디오 스트림 시작
25+
useEffect(() => {
26+
startStreaming();
27+
}, []);
28+
29+
return (
30+
<div className="p-4">
31+
<SubHeader title="카메라 확인" />
32+
33+
<div className="flex flex-col items-center gap-4 mt-4">
34+
{/* 비디오 화면 */}
35+
<video
36+
ref={videoRef}
37+
autoPlay
38+
playsInline
39+
className="w-[90vw] h-[60vh] object-cover border border-gray-300 rounded"
40+
style={{ transform: "scaleX(-1)" }} // 좌우 반전
41+
/>
42+
<div className="text-sm text-gray-600">
43+
카메라가 정상적으로 작동하고 있는지 확인하세요.
44+
</div>
45+
</div>
46+
47+
<div className="fixed bottom-6 right-6">
48+
<NextButton
49+
action="/exam-room"
50+
title="시험장 접속"
51+
isAvailable={true}
52+
/>
53+
</div>
54+
</div>
55+
);
56+
};
57+
58+
export default CameraPage;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use client";
2+
3+
import NextButton from "@/components/common/NextButton";
4+
import SubHeader from "@/components/common/SubHeader";
5+
import InputTestCode from "@/components/enterTestCode/InputTestCode";
6+
import React, { useEffect, useState } from "react";
7+
8+
const EnterPage = () => {
9+
const [code, setCode] = useState("");
10+
const [isAvailable, setIsAvailable] = useState(false);
11+
12+
useEffect(() => {
13+
if (code != "") {
14+
setIsAvailable(true);
15+
}
16+
}, [code]);
17+
18+
return (
19+
<div className="bg-white w-screen">
20+
<SubHeader title="시험 시작하기" />
21+
<div className="mt-[22vh]">
22+
<InputTestCode setCode={setCode} code={code} />
23+
</div>
24+
<div className="fixed bottom-6 right-0">
25+
<NextButton
26+
action="/agreement"
27+
title="NEXT"
28+
isAvailable={isAvailable}
29+
/>
30+
</div>
31+
</div>
32+
);
33+
};
34+
35+
export default EnterPage;

0 commit comments

Comments
 (0)