diff --git a/back/src/controllers/dietController.js b/back/src/controllers/dietController.js new file mode 100644 index 0000000000000000000000000000000000000000..d81b194e26d7381ee2ec6279aef8666cb4cf073c --- /dev/null +++ b/back/src/controllers/dietController.js @@ -0,0 +1,335 @@ +const { UserAchievement, UserDiet } = require('../models/user'); +const Food100 = require('../models/food100'); + +const getDietData = async (user_id, date = null, start_date = null, end_date = null) => { + let dietData; + // 날짜 형식 변환 함수 + const formatDate = (dateString, isEndDate = false) => { + const date = new Date(dateString); + const kstDate = new Date(date.getTime() + (9 * 60 * 60 * 1000)); // UTC+9 + + if (isEndDate) { + // 종료일은 23:59:59.999로 설정 + kstDate.setHours(23, 59, 59, 999); + } else { + // 시작일은 00:00:00.000으로 설정 + kstDate.setHours(0, 0, 0, 0); + } + + return kstDate; + }; + + if (start_date && end_date) { + // 기간 조회 + const formattedStartDate = formatDate(start_date); + const formattedEndDate = formatDate(end_date, true); + console.log('Formatted dates:', formattedStartDate, formattedEndDate); + + // 수정된 쿼리: findOne 대신 find 사용 + dietData = await UserDiet.find({ + user_id, + 'diets.date': { + $gte: formattedStartDate, + $lte: formattedEndDate + } + }).lean(); + + if (!dietData || dietData.length === 0) { + throw new Error('식단 정보가 없습니다.'); + } + + // 모든 문서의 diets를 하나의 배열로 합치고 날짜 범위로 필터링 + const allDiets = dietData.reduce((acc, doc) => { + const filteredDiets = doc.diets.filter(diet => + diet.date >= formattedStartDate && + diet.date <= formattedEndDate + ).map(diet => ({ + date: diet.date, + meals: { + breakfast: diet.meals.breakfast || [], + lunch: diet.meals.lunch || [], + dinner: diet.meals.dinner || [] + }, + _id: diet._id + })); + return [...acc, ...filteredDiets]; + }, []); + + if (allDiets.length === 0) { + throw new Error('해당 기간의 식단 정보가 없습니다.'); + } + + // 날짜순으로 정렬 + allDiets.sort((a, b) => b.date - a.date); + + return { + diets: allDiets + }; + } else if (date) { + // 특정 날짜 조회 + const startOfDay = formatDate(date); + const endOfDay = formatDate(date, true); + + dietData = await UserDiet.findOne( + { + user_id, + 'diets.date': { + $gte: startOfDay, + $lte: endOfDay + } + }, + { 'diets.$': 1 } + ).lean(); + } else { + // 최신 데이터 조회 + dietData = await UserDiet.findOne( + { user_id } + ).sort({ 'diets.date': -1 }).limit(14).lean(); + } + + if (!dietData || dietData.length === 0 || !dietData[0].diets || dietData[0].diets.length === 0) { + throw new Error('식단 정보가 없습니다.'); + } + + return { + diets: dietData[0].diets.map(diet => ({ + date: diet.date, + meals: diet.meals + })) + }; +}; + +const dietController = { + getDiet: async (req, res) => { + try { + const { user_id, user_name } = req.user; + const { date, start_date, end_date } = req.query; + + const result = await getDietData(user_id, date, start_date, end_date); + res.json({ + user_name, + ...result + }); + console.log(result); + + } catch (error) { + console.error('Error in getDiet:', error); + res.status(404).json({ + status: 'error', + message: error.message + }); + } + }, + + createDiet: async (req, res) => { + try { + const { user_id } = req.user; + const { date, mealtime, food_id, grams } = req.body; + + if (!date || !mealtime || !food_id || !grams) { + return res.status(400).json({ + status: 'error', + message: '필수 입력값이 누락되었습니다.' + }); + } + + // mealtime 유효성 검사 + if (!['breakfast', 'lunch', 'dinner'].includes(mealtime)) { + return res.status(400).json({ + status: 'error', + message: '잘못된 식사 시간입니다.' + }); + } + + // 해당 날짜의 사용자 식단 찾기 + let userDiet = await UserDiet.findOne({ + user_id, + 'diets.date': new Date(date) + }); + + if (!userDiet) { + // 해당 날짜의 식단이 없으면 새로 생성 + userDiet = new UserDiet({ + user_id, + diets: [{ + date: new Date(date), + meals: { + breakfast: [], + lunch: [], + dinner: [] + } + }] + }); + } + + // 해당 날짜의 diet 찾기 + const dietIndex = userDiet.diets.findIndex( + diet => diet.date.getTime() === new Date(date).getTime() + ); + + if (dietIndex === -1) { + // 해당 날짜의 diet가 없으면 새로 추가 + userDiet.diets.push({ + date: new Date(date), + meals: { + breakfast: [], + lunch: [], + dinner: [] + } + }); + } + + // 식사 추가 + const targetDiet = userDiet.diets[dietIndex !== -1 ? dietIndex : userDiet.diets.length - 1]; + + // 동일한 음식이 있는지 확인 + const existingFoodIndex = targetDiet.meals[mealtime].findIndex( + meal => meal.food_id === food_id + ); + + if (existingFoodIndex !== -1) { + // 이�� 존재하는 음식이면 그램수 업데이트 + targetDiet.meals[mealtime][existingFoodIndex].grams = grams; + } else { + // 새로운 음식 추가 + targetDiet.meals[mealtime].push({ + food_id, + grams + }); + } + + await userDiet.save(); + + res.status(201).json({ + status: 'success', + message: '식단이 등록되었습니다.', + data: { + date: targetDiet.date, + mealtime, + food_id, + grams + } + }); + + } catch (error) { + console.error('Error in createDiet:', error); + res.status(500).json({ + status: 'error', + message: error.message + }); + } + }, + deleteDiet: async (req, res) => { + try { + const { user_id } = req.user; + const { date, mealtime, food_id } = req.body; + + if (!date || !mealtime || !food_id) { + return res.status(400).json({ + status: 'error', + message: '필수 입력값이 누락되었습니다.' + }); + } + + // mealtime 유효성 검사 + if (!['breakfast', 'lunch', 'dinner'].includes(mealtime)) { + return res.status(400).json({ + status: 'error', + message: '잘못된 식사 시간입니다.' + }); + } + + // 해당 날짜의 사용자 식단 찾기 + const userDiet = await UserDiet.findOne({ + user_id, + 'diets.date': new Date(date) + }); + + if (!userDiet) { + return res.status(404).json({ + status: 'error', + message: '해당 날짜의 식단이 없습니다.' + }); + } + + // 해당 날짜의 diet 찾기 + const dietIndex = userDiet.diets.findIndex( + diet => diet.date.getTime() === new Date(date).getTime() + ); + + if (dietIndex === -1) { + return res.status(404).json({ + status: 'error', + message: '해당 날짜의 식단이 없습니다.' + }); + } + + // 해당 음식 찾기 및 삭제 + const targetDiet = userDiet.diets[dietIndex]; + const foodIndex = targetDiet.meals[mealtime].findIndex( + meal => meal.food_id === food_id + ); + + if (foodIndex === -1) { + return res.status(404).json({ + status: 'error', + message: '해당 음식을 찾을 수 없습니다.' + }); + } + + // 음식 삭제 + targetDiet.meals[mealtime].splice(foodIndex, 1); + + // 해당 식사 시간 음식이 모두 비었고, 다른 식사 시간도 비어있다면 해당 날짜 전체 삭제 + if (targetDiet.meals[mealtime].length === 0 && + targetDiet.meals.breakfast.length === 0 && + targetDiet.meals.lunch.length === 0 && + targetDiet.meals.dinner.length === 0) { + userDiet.diets.splice(dietIndex, 1); + } + + // diets 배열이 비어있다면 문서 전체 삭제 + if (userDiet.diets.length === 0) { + await UserDiet.deleteOne({ _id: userDiet._id }); + } else { + await userDiet.save(); + } + + res.json({ + status: 'success', + message: '식단이 삭제되었습니다.', + data: { + date, + mealtime, + food_id + } + }); + + } catch (error) { + console.error('Error in deleteDiet:', error); + res.status(500).json({ + status: 'error', + message: error.message + }); + } + }, + + getFood: async (req, res) => { + try { + const foods = await Food100.find(); + res.json({ + status: 'success', + message: '음식 목록을 회했습니다.', + foods + }); + } catch (error) { + console.error('Error in getFood:', error); + res.status(500).json({ + status: 'error', + message: error.message + }); + } + } +}; + +module.exports = dietController; \ No newline at end of file diff --git a/back/src/controllers/userController.js b/back/src/controllers/userController.js index b0e7be51fe027b13d2579509d8a9624df0e0595b..1e73a023943ab7ea25df4ae8a005388af61bd967 100644 --- a/back/src/controllers/userController.js +++ b/back/src/controllers/userController.js @@ -7,48 +7,44 @@ 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, + try { + // 가장 최근 데이터 찾기 + const latestAchievement = await UserAchievement.findOne( + { user_id }, + { achievements: { $slice: -1 } } + ); + + if (date) { + const targetDate = new Date(date); + targetDate.setUTCHours(0, 0, 0, 0); + + // 해당 날짜의 데이터 찾기 + const achievement = await UserAchievement.findOne( + { + user_id, 'achievements.date': { - $gte: startOfDay, - $lte: endOfDay + $eq: targetDate } + }, + { + 'achievements.$': 1 } - }; + ); + + if (achievement && achievement.achievements.length > 0) { + return achievement.achievements[0]; + } + + // 해당 날짜의 데이터가 없으면, 그 이전의 가장 최근 데이터 사용 + if (latestAchievement && latestAchievement.achievements.length > 0) { + const latestDate = new Date(latestAchievement.achievements[0].date); + if (targetDate >= latestDate) { + return latestAchievement.achievements[0]; + } + } } - 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] || { + return { user_height: null, user_weight: null, goal_weight: null, @@ -182,7 +178,7 @@ const userController = { } }, process.env.JWT_ACCESS_SECRET, - { expiresIn: '1h' } + { expiresIn: '1d' } ); existingSession.token_pair.access_token = accessToken; @@ -410,129 +406,146 @@ const userController = { } }, 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); + try { + const user = await User.findOne({ user_id: req.user.user_id }); + if (!user) { + return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' }); + } - res.json({ - success: true, - message: '프로필 조회가 완료되었습니다', + // 가장 최근의 achievement 데이터 조회 + const latestAchievement = await UserAchievement.findOne( + { user_id: req.user.user_id }, + { achievements: { $slice: -1 } } + ); + + const profileData = { user_id: user.user_id, user_name: user.user_name, - user_gender: user.user_gender === "female" ? 1 : 0, + user_gender: user.user_gender, + user_birth: user.user_birth, 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: '프로필 조회 중 오류가 발생했습니다' - }); + user_created_at: user.user_created_at, + user_height: latestAchievement?.achievements[0]?.user_height || null, + user_weight: latestAchievement?.achievements[0]?.user_weight || null + }; + + res.json(profileData); + } catch (error) { + console.error('Error in getProfile:', error); + res.status(500).json({ 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 } - ); + try { + const { user_name, user_password, user_email, user_height, user_weight } = req.body; + const user = await User.findOne({ user_id: req.user.user_id }); + + if (!user) { + return res.status(404).json({ message: '사용자를 찾을 수 없습니다.' }); } - const today=new Date(); - today.setHours(0, 0, 0, 0); + // 기본 사용자 정보 업데이트 + if (user_name) user.user_name = user_name; + if (user_password) user.user_password = await userController.hashPassword(user_password); + if (user_email) user.user_email = user_email; + + await user.save(); - const userAchievement=await UserAchievement.findOne({ user_id }); + // height나 weight가 변경된 경우에만 achievement 업데이트 + if (user_height !== undefined || user_weight !== undefined) { + const today = new Date(); + today.setHours(today.getHours() + 9); // KST로 변환 + today.setHours(0, 0, 0, 0); // 시간 초기화 + + let userAchievement = await UserAchievement.findOne({ user_id: req.user.user_id }); + + if (!userAchievement) { + userAchievement = new UserAchievement({ + user_id: req.user.user_id, + achievements: [] + }); + } - if(userAchievement){ - const todayAchievement=userAchievement.achievements.find( - a => isSameDay(a.date, today) + // 오늘 날짜의 achievement가 있는지 확인 + const todayAchievement = userAchievement.achievements.find( + a => isSameDay(new Date(a.date), today) ); - if(todayAchievement){ - if(user_height !== undefined) todayAchievement.user_height=user_height; - if(user_weight !== undefined) todayAchievement.user_weight=user_weight; - }else{ + if (todayAchievement) { + // 오늘 데이터가 있으면 업데이트 + if (user_height !== undefined) todayAchievement.user_height = user_height; + if (user_weight !== undefined) todayAchievement.user_weight = user_weight; + } else { + // 오늘 데이터가 없으면 새로 추가 + const lastAchievement = userAchievement.achievements[userAchievement.achievements.length - 1] || {}; userAchievement.achievements.push({ date: today, - user_height: user_height, - user_weight: user_weight + user_height: user_height !== undefined ? user_height : lastAchievement.user_height, + user_weight: user_weight !== undefined ? user_weight : lastAchievement.user_weight, + goal_weight: lastAchievement.goal_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 }); + // 업데이트된 프로필 정보 조회 + const updatedProfile = await userController.getProfile(req, { + json: (data) => data, + status: () => ({ json: (data) => data }) + }); 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 - }) + message: '프로필이 성공적으로 업데이트되었습니다.', + data: updatedProfile }); - }catch(error){ - if(error.name === 'ValidationError'){ - return res.status(400).json({ - success: false, - message: '입력값이 유효하지 않습니다', - errors: Object.values(error.errors).map(err => err.message) + } catch (error) { + console.error('Error in updateProfile:', error); + res.status(500).json({ message: '프로필 업데이트 중 오류가 발생했습니다.' }); + } + }, + getWeightHistory: async (req, res) => { + try { + const user_id = req.user.user_id; + + // 현재 날짜 KST 기준으로 설정 + const endDate = new Date(); + endDate.setHours(endDate.getHours() + 9); // KST로 변환 + endDate.setHours(23, 59, 59, 999); // 해당 날짜의 마지막 시간으로 설정 + + const startDate = new Date(endDate); + startDate.setDate(endDate.getDate() - 13); + startDate.setHours(0, 0, 0, 0); // 시작 날짜의 시작 시간으로 설정 + + const weightHistory = []; + + for (let i = 0; i <= 13; i++) { + const currentDate = new Date(endDate); + currentDate.setDate(endDate.getDate() - i); + // 각 날짜의 시작 시간으로 설정 (getAchievement 함수에서 사용) + currentDate.setHours(0, 0, 0, 0); + + const achievement = await getAchievement(user_id, currentDate); + + weightHistory.push({ + date: currentDate, + weight: achievement.user_weight }); } + + // 날짜 오름차순으로 정렬 + weightHistory.sort((a, b) => new Date(a.date) - new Date(b.date)); + + res.json({ + success: true, + data: weightHistory + }); + } catch (error) { + console.error('Error fetching weight history:', error); res.status(500).json({ success: false, - message: '프로필 업데이트 중 오류가 발생했습니다' + message: '체중 기록을 가져오는 중 오류가 발생했습니다' }); } } diff --git a/back/src/index.js b/back/src/index.js index 4ace9448d8a42c4a05c03ed46a887017b38707bd..36f029ff56419ce954ea79c977f42ae6f49a3669 100644 --- a/back/src/index.js +++ b/back/src/index.js @@ -53,11 +53,13 @@ const etcRouter = require('./routers/etcRouter'); const videoRouter = require('./routers/videoRouter'); const habitRouter = require('./routers/habittrackerRouter'); const routineRouter = require('./routers/routineRouter'); +const dietRouter = require('./routers/dietRouter'); app.use('/api/user', userRouter); app.use('/api/video', videoRouter); app.use('/api/habitTracker', habitRouter); app.use('/api/routine', routineRouter); +app.use('/api/diet', dietRouter); app.use('/api', etcRouter); app.listen(port, () => { diff --git a/back/src/initDB.js b/back/src/initDB.js index 5ef4c135dc0a3d21ea3877a0266e70dede69fddb..7c699df874fd89252b92af017c0a83cadd5b2f28 100644 --- a/back/src/initDB.js +++ b/back/src/initDB.js @@ -1,6 +1,6 @@ const mongoose = require('mongoose'); const { User, UserAchievement, UserDiet } = require('./models/user'); -const { Food100 } = require('./models/food100'); +const Food100 = require('./models/food100'); const HabitTracker = require('./models/habittracker'); const Muscle = require('./models/muscle'); const Routine = require('./models/routine'); diff --git a/back/src/middleware/authMiddleware.js b/back/src/middleware/authMiddleware.js index d93b7eb2b34fc2ffcd350ee4141bd83f920ca16b..81464fb5e61adb04d4927cb5d4e07d475de7aad2 100644 --- a/back/src/middleware/authMiddleware.js +++ b/back/src/middleware/authMiddleware.js @@ -91,6 +91,7 @@ const authMiddleware = { const user = await findUser(decoded, token); if (!user) { + console.log(decoded); return next(createError(401, ERROR_MESSAGES.INVALID_SESSION)); } diff --git a/back/src/models/food100.js b/back/src/models/food100.js index f336690decfddc52d1b9f70ff0b44d6b237d3edf..fcb97706ec196c40df27ac87312b7c47d0e38270 100644 --- a/back/src/models/food100.js +++ b/back/src/models/food100.js @@ -28,4 +28,4 @@ const food100Schema = new mongoose.Schema({ const Food100 = mongoose.model('Food100', food100Schema); -module.exports = { Food100 }; \ No newline at end of file +module.exports = Food100; \ No newline at end of file diff --git a/back/src/models/user.js b/back/src/models/user.js index f4faa2388aefaf0167820d8694e548d7059987be..1f724045bf60a8c2073f8ef3ef723a89070f4559 100644 --- a/back/src/models/user.js +++ b/back/src/models/user.js @@ -1,12 +1,17 @@ const mongoose = require('mongoose'); -const dateOnly=function(date) { +// KST로 날짜 변환하는 유틸리티 함수 +const convertToKST = (date) => { + if (!date) return date; + const kstDate = new Date(date); + kstDate.setHours(kstDate.getHours() + 9); + return kstDate; +}; + +const dateOnly = function(date) { if(date){ - if (typeof date === 'string') { - const [year, month, day] = date.split('-').map(Number); - return new Date(year, month - 1, day); - } - return new Date(date.getFullYear(), date.getMonth(), date.getDate()); + const kstDate = convertToKST(date); + return new Date(kstDate.getFullYear(), kstDate.getMonth(), kstDate.getDate()); } return date; }; @@ -161,7 +166,18 @@ const userAchievementSchema = new mongoose.Schema({ date: { type: Date, required: true, - set: dateOnly + set: function(date) { + if(date){ + // KST로 변환하고 시간을 00:00:00으로 설정 + const kstDate = convertToKST(date); + kstDate.setHours(0, 0, 0, 0); + return kstDate; + } + return date; + }, + get: function(date) { + return convertToKST(date); + } }, user_height: { type: Number, @@ -195,18 +211,8 @@ const userDietSchema = new mongoose.Schema({ required: true, set: dateOnly }, - meals: [{ - diet_id: { - type: String, - required: true, - unique: true - }, - mealtime: { - type: String, - enum: ['breakfast', 'lunch', 'dinner', 'snack'], - required: true - }, - foods: [{ + meals: { + breakfast: [{ food_id: { type: String, required: true, @@ -216,19 +222,46 @@ const userDietSchema = new mongoose.Schema({ type: Number, required: true, min: 0 - } + }, + _id: false + }], + lunch: [{ + food_id: { + type: String, + required: true, + ref: 'Food100' + }, + grams: { + type: Number, + required: true, + min: 0 + }, + _id: false + }], + dinner: [{ + food_id: { + type: String, + required: true, + ref: 'Food100' + }, + grams: { + type: Number, + required: true, + min: 0 + }, + _id: false }] - }] + } }] }, { timestamps: true }); -// 인덱스 추가 +// 복합 인덱스 수정 +userDietSchema.index({ user_id: 1, 'diets.date': 1 }, { unique: true }); + +// 인덱스 가 userAchievementSchema.index({ user_id: 1, 'achievements.date': 1 }); -userDietSchema.index({ user_id: 1, 'diets.date': 1 }); -userDietSchema.index({ 'diets.meals.diet_id': 1 }, { unique: true }); -userDietSchema.index({ 'diets.date': 1, 'diets.meals.mealtime': 1 }, { unique: true }); // 모델 생성 const User = mongoose.model('User', userSchema); diff --git a/back/src/routers/dietRouter.js b/back/src/routers/dietRouter.js new file mode 100644 index 0000000000000000000000000000000000000000..b40836e7e4f545c3e841336eca9ad29a98b289e7 --- /dev/null +++ b/back/src/routers/dietRouter.js @@ -0,0 +1,11 @@ +const express = require('express'); +const router = express.Router(); +const dietController = require('../controllers/dietController'); +const authMiddleware = require('../middleware/authMiddleware'); + +router.get('/', authMiddleware.authenticate(['user_id', 'user_name']), dietController.getDiet); +router.post('/', authMiddleware.authenticate(['user_id']), dietController.createDiet); +router.delete('/', authMiddleware.authenticate(['user_id']), dietController.deleteDiet); +router.get('/food', dietController.getFood); + +module.exports = router; \ No newline at end of file diff --git a/back/src/routers/userRouter.js b/back/src/routers/userRouter.js index 5563f88b19085fc7f3b02bf3dee0b45b13f02191..cf3a450582d1a7b02c6069dbd7821a793e2d5db5 100644 --- a/back/src/routers/userRouter.js +++ b/back/src/routers/userRouter.js @@ -22,5 +22,6 @@ router.delete('/withdraw', authMiddleware.authenticate(['user_id', 'user_email', router.delete('/confirm-hard-delete', authMiddleware.authenticate(['user_id', 'user_name', 'user_email', 'type']), userController.confirmHardDelete); router.post('/cancel-hard-delete', authMiddleware.authenticate(['user_id', 'user_name', 'user_email','type']), userController.cancelHardDelete); +router.get('/weight-history', authMiddleware.authenticate(['user_id']), userController.getWeightHistory); module.exports = router; \ No newline at end of file diff --git a/front/src/pages/MyPage.jsx b/front/src/pages/MyPage.jsx index a73b11552dcead6967415dc5f2984477e6efac09..8dfc2e3459b6c333895f2c00ccc0b0abb7e9a973 100644 --- a/front/src/pages/MyPage.jsx +++ b/front/src/pages/MyPage.jsx @@ -88,6 +88,17 @@ function MyPage(){ } } } + // 날짜 포맷팅 함수 추가 + const formatBirthDate = (dateString) => { + const date = new Date(dateString); + return `${date.getFullYear()}년 ${String(date.getMonth() + 1).padStart(2, '0')}월 ${String(date.getDate()).padStart(2, '0')}일`; + }; + + const formatCreatedAt = (dateString) => { + const date = new Date(dateString); + return `${date.getFullYear()}년 ${String(date.getMonth() + 1).padStart(2, '0')}월 ${String(date.getDate()).padStart(2, '0')}일 ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`; + }; + if (!user && !window.localStorage.getItem('accessToken')) return (<h2>표시할 사항이 없습니다.</h2>); @@ -137,7 +148,7 @@ function MyPage(){ </tr> <tr> <th>생년월일</th> - <td>{`${user.user_birth}`}</td> + <td>{formatBirthDate(user.user_birth)}</td> </tr> <tr> <th>이메일</th> @@ -157,7 +168,7 @@ function MyPage(){ </tr> <tr> <th>가입일시</th> - <td>{`${user.user_created_at}`}</td> + <td>{formatCreatedAt(user.user_created_at)}</td> </tr> <tr> <th></th> @@ -186,7 +197,7 @@ function MyPage(){ ✕ </button> <form onSubmit={handleWithdraw}> - <h2 className="modal-title">회원 탈퇴</h2> + <h2 className="modal-title">회원 탈��</h2> <div> <label>비밀번호를 입력해주세요</label> <input diff --git a/front/src/pages/diet/Diet.jsx b/front/src/pages/diet/Diet.jsx index 59431ab49e11181a3c6578b0e0faad37cb6fbcca..00fa2bd42eb9f6eb8226ba1e75268b8a1a5ac07c 100644 --- a/front/src/pages/diet/Diet.jsx +++ b/front/src/pages/diet/Diet.jsx @@ -16,43 +16,54 @@ function App() { useEffect(() => { const fetchDietData = async () => { try { - const startDate = new Date(); const endDate = new Date(); - endDate.setDate(startDate-13); + const startDate = new Date(); + startDate.setDate(endDate.getDate() - 13); const data = await getDietData(null, startDate, endDate); setDiet(data); setUpdate(false); } catch (err) { - alert(err); + alert(err.message); } }; - fetchDietData(); + try { + fetchDietData(); + } catch (err) { + console.error('Error fetching diet data:', err); + } }, [update]); useEffect(() => { const fetchUser = async () => { try { - const userData = await getUserData(['user_name', 'user_gender', 'user_birth']); + const userData = await getUserData(); + if(!userData.user_weight||!userData.user_height){ + alert("회원님의 키와 몸무게가 저장되어있지 않습니다. My Page에서 정보를 입력해주세요."); + window.location.href = "/mypage"; + } setUser(userData); } catch (err) { - alert(err); + alert(err.message); } }; - fetchUser(); + try { + fetchUser(); + } catch (err) { + console.error('Error fetching user data:', err); + } }, []); const handleButtonClick = (act) => { setActivity(act); }; - return ( <div id='Diet-container'> <div id='SideBar'> - <BmrDisplay user={user} diet={diet} activity = {activity} /> + <BmrDisplay user={user} diet={diet} activity={activity} /> <WeightBar diet={diet} /> <Checkactivity onButtonClick={handleButtonClick} /> </div> diff --git a/front/src/pages/diet/MealRecord.css b/front/src/pages/diet/MealRecord.css index d3cd881ce279fbd058850967b01dabc34f01a395..4108d0cdb82fbbd3028338721552726d99870312 100644 --- a/front/src/pages/diet/MealRecord.css +++ b/front/src/pages/diet/MealRecord.css @@ -28,14 +28,19 @@ } .modal { - background: white; - padding: 40px; + background-color: white; + padding: 20px; border-radius: 5px; - text-align: center; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + width: 300px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; } .mini { + width: 100%; display: flex; flex-direction: column; align-items: center; diff --git a/front/src/pages/diet/MealRecord.jsx b/front/src/pages/diet/MealRecord.jsx index 50c9bb47c777b947fe961da1bb070dc5dc76c601..638681720064d307de40c000d0b0b5be15e91b53 100644 --- a/front/src/pages/diet/MealRecord.jsx +++ b/front/src/pages/diet/MealRecord.jsx @@ -1,176 +1,118 @@ import React, { useState, useEffect } from 'react'; import './MealRecord.css' -import { createDiet, deleteDiet } from './api'; +import { createDiet, deleteDiet, getFoodData } from './api'; -const recordedData = { - "2024-12-03": { - breakfast: [ - { food: "계란", calories: 70, carbs: 1, protein: 6, fat: 5 }, - { food: "밥", calories: 300, carbs: 68, protein: 6, fat: 0.5 } - ], - lunch: [ - { food: "닭가슴살", calories: 120, carbs: 0, protein: 23, fat: 2 } - ], - dinner: [ - { food: "사과", calories: 80, carbs: 21, protein: 0.5, fat: 0.3 } - ] - }, - "2024-11-01": { - breakfast: [ - { food: "토스트", calories: 150, carbs: 27, protein: 4, fat: 3 }, - { food: "우유", calories: 100, carbs: 12, protein: 8, fat: 4 } - ], - lunch: [ - { food: "삼겹살", calories: 400, carbs: 0, protein: 20, fat: 35 } - ], - dinner: [ - { food: "샐러드", calories: 50, carbs: 5, protein: 1, fat: 0.5 } - ] - }, - "2024-11-03": { - breakfast: [ - { food: "시리얼", calories: 200, carbs: 45, protein: 6, fat: 2 } - ], - lunch: [ - { food: "볶음밥", calories: 500, carbs: 60, protein: 10, fat: 20 } - ], - dinner: [ - { food: "고구마", calories: 120, carbs: 30, protein: 1, fat: 0 } - ] - }, - "2024-11-04": { - breakfast: [ - { food: "베이글", calories: 250, carbs: 50, protein: 9, fat: 1 }, - { food: "크림치즈", calories: 100, carbs: 2, protein: 2, fat: 10 } - ], - lunch: [ - { food: "김밥", calories: 300, carbs: 40, protein: 8, fat: 5 } - ], - dinner: [ - { food: "미역국", calories: 50, carbs: 4, protein: 2, fat: 1 } - ] - }, - "2024-11-05": { - breakfast: [ - { food: "오트밀", calories: 150, carbs: 27, protein: 5, fat: 3 }, - { food: "바나나", calories: 90, carbs: 23, protein: 1, fat: 0.3 } - ], - lunch: [ - { food: "라면", calories: 500, carbs: 80, protein: 10, fat: 15 } - ], - dinner: [ - { food: "돼지고기", calories: 250, carbs: 0, protein: 18, fat: 20 } - ] - }, - "2024-11-06": { - breakfast: [ - { food: "빵", calories: 160, carbs: 30, protein: 4, fat: 3 } - ], - lunch: [ - { food: "햄버거", calories: 600, carbs: 45, protein: 25, fat: 30 } - ], - dinner: [ - { food: "오렌지", calories: 60, carbs: 15, protein: 1, fat: 0 } - ] - }, - "2024-11-07": { - breakfast: [ - { food: "커피", calories: 10, carbs: 0, protein: 0, fat: 0 }, - { food: "도넛", calories: 250, carbs: 35, protein: 4, fat: 10 } - ], - lunch: [ - { food: "스파게티", calories: 450, carbs: 60, protein: 12, fat: 15 } - ], - dinner: [ - { food: "두부조림", calories: 100, carbs: 5, protein: 8, fat: 5 } - ] - }, - "2024-11-09": { - breakfast: [ - { food: "핫케이크", calories: 300, carbs: 40, protein: 5, fat: 10 } - ], - lunch: [ - { food: "초밥", calories: 400, carbs: 50, protein: 10, fat: 5 } - ], - dinner: [ - { food: "된장국", calories: 60, carbs: 6, protein: 3, fat: 1 } - ] - }, - "2024-11-10": { - breakfast: [ - { food: "토스트", calories: 180, carbs: 30, protein: 5, fat: 4 } - ], - lunch: [ - { food: "볶음우동", calories: 450, carbs: 70, protein: 8, fat: 12 } - ], - dinner: [ - { food: "야채샐러드", calories: 60, carbs: 10, protein: 2, fat: 1 } - ] - } -}; - - -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 }, -]; - - -function MealRecord(diet) { - const [data, setData] = useState(null); +function MealRecord({ diet }) { + const [data, setData] = useState({}); const [selectedDate, setSelectedDate] = useState(null); const [selectedMeal, setSelectedMeal] = useState("breakfast"); const [isModalOpen, setIsModalOpen] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [selectedFood, setSelectedFood] = useState(null); const [foodWeight, setFoodWeight] = useState(100); + const [dateList, setDateList] = useState([]); + const [foodData, setFoodData] = useState([]); - const handleSelectDate = (date) => setSelectedDate(date); + const handleSelectDate = (date) => { + setSelectedDate(date); + if (!data[date]) { + setData(prevData => ({ + ...prevData, + [date]: { + breakfast: [], + lunch: [], + dinner: [] + } + })); + } + }; const handleSelectMeal = (meal) => setSelectedMeal(meal); const getTodayDate = (date) => { - const today = new Date(date); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; + const kstDate = new Date(date); + kstDate.setHours(kstDate.getHours() + 9); // UTC+9 + return kstDate.toISOString().split('T')[0]; }; - useEffect(() => { - if (diet && Array.isArray(diet)) { - const mealsData = diet.map(item => item.meals).flat(); - setData(mealsData); + const fetchFoodData = async (query = "") => { + try { + const foods = await getFoodData(query); + setFoodData(foods); + } catch (error) { + console.error("Error fetching food data:", error); } - }, [diet]); + }; + + useEffect(() => { + const generateDateList = () => { + const dates = []; + const today = new Date(); + today.setHours(today.getHours() + 9); // KST로 설정 + const initialData = {}; + + for (let i = 0; i < 14; i++) { + const date = new Date(today); + date.setDate(today.getDate() - i); + const formattedDate = date.toISOString().split('T')[0]; + dates.push(formattedDate); + + if (!data[formattedDate]) { + initialData[formattedDate] = { + breakfast: [], + lunch: [], + dinner: [] + }; + } + } + + setDateList(dates); + setData(prevData => ({ + ...prevData, + ...initialData + })); + }; + generateDateList(); + fetchFoodData(); + }, []); + + useEffect(() => { + if (diet?.diets) { + const newData = {}; + + diet.diets.forEach(dietItem => { + const formattedDate = new Date(dietItem.date).toISOString().split('T')[0]; + + newData[formattedDate] = { + breakfast: dietItem.meals.breakfast.map(meal => ({ + ...meal, + food_id: meal.food_id, + grams: meal.grams + })) || [], + lunch: dietItem.meals.lunch.map(meal => ({ + ...meal, + food_id: meal.food_id, + grams: meal.grams + })) || [], + dinner: dietItem.meals.dinner.map(meal => ({ + ...meal, + food_id: meal.food_id, + grams: meal.grams + })) || [] + }; + }); + + setData(prevData => ({ + ...prevData, + ...newData + })); + + if (diet.diets.length > 0 && !selectedDate) { + const firstDate = new Date(diet.diets[0].date).toISOString().split('T')[0]; + setSelectedDate(firstDate); + } + } + }, [diet, foodData]); const handleAddFood = async () => { if (!selectedFood || foodWeight <= 0) { @@ -179,11 +121,36 @@ function MealRecord(diet) { } try { - await createDiet(selectedDate, selectedMeal, selectedFood.food_id, foodWeight); - setIsModalOpen(false); - setSearchQuery(""); - setFoodWeight(100); - setSelectedFood(null); + const response = await createDiet({ + date: selectedDate, + mealtime: selectedMeal, + food_id: selectedFood.food_id, + grams: foodWeight + }); + + if (response) { + const newFoodItem = { + food_id: selectedFood.food_id, + grams: foodWeight, + foodInfo: selectedFood + }; + + setData(prevData => ({ + ...prevData, + [selectedDate]: { + ...prevData[selectedDate], + [selectedMeal]: [ + ...prevData[selectedDate][selectedMeal], + newFoodItem + ] + } + })); + + setIsModalOpen(false); + setSearchQuery(""); + setFoodWeight(100); + setSelectedFood(null); + } } catch (err) { alert(err); } @@ -191,39 +158,64 @@ function MealRecord(diet) { const handleDeleteFood = async (meal, index) => { try { - await deleteDiet(selectedDate, selectedMeal, selectedFood.food_id); + const foodToDelete = data[selectedDate][meal][index]; + + const response = await deleteDiet({ + date: selectedDate, + mealtime: meal, + food_id: foodToDelete.food_id + }); + + if (response.status === 'success') { + setData(prevData => ({ + ...prevData, + [selectedDate]: { + ...prevData[selectedDate], + [meal]: prevData[selectedDate][meal].filter((_, i) => i !== index) + } + })); + } } catch (err) { - alert(err); + console.error('Delete error:', err); + alert(err.message || '삭제 중 오류가 발생했습니다.'); } }; const calculateTotal = (meal, nutrient) => { - return data[selectedDate][meal].reduce((acc, item) => acc + item[nutrient], 0); + if (!selectedDate || !data[selectedDate] || !data[selectedDate][meal]) return 0; + + return data[selectedDate][meal].reduce((acc, item) => { + if (!item.foodInfo) return acc; + const value = nutrient === 'calories' ? item.foodInfo.energy_kcal : item.foodInfo[nutrient]; + return acc + (value * item.grams / 100); + }, 0); }; const calculateTotalSum = (nutrient) => { - let total = 0; - ["breakfast", "lunch", "dinner"].map((meal, index) => ( - total += calculateTotal(meal, nutrient) - )) - return total; + if (!selectedDate) return 0; + + return ["breakfast", "lunch", "dinner"].reduce((total, meal) => { + return total + calculateTotal(meal, nutrient); + }, 0); }; - const filteredFoods = foodOptions.filter((food) => - food.name.includes(searchQuery) + const filteredFoods = foodData.filter(food => + food.food_name.toLowerCase().includes(searchQuery.toLowerCase()) ); - // 음식 선택 const handleSelectFood = (food) => { setSelectedFood(food); }; + const handleSearchChange = (e) => { + setSearchQuery(e.target.value); + }; return ( <div className='MealRecord-container'> <div className='datelist' style={{ overflowY: 'scroll' }}> <ul className='list'> - {data && Object.keys(data).map((date) => ( + {dateList.map((date) => ( <li key={date} onClick={() => handleSelectDate(date)} @@ -231,13 +223,13 @@ function MealRecord(diet) { backgroundColor: date === selectedDate ? '#eee' : 'transparent', }} > - {getTodayDate(data.date)} + {date} </li> ))} </ul> </div> <div className='record'> - <span>{getTodayDate(selectedDate)} 식사 기록</span> + <span>{selectedDate ? `${selectedDate} 식사 기록` : '날짜를 선택하세요'}</span> <table> <thead> <tr> @@ -287,13 +279,33 @@ function MealRecord(diet) { </tr> </thead> <tbody> - {data[selectedDate][selectedMeal].map((item, index) => ( + {data[selectedDate] && data[selectedDate][selectedMeal].map((item, index) => ( <tr key={index}> - <td>{item.food}</td> - <td>{item.calories.toFixed(1)} kcal</td> - <td>{item.carbs.toFixed(1)} g</td> - <td>{item.protein.toFixed(1)} g</td> - <td>{item.fat.toFixed(1)} g</td> + <td> + {item.foodInfo + ? `${item.foodInfo.food_name} (${item.grams}g)` + : `음식 ID: ${item.food_id} (${item.grams}g)`} + </td> + <td> + {item.foodInfo + ? (item.grams * item.foodInfo.energy_kcal / 100).toFixed(1) + : '정보 없음'} kcal + </td> + <td> + {item.foodInfo + ? (item.grams * item.foodInfo.carbs / 100).toFixed(1) + : '정보 없음'} g + </td> + <td> + {item.foodInfo + ? (item.grams * item.foodInfo.protein / 100).toFixed(1) + : '정보 없음'} g + </td> + <td> + {item.foodInfo + ? (item.grams * item.foodInfo.fat / 100).toFixed(1) + : '정보 없음'} g + </td> <button onClick={() => handleDeleteFood(selectedMeal, index)}>삭제</button> </tr> ))} @@ -310,26 +322,36 @@ function MealRecord(diet) { <h4>음식 검색</h4> <input type="text" - placeholder="음식 이름" + placeholder="음식 이름을 입력하세요" value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={handleSearchChange} /> <ul className="list"> - {filteredFoods.map((food, index) => ( - <li key={index} onClick={() => handleSelectFood(food)} style={{ cursor: 'pointer' }}> - {food.name} + {filteredFoods.map((food) => ( + <li + key={food.food_id} + onClick={() => handleSelectFood(food)} + style={{ cursor: 'pointer' }} + > + {food.food_name} ({food.energy_kcal}kcal/100g) </li> ))} </ul> <div className='under'> {selectedFood && ( <div> - <p>선택: {selectedFood.name}</p> + <p>선택한 음식: {selectedFood.food_name}</p> + <p>100g 당 영양성분:</p> + <p>칼로리: {selectedFood.energy_kcal}kcal</p> + <p>탄수화물: {selectedFood.carbs}g</p> + <p>단백질: {selectedFood.protein}g</p> + <p>지방: {selectedFood.fat}g</p> <input type="number" - placeholder="무게 (g)" + placeholder="섭취량 (g)" value={foodWeight} onChange={(e) => setFoodWeight(Number(e.target.value))} + min="0" /> g </div> )} @@ -337,14 +359,18 @@ function MealRecord(diet) { {selectedFood && ( <button onClick={handleAddFood}>추가</button> )} - <button onClick={() => setIsModalOpen(false)}>닫기</button> + <button onClick={() => { + setIsModalOpen(false); + setSearchQuery(''); + setSelectedFood(null); + setFoodWeight(100); + }}>닫기</button> </div> </div> </div> </div> </div> )} - </div> ); } diff --git a/front/src/pages/diet/WeightChart.jsx b/front/src/pages/diet/WeightChart.jsx index a9881025bb7a42ab406c7c944dcd87dcc083d33b..02caba9c06bdf810da519c5077876a475e1275f1 100644 --- a/front/src/pages/diet/WeightChart.jsx +++ b/front/src/pages/diet/WeightChart.jsx @@ -1,6 +1,7 @@ import { Line } from "react-chartjs-2"; import React, { useState, useEffect } from 'react'; import './WeightChart.css' +import { getWeightHistory } from './api'; import { Chart as ChartJS, @@ -60,32 +61,18 @@ const options = { legend: { display: false, }, - }, - tooltip: { - callbacks: { - label: (context) => { - const label = context.dataset.label || ''; - const value = context.raw; - return `${label}: ${value} kg`; + tooltip: { + callbacks: { + label: (context) => { + const label = context.dataset.label || ''; + const value = context.raw; + return `${label}: ${value} kg`; + }, }, }, }, }; -let diet = [ - { weight: 54 }, - { weight: 54 }, - { weight: 54 }, - { weight: 53 }, - { weight: 53 }, - { weight: 53 }, - { weight: 52 }, - { weight: 52 }, - { weight: 51 }, -]; - - - function WeightChart() { const [chartData, setChartData] = useState({ labels: [], @@ -98,7 +85,84 @@ function WeightChart() { }, ], }); - const [weight, setWeight] = useState(0); + + const [chartOptions, setChartOptions] = useState(options); + + useEffect(() => { + const fetchWeightHistory = async () => { + try { + const data = await getWeightHistory(); + + if (!data || data.length === 0) { + console.log('데이터가 없습니다'); + return; + } + + const weights = data.map(item => item.weight); + + // null 값 처리 후 최대/최소값 계산 + const validWeights = weights.filter(w => w !== null); + const minWeight = Math.min(...validWeights); + const maxWeight = Math.max(...validWeights); + + // 범위의 10% 계산 + const range = maxWeight - minWeight; + const padding = range * 0.1; + + // y축 범위 설정 + setChartOptions(prev => ({ + ...prev, + scales: { + ...prev.scales, + y: { + ...prev.scales.y, + min: Math.floor(minWeight - padding), + max: Math.ceil(maxWeight + padding), + } + } + })); + + // 기존의 차트 데이터 설정 로직 + const labels = data.map(item => { + const date = new Date(item.date); + return `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`; + }); + + // null 값을 이후 날짜의 가장 가까운 weight 값으로 채우기 + for (let i = 0; i < weights.length; i++) { + if (weights[i] === null) { + let nextValue = null; + for (let j = i + 1; j < weights.length; j++) { + if (weights[j] !== null) { + nextValue = weights[j]; + break; + } + } + weights[i] = nextValue; + } + } + + // 차트 데이터 설정 + setChartData(prevState => ({ + labels: labels, + datasets: [ + { + label: "Weight", + data: weights, + backgroundColor: "#FFCA29", + borderColor: "#FFCA29", + tension: 0.4, // 선을 부드럽게 만듦 + pointRadius: 4, // 포인트 크기 + }, + ], + })); + } catch (error) { + console.error('체중 데이터 가져오기 실패:', error); + } + }; + + fetchWeightHistory(); + }, []); const getTodayDate = (date) => { const today = new Date(date); @@ -108,62 +172,10 @@ function WeightChart() { return `${year}-${month}-${day}`; }; - useEffect(() => { - const toda = new Date(); - for (let i = 0; i < 9; i++) { - diet[i].date = new Date(); - diet[i].date.setDate(toda.getDate() - i); - } - - if (diet && Array.isArray(diet)) { - let labels = Array(14).fill(""); - let weights = Array(14).fill(null); - /* - const itemMap = new Map(diet.map((item, index) => [item.date.toISOString().split("T")[0], index])); - const tmp = new Date(diet[diet.length - 1].date); - for (let i = 13; i >= 0; i--) { - tmp.setDate(tmp.getDate() + 1); - const targetIndex = itemMap.get(tmp.toISOString().split("T")[0]) ?? -1; - if (targetIndex != -1) { - labels[i] = getTodayDate(diet[targetIndex].date); - weights[i] = diet[targetIndex].achievement.weight; - } else if (i < 13) { - labels[i] = labels[i + 1]; - weights[i] = weights[i + 1]; - } - } - */ - const itemMap = new Map(diet.map((item, index) => [item.date.toISOString().split("T")[0], index])); - for (let i = 0; i < 14; i++) { - const tmp = new Date(); - tmp.setDate(toda.getDate() - i); - const targetIndex = itemMap.get(tmp.toISOString().split("T")[0]) ?? -1; - if (targetIndex != -1) { - labels[i] = getTodayDate(diet[targetIndex].date); - weights[i] = diet[targetIndex].weight; - } else if (i > 0) { - labels[i] = labels[i - 1]; - weights[i] = weights[i - 1]; - } - } - setChartData({ - labels: labels, - datasets: [ - { - label: "Weight", - data: weights, - backgroundColor: "#FFCA29", - borderColor: "#FFCA29", - }, - ], - }); - } - }, [diet]); - return ( <div className='WeightChart-container'> <div className="graph"> - <Line options={options} data={chartData} /> + <Line options={chartOptions} data={chartData} /> </div> </div> ); diff --git a/front/src/pages/diet/api.js b/front/src/pages/diet/api.js index 993a1568b264f0e6943ef75d12981159f02b0126..baab8a07b23c97f0952c72a9771c0ea5c2588fef 100644 --- a/front/src/pages/diet/api.js +++ b/front/src/pages/diet/api.js @@ -14,34 +14,80 @@ async function fetchWithOptions(url, options) { } } +// KST로 날짜 변환하는 함수 +const convertToKST = (dateStr) => { + const date = new Date(dateStr); + date.setHours(date.getHours() + 9); // UTC+9 + return date.toISOString().split('T')[0]; +}; + const getDietData = async (date = null, start_date = null, end_date = null) => { const queryParams = new URLSearchParams(); - if (date) queryParams.append('date', date); - if (start_date) queryParams.append('start_date', start_date); - if (end_date) queryParams.append('end_date', end_date); + + if (date) { + queryParams.append('date', convertToKST(date)); + } + if (start_date) { + queryParams.append('start_date', convertToKST(start_date)); + } + if (end_date) { + // end_date는 해당 일자의 마지막 시간으로 설정 + const endDate = new Date(end_date); + endDate.setHours(32, 59, 59, 999); // KST 23:59:59.999 + queryParams.append('end_date', convertToKST(endDate)); + } + + // 먼저 음식 데이터를 가져옵니다 + const foodResponse = await getFoodData(); + const foodMap = foodResponse.reduce((acc, food) => { + acc[food.food_id] = food; + return acc; + }, {}); - return await fetchWithOptions(`${URL}?${queryParams.toString()}`, { + const dietResponse = await fetchWithOptions(`${URL}?${queryParams.toString()}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('accessToken')}` }, }); + + // 각 식단의 음식 정보를 추가하고 날짜별로 정렬 + const dietsWithFoodInfo = dietResponse.diets + .map(diet => ({ + date: convertToKST(diet.date), + meals: { + breakfast: diet.meals.breakfast.map(meal => ({ + ...meal, + foodInfo: foodMap[meal.food_id] || null + })), + lunch: diet.meals.lunch.map(meal => ({ + ...meal, + foodInfo: foodMap[meal.food_id] || null + })), + dinner: diet.meals.dinner.map(meal => ({ + ...meal, + foodInfo: foodMap[meal.food_id] || null + })) + } + })) + .sort((a, b) => new Date(b.date) - new Date(a.date)); + + return { + user_name: dietResponse.user_name, + diets: dietsWithFoodInfo + }; }; -const getUserData = async (fields = []) => { - const queryParams = new URLSearchParams(); - if (fields.length > 0) { - queryParams.append('fields', fields.join(',')); - } - return await fetchWithOptions(`api/user/profile?${queryParams.toString()}`), { +const getUserData = async () => { + return await fetchWithOptions(`api/user/profile`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('accessToken')}` }, - }; -} + }); +}; const createDiet = async ({ date, mealtime, food_id, grams }) => { return await fetchWithOptions(URL, { @@ -61,8 +107,46 @@ const deleteDiet = async ({ date, mealtime, food_id }) => { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('accessToken')}` }, - body: JSON.stringify({ date, mealtime, food_id }), + body: JSON.stringify({ + date: date, + mealtime: mealtime, + food_id: food_id + }), }); }; -export { getDietData, createDiet, deleteDiet, getUserData }; \ No newline at end of file +const getFoodData = async () => { + const response = await fetchWithOptions(`${URL}/food`, { + method: 'GET', + }); + return response.foods.map((food) => ({ + food_id: food.food_id, + food_name: food.food_name, + energy_kcal: food.energy_kcal, + carbs: food.carbs, + protein: food.protein, + fat: food.fat + })); +}; + +const getWeightHistory = async () => { + const response = await fetchWithOptions('/api/user/weight-history', { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('accessToken')}` + }, + }); + + // 날짜를 KST로 변환하고 데이터 정렬 + const sortedData = response.data + .map(item => ({ + date: convertToKST(item.date), + weight: item.user_weight + })) + .sort((a, b) => new Date(a.date) - new Date(b.date)); + + return sortedData; +}; + +export { getDietData, createDiet, deleteDiet, getUserData, getFoodData, getWeightHistory }; \ No newline at end of file