Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FE: [feat] 수험자 페이지 구축 #38

Merged
merged 12 commits into from
Nov 17, 2024
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
1 change: 0 additions & 1 deletion src/frontend/eyesee-admin/src/apis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ function createInstance(type: string) {
* 이 함수는 기본 URL만 설정된 Axios 인스턴스를 생성합니다.
* 인증 토큰이 필요하지 않은 API 요청(예: 로그인, 회원가입 등)에 사용됩니다.
*/
// Todo: .env 파일에서 환경변수로 api 주소를 가져오고 있지만, 서버 주소가 나오면 env 파일 없이도 사용 가능하게 수정해야 함.
function createInstanceWithoutAuth() {
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_KEY,
Expand Down
12 changes: 7 additions & 5 deletions src/frontend/eyesee-user/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@
"lint": "next lint"
},
"dependencies": {
"axios": "^1.7.7",
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028",
"next": "15.0.2"
"react-dom": "19.0.0-rc-02c0e824-20241028"
},
"devDependencies": {
"typescript": "^5",
"@svgr/webpack": "^8.1.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "15.0.2",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "15.0.2"
"typescript": "^5"
}
}
2,107 changes: 2,047 additions & 60 deletions src/frontend/eyesee-user/pnpm-lock.yaml

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions src/frontend/eyesee-user/src/apis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getAccessToken } from "@/utils/auth";
import axios, { AxiosInstance } from "axios";

/**
* Axios 인스턴스에 인터셉터를 설정하는 함수
*
* @function setInterceptors
* @param {AxiosInstance} instance - 인터셉터를 설정할 Axios 인스턴스
* @returns {AxiosInstance} 인터셉터가 설정된 Axios 인스턴스
*
* @description
* 이 함수는 요청 인터셉터를 설정하여 각 API 요청에 인증 토큰을 추가합니다.
* 클라이언트 사이드에서만 토큰을 추가하며, 서버 사이드 렌더링 시에는 토큰을 추가하지 않습니다.
*/

