<추가/수정 사항>
※난이도를 상/중/하로 분류
※힌트 추가(글자 수, 연관 단어)
-백엔드-
model - <gameModel.js>
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const gameSchema = new Schema({
_id: { type: String, required: true },
title: { type: String, require: true, unique: true },
image: { type: String, require: true },
level: { type: String, require: true }, //난이도
length: { type: String, require: true }, //힌트
hint: { type: String, require: true },
});
module.exports = mongoose.model("Game", gameSchema);
controller - <gameController.js>
//Get random Image, /image : 이미지 가져오기
let imageID = [];
const getImage = asynchHandler(async (req, res) => {
const { level } = req.body; //프론트에서 level 값 받아오기
if (imageID.length >= 10) {
imageID = [];
res.send({ message: "게임이 종료되었습니다." });
} else {
const game = await Game.aggregate([
{ $match: { _id: { $nin: imageID }, level: level } }, //레벨 지정
{ $sample: { size: 1 } },
]);
imageID.push(game[0]._id);
res.status(200).send({ game: game, count: imageID.length });
}
console.log(imageID);
});
//Post reset Image ID, /image/reset : 게임 초기화
const resetImageID = asynchHandler(async (req, res) => {
imageID = [];
res.status(200).send({ message: "게임 데이터가 초기화되었습니다." }); //오류 수정
});
//Post Image, /image : 이미지 데이터 등록
const postImage = asynchHandler(async (req, res) => {
const { title, level, length, hint } = req.body;
const existingTitle = await User.findOne({ title });
if (existingTitle)
return res.status(401).json({ message: "이미 저장된 데이터입니다." });
const image = req.file.filename;
await Game.create({ _id: title, title, image, level, length, hint }); //document 이름을 title로 지정
res.status(201).send({ message: "등록되었습니다." });
});
route - <gameRoute.js>
const express = require("express");
const router = express.Router();
const {
postCanvas,
getImage,
resetImageID,
postImage,
addImageScore,
addCombineScore,
} = require("../controller/gameController");
const { upload } = require("../config/multer");
const { authUser } = require("../middleware/authMiddleware");
router.route("/canvas").post(postCanvas);
router.route("/game").post(upload.single("image"), postImage);
router.route("/gameData").post(getImage); //게임 데이터 클라이언트로 보내기
router.route("/game/reset").post(resetImageID);
router.route("/imageScore").post(authUser, addImageScore);
router.route("/combineScore").post(authUser, addCombineScore);
module.exports = router;
-프론트엔드-
page - Game - <ImageLevel.js>
import React, { useState } from "react";
import "../../css/game.css";
import ImageGame from "./ImageGame";
import axios from "axios";
function ImageLevel() {
const [levelList, setLevelList] = useState(true); //레벨 버튼 state
const [high, setHight] = useState(false); //상 state
const [middle, setMiddle] = useState(false); //중 state
const [low, setLow] = useState(false); //하 state
const highGame = () => { //상 게임 함수
axios.post("http://localhost:5000/game/reset"); //게임 초기화
setHight(true); //상 게임 컴포넌트 활성화
setLevelList(false); //난이도 버튼 비활성화
};
const middleGame = () => { //중 게임 함수
axios.post("http://localhost:5000/game/reset"); //게임 초기화
setMiddle(true); //중 게임 컴포넌트 활성화
setLevelList(false); //난이도 버튼 비활성화
};
const lowGame = () => { //하 게임 함수
axios.post("http://localhost:5000/game/reset"); //게임 초기화
setLow(true); //하 게임 컴포넌트 활성화
setLevelList(false); //난이도 버튼 비활성화
};
return (
<div className="imageGameContainer">
<h1>이미지 게임</h1>
{levelList && (
<div className="levelBtn">
<button onClick={highGame}>상</button>
<button onClick={middleGame}>중</button>
<button onClick={lowGame}>하</button>
</div>
)}
{high && <ImageGame gameLevel={"상"} />}
{middle && <ImageGame gameLevel={"중"} />}
{low && <ImageGame gameLevel={"하"} />}
</div>
);
}
export default ImageLevel;
page - Game - <ImageGame.js>
import axios from "axios";
import React, { useCallback, useEffect, useState } from "react";
import "../../css/game.css";
import Canvas from "../../component/Canvas";
import Typing from "../../component/Typing";
import { useNavigate } from "react-router-dom";
function ImageGame({ gameLevel }) {
const navigate = useNavigate();
const winNum = 1; //서버로 보낼 점수 1점
const [imageData, setImageData] = useState(""); //이미지, 텍스트 데이터
const [quiz, setQuiz] = useState(""); //제시된 텍스트 퀴즈
const [hint, setHint] = useState(""); //힌트
const [length, setLength] = useState(""); //글자 수
const [round, setRound] = useState(1); //라운드
const [score, setScore] = useState(0); //점수
const [gameOver, setGameOver] = useState(false); //게임 끝 여부
const [checkQuiz, setCheckQuiz] = useState(false); //정답, 오답 확인
const [moreChance, setMoreChance] = useState(0); //재도전 제공
const [answerObj, setAnswerObj] = useState(false); //캔버스, 타이핑 변환
const [answerObjName, setAnswerObjName] = useState("타이핑"); //캔버스, 타이핑 변환 버튼 이름
const [answerObjButton, setAnswerObjButton] = useState(false); //캔버스, 타이핑 변환 버튼
const checkTrue = () => {
setCheckQuiz(true);
setAnswerObjButton(true);
};
const resetGame = () => window.location.reload();
const toggleAnswerObj = () => {
setAnswerObj((answerObj) => !answerObj);
setAnswerObjName((prev) => (prev === "타이핑" ? "캔버스" : "타이핑"));
};
const checkAnswer = (text) => {
if (text === quiz) {
alert("정답입니다.");
checkTrue();
setScore((score) => score + 10);
} else {
if (moreChance === 0) {
alert("오답입니다. 한 번 더 시도해보세요.");
setMoreChance((moreChance) => moreChance + 1);
} else if (moreChance === 1) {
alert("오답입니다. 다음 라운드로 넘어갑니다.");
setMoreChance(0);
checkTrue();
}
}
};
const fetchData = async () => {
try {
await axios
.post("http://localhost:5000/gameData", { level: gameLevel }) //서버로 level 보내기
.then((res) => {
if (res.data.game && res.data.game.length > 0) {
setImageData(res.data.game[0].image);
setQuiz(res.data.game[0].title);
setRound(res.data.count);
setHint(res.data.game[0].hint);
setLength(res.data.game[0].length);
if (round >= 10) {
setGameOver(true);
alert(res.data.message);
}
} else {
setGameOver(true);
alert(res.data.message);
}
});
} catch (err) {
console.error(err);
}
};
const updateScore = useCallback(async () => {
const token = localStorage.getItem("token");
const headerData = {
headers: {
Authorization: `Bearer ${token}`,
},
withCredentials: true,
};
try {
await axios.post(
"http://localhost:5000/imageScore",
{ imageScore: winNum },
headerData
);
} catch (err) {
if (err.response.status === 401) {
try {
const refreshRes = await axios.post(
"http://localhost:5000/refresh",
{},
{ withCredentials: true }
);
const newToken = refreshRes.data.token;
localStorage.setItem("token", newToken);
axios.defaults.headers.common["Authorization"] = `Bearer ${newToken}`;
updateScore();
} catch (err) {
console.error(err);
localStorage.removeItem("token");
}
} else {
console.error(err);
localStorage.removeItem("token");
}
}
}, []);
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [winNum]);
useEffect(() => {
if (score >= 100 && localStorage.getItem("token")) updateScore();
}, [score, updateScore]);
return (
<div className="imageGameContainer">
<div className="imageDiv">
{gameOver ? (
<div>
<h1>Game Over, 점수: {score} / 100</h1>
<button onClick={resetGame}>난이도 선택</button>
<button onClick={() => navigate("/")}>홈으로</button>
</div>
) : (
<div>
<div className="roundDiv">
<h2>Round: {round} / 10</h2>
<button onClick={toggleAnswerObj} disabled={answerObjButton}>
{answerObjName}
</button>
</div>
{imageData ? ( //레이아웃 변경을 방지에서 CLS의 성능을 높임
<img
alt="이미지"
src={`http://localhost:5000/file/${imageData}`}
/>
) : (
<div style={{ width: "500px", height: "300px" }}></div>
)}
</div>
)}
</div>
{!gameOver && (
<div>
<h2>
글자 수: {length}, 힌트: {hint} <!--힌트 추가-->
</h2>
{answerObj ? (
<Typing
checkAnswer={checkAnswer}
fetchData={fetchData}
quiz={quiz}
checkQuiz={checkQuiz}
setCheckQuiz={setCheckQuiz}
setAnswerObjButton={setAnswerObjButton}
/>
) : (
<Canvas
checkAnswer={checkAnswer}
fetchData={fetchData}
quiz={quiz}
checkQuiz={checkQuiz}
setCheckQuiz={setCheckQuiz}
setAnswerObjButton={setAnswerObjButton}
/>
)}
</div>
)}
</div>
);
}
export default ImageGame;
page - Game - <CombineLevel.js >
import React, { useState } from "react";
import "../../css/game.css";
import CombineGame from "./CombineGame";
import axios from "axios";
function CombineLevel() {
const [levelList, setLevelList] = useState(true); //레벨 버튼 state
const [high, setHight] = useState(false); //상 state
const [middle, setMiddle] = useState(false); //중 state
const [low, setLow] = useState(false); //하 state
const highGame = () => { //상 게임 함수
axios.post("http://localhost:5000/game/reset"); //게임 초기화
setHight(true); //상 게임 컴포넌트 활성화
setLevelList(false); //난이도 버튼 비활성화
};
const middleGame = () => { //중 게임 함수
axios.post("http://localhost:5000/game/reset"); //게임 초기화
setMiddle(true); //중 게임 컴포넌트 활성화
setLevelList(false); //난이도 버튼 비활성화
};
const lowGame = () => { //하 게임 함수
axios.post("http://localhost:5000/game/reset"); //게임 초기화
setLow(true); //하 게임 컴포넌트 활성화
setLevelList(false); //난이도 버튼 비활성화
};
return (
<div className="imageGameContainer">
<h1>낱말 조합</h1>
{levelList && (
<div className="levelBtn">
<button onClick={highGame}>상</button>
<button onClick={middleGame}>중</button>
<button onClick={lowGame}>하</button>
</div>
)}
{high && <CombineGame gameLevel={"상"} />}
{middle && <CombineGame gameLevel={"중"} />}
{low && <CombineGame gameLevel={"하"} />}
</div>
);
}
export default CombineLevel;
page - Game - <CombineGame.js>
import axios from "axios";
import React, { useCallback, useEffect, useState } from "react";
import "../../css/game.css";
import Canvas from "../../component/Canvas";
import Typing from "../../component/Typing";
import { CHO, JUNG, JONG } from "../../component/Word";
import { useNavigate } from "react-router-dom";
function CombineGame({ gameLevel }) {
const navigate = useNavigate();
const winNum = 1; //서버로 보낼 점수 1점
const [charArray, setCharArray] = useState([]); //랜덤 문자 배열
const [quiz, setQuiz] = useState(""); //제시된 텍스트 퀴즈
const [hint, setHint] = useState(""); //힌트
const [length, setLength] = useState(""); //글자 수
const [round, setRound] = useState(1); //라운드
const [score, setScore] = useState(0); //점수
const [gameOver, setGameOver] = useState(false); //게임 끝 여부
const [checkQuiz, setCheckQuiz] = useState(false); //정답, 오답 확인
const [moreChance, setMoreChance] = useState(0); //재도전 제공
const [answerObj, setAnswerObj] = useState(false); //캔버스, 타이핑 변환
const [answerObjName, setAnswerObjName] = useState("타이핑"); //캔버스, 타이핑 변환 버튼 이름
const [answerObjButton, setAnswerObjButton] = useState(false); //캔버스, 타이핑 변환 버튼
//초성, 중성, 종성 분리
const separateText = () => {
const result = [];
for (let char of quiz) {
const unicode = char.charCodeAt(0) - 44032;
const choIndex = parseInt(unicode / 588);
const jungIndex = parseInt((unicode - choIndex * 588) / 28);
const jongIndex = parseInt(unicode % 28);
const choChar = CHO[choIndex];
const jungChar = JUNG[jungIndex];
const jongChar = JONG[jongIndex];
result.push(choChar, jungChar, jongChar);
}
return result;
};
const resetButton = () => window.location.reload();
const checkTrue = () => {
setCheckQuiz(true);
setAnswerObjButton(true);
};
const toggleAnswerObj = () => {
setAnswerObj((answerObj) => !answerObj);
setAnswerObjName((prev) => (prev === "타이핑" ? "캔버스" : "타이핑"));
};
const checkAnswer = (text) => {
if (text === quiz) {
alert("정답입니다.");
checkTrue();
setScore((score) => score + 10);
} else {
if (moreChance === 0) {
alert("오답입니다. 한 번 더 시도해보세요.");
setMoreChance((moreChance) => moreChance + 1);
} else if (moreChance === 1) {
alert("오답입니다. 다음 라운드로 넘어갑니다.");
setMoreChance(0);
checkTrue();
}
}
};
const fetchData = async () => {
try {
await axios
.post("http://localhost:5000/gameData", { level: gameLevel }) //서버로 level 보내기
.then((res) => {
if (res.data.game && res.data.game.length > 0) {
setQuiz(res.data.game[0].title);
setRound(res.data.count);
setHint(res.data.game[0].hint);
setLength(res.data.game[0].length);
if (round >= 10) {
setGameOver(true);
alert(res.data.message);
}
} else {
setGameOver(true);
alert(res.data.message);
}
});
} catch (err) {
console.error(err);
}
};
const updateScore = useCallback(async () => {
const token = localStorage.getItem("token");
const headerData = {
headers: {
Authorization: `Bearer ${token}`,
},
withCredentials: true,
};
try {
await axios.post(
"http://localhost:5000/combineScore",
{ combineScore: winNum },
headerData
);
} catch (err) {
if (err.response.status === 401) {
try {
const refreshRes = await axios.post(
"http://localhost:5000/refresh",
{},
{ withCredentials: true }
);
const newToken = refreshRes.data.token;
localStorage.setItem("token", newToken);
axios.defaults.headers.common["Authorization"] = `Bearer ${newToken}`;
updateScore();
} catch (err) {
console.error(err);
localStorage.removeItem("token");
}
} else {
console.error(err);
localStorage.removeItem("token");
}
}
}, [winNum]);
useEffect(() => {
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setCharArray(
separateText()
.sort(() => Math.random() - 0.5)
.filter((char) => char !== "")
);
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [quiz]);
useEffect(() => {
if (score >= 100 && localStorage.getItem("token")) updateScore();
}, [score, updateScore]);
return (
<div className="combineGameContainer">
<div className="combineDiv">
{gameOver ? (
<div>
<h1>Game Over, 점수: {score} / 100</h1>
<button onClick={resetButton}>다시하기</button>
<button onClick={() => navigate("/")}>홈으로</button>
</div>
) : (
<div>
<div className="roundDiv">
<h2>Round: {round} / 10</h2>
<button onClick={toggleAnswerObj} disabled={answerObjButton}>
{answerObjName}
</button>
</div>
<div className="textQuizDiv">
<span>{charArray.join(" , ")}</span>
</div>
</div>
)}
</div>
{!gameOver && (
<div>
<h2>
글자 수: {length}, 힌트: {hint} <!--힌트 추가-->
</h2>
{answerObj ? (
<Typing
checkAnswer={checkAnswer}
fetchData={fetchData}
quiz={quiz}
checkQuiz={checkQuiz}
setCheckQuiz={setCheckQuiz}
setAnswerObjButton={setAnswerObjButton}
/>
) : (
<Canvas
checkAnswer={checkAnswer}
fetchData={fetchData}
quiz={quiz}
checkQuiz={checkQuiz}
setCheckQuiz={setCheckQuiz}
setAnswerObjButton={setAnswerObjButton}
/>
)}
</div>
)}
</div>
);
}
export default CombineGame;
<App.js>
import React from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Nav from "./component/Nav";
import Login from "./page/User/Login";
import Register from "./page/User/Register";
import Home from "./page/Home";
import ImageRegist from "./page/Game/ImageRegist";
import IdFind from "./page/User/ID/IdFind";
import PwdFind from "./page/User/PWD/PwdFind";
import MyPage from "./page/User/MyPage";
import LearningPage from "./page/Learn/LearningPage";
import Footer from "./component/footer";
import "bootstrap/dist/css/bootstrap.min.css";
import Introduce from "./page/introduce";
import ImageLevel from "./page/Game/ImageLevel";
import CombineLevel from "./page/Game/CombineLevel";
function App() {
return (
<div>
<BrowserRouter>
<Nav />
<Routes>
<Route index element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/find_id" element={<IdFind />} />
<Route path="/find_pwd" element={<PwdFind />} />
<Route path="/register" element={<Register />} />
<Route path="/mypage" element={<MyPage />} />
<Route path="/image/add" element={<ImageRegist />} />
<Route path="/imageGame" element={<ImageLevel />} /> <!--이미지 게임 레벨 지정-->
<Route path="/combineGame" element={<CombineLevel />} /> <!--낱말 조합 레벨 지정-->
<Route path="/learn" element={<LearningPage />} />
<Route path="/introduce" element={<Introduce />} />
</Routes>
</BrowserRouter>
<Footer />
</div>
);
}
export default App;
css - <game.css>
.levelBtn button {
background-color: #a0cbe7;
color: white;
border: none;
border-radius: 4px;
padding: 10px 30px;
cursor: pointer;
font-size: 16px;
margin: 5px;
}
.levelBtn button:hover {
background-color: #8cb4d6;
}
'프로젝트 > 한글 게임' 카테고리의 다른 글
19. 종성 ' ' 제외, 쉼표(,) 추가 (0) | 2024.06.07 |
---|---|
18. 초성/중성/종성 파일 모듈화 (0) | 2024.05.20 |
17. 포인트 기능 (0) | 2024.05.11 |
16. 무작위 나열 (0) | 2024.04.29 |
15. 초성/중성/종성 분리 (0) | 2024.04.29 |