서비스 주요 기능 소개
1. 인증 / 인가
1-1. 회원가입
1-2. 로그인
1-3. 회원정보 수정 (패스워드 재설정)
1-4. 탈퇴2. 학급 / 학생 관리
2-1. 학급 목록
2-2. 학급 추가
2-3. 학급 편집
2-4. 학생 목록
2-5. 학생 추가
2-6. 학생 패스워드 초기화3. 경매 관리
3-1. 경매 등록
3-2. 경매 입찰
3-3. 경매 낙찰 및 유찰
3-4. 경매 댓글 관리
3-5. 경매 홀드4. 은행 관리
4-1. 적금 목록
4-2. 적금 가입 및 해지5. 쿠폰 관리
5-1. 쿠폰 등록
5-2. 쿠폰 목록
5-3. 쿠폰 삭제6. 리워드 관리
6-1. 리워드 등록
6-2. 리워드 삭제
6-3. 리워드 전송7. 통계 관리
7-1. 학급 통계
7-2. 학생 통계 (가계부)8 대포 게임
8-1. 대포 게임

프론트엔드 개발자로 팀에 합류하게 되었고, 1학기 관통 프로젝트 진행시에 배웠던 툴이 VUE 였기 때문에 VUE로 개발을 했었지만, 현업에서는 REACT를 대부분 사용한다는 것을 깨닫고, 1학기 이후 방학 기간동안 다른 반 팀원들과 함께 쇼핑몰 클론 코딩 프로젝트를 REACT로 진행하였고, 얄팍하게나마 REACT에 대한 기초 지식을 쌓아 프로젝트 진행시에 조금이나마 도움이 되었다.
이번 프로젝트때 맡은 기능은
- 인증 / 인가
1-1. 회원가입
1-2. 로그인
1-3. 회원정보 수정 (패스워드 재설정)
1-4. 탈퇴
- 학급 / 학생 관리
2-1. 학급 목록
2-2. 학급 추가
2-3. 학급 편집
2-4. 학생 목록
2-5. 학생 추가
2-6. 학생 패스워드 초기화
- 통계 관리
7-1. 학급 통계
7-2. 학생 통계 (가계부)
+) 학생 모델링, 렌더링, 웹소켓 사용하여 실시간 이용자들 채팅 기능, 아바타 변경 기능
이었는데, 서비스에 초기에 게이미피케이션을 도입해서 학생들에게 경매에 대한 어려움을 줄여주고, 장벽을 낮춰주자는 의도로 기획한 학생 측 캐릭터 모델링과 아바타 구축을 진행 하였고 이 때, 해당 학급과 은행, 경매장 건물 모델링을 진행하였다.
React 애플리케이션에서 데이터를 관리하기 위한 라이브러리
Axios 사용시에 리액트 쿼리를 사용해서 api 호출을 하였는데, api 관련 로직들을 한 jsx파일에 모아두고, 쿼리를 사용해서 단순하게 api를 처리할 수 있었다.
다음 프로젝트에서도 사용해보고 싶다.




학생들마다 각기의 캐릭터를 보유하고 있고, 메인 페이지에 모델링 된 학급의 UI와 아바타들을 띄우게 되다보니 자연스레 채팅이나, 캐릭터 움직임 구현에 대한 필요가 느껴졌고, 소켓에 대한 지식이 아예 없었고, 시간 또한 여유롭지 못한 상황에서 진행이 되었으므로 백엔드에서 소켓을 열기에는 무리가 있었다.
이에, 프론트엔드 단독으로 소켓을 열어서, 데이터를 주고받는 통신이 아닌, 아이들이 메인페이지에 접속했을때, 경제 금융 플랫폼에 대한 딱딱한 이미지와 거부감을 줄이고 친구들과 실시간 양방향 소통을 하여서 머무르는 시간을 늘리게 하고자 하였다.

