회원가입 로그인 기능 작성

This commit is contained in:
fiore 2025-03-11 19:44:59 +09:00
parent 7028fe5103
commit ff9ed0d462
18 changed files with 3391 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# 디폴트 무시된 파일
/shelf/
/workspace.xml
# 에디터 기반 HTTP 클라이언트 요청
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/Degullmok-server.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Degullmok-server.iml" filepath="$PROJECT_DIR$/.idea/Degullmok-server.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

95
app.js Normal file
View File

@ -0,0 +1,95 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var mongodb = require('mongodb');
var MongoClient = mongodb.MongoClient;
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var leaderboardRouter = require('./routes/leaderboard');
const session = require('express-session');
var fileStore = require('session-file-store')(session);
var app = express();
app.use(session({
secret: process.env.SESSION_SECRET || "session-login",
resave: false,
saveUninitialized: false,
store: new fileStore({
path: './sessions/',
ttl: 24 * 60 * 60,
reapInterval: 60 * 60
}),
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000,
}
}));
async function connectDB(){
var databaseUrl = "mongodb://localhost:27017/DegullMok";
// MongoDB 연결
try {
const client = await MongoClient.connect(databaseUrl,{
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = client.db('DegullMok'); // 사용할 데이터베이스 선택
app.set('database', db); // DB를 app에 저장
console.log("Database Connected : " + databaseUrl);
// 연결 종료 처리
process.on("SIGINT", async ()=> {
await database.close();
console.log("Database Connected");
process.exit(0);
})
} catch (err){
console.error("DB 연결 실패: " + err);
process.exit(1);
}
}
connectDB().catch(err => {
console.error("초기 DB 연결 실패: " + err);
process.exit(1);
});
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/leaderboard', leaderboardRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

96
bin/www Normal file
View File

@ -0,0 +1,96 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('tictactoe-server:server');
var http = require('http');
var game = require('../game');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* 게임 서버 실행
*/
game(server);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

56
game.js Normal file
View File

@ -0,0 +1,56 @@
const {v4: uuidv4} = require('uuid');
module.exports = function(server) {
const io = require('socket.io')(server, {
transports: ['websocket']
});
// 방 정보
var rooms = [];
var socketRooms = new Map();
io.on('connection', function(socket) {
console.log('Connected: ' + socket.id);
if (rooms.length > 0) {
var roomId = rooms.shift();
socket.join(roomId)
socket.emit('joinRoom', { roomId: roomId });
socket.to(roomId).emit('startGame', { roomId: socket.id });
socketRooms.set(socket.id, roomId);
} else {
var roomId = uuidv4();
socket.join(roomId);
socket.emit('createRoom', { room: roomId });
rooms.push(roomId);
socketRooms.set(socket.id, roomId);
}
socket.on('leaveRoom', function(roomData) {
socket.leave(roomData.roomId);
socket.emit('exitRoom');
socket.to(roomData.roomId).emit('endGame');
// 방 만든 후 혼자 들어갔다가 나갈 때 rooms에서 방 삭제
var roomId = socketRooms.get(socket.id);
const roomIdx = rooms.indexOf(roomId);
if (roomIdx !== -1) {
rooms.splice(roomIdx, 1);
console.log('Room removed:', roomId);
}
socketRooms.delete(socket.id);
});
socket.on('doPlayer', function(moveData) {
const roomId = moveData.roomId;
const position = moveData.position;
socket.to(roomId).emit('doOpponent', { position: position });
});
socket.on('disconnect', function(reason) {
console.log('Disconnected: ' + socket.id + ', Reason: ' + reason);
});
});
};

2811
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "tictactoe-server",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"bcrypt": "^5.1.1",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "^4.21.2",
"express-session": "^1.18.1",
"http-errors": "~1.6.3",
"mongodb": "^6.14.1",
"morgan": "~1.9.1",
"pug": "^3.0.3",
"session-file-store": "^1.5.0",
"socket.io": "^4.8.1",
"uuid": "^11.1.0"
}
}

View File

@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

21
readme.md Normal file
View File

@ -0,0 +1,21 @@
TODO : 유저 컬렉션 항목 추가
- int : 코인 수량
- int : 급수
- int : 점수
- int : 승
- int : 패
- int : 무승부
TODO : 리더보드 로직 수정
- 급수별 별도의 리더보드 표시
- 같은 급수일 경우 승률로 정렬
TODO : 실시간 소켓 통신
- 타이머 동기화
- 타이머가 시작된 정보를 클라이언트로부터 받음
- 혹은 서버가 타이머 정보를 두 플레이어에게 내려줌
- 시간 초과시 처리 : 클라이언트가 처리 OR 서버에서 처리

9
routes/index.js Normal file
View File

@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

34
routes/leaderboard.js Normal file
View File

@ -0,0 +1,34 @@
var express = require('express');
var router = express.Router();
// 랭킹 조회
router.get("/", async function (req, res, next) {
try {
if(!req.session.isAuthenticated) {
return res.status(403).send("로그인이 필요합니다.");
}
var database = req.app.get('database');
var users = database.collection('users');
var allUsers = await users
.find({ }, { projection: { username:1, nickname: 1, score: 1 } })
.sort({ score: -1 }) // 점수가 높은 순으로 정렬
.toArray();
var result = allUsers.map((user) => (
{
username: user.username,
nickname: user.nickname,
score: user.score || 0,
}
))
res.json({ leaderboardDatas : result ?? [] });
} catch(err) {
console.error(err);
res.status(500).send("서버 오류가 발생했습니다.");
}
})
module.exports = router;

186
routes/users.js Normal file
View File

@ -0,0 +1,186 @@
var express = require('express');
var router = express.Router();
var bcrypt = require('bcrypt');
const {ObjectId} = require("mongodb");
var saltRounds = 10;
var ResponseType = {
INVALID_USERNAME: 0,
INVALID_PASSWORD: 1,
SUCCESS: 2
}
/* GET users listing. */
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
// 회원 가입
router.post('/signup', async function (req, res, next) {
try {
var username = req.body.username;
var password = req.body.password;
var nickname = req.body.nickname;
var profileImageIndex = req.body.imageindex ?? 0;
// 입력값 검증
if( !username || !password || !nickname ){
return res.status(400).send("모든 필드를 입력하세요.")
}
var database = req.app.get('database');
var users = database.collection('users');
const existingUser = await users.findOne({username: username})
if(existingUser){
return res.status(409).send("이미 존재하는 사용자입니다.")
}
// 비밀번호 암호화
var salt = bcrypt.genSaltSync(saltRounds);
var hash = bcrypt.hashSync(password, salt);
// 신규 유저를 DB에 저장
await users.insertOne({
username: username,
password: hash,
nickname: nickname,
profileImageIndex: profileImageIndex,
rating: 18,
score:0
});
res.status(201).send("사용자가 성공적으로 생성되었습니다.");
} catch (err){
console.log("사용자 추가중 오류 발생 : " + err);
res.status(500).send("서버 오류가 발생했습니다.");
}
})
// 로그인
router.post("/signin", async function (req, res, next) {
try {
var username = req.body.username;
var password = req.body.password;
var database = req.app.get('database');
var users = database.collection('users');
// 입력값 검증
if (!username || !password) {
return res.status(400).send("모든 필드를 입력해주세요.");
}
const existingUser = await users.findOne({username: username});
if(existingUser){
var compareResult = bcrypt.compareSync(password, existingUser.password);
if(compareResult){
req.session.isAuthenticated = true;
req.session.userId = existingUser._id.toString();
req.session.username = existingUser.username;
req.session.nickname = existingUser.nickname;
req.session.profileImageIndex = existingUser.profileImageIndex || 0;
req.session.rating = existingUser.rating;
req.session.score = existingUser.score;
res.json({
result: ResponseType.SUCCESS,
imageindex: existingUser.imageindex,
rating: existingUser.rating,
score: existingUser.score,
});
} else {
res.json({result : ResponseType.INVALID_PASSWORD});
}
} else {
res.json({result : ResponseType.INVALID_USERNAME});
}
}catch (err){
console.log("로그인 중 오류 발생 : ", err);
res.status(500).send("서버 오류가 발생했습니다.");
}
})
// 로그 아웃
router.post("/signout", async function (req, res, next) {
req.session.destroy((err) => {
if(err) {
console.log("로그아웃 중 오류 발생");
return res.status(500).send("서버 오류가 발생했습니다.");
}
res.status(200).send("로그아웃 되었습니다.")
});
});
// 점수 추가
router.post("/addscore", async function (req, res, next) {
try {
if(!req.session.isAuthenticated) {
return res.status(400).send("로그인이 필요합니다.");
}
var userId = req.session.userId;
var score = req.body.score;
// 점수 유효성 검사
if(!score || isNaN(score)) {
return res.status(400).send("유효한 점수를 입력해주세요.");
}
var database = req.app.get('database');
var users = database.collection('users');
const result = await users.updateOne(
{_id: ObjectId.createFromHexString(userId) },
{
$set: {
score: Number(score),
updatedAt: new Date()
}
}
);
if(result.matchedCount === 0 ) {
return res.status(400).send("사용자를 찾을 수 없습니다.");
}
res.status(200).json({message: "점수가 성공적으로 업데이트되었습니다."});
} catch(err) {
console.log("점수 추가 중 오류 발생 : ",err);
res.status(500).send("서버 오류가 발생했습니다.");
}
})
// 점수 조회
router.get("/score", async function (req, res, next) {
try {
if(!req.session.isAuthenticated) {
return res.status(403).send("로그인이 필요합니다.");
}
var userId = req.session.userId;
var database = req.app.get('database');
var users = database.collection('users');
const user = await users.findOne({_id: ObjectId.createFromHexString(userId) });
if(!user){
return res.status(404).send("사용자를 찾을 수 없습니다.");
}
res.json({
id: user._id.toString(),
username: user.username,
nickname: user.nickname,
score: Number(user.score) || 0,
})
} catch (err) {
console.error("점수 조회 중 오류 발생 : ", err);
res.status(500).send("서버 오류가 발생했습니다.")
}
})
module.exports = router;

View File

@ -0,0 +1 @@
{"cookie":{"originalMaxAge":86400000,"expires":"2025-03-12T08:52:52.709Z","secure":false,"httpOnly":true,"path":"/"},"isAuthenticated":true,"userId":"67cff9e05e33682110dd38cf","username":"test2","nickname":"testuser","__lastAccess":1741683172709,"profileImageIndex":1,"rating":18,"score":0}

6
views/error.pug Normal file
View File

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

5
views/index.pug Normal file
View File

@ -0,0 +1,5 @@
extends layout
block content
h1= title
p Welcome to #{title}

7
views/layout.pug Normal file
View File

@ -0,0 +1,7 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content