function setInterceptors(instance: AxiosInstance) {
instance.interceptors.request.use(
(config) => {
if (typeof window !== "undefined" && config.headers) {
config.headers.Authorization = `Bearer ${getAccessToken()}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

return instance;
}

/**
* 인증이 필요한 API 요청을 위한 Axios 인스턴스를 생성하는 함수
*
* @function createInstance
* @returns {AxiosInstance} 인터셉터가 설정된 Axios 인스턴스
*
* @description
* 이 함수는 기본 URL이 설정된 Axios 인스턴스를 생성하고,
* setInterceptors 함수를 통해 인증 토큰을 자동으로 추가하는 인터셉터를 설정합니다.
*/
function createInstance() {
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_KEY,
});
return setInterceptors(instance);
}

/**
* 인증이 필요하지 않은 API 요청을 위한 Axios 인스턴스를 생성하는 함수
*
* @function createInstanceWithoutAuth
* @returns {AxiosInstance} 기본 설정만 된 Axios 인스턴스
*
* @description
* 이 함수는 기본 URL만 설정된 Axios 인스턴스를 생성합니다.
* 인증 토큰이 필요하지 않은 API 요청(예: 로그인, 회원가입 등)에 사용됩니다.
*/
function createInstanceWithoutAuth() {
const instance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_KEY,
});
return instance;
}

/**
* 인증이 필요한 API 요청에 사용할 Axios 인스턴스
* @const {AxiosInstance}
*/
export const api = createInstance();

/**
* 인증이 필요하지 않은 API 요청에 사용할 Axios 인스턴스
* @const {AxiosInstance}
*/
export const apiWithoutAuth = createInstanceWithoutAuth();
12 changes: 10 additions & 2 deletions src/frontend/eyesee-user/src/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import HomeHeader from "@/components/home/HomeHeader";
import Explain from "@/assets/icons/Explain.svg";
import HomeCta from "@/components/home/HomeCta";

const HomePage = () => {
return (
<div>
<div>유저 페이지</div>
<div className="bg-bgGradient w-screen h-screen flex flex-col justify-between">
<HomeHeader />
<div className="mb-[10vh]">
<Explain className="mx-[8vw] mb-[12vh]" />
<HomeCta />
</div>
</div>
);
};
Expand Down
40 changes: 40 additions & 0 deletions src/frontend/eyesee-user/src/app/agreement/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use client";

import AgreementSection from "@/components/agreement/AgreementSection";
import NextButton from "@/components/common/NextButton";
import SubHeader from "@/components/common/SubHeader";
import { Agreements } from "@/types/agreements";
import { useEffect, useState } from "react";

const AgreementPage = () => {
const [isAvailable, setIsAvailable] = useState(false);
const [agreements, setAgreements] = useState<Agreements>({
all: false,
personalInfo: false,
thirdParty: false,
});

useEffect(() => {
if (agreements.personalInfo) {
setIsAvailable(true);
} else {
setIsAvailable(false);
}
}, [agreements.personalInfo]);

return (
<div>
<SubHeader title="시험 시작하기" />
<AgreementSection agreements={agreements} setAgreements={setAgreements} />
<div className="fixed bottom-6 right-0">
<NextButton
action="/information"
title="NEXT"
isAvailable={isAvailable}
/>
</div>
</div>
);
};

export default AgreementPage;
58 changes: 58 additions & 0 deletions src/frontend/eyesee-user/src/app/camera/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import NextButton from "@/components/common/NextButton";
import SubHeader from "@/components/common/SubHeader";
import { useRef, useEffect } from "react";

const CameraPage = () => {
const videoRef = useRef<HTMLVideoElement>(null);

// 비디오 스트림 가져오기
const startStreaming = async () => {
try {
const constraints = { video: true };
const stream = await navigator.mediaDevices.getUserMedia(constraints);
if (videoRef.current) {
videoRef.current.srcObject = stream;
}
} catch (error) {
console.error("비디오 스트림 가져오기 오류:", error);
alert("카메라 권한을 허용해주세요.");
}
};

// 컴포넌트 마운트 시 비디오 스트림 시작
useEffect(() => {
startStreaming();
}, []);

return (
<div className="p-4">
<SubHeader title="카메라 확인" />

<div className="flex flex-col items-center gap-4 mt-4">
{/* 비디오 화면 */}
<video
ref={videoRef}
autoPlay
playsInline
className="w-[90vw] h-[60vh] object-cover border border-gray-300 rounded"
style={{ transform: "scaleX(-1)" }} // 좌우 반전
/>
<div className="text-sm text-gray-600">
카메라가 정상적으로 작동하고 있는지 확인하세요.
</div>
</div>

<div className="fixed bottom-6 right-6">
<NextButton
action="/exam-room"
title="시험장 접속"
isAvailable={true}
/>
</div>
</div>
);
};

export default CameraPage;
35 changes: 35 additions & 0 deletions src/frontend/eyesee-user/src/app/enter/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";

import NextButton from "@/components/common/NextButton";
import SubHeader from "@/components/common/SubHeader";
import InputTestCode from "@/components/enterTestCode/InputTestCode";
import React, { useEffect, useState } from "react";

const EnterPage = () => {
const [code, setCode] = useState("");
const [isAvailable, setIsAvailable] = useState(false);

useEffect(() => {
if (code != "") {
setIsAvailable(true);
}
}, [code]);

return (
<div className="bg-white w-screen">
<SubHeader title="시험 시작하기" />
<div className="mt-[22vh]">
<InputTestCode setCode={setCode} code={code} />
</div>
<div className="fixed bottom-6 right-0">
<NextButton
action="/agreement"
title="NEXT"
isAvailable={isAvailable}
/>
</div>
</div>
);
};

export default EnterPage;
Loading