//Server/index.js
import { Server as SocketIOServer } from "socket.io";
import { createServer as createHttpsServer } from "https";
import fs from "fs";
const origin = "https://i10a306.p.ssafy.io";
let httpsOptions;
try {
httpsOptions = {
key: fs.readFileSync('/etc/letsencrypt/live/i10a306.p.ssafy.io/privkey.pem', 'utf8'),
cert: fs.readFileSync('/etc/letsencrypt/live/i10a306.p.ssafy.io/fullchain.pem', 'utf8'),
};
// 인증서 로드 성공 메시지
console.log('SSL/TLS 인증서가 성공적으로 로드되었습니다.');
} catch (error) {
// 에러 로깅
console.error('SSL/TLS 인증서 로드 중 에러 발생:', error);
}
const httpsServer = createHttpsServer(httpsOptions);
const io = new SocketIOServer(httpsServer, {
cors: {
origin, // 허용할 CORS origin
methods: ["GET", "POST"] // 허용할 HTTP 메소드
},
});
httpsServer.listen(3001, () => {
console.log('HTTPS 서버가 포트 3001에서 시작되었습니다.');
});
console.log("Server started on port 3000, allowed cors origin: " + origin);
const roomId = []
const characters = []
const generateRandomPosition = () => {
const minX = 1.4;
const maxX = 2.224 + 3;
const minZ = -1.781 - 1.5 ;
const maxZ = -1.781 + 1.5;
const x = minX + Math.random() * (maxX - minX);
const z = minZ + Math.random() * (maxZ - minZ);
return [x, 0, z];
};
const generateRandomHexColor = () => {
return "#" + Math.floor(Math.random() * 16777215).toString(16);
};
const createCharacter = (model) => {
const [baseX, baseY, baseZ] = generateRandomPosition(); // 랜덤한 기준 포지션 생성
const deltaX = (Math.random() * 0.3) - 0.6; // x 값에 더해줄 랜덤한 값 생성 (0.3 ~ 0.65)
const deltaZ = (Math.random() * 0.3) + 0.6; // z 값에 더해줄 랜덤한 값 생성 (0.3 ~ 0.65)
// 기준 포지션에 더해진 값을 사용하여 캐릭터의 위치를 설정
const position = [baseX + deltaX, baseY, baseZ + deltaZ];
const selectedCharacter = model.profileImgUrl.split('/').pop().replace('.png', '');
const id = model.no;
const gradeNo = model.gradeNo;
const name = model.name;
return {
id,
name,
gradeNo,
position,
selectedCharacter
};
};
io.on("connection", (socket) => {
console.log("user connected:", socket.id);
socket.on("joinRoom", (gradeNo) => {
socket.join(gradeNo);
console.log(`User ${socket.id} joined room ${gradeNo}`);
});
socket.on("characters", (data) => {
// 여기서 models 이벤트를 수신하여 원하는 작업을 수행합니다.
console.log("Received models data:", data);
data.forEach(model => {
// 중복 추가 방지
const gradeNo = model.gradeNo;
if (!roomId[gradeNo]) {
roomId[gradeNo] = [];
}
if (!roomId[gradeNo].some(character => character.id === model.no)) {
const character = createCharacter(model);
roomId[gradeNo].push(character);
}
// Object.keys(roomId).forEach(gradeNo => {
// io.emit(`characters-${gradeNo}`, roomId[gradeNo]);
// });
io.to(gradeNo).emit(`characters-${gradeNo}`, roomId[gradeNo]);
});
});
socket.on("leaveRoom", (gradeNo) => {
roomId[gradeNo] = []
})
socket.on("move", (position, characters, userId, gradeNo) => {
characters.forEach((character) => {
if (character.id === userId) {
character.position = position
}
})
io.to(gradeNo).emit(`characters-${gradeNo}`, characters);
});
socket.on("chatMessage", (message, userId, gradeNo) => {
console.log(userId)
console.log(message)
// Assuming the message object has a sender and content property
io.to(gradeNo).emit("playerChatMessage", {
id: userId,
message,
});
});
socket.on("disconnect", () => {
console.log("user disconnected");
const characters = []
console.log
// Broadcast the updated characters array to all connected clients
io.emit("characters", characters);
});
});
//BID/src/Component/Models/SocketManager.jsx
import { useEffect } from "react"
import {io} from "socket.io-client"
import {useAtom, atom} from 'jotai'
import { useSelector } from "react-redux";
import { modelSelector } from "../../Store/modelSlice";
export const socket = io("https://i10a306.p.ssafy.io:3001");
export const charactersAtom = atom([])
export const userAtom = atom(null);
export const SocketManager = () => {
const myInfo = useSelector(modelSelector);
const gradeNo = myInfo.model.gradeNo
const [, setCharacters] = useAtom(charactersAtom)
const [, setUser] = useAtom(userAtom);
useEffect(() => {
const eventKey = `characters-${gradeNo}`;
const onCharacters = (newCharacters) => {
setCharacters((currentCharacters) => {
// 현재 상태와 새로운 데이터가 동일한지 검사
if (JSON.stringify(currentCharacters) === JSON.stringify(newCharacters)) {
// 데이터가 변경되지 않았다면 상태 업데이트 없이 현재 상태를 유지
return currentCharacters;
}
// 데이터가 변경되었다면 새로운 상태로 업데이트
return newCharacters;
});
};
socket.on(eventKey, onCharacters);
// 컴포넌트 언마운트 시 이벤트 리스너 제거
return () => {
socket.off(eventKey, onCharacters);
};
}, [gradeNo]); // 의존성 배열에 gradeNo 추가
}
npx gltfjsx [파일명].gltf
해당 명령어를 치면
/*
Auto-generated by: https://github.com/pmndrs/gltfjsx
*/
import React from "react";
import { useGLTF } from "@react-three/drei";
export default function BlackBoard(props) {
const { nodes, materials } = useGLTF("/models/blackboard.glb");
return (
<group {...props} dispose={null}>
<group
position={[1.198, 1.229, -3.111]}
rotation={[Math.PI / 2, 0, -Math.PI / 2]}
scale={[0.753, 0.408, 0.408]}
>
<mesh
castShadow
receiveShadow
geometry={nodes.Plane008.geometry}
material={materials["Material.026"]}
/>
<mesh
castShadow
receiveShadow
geometry={nodes.Plane008_1.geometry}
material={materials["Material.118"]}
/>
</group>
</group>
);
}
useGLTF.preload("/blackboard.glb");
해당 모델에 대한 script 가 작성이 된다.
위 모델을 불러오기 위해선 react three fiber에서는 Canvas 태그로 감싸져 있어야 한다.
Camera와 Light와 같은 주요 객체에 대한 설정을 추가하여야 하고, OrbitControls를 추가하여 카메라를 돌리며 다양한 각도에서 확인할 수 있다.
return (
<>
<SocketManager />
<Canvas
style={{ width: "100%", height: "70vh" }}
camera={{ position: [12, 10, 20], fov: 30 }}
>
<CameraControls minPolarAngle={5} maxPolarAngle={Math.PI / 1} />
<directionalLight
position={[1, 1, 1]}
castShadow
intensity={3}
></directionalLight>
<OrbitControls
enableZoom={false} // 확대/축소 비활성화
enablePan={false} // 패닝 비활성화
/>
<ambientLight intensity={1.7} />
<OrbitControls />
<group scale={20} position={[0, 0, 0]}>
<mesh
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
>
<Classroom/>
</mesh>
{characters.map((character) => (
<CharacterModel
key={character.id}
id={character.id}
name={character.name}
position={
new THREE.Vector3(
character.position[0],
character.position[1],
character.position[2]
)
}
selectedCharacter={character.selectedCharacter}
myModelNo={myInfo.myInfo.model.no} // 내 캐릭터의 번호 전달
/>
))}
<BlackBoard />
<Cactus />
<Alarm onClick={handleAlarmClick} />
<Bank />
<BiddingPlace />
<SnowBean />
</group>
<OrbitControls
makeDefault
minAzimuthAngle={2}
maxAzimuthAngle={2}
minPolarAngle={Math.PI / 2.55}
maxPolarAngle={Math.PI / 2.55}
enableZoom={false}
enablePan={false}
enableRotate={false} // 회전 비활성화
zoomSpeed={1}
/>
<PerspectiveCamera makeDefault position={[0, 10, 190]} />
</Canvas>
{isAlarm && <RealTimeModal handleClick={handleAlarmClick} />}
</>
);
}
해당 해서 Canvas로 해당 모델들을 싼후, directionalight, ambientLight 를 추가해서 밝기에 대한 조정을 해주었고, OrbitControls 함수와 PerspectiveCamera를 통해서 카메라 각도를 조절해주었다.
React Three Fiber는 Three.js를 보다 편리하고 쉽게 사용할 수 있도록, 도움을 주는 라이브러리이다.
일반적으로 Three.js는 WebGL을 기반으로 하며, 직접적으로 JavaScript로 3D 그래픽을 작성하고 조작하는 데 사용되는데, Three.js는 DOM과 직접 상호 작용하지 않으며, 따라서 React와 함께 사용할 때 몇 가지 문제가 발생할 수 있어 초기 세팅이 React Three Fiber에 비해 훨씬 복잡하다.
해당 이유로, 짧은 시간 내에 3D 모델들과 캐릭터의 움직임만 간단하게 구현 하면 됐던 프로젝트여서 react three fiber를 이용하여서 구현하게 되었다.