Skip to content
Snippets Groups Projects
Commit cc12e6af authored by 문경호's avatar 문경호
Browse files

Merge branch 'develop' into 'feat/루틴'

# Conflicts:
#   front/src/pages/routine/progress.jsx
parents b9272a12 30557ecb
Branches feat/루틴
No related tags found
1 merge request!43Feat/루틴
Showing
with 3643 additions and 37 deletions
...@@ -8,5 +8,15 @@ DB_PASSWORD=samplepwd ...@@ -8,5 +8,15 @@ DB_PASSWORD=samplepwd
DB_NAME=datadb DB_NAME=datadb
# 서버 이름(localhost일시 자동으로 도메인 설정) # 서버 이름(localhost일시 자동으로 도메인 설정)
SERVER_NAME=localhost SERVER_NAME=localhost
# JWT 액세스 시크릿 키
JWT_ACCESS_SECRET=sampleaccesssecret
# JWT 리프레시 시크릿 키
JWT_REFRESH_SECRET=samplerefreshsecret
# JWT 탈퇴 시크릿 키
JWT_DELETE_SECRET=sampledeletesecret
# 이메일 유저
EMAIL_USER=example@gmail.com
# 이메일 비밀번호(2단계 인증 사용, 앱 비밀번호)
EMAIL_APP_PASSWORD=
# 유튜브 API 키 # 유튜브 API 키
YOUTUBE_API_KEY=sampleapikey YOUTUBE_API_KEY='AIzaSyAtnFTu-E6GUePD2AYOXwa2YXQugbb08Jc'
\ No newline at end of file \ No newline at end of file
...@@ -4,3 +4,4 @@ db/data ...@@ -4,3 +4,4 @@ db/data
.env .env
ssl/*.pem ssl/*.pem
ssl/certbot/* ssl/certbot/*
back/src/logs/*
\ No newline at end of file
File added
...@@ -2,5 +2,5 @@ FROM node:20.18.0-alpine ...@@ -2,5 +2,5 @@ FROM node:20.18.0-alpine
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN npm install RUN npm install
CMD npm run start CMD ./start.sh
EXPOSE 8080 EXPOSE 8080
\ No newline at end of file
This diff is collapsed.
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"start": "node src/index.js", "start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [], "keywords": [],
...@@ -11,7 +12,21 @@ ...@@ -11,7 +12,21 @@
"license": "ISC", "license": "ISC",
"description": "", "description": "",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.21.1" "dotenv": "^16.4.5",
"express": "^4.21.1",
"express-rate-limit": "^7.4.1",
"express-validator": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.8.3",
"nodemailer": "^6.9.16",
"ua-parser-js": "^2.0.0",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"nodemon": "^3.1.7"
} }
} }
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>계정 삭제 취소</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
width: 100%;
max-width: 500px;
padding: 20px;
}
.deny-box {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.error-icon {
font-size: 48px;
margin: 20px 0;
}
.error-text {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.sub-text {
color: #666;
margin-bottom: 30px;
}
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.home-btn {
background-color: #4444ff;
color: white;
}
.home-btn:hover {
background-color: #0000ff;
}
.message {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
display: none;
}
.message.info {
display: block;
background-color: #cce5ff;
color: #004085;
}
.message.error {
display: block;
background-color: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<div class="container">
<div class="deny-box">
<h1>계정 삭제 취소</h1>
<div class="error-icon"></div>
<p class="error-text">계정 삭제를 취소하시겠습니까?</p>
<p class="sub-text">바로 계정을 사용 할 수 없습니다.</p>
<div class="button-group">
<button id="confirmBtn" class="btn home-btn">삭제 취소하기 (계정 복구)
</button>
<button id="closeBtn" class="btn cancel-btn">창 닫기</button>
</div>
<div id="message" class="message"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const confirmBtn = document.getElementById('confirmBtn');
const closeBtn = document.getElementById('closeBtn');
const messageDiv = document.getElementById('message');
confirmBtn.addEventListener('click', async () => {
try {
const response = await fetch(`/api/user/cancel-hard-delete?token=${token}`, {
method: 'POST'
});
if (response.ok) {
const data = await response.json();
messageDiv.textContent = `${data.message}`;
messageDiv.className = 'message info';
setTimeout(() => {
window.close();
}, 2000);
} else {
const text = await response.text();
throw new Error(text);
}
} catch (error) {
messageDiv.textContent = error.message;
messageDiv.className = 'message error';
}
});
closeBtn.addEventListener('click', () => {
window.close();
});
});
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>계정 삭제 확인</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Arial', sans-serif;
background-color: #f5f5f5;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
width: 100%;
max-width: 500px;
padding: 20px;
}
.confirm-box {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
}
.warning-icon {
font-size: 48px;
margin: 20px 0;
}
.warning-text {
font-size: 18px;
color: #333;
margin-bottom: 10px;
}
.sub-text {
color: #ff4444;
margin-bottom: 30px;
}
.button-group {
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #e0e0e0;
color: #333;
}
.confirm-btn {
background-color: #ff4444;
color: white;
}
.cancel-btn:hover {
background-color: #d0d0d0;
}
.confirm-btn:hover {
background-color: #ff0000;
}
.message {
margin-top: 20px;
padding: 10px;
border-radius: 5px;
display: none;
}
.message.info {
display: block;
background-color: #ffe5e5;
color: #850000;
}
.message.error {
display: block;
background-color: #ff4444;
color: white;
}
</style>
</head>
<body>
<div class="container">
<div class="confirm-box">
<h1>계정 삭제 확인</h1>
<div class="warning-icon">⚠️</div>
<p class="warning-text">정말로 계정을 삭제하시겠습니까?</p>
<p class="sub-text">이 작업은 되돌릴 수 없으며, 모든 데이터가 영구적으로 삭제됩니다.</p>
<div class="button-group">
<button id="confirmBtn" class="btn confirm-btn">예, 삭제합니다</button>
</div>
<div id="message" class="message"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const urlParams = new URLSearchParams(window.location.search);
const token = urlParams.get('token');
const confirmBtn = document.getElementById('confirmBtn');
const cancelBtn = document.getElementById('cancelBtn');
const messageDiv = document.getElementById('message');
confirmBtn.addEventListener('click', async () => {
try {
const response = await fetch(`/api/user/confirm-hard-delete?token=${token}`, {
method: 'DELETE'
});
if (response.ok) {
const data = await response.json();
messageDiv.textContent = `${data.message}`;
messageDiv.className = 'message info';
setTimeout(() => {
window.close();
}, 2000);
} else {
const text = await response.text();
throw new Error(text);
}
} catch (error) {
messageDiv.textContent = error.message;
messageDiv.className = 'message error';
}
});
});
</script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>만료된 페이지</title>
<style>
body {
font-family: 'Arial', sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
background-color: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 400px;
width: 90%;
}
h1 {
color: #333;
margin-bottom: 1.5rem;
}
.button-group {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 2rem;
}
button {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s;
}
#confirmBtn {
background-color: #dc3545;
color: white;
}
#confirmBtn:hover {
background-color: #c82333;
}
#cancelBtn {
background-color: #6c757d;
color: white;
}
#cancelBtn:hover {
background-color: #5a6268;
}
.message {
margin-top: 1rem;
padding: 1rem;
border-radius: 4px;
display: none;
}
.message.success {
background-color: #d4edda;
color: #155724;
display: block;
}
.message.error {
background-color: #f8d7da;
color: #721c24;
display: block;
}
.message.info {
background-color: #cce5ff;
color: #004085;
display: block;
}
</style>
</head>
<body>
<div class="container">
<div class="deny-box">
<h1>만료된 토큰</h1>
<div class="error-icon"></div>
<p class="error-text">유효하지 않은 토큰입니다</p>
<p class="sub-text">토큰이 만료되었거나 올바르지 않습니다.</p>
<div class="button-group">
<button id="homeBtn" class="btn home-btn">홈으로 돌아가기</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const homeBtn = document.getElementById('homeBtn');
homeBtn.addEventListener('click', () => {
window.location.href = '/';
});
});
</script>
</body>
</html>
File added
const path = require('path');
const etcController = {
getUptime: (req, res) => {
const uptime = process.uptime();
res.json({ uptime });
},
handleConfirmDelete: (req, res) => {
res.sendFile(path.join(__dirname, '../../public/confirm-delete.html'));
},
handleCancelDelete: (req, res) => {
res.sendFile(path.join(__dirname, '../../public/cancel-delete.html'));
}
};
module.exports = etcController;
\ No newline at end of file
const { minutesToSeconds, secondsToMinutes } = require('../utils/timeconvert');
//habittracker구조
const HabitTracker = require('../models/habittracker');
//goal_weight는 habit tracker schema 안에 존재하도록
const habittrackerController = {
setGoal: async (req, res) => {
const { goal_weekly, goal_daily, goal_daily_time, goal_weight } =
req.body;
//시간 변환
const dailyTimeSeconds = minutesToSeconds(goal_daily_time);
try {
const newGoal = new HabitTracker({
user_id: req.user.user_id, //미들웨어에서 설정한 user_id 가져온다
goal_weekly,
goal_daily,
goal_daily_time: dailyTimeSeconds,
goal_weight,
});
await newGoal.save();
res.status(201).json(newGoal);
} catch (error) {
console.error(error);
res.status(500).json({
message: 'failed to add goal',
error: error.message,
});
}
},
getGoal: async (req, res) => {
try {
const goals = await HabitTracker.find({
user_id: req.user.user_id,
});
if (goals.length === 0) {
//만약 비어있다면 dummy data 반환
const dummyGoal = [
{
user_id: req.user.user_id,
goal_weekly: null,
goal_daily: [
false,
false,
false,
false,
false,
false,
false,
],
goal_daily_time: '00:00',
goal_weight: null,
},
];
return res.status(200).json(dummyGoal);
}
const habitTrackergoal = goals.map(goal => {
return {
user_id: req.user.user_id,
goal_weekly: goal.goal_weekly,
goal_daily: goal.goal_daily,
goal_daily_time: secondsToMinutes(goal.goal_daily_time),
goal_weight: goal.goal_weight,
};
});
res.status(200).json(habitTrackergoal);
} catch (error) {
res.status(500).json({
message: 'Failed to get habitTracker goals',
error: error.message,
});
}
},
getEveryRecords: async (req, res) => {
const { period } = req.query.period;
try {
//정규식
const regex = new RegExp(`^${period}`);
//해당 월에 해당하는 운동 기록들을 가져옴
const monthlyRecords = Record.find({
user_id: req.user.user_id, //미들웨어에 있는 user_id
date: { $regex: regex },
});
res.status(200).json(monthlyRecords);
} catch (error) {
res.status(500).json({
message: 'Failed to get habitTracker monthly records',
error: error.message,
});
}
},
};
module.exports = habittrackerController;
const express = require('express');
const router = express.Router();
const minutesToSeconds = require('../utils/timeconvert');
//운동 영상 구조
const Video = require('../models/video');
//루틴 구조
const Routine = require('../models/routine');
// 기록할 DB 구조
const Record = require('../models/records');
const routineController = {
recordRoutine: async (req, res) => {
const { date, video_id, video_time, video_tag } = req.body;
try {
const newRecord = new Routine({
user_id: req.user.user_id, //미들웨어에서 받아온 user_id
date,
video_id,
video_tag,
video_time: minutesToSeconds(video_time),
});
await newRecord.save();
res.status(201).send('Routine records added');
} catch (error) {
console.error(error);
res.status(500).send('Failed to add workout records');
}
},
getRoutine: async (req, res) => {
try {
const userRoutine = await Routine.find({
user_id: req.user.user_id,
}).populate('routine_exercises.video');
if (userRoutine.length === 0) {
// 아무런 루틴도 없다면 null 로 채워진 루틴 하나를 return
const dummyRoutine = [
{
routine_id: null,
routine_name: null,
routine_exercises: [
{
video: {
video_id: null,
video_time: null,
video_tag: null,
},
},
],
},
];
return res.json(dummyRoutine);
}
return res.status(200).json(userRoutine);
} catch (error) {
console.error(error);
res.status(500).json({
message: 'Failed to get user routine',
error: error.message,
});
}
},
getRoutineExercise: async (req, res) => {
const { routine_name } = req.body;
try {
const routine = await Routine.findOne({
user_id: req.user.user_id,
routine_name,
}).populate('routine_exercises.video');
if (!routine) {
return res.status(404).json({ message: 'Routine not Found' });
}
res.status(200).json(routine.routine_exercises);
} catch (error) {
console.error(error);
res.status(500).json({
message: 'Failed to get routine exercise',
error: error.message,
});
}
},
createRoutine: async (req, res) => {
const { routine_name } = req.body;
try {
const newRoutine = new Routine({
user_id: req.user.user_id,
routine_name,
});
await newRoutine.save();
res.status(201).send('new routine folder was created');
} catch (error) {
console.error(error);
res.status(500).send('Failed to add new routine folder');
}
},
deleteRoutine: async (req, res) => {
const { routine_name } = req.body;
try {
const deletedRoutine = await Routine.findOneAndDelete({
user_id: req.user.user_id, //미들웨어에서 가져온 user_id
routine_name,
});
// 성공적으로 삭제되었을 때
res.status(200).json({ message: 'Routine deleted' });
} catch (error) {
console.error(error);
res.status(500).json({
message: 'Failed to delete routine',
error: error.message,
});
}
},
addRoutine: async (req, res) => {
const { routine_name, video_id } = req.body;
try {
const selectedVideo = await Video.findOne({ video_id: video_id });
if (!selectedVideo) {
return res
.status(404)
.json({ message: 'Workout Video is not found' });
}
//이걸 matchRoutine의 routine_Exercise로 추가해야함
const updatedRoutine = await Routine.findOneAndUpdate(
{ user_id: req.user.user_id, routine_name },
{ $push: { routine_exercises: { video: selectedVideo._id } } },
{ new: true }
);
res.status(201).json(updatedRoutine);
} catch (error) {
console.error(error);
res.status(500).json({
message: 'Failed to add routine videos to Routine',
error: error.message,
});
}
},
deleteRoutineComponent: async (req, res) => {
const { routine_name, video_id } = req.body;
try {
const videoContent = await Video.findOne({ video_id });
if (!videoContent) {
return res.status(404).json({
message: 'Workout Video not found',
});
}
const deletedRoutineComponent = await Routine.updateMany(
{ user_id: req.user.user_id },
{ $pull: { routine_exercises: { video: videoContent._id } } },
{ new: true }
);
res.status(200).json(updatedRoutine);
} catch (error) {
console.error(error);
res.status(500).json({
message: 'Failed to delete video from routines',
error: error.message,
});
}
},
};
module.exports = routineController;
\ No newline at end of file
const {User, UserAchievement} = require('../models/user');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const UAParser = require('ua-parser-js');
const emailService = require('../utils/emailService');
const SALT_ROUNDS=12;
const getAchievement = async (user_id, date = null) => {
try{
let matchStage={ $match: { user_id: user_id } };
if(date){
const startOfDay=new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay=new Date(date);
endOfDay.setHours(23, 59, 59, 999);
matchStage = {
$match: {
user_id: user_id,
'achievements.date': {
$gte: startOfDay,
$lte: endOfDay
}
}
};
}
const achievement=await UserAchievement.aggregate([
matchStage,
{ $unwind: '$achievements' },
// 날짜가 주어진 경우 해당 날짜의 데이터만, 아닌 경우 가장 최근 데이터
date ?
{ $match: {
'achievements.date': {
$gte: new Date(date).setHours(0, 0, 0, 0),
$lte: new Date(date).setHours(23, 59, 59, 999)
}
}} :
{ $sort: { 'achievements.date': -1 } },
{ $limit: 1 },
{ $project: {
user_height: '$achievements.user_height',
user_weight: '$achievements.user_weight',
goal_weight: '$achievements.goal_weight',
date: '$achievements.date'
}}
]);
return achievement[0] || {
user_height: null,
user_weight: null,
goal_weight: null,
date: date || null
};
} catch (error) {
console.error('Error fetching achievement:', error);
throw error;
}
};
const isSameDay=(date1, date2) => {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
};
const userController = {
hashPassword: async (user_password) => {
return await bcrypt.hash(user_password,SALT_ROUNDS);
},
createUser: async (req, res) => {
try {
const {user_name, user_id, user_password, user_gender, user_email, user_birth} = req.body;
const user = new User({
user_name,
user_id,
user_password: await userController.hashPassword(user_password),
user_gender: user_gender ? "female" : "male",
user_email,
user_birth,
tokens: {
access_sessions: [],
delete_sessions: []
}
});
await user.save();
res.json({user_id: user.user_id, message: '회원가입이 완료되었습니다'});
} catch(error) {
if(error.code === 11000) res.status(400).json({message: '이미 존재하는 아이디입니다'});
else res.status(500).json({message: `${error}`});
}
},
signIn: async (req, res) => {
try {
const {user_id, user_password} = req.body;
const user = await User.findOne({user_id: user_id});
if(!user) {
return res.status(400).json({
success: false,
message: '존재하지 않는 아이디입니다'
});
}
if(user.is_deleted) {
return res.status(400).json({
success: false,
message: '탈퇴 대기 중인 계정입니다'
});
}
if(user.lock_until && user.lock_until > Date.now()){
const remainingTime=Math.ceil((user.lock_until - Date.now())/1000);
return res.status(429).json({
message: `계정이 잠겼습니다. ${remainingTime}초 후에 다시 시도해주세요.`,
remainingTime
});
}
if (user.lock_until && user.lock_until <= Date.now()) {
user.login_attempts = 0;
user.lock_until = null;
await user.save();
}
const isPasswordValid = await bcrypt.compare(user_password, user.user_password);
if(!isPasswordValid) {
user.login_attempts++;
if(user.login_attempts>=5){
user.lock_until=new Date(Date.now() + 5*60*1000);
await user.save();
return res.status(429).json({message: '비밀번호를 5회 이상 틀렸습니다. 5분 후에 다시 시도해주세요.'});
}
await user.save();
return res.status(400).json({message: `비밀번호가 일치하지 않습니다. 남은 시도 횟수: ${5 - user.login_attempts}회`});
}
user.login_attempts = 0;
user.lock_until = null;
const userAgent = req.headers['user-agent'];
const parser = new UAParser(userAgent);
const result = parser.getResult();
const clientIp = req.ip;
const deviceInfo = {
ua: userAgent,
browser: {
name: result.browser.name ?? 'unknown',
version: result.browser.version ?? 'unknown'
},
os: {
name: result.os.name ?? 'unknown',
version: result.os.version ?? 'unknown'
},
device: {
vendor: result.device.vendor ?? 'unknown',
model: result.device.model ?? 'unknown',
type: result.device.type ?? 'desktop'
},
ip: clientIp,
last_login_at: new Date(),
last_used: new Date()
};
const refreshTokenFromCookie = req.cookies.refreshToken;
let accessToken, refreshToken;
if (refreshTokenFromCookie) {
const existingSession = user.tokens?.access_sessions?.find(
session => session.token_pair.refresh_token === refreshTokenFromCookie
);
if (existingSession) {
refreshToken = refreshTokenFromCookie;
accessToken = jwt.sign({
type: 'ACCESS',
user_id: user.user_id,
deviceInfo: {
browser: deviceInfo.browser.name,
os: deviceInfo.os.name,
device: deviceInfo.device.type
}
},
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '1h' }
);
existingSession.token_pair.access_token = accessToken;
existingSession.device_info = deviceInfo;
}
}
if (!refreshToken) {
refreshToken = jwt.sign({
type: 'REFRESH',
user_id: user.user_id,
deviceInfo: {
browser: deviceInfo.browser.name,
os: deviceInfo.os.name,
device: deviceInfo.device.type
}
},
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '14d' }
);
accessToken = jwt.sign({
type: 'ACCESS',
user_id: user.user_id,
deviceInfo: {
browser: deviceInfo.browser.name,
os: deviceInfo.os.name,
device: deviceInfo.device.type
}
},
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '1h' }
);
const MAX_SESSIONS = 5;
if (!user.tokens) {
user.tokens = {
access_sessions: [],
delete_sessions: []
};
}
if (user.tokens.access_sessions.length >= MAX_SESSIONS) {
user.tokens.access_sessions.sort((a, b) =>
new Date(a.device_info.last_used) - new Date(b.device_info.last_used)
);
user.tokens.access_sessions.shift();
}
user.tokens.access_sessions.push({
token_pair: {
access_token: accessToken,
refresh_token: refreshToken
},
device_info: deviceInfo
});
}
await user.save();
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 14 * 24 * 60 * 60 * 1000,
path: '/api'
});
res.json({
message: 'signIn 성공',
accessToken,
user_id: user.user_id,
user_name: user.user_name
});
} catch(error) {
console.error('SignIn Error:', error);
res.status(500).json({ message: error.message });
}
},
signOut: async (req, res) => {
try {
const user_id = req.user.user_id;
const accessToken = req.headers.authorization?.split(' ')[1];
const result = await User.updateOne(
{
user_id,
'tokens.access_sessions': {
$elemMatch: {
'token_pair.access_token': accessToken
}
}
},
{
$pull: {
'tokens.access_sessions': {
'token_pair.access_token': accessToken
}
}
}
);
if(result.modifiedCount === 0) {
return res.status(401).json({
success: false,
message: '유효하지 않은 세션입니다'
});
}
res.clearCookie('refreshToken', {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api'
});
res.json({
success: true,
message: '로그아웃 되었습니다'
});
} catch(error) {
console.error('SignOut Error:', error);
res.status(500).json({
success: false,
message: '로그아웃 처리 중 오류가 발생했습니다'
});
}
},
deleteUser: async (req, res) => {
try {
const {user_id, user_email, user_name, deviceInfo} = req.user;
const user_password = req.body.user_password;
const user = await User.findOne({
user_id,
is_deleted: false
});
if(!user) {
return res.status(404).json({
success: false,
message: '사용자를 찾을 수 없습니다'
});
}
const isPasswordValid = await bcrypt.compare(user_password, user.user_password);
if(!isPasswordValid) {
return res.status(400).json({
success: false,
message: '비밀번호가 일치하지 않습니다'
});
}
const deleteToken = jwt.sign({
type: 'DELETE',
user_id: user.user_id,
deviceInfo: deviceInfo
},
process.env.JWT_DELETE_SECRET,
{ expiresIn: '24h' }
);
user.tokens.delete_sessions.push({
delete_token: deleteToken,
device_info: deviceInfo
});
user.tokens.access_sessions = [];
user.is_deleted = true;
user.deleted_at = new Date();
await user.save();
res.clearCookie('refreshToken', {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api'
});
res.json({
success: true,
message: '회원탈퇴가 완료되었습니다. 확인 이메일을 확인해주세요.'
});
await emailService.sendDeleteConfirmation(user_email, user_id, user_name, deviceInfo, deleteToken);
} catch(error) {
res.status(500).json({
success: false,
message: '회원 탈퇴 처리 중 오류가 발생했습니다'
});
}
},
confirmHardDelete: async (req, res) => {
try{
const {user_id, user_name, user_email}=req.user;
await User.deleteOne({ user_id: user_id });
res.json({ message: `${user_id}님의 계정이 완전히 삭제되었습니다.` });
await emailService.sendHardDelete(user_email, user_name);
}catch(error){
console.error(error);
res.status(500).json({ message: '계정 삭제 중 오류가 발생했습니다.' });
}
},
cancelHardDelete: async (req, res) => {
try{
const {user_id, user_name, user_email}=req.user;
const user=await User.findOneAndUpdate(
{ user_id: user_id, is_deleted: true },
{
is_deleted: false,
tokens: {
access_sessions: [],
delete_sessions: []
}
},
{ new: true }
);
if(!user) return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' });
res.json({ message: `${user_id}님의 삭제가 취소되었습니다.` });
await emailService.sendCancelDelete(user_email, user_name);
}catch(error){
console.error(error);
res.status(500).json({ message: '서버 오류가 발생했습니다.' });
}
},
getProfile: async (req, res) => {
try{
const user_id=req.user.user_id;
const user=await User.findOne({ user_id: user_id });
const achievement=await getAchievement(user_id);
res.json({
success: true,
message: '프로필 조회가 완료되었습니다',
user_id: user.user_id,
user_name: user.user_name,
user_gender: user.user_gender === "female" ? 1 : 0,
user_email: user.user_email,
user_birth: user.user_birth.toLocaleDateString('ko-KR',{
year: 'numeric',
month: '2-digit',
day: '2-digit',
}),
user_height: achievement.user_height ?? null,
user_weight: achievement.user_weight ?? null,
user_created_at: user.user_created_at.toLocaleDateString('ko-KR',{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
});
}catch(error){
res.status(500).json({
success: false,
message: '프로필 조회 중 오류가 발생했습니다'
});
}
},
updateProfile: async (req, res) => {
try{
const user_id=req.user.user_id;
const updates=req.body;
const { user_height, user_weight }=updates;
delete updates.user_height;
delete updates.user_weight;
if(Object.keys(updates).length > 0){
if(updates.user_password) updates.user_password = await userController.hashPassword(updates.user_password);
if('user_gender' in updates) updates.user_gender = updates.user_gender === 0 ? "male" : "female";
await User.findOneAndUpdate(
{ user_id: user_id },
{ $set: updates },
{ runValidators: true }
);
}
const today=new Date();
today.setHours(0, 0, 0, 0);
const userAchievement=await UserAchievement.findOne({ user_id });
if(userAchievement){
const todayAchievement=userAchievement.achievements.find(
a => isSameDay(a.date, today)
);
if(todayAchievement){
if(user_height !== undefined) todayAchievement.user_height=user_height;
if(user_weight !== undefined) todayAchievement.user_weight=user_weight;
}else{
userAchievement.achievements.push({
date: today,
user_height: user_height,
user_weight: user_weight
});
}
await userAchievement.save();
}else{
const newAchievement=new UserAchievement({
user_id,
achievements: [{
date: today,
user_height: user_height,
user_weight: user_weight,
}]
});
await newAchievement.save();
}
const achievement=await getAchievement(user_id);
const user=await User.findOne({ user_id: user_id });
res.json({
success: true,
message: '프로필이 업데이트되었습니다',
user_id: user.user_id,
user_name: user.user_name,
user_gender: user.user_gender === "female" ? 1 : 0,
user_email: user.user_email,
user_birth: user.user_birth.toLocaleDateString('ko-KR').split('T')[0],
user_height: achievement.user_height ?? null,
user_weight: achievement.user_weight ?? null,
user_created_at: user.user_created_at.toLocaleDateString('ko-KR',{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
});
}catch(error){
if(error.name === 'ValidationError'){
return res.status(400).json({
success: false,
message: '입력값이 유효하지 않습니다',
errors: Object.values(error.errors).map(err => err.message)
});
}
res.status(500).json({
success: false,
message: '프로필 업데이트 중 오류가 발생했습니다'
});
}
}
}
module.exports = userController;
\ No newline at end of file
const minutesToSeconds = require('../utils/timeconvert');
//운동 영상 구조
const Video = require('../models/video');
const videoController = {
getVideo: async (req, res) => {
try {
const video_per_page = parseInt(req.query.video_per_page) || 10;
const last_id = req.query.last_id;
//전체 데이터 수
const totalVideos = await Video.countDocuments();
// last_id 기반 쿼리 조건 설정
const query = last_id ? { _id: { $gt: last_id } } : {};
//해당 페이지(오름차순)
const videos = await Video.find(query)
.sort({ _id: 1 })
.limit(video_per_page);
res.json({
video_per_page,
videos,
last_id:
videos.length > 0 ? videos[videos.length - 1]._id : null, //다음페이지 여부
});
} catch (error) {
res.status(500).json({
message: 'failed to retrieve Workout videos',
error: error.message,
});
}
},
filterVideo: async (req, res) => {
try {
const tags = req.query.video_tag;
const video_tag = tags ? tags.split(' ') : []; //video_tag 배열처리
//시간 undefined 방지를 위한 기본값 설정
let video_min_time = minutesToSeconds('00:00');
let video_max_time = minutesToSeconds('1440:00');
//00:00 입력에 대한 기본값 설정
if (
req.query.video_time_from === '00:00' &&
req.query.video_time_to === '00:00'
) {
video_min_time = minutesToSeconds('00:00');
video_max_time = minutesToSeconds('1440:00');
} else if (req.query.video_time_from && req.query.video_time_to) {
video_min_time = minutesToSeconds(req.query.video_time_from);
video_max_time = minutesToSeconds(req.query.video_time_to);
}
const video_level = req.query.video_level;
const video_per_page = parseInt(req.query.video_per_page) || 10;
const last_id = req.query.last_id; //커서페이징
const filter = {
video_length: { $gte: video_min_time, $lte: video_max_time },
};
// video_tag가 존재하면 필터에 추가
if (video_tag && Array.isArray(video_tag)) {
const tagregex = video_tag.map(tag => `(${tag})`).join('|');
// video_tag가 존재하고 Advanced가 존재하는 경우
if (video_level) {
filter.video_tag = {
$regex: `(?=.*advanced)(?=.*(${tagregex}))`,
$options: 'i',
};
} else {
filter.video_tag = {
$regex: `(?=.*(${tagregex}))`,
$options: 'i',
};
}
} else {
// advanced 만 존재할 때
if (video_level) {
filter.video_tag = {
$regex: `(?=.*advanced)`,
$options: `i`,
};
}
}
const totalVideos = await Video.find(filter).countDocuments(); //filter된 영상 수
if (last_id) {
//페이징 여부에 따른 조건 추가
filter._id = { $gt: last_id };
}
const videos = await Video.find(filter)
.sort({ _id: 1 })
.limit(video_per_page);
res.json({
totalVideos,
videos,
last_id: videos.length ? videos[videos.length - 1]._id : null,
});
} catch (error) {
console.error('tag error', error);
res.status(500).json({
message: 'failed to retrieve filtered workout videos',
error: error.message,
});
}
},
};
module.exports = videoController;
const mongoose = require('mongoose');
const express = require('express'); const express = require('express');
const cors = require('cors'); // CORS 미들웨어 추가 const cookieParser = require('cookie-parser');
const path = require('path');
const authMiddleware = require('./middleware/authMiddleware');
const initDB = require('./initDB');
const { logRequest } = require('./utils/logger');
const app = express(); const app = express();
const port = 8080; const port = 8080;
app.use(cors()); // 모든 요청에 대해 CORS 허용 const cors = require('cors');
app.use(cors({
origin: `https://${process.env.SERVER_NAME}`,
credentials: true
}));
app.use(express.static(path.join(__dirname, '../public')));
const mongoUrl = `mongodb://wss-db:27017`;
mongoose.connect(mongoUrl);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.set('trust proxy', true);
// 서버 시작 시간을 기록합니다. app.disable('x-powered-by');
const serverStartTime = Date.now();
// 업타임을 계산하여 반환하는 엔드포인트를 추가합니다. app.use('/api', (req, res, next) => {
app.get('/api/uptime', (req, res) => { const startTime = Date.now();
const uptime = Date.now() - serverStartTime; res.on('finish', () => logRequest(req, res, startTime));
console.log(`Server uptime: ${Math.floor(uptime / 1000)} seconds`); next();
res.send(`Server uptime: ${Math.floor(uptime / 1000)} seconds`);
}); });
app.use(authMiddleware.requestLogger, authMiddleware.securityHeaders, authMiddleware.apiLimiter);
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', async () => {
console.log('Connected to MongoDB');
try {
await initDB();
console.log('Database initialized successfully');
} catch (error) {
console.error('Failed to initialize database:', error);
}
});
const userRouter = require('./routers/userRouter');
const etcRouter = require('./routers/etcRouter');
const videoRouter = require('./routers/videoRouter');
const habitRouter = require('./routers/habittrackerRouter');
const routineRouter = require('./routers/routineRouter');
app.use('/api/user', userRouter);
app.use('/api/video', videoRouter);
app.use('/api/habitTracker', habitRouter);
app.use('/api/routine', routineRouter);
app.use('/api', etcRouter);
app.listen(port, () => { app.listen(port, () => {
console.log(`Backend server is running on http://172.20.0.3:${port}`); console.log(`Backend server is running on http://172.20.0.3:${port}`);
}); });
const mongoose = require('mongoose');
const { User, UserAchievement, UserDiet } = require('./models/user');
const Food100 = require('./models/food100');
const HabitTracker = require('./models/habittracker');
const Muscle = require('./models/muscle');
const Routine = require('./models/routine');
const Video = require('./models/video');
// 타임스탬프 옵션 (KST 시간 사용)
const timestampOptions = {
timestamps: {
currentTime: () => new Date(Date.now() + (9 * 60 * 60 * 1000)) // KST (+9시간)
}
};
const foodOptions = [
{ name: "계란", calories: 70, carbs: 1, protein: 6, fat: 5 },
{ name: "", calories: 300, carbs: 68, protein: 6, fat: 0.5 },
{ name: "닭가슴살", calories: 120, carbs: 0, protein: 23, fat: 2 },
{ name: "사과", calories: 80, carbs: 21, protein: 0.5, fat: 0.3 },
{ name: "고구마", calories: 86, carbs: 20, protein: 1.6, fat: 0.1 },
{ name: "바나나", calories: 89, carbs: 23, protein: 1.1, fat: 0.3 },
{ name: "오렌지", calories: 62, carbs: 15, protein: 1.2, fat: 0.2 },
{ name: "소고기", calories: 250, carbs: 0, protein: 26, fat: 17 },
{ name: "돼지고기", calories: 242, carbs: 0, protein: 27, fat: 14 },
{ name: "고등어", calories: 189, carbs: 0, protein: 20, fat: 12 },
{ name: "연어", calories: 206, carbs: 0, protein: 22, fat: 13 },
{ name: "두부", calories: 76, carbs: 1.9, protein: 8, fat: 4.8 },
{ name: "김치", calories: 33, carbs: 6.1, protein: 1.1, fat: 0.2 },
{ name: "우유", calories: 42, carbs: 5, protein: 3.4, fat: 1 },
{ name: "요거트", calories: 59, carbs: 3.6, protein: 10, fat: 0.4 },
{ name: "치킨", calories: 239, carbs: 0, protein: 27, fat: 14 },
{ name: "고추", calories: 40, carbs: 9, protein: 2, fat: 0.4 },
{ name: "양파", calories: 40, carbs: 9, protein: 1.1, fat: 0.1 },
{ name: "당근", calories: 41, carbs: 10, protein: 0.9, fat: 0.2 },
{ name: "감자", calories: 77, carbs: 17, protein: 2, fat: 0.1 },
{ name: "브로콜리", calories: 34, carbs: 7, protein: 2.8, fat: 0.4 },
{ name: "호박", calories: 26, carbs: 6.5, protein: 1, fat: 0.1 },
{ name: "치즈", calories: 402, carbs: 1.3, protein: 25, fat: 33 },
{ name: "", calories: 145, carbs: 1.3, protein: 20, fat: 7 },
{ name: "소시지", calories: 301, carbs: 2, protein: 11, fat: 28 },
{ name: "초콜릿", calories: 546, carbs: 61, protein: 4.9, fat: 31 },
{ name: "아몬드", calories: 576, carbs: 21, protein: 21, fat: 49 },
{ name: "땅콩", calories: 567, carbs: 16, protein: 25, fat: 49 },
{ name: "식빵", calories: 265, carbs: 49, protein: 9, fat: 3.2 },
{ name: "파스타", calories: 131, carbs: 25, protein: 5, fat: 1.1 },
];
const initDB = async () => {
try {
// 현재 데이터베이스의 모든 컬렉션 출력
const collections = await mongoose.connection.db.listCollections().toArray();
console.log('현재 데이터베이스의 컬렉션 목록:');
collections.forEach(collection => {
console.log(' -', collection.name);
});
// 각 스키마에 타임스탬프 옵션 적용
if (!mongoose.models.User) {
const schema = User;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('User', schema);
console.log('User 스키마가 생성되었습니다.');
}
if (!mongoose.models.UserAchievement) {
const schema = UserAchievement;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('UserAchievement', schema);
console.log('UserAchievement 스키마가 생성되었습니다.');
}
if (!mongoose.models.UserDiet) {
const schema = UserDiet;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('UserDiet', schema);
console.log('UserDiet 스키마가 생성되었습니다.');
}
// Food100 컬렉션 초기화
await Food100.deleteMany({});
console.log('Food100 컬렉션이 초기화되었습니다.');
// Food100 데이터 추가
const foodData = foodOptions.map(item => ({
food_name: item.name,
energy_kcal: item.calories,
carbohydrate: item.carbs,
protein: item.protein,
fat: item.fat
}));
await Food100.insertMany(foodData);
console.log('Food100 데이터가 성공적으로 추가되었습니다.');
// 추가 스키마들
if (!mongoose.models.HabitTracker) {
const schema = HabitTracker;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('HabitTracker', schema);
console.log('HabitTracker 스키마가 생성되었습니다.');
}
if (!mongoose.models.Muscle) {
const schema = Muscle;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('Muscle', schema);
console.log('Muscle 스키마가 생성되었습니다.');
}
if (!mongoose.models.Routine) {
const schema = Routine;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('Routine', schema);
console.log('Routine 스키마가 생성되었습니다.');
}
if (!mongoose.models.Video) {
const schema = Video;
schema.set('timestamps', timestampOptions.timestamps);
mongoose.model('Video', schema);
console.log('Video 스키마가 생성되었습니다.');
}
console.log('모든 스키마 확인이 완료되었습니다.');
} catch (error) {
console.error('스키마 초기화 중 오류 ���생:', error);
throw error;
}
};
module.exports = initDB;
\ No newline at end of file
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const { User } = require('../models/user');
const UAParser = require('ua-parser-js');
const ERROR_MESSAGES = {
LOGIN_REQUIRED: '로그인이 필요한 서비스입니다',
INVALID_TOKEN: '유효하지 않은 토큰입니다',
EXPIRED_TOKEN: '토큰이 만료되었습니다',
INVALID_SESSION: '유효하지 않은 세션입니다',
PENDING_DELETION: '탈퇴 대기중인 사용자입니다',
INVALID_FIELD: '허용되지 않은 필드가 요청되었습니다',
EXPIRED_DELETE_TOKEN: '만료된 탈퇴 토큰입니다'
};
const ALLOWED_FIELDS = [
'user_id',
'user_name',
'user_email',
'user_gender',
'user_birth',
'lock_until',
'login_attempts',
'user_height',
'user_weight',
'user_created_at',
'type',
'deviceInfo'
];
const authMiddleware = {
securityHeaders: (req, res, next) => {
try {
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('X-Frame-Options', 'DENY');
next();
} catch (error) {
console.error('Error in securityHeaders:', error);
if (!res.headersSent) {
res.status(500).send('Internal Server Error');
}
}
},
apiLimiter: rateLimit({
windowMs: 10 * 1000,
max: 100,
message: {
message: '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.'
},
keyGenerator: (req) => {
return req.headers['x-real-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0] ||
req.ip;
}
}),
authenticate: (fields = ['user_id']) => async (req, res, next) => {
try {
const token = extractToken(req);
const refreshToken = req.cookies?.refreshToken;
if (!token) {
return next(createError(401, ERROR_MESSAGES.LOGIN_REQUIRED));
}
const invalidFields = fields.filter(field => !ALLOWED_FIELDS.includes(field));
if (invalidFields.length > 0) {
return res.status(400).json({
success: false,
message: ERROR_MESSAGES.INVALID_FIELD
});
}
let decoded = jwt.decode(token);
if (!decoded || !decoded.user_id) {
return next(createError(401, ERROR_MESSAGES.INVALID_TOKEN));
}
if (isTokenExpired(decoded)) {
if (decoded.type === 'DELETE') {
return next(createError(401, ERROR_MESSAGES.EXPIRED_DELETE_TOKEN));
}
if (refreshToken) {
decoded = await handleRefreshToken(refreshToken, res, next);
} else {
return next(createError(401, ERROR_MESSAGES.EXPIRED_TOKEN));
}
}
const user = await findUser(decoded, token);
if (!user) {
return next(createError(401, ERROR_MESSAGES.INVALID_SESSION));
}
if (user.is_deleted && decoded.type !== 'DELETE') {
return next(createError(401, ERROR_MESSAGES.PENDING_DELETION));
}
req.user = filterUserFields(user, fields, req);
next();
} catch (error) {
next(createError(401, ERROR_MESSAGES.INVALID_TOKEN));
}
},
requestLogger: (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
},
};
function extractToken(req) {
const headerToken = req.headers.authorization?.split(' ')[1];
const queryToken = req.query.token;
return headerToken || queryToken;
}
function isTokenExpired(decoded) {
return decoded.exp && decoded.exp * 1000 < Date.now();
}
async function handleRefreshToken(refreshToken, res, next) {
const refreshDecoded = jwt.decode(refreshToken);
if (!refreshDecoded || !refreshDecoded.user_id) {
return next(createError(401, ERROR_MESSAGES.INVALID_TOKEN));
}
const user = await User.findOne({
user_id: refreshDecoded.user_id,
'tokens.access_sessions': {
$elemMatch: {
'token_pair.refresh_token': refreshToken
}
}
});
if (!user) {
return next(createError(401, ERROR_MESSAGES.INVALID_SESSION));
}
const newAccessToken = jwt.sign({
type: 'ACCESS',
user_id: user.user_id,
deviceInfo: refreshDecoded.deviceInfo,
isRefreshGenerated: true
},
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '1h' }
);
await User.updateOne(
{
user_id: refreshDecoded.user_id,
'tokens.access_sessions.token_pair.refresh_token': refreshToken
},
{
$set: {
'tokens.access_sessions.$.token_pair.access_token': newAccessToken,
'tokens.access_sessions.$.device_info.last_used': new Date()
}
}
);
res.setHeader('Authorization', `Bearer ${newAccessToken}`);
return jwt.decode(newAccessToken);
}
async function findUser(decoded, token) {
if (decoded.type === 'DELETE') {
return await User.findOne({
user_id: decoded.user_id,
'tokens.delete_sessions': {
$elemMatch: {
'delete_token': token
}
}
});
} else if (decoded.type === 'ACCESS') {
return await User.findOne({
user_id: decoded.user_id,
'tokens.access_sessions': {
$elemMatch: {
'token_pair.access_token': token
}
}
});
}
}
function filterUserFields(user, fields, req) {
const userAgent = req.headers['user-agent'];
const parser = new UAParser(userAgent);
const result = parser.getResult();
const clientIp = req.ip;
const deviceInfo = {
browser: {
name: result.browser.name ?? 'unknown'
},
os: {
name: result.os.name ?? 'unknown'
},
device: {
type: result.device.type ?? 'desktop'
},
ip: clientIp
};
return {
...fields.reduce((acc, field) => {
if (user[field] !== undefined) acc[field] = user[field];
return acc;
}, {}),
deviceInfo
};
}
function createError(status, message) {
const error = new Error(message);
error.status = status;
return error;
}
module.exports = authMiddleware;
\ No newline at end of file
const mongoose = require('mongoose');
const food100Schema = new mongoose.Schema({
food_id: {
type: String,
required: true,
unique: true
},
food_name: {
type: String,
required: true
},
energy_kcal: {
type: Number
},
carbohydrate: {
type: Number
},
protein: {
type: Number
},
fat: {
type: Number
},
dietary_fiber: {
type: Number
},
sugar: {
type: Number
},
salt: {
type: Number
},
vitamin: {
type: String
},
mineral: {
type: String
}
}, {
timestamps: true
});
const Food100 = mongoose.model('Food100', food100Schema);
module.exports = { Food100 };
\ No newline at end of file
const mongoose = require('mongoose');
const habitTrackerSchema = new mongoose.Schema(
{
user_id: {
type: String,
required: true,
ref: 'user',
},
goal_weekly: {
//일주일에 몇 회 할건지
type: Number,
min: 0,
},
goal_daily: {
//일주일 중 어떤 요일에 운동할건지 0, 1로 체크
type: [Number],
default: [0, 0, 0, 0, 0, 0, 0],
},
goal_daily_time: {
// 00:00 형태로 입력받는데 seconds로 변환해서 DB에 들어갑니다
type: Number,
min: 0,
},
goal_weight: {
//해빗트래커 페이지에서 받아온다
type: Number,
min: 0,
},
},
{
timestamps: true,
}
);
const HabitTracker = mongoose.model('HabitTracker', habitTrackerSchema);
module.exports = { HabitTracker };
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment