diff --git a/.gitignore b/.gitignore index e7799bd1229836a1f789f1800487a9fdd5207eb8..b92da52e3436c264764d9bf3e9f364b95ba14a0e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ db/data .env ssl/*.pem ssl/certbot/* -back/src/logs/* \ No newline at end of file +back/src/logs/* diff --git a/back/src/initDB.js b/back/src/initDB.js index 8026af3babaee015814b1a456622c33d321770b5..5ef4c135dc0a3d21ea3877a0266e70dede69fddb 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'); @@ -14,36 +14,36 @@ const timestampOptions = { }; 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 }, + {food_id:1, food_name: "계란", energy_kcal: 70, carbs: 1, protein: 6, fat: 5 }, + {food_id:2, food_name: "밥", energy_kcal: 300, carbs: 68, protein: 6, fat: 0.5 }, + {food_id:3, food_name: "닭가슴살", energy_kcal: 120, carbs: 0, protein: 23, fat: 2 }, + {food_id:4, food_name: "사과", energy_kcal: 80, carbs: 21, protein: 0.5, fat: 0.3 }, + {food_id:5, food_name: "고구마", energy_kcal: 86, carbs: 20, protein: 1.6, fat: 0.1 }, + {food_id:6, food_name: "바나나", energy_kcal: 89, carbs: 23, protein: 1.1, fat: 0.3 }, + {food_id:7, food_name: "오렌지", energy_kcal: 62, carbs: 15, protein: 1.2, fat: 0.2 }, + {food_id:8, food_name: "소고기", energy_kcal: 250, carbs: 0, protein: 26, fat: 17 }, + {food_id:9, food_name: "돼지고기", energy_kcal: 242, carbs: 0, protein: 27, fat: 14 }, + {food_id:10, food_name: "고등어", energy_kcal: 189, carbs: 0, protein: 20, fat: 12 }, + {food_id:11, food_name: "연어", energy_kcal: 206, carbs: 0, protein: 22, fat: 13 }, + {food_id:12, food_name: "두부", energy_kcal: 76, carbs: 1.9, protein: 8, fat: 4.8 }, + {food_id:13, food_name: "김치", energy_kcal: 33, carbs: 6.1, protein: 1.1, fat: 0.2 }, + {food_id:14, food_name: "우유", energy_kcal: 42, carbs: 5, protein: 3.4, fat: 1 }, + {food_id:15, food_name: "요거트", energy_kcal: 59, carbs: 3.6, protein: 10, fat: 0.4 }, + {food_id:16, food_name: "치킨", energy_kcal: 239, carbs: 0, protein: 27, fat: 14 }, + {food_id:17, food_name: "고추", energy_kcal: 40, carbs: 9, protein: 2, fat: 0.4 }, + {food_id:18, food_name: "양파", energy_kcal: 40, carbs: 9, protein: 1.1, fat: 0.1 }, + {food_id:19, food_name: "당근", energy_kcal: 41, carbs: 10, protein: 0.9, fat: 0.2 }, + {food_id:20, food_name: "감자", energy_kcal: 77, carbs: 17, protein: 2, fat: 0.1 }, + {food_id:21, food_name: "브로콜리", energy_kcal: 34, carbs: 7, protein: 2.8, fat: 0.4 }, + {food_id:22, food_name: "호박", energy_kcal: 26, carbs: 6.5, protein: 1, fat: 0.1 }, + {food_id:23, food_name: "치즈", energy_kcal: 402, carbs: 1.3, protein: 25, fat: 33 }, + {food_id:24, food_name: "햄", energy_kcal: 145, carbs: 1.3, protein: 20, fat: 7 }, + {food_id:25, food_name: "소시지", energy_kcal: 301, carbs: 2, protein: 11, fat: 28 }, + {food_id:26, food_name: "초콜릿", energy_kcal: 546, carbs: 61, protein: 4.9, fat: 31 }, + {food_id:27, food_name: "아몬드", energy_kcal: 576, carbs: 21, protein: 21, fat: 49 }, + {food_id:28, food_name: "땅콩", energy_kcal: 567, carbs: 16, protein: 25, fat: 49 }, + {food_id:29, food_name: "식빵", energy_kcal: 265, carbs: 49, protein: 9, fat: 3.2 }, + {food_id:30, food_name: "파스타", energy_kcal: 131, carbs: 25, protein: 5, fat: 1.1 }, ]; const initDB = async () => { @@ -83,9 +83,10 @@ const initDB = async () => { // Food100 데이터 추가 const foodData = foodOptions.map(item => ({ - food_name: item.name, - energy_kcal: item.calories, - carbohydrate: item.carbs, + food_id: item.food_id, + food_name: item.food_name, + energy_kcal: item.energy_kcal, + carbs: item.carbs, protein: item.protein, fat: item.fat })); diff --git a/back/src/models/food100.js b/back/src/models/food100.js index c52150453dc4106fc2789bbac6f3cf997e7875d6..f336690decfddc52d1b9f70ff0b44d6b237d3edf 100644 --- a/back/src/models/food100.js +++ b/back/src/models/food100.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const food100Schema = new mongoose.Schema({ food_id: { - type: String, + type: Number, required: true, unique: true }, @@ -13,7 +13,7 @@ const food100Schema = new mongoose.Schema({ energy_kcal: { type: Number }, - carbohydrate: { + carbs: { type: Number }, protein: { @@ -21,21 +21,6 @@ const food100Schema = new mongoose.Schema({ }, fat: { type: Number - }, - dietary_fiber: { - type: Number - }, - sugar: { - type: Number - }, - salt: { - type: Number - }, - vitamin: { - type: String - }, - mineral: { - type: String } }, { timestamps: true diff --git a/front/nginx/env.sh b/front/nginx/env.sh index b86c8f6e30e040643ed81866e2211d730d8fb8e2..7f4368e63d49cee532d5329e4dcad5975f5225e9 100755 --- a/front/nginx/env.sh +++ b/front/nginx/env.sh @@ -5,5 +5,14 @@ else envsubst '${SERVER_NAME}' < /etc/nginx/templates/server-nginx.conf.template > /etc/nginx/nginx.conf fi +# locations.conf의 내용으로 # INSERT_LOCATIONS_HERE를 교체 +sed -i -e '/# INSERT_LOCATIONS_HERE/r /etc/nginx/locations.conf' -e '/# INSERT_LOCATIONS_HERE/d' /etc/nginx/nginx.conf +# front/nginx/env.sh +if [ "$SERVER_NAME" = "localhost" ]; then + envsubst '${SERVER_NAME}' < /etc/nginx/templates/local-nginx.conf.template > /etc/nginx/nginx.conf +else + envsubst '${SERVER_NAME}' < /etc/nginx/templates/server-nginx.conf.template > /etc/nginx/nginx.conf +fi + # locations.conf의 내용으로 # INSERT_LOCATIONS_HERE를 교체 sed -i -e '/# INSERT_LOCATIONS_HERE/r /etc/nginx/locations.conf' -e '/# INSERT_LOCATIONS_HERE/d' /etc/nginx/nginx.conf \ No newline at end of file diff --git a/front/package-lock.json b/front/package-lock.json index f55869cf2e92bf918b1ee3f31df969b2128d691b..428df749b87a5e2a016caa0493e18f3a52d79429 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -11,11 +11,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "chart.js": "^4.4.6", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", "react-router-dom": "^7.0.1", "react-scripts": "5.0.1", + "styled-components": "^6.1.13", "web-vitals": "^2.1.4" } }, @@ -2383,6 +2386,27 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -3010,6 +3034,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -4206,6 +4236,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/stylis": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", + "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT" + }, "node_modules/@types/testing-library__jest-dom": { "version": "5.14.9", "resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.9.tgz", @@ -5723,6 +5759,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -5789,6 +5834,18 @@ "node": ">=10" } }, + "node_modules/chart.js": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz", + "integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-types": { "version": "11.2.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", @@ -6424,6 +6481,17 @@ "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "license": "MIT" }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", @@ -15229,6 +15297,12 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -15845,6 +15919,68 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.13.tgz", + "integrity": "sha512-M0+N2xSnAtwcVAQeFEsGWFFxXDftHUD7XrKla06QbpUMmbmtFBMMTcKWvFXtWxuD5qQkB8iU5gk6QASlx2ZRMw==", + "license": "MIT", + "dependencies": { + "@emotion/is-prop-valid": "1.2.2", + "@emotion/unitless": "0.8.1", + "@types/stylis": "4.2.5", + "css-to-react-native": "3.2.0", + "csstype": "3.1.3", + "postcss": "8.4.38", + "shallowequal": "1.1.0", + "stylis": "4.3.2", + "tslib": "2.6.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/styled-components/node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD" + }, "node_modules/stylehacks": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", @@ -15861,6 +15997,12 @@ "postcss": "^8.2.15" } }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", diff --git a/front/package.json b/front/package.json index 949176f5097b01c69350a5be9a4d10e98060b1a2..91aa7a8e574a6bc9c9ee69e02e553815ce8a99c2 100644 --- a/front/package.json +++ b/front/package.json @@ -6,11 +6,14 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "chart.js": "^4.4.6", "react": "^18.3.1", "react-beautiful-dnd": "^13.1.1", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.3.1", "react-router-dom": "^7.0.1", "react-scripts": "5.0.1", + "styled-components": "^6.1.13", "web-vitals": "^2.1.4" }, "devDependencies": { diff --git a/front/src/App.jsx b/front/src/App.jsx index d9c96826a44e77a68ca431ea693c0c62cb0da834..abb3590ac8087345cac04f12adb1279f639aa11e 100644 --- a/front/src/App.jsx +++ b/front/src/App.jsx @@ -8,6 +8,7 @@ import Footer from './components/common/Footer' import Sign from './pages/Sign'; import MyPage from './pages/MyPage'; import Routine from './pages/routine/Routine'; +import Diet from './pages/diet/Diet'; import './App.css'; @@ -24,6 +25,7 @@ function App(){ <Route path="/sign" element={<Sign />} /> <Route path="/mypage" element={<MyPage />} /> // <Route path="/routine" element={<Routine />} /> + <Route path="/diet" element={<Diet />} /> </Routes> </div> <Footer /> diff --git a/front/src/pages/Home.css b/front/src/pages/Home.css new file mode 100644 index 0000000000000000000000000000000000000000..908cc2f58cbcc53ef7aa596725d56b5db678d5b5 --- /dev/null +++ b/front/src/pages/Home.css @@ -0,0 +1,45 @@ +.Home { + text-align: center; + position: relative; + width: 1325px; + height: 714px; +} + +.hero { + margin-bottom: 4rem; +} + +.hero h1 { + font-size: 55px; + font-weight: bold; + color: #B87EED; + margin: 0.5rem 0; + line-height: 0.6; +} + +.hero h2 { + font-size: 55px; + font-weight: bold; + color: #8C7DFF; + /* 라벤더 색 */ + margin: 0.5rem 0; +} + +.hero p { + font-size: 20px; + color: #a4d57b; + /* 연한 초록색 */ +} + +.content-grid { + display: flex; + justify-content: center; + gap: 3rem; +} + +.content-box { + width: 200px; + height: 200px; + background-color: #f5f5f5; + border-radius: 10px; +} \ No newline at end of file diff --git a/front/src/pages/Home.jsx b/front/src/pages/Home.jsx index f0e2638cacc1e4f886a72c4d30a0772a63a266eb..a993cb4273abbc2f04c31cd57cf576bda3b4a358 100644 --- a/front/src/pages/Home.jsx +++ b/front/src/pages/Home.jsx @@ -1,9 +1,22 @@ -import react from 'react'; +import React from "react"; +import "./Home.css"; -function Home(){ - return ( - <h1>Home</h1> - ); +function Home() { + return ( + <div className="Home"> + <header className="hero"> + <h1>Experience Fitness Your Way </h1> + <h2>with Custom Workout Routine</h2> + <p>Unlock a Personalized Training Plan Designed to Fit Your Goals, Schedule, and Lifestyle.</p> + </header> + <div className="content-grid"> + <div className="content-box"></div> + <div className="content-box"></div> + <div className="content-box"></div> + <div className="content-box"></div> + </div> + </div> + ); } export default Home; diff --git a/front/src/pages/diet/BmrDisplay.css b/front/src/pages/diet/BmrDisplay.css new file mode 100644 index 0000000000000000000000000000000000000000..832bb24a3c01b262f605cd7fe96d63b02fe5a9d9 --- /dev/null +++ b/front/src/pages/diet/BmrDisplay.css @@ -0,0 +1,59 @@ +.BmrDisplay-container h1 { + width: 100%; + height: 70px; + + font-weight: 700; + font-size: 30px; + line-height: 78px; + align-items: center; + text-align: center; + color: #000000; +} + +.BmrDisplay-container p { + + width: 150px; + height: 32px; + + font-weight: 600; + font-size: 20px; + line-height: 48px; + margin-bottom: 5px; + margin-top: -15px; + align-items: center; + text-align: center; + color: #000000; +} + +.BmrDisplay-container span { + width: 150px; + height: 52px; + font-weight: 700; + font-size: 40px; + text-align: center; + + color: #000000; +} + +.calorie-info { + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + gap: 40px; + + width: 320px; + height: 150px; +} + +.calorie-box { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + width: 140px; + height: 140px; + + background: #DFDFE4; +} \ No newline at end of file diff --git a/front/src/pages/diet/BmrDisplay.jsx b/front/src/pages/diet/BmrDisplay.jsx new file mode 100644 index 0000000000000000000000000000000000000000..345f48030e3f19717725cc60b8cf502382a423f3 --- /dev/null +++ b/front/src/pages/diet/BmrDisplay.jsx @@ -0,0 +1,55 @@ +import NutrientDisplay from './NutrientDisplay'; +import React, { useEffect, useState } from 'react'; +import './BmrDisplay.css'; + +function BmrDisplay({ user, diet }) { + const [bmr, setBmr] = useState(0); + const [achievement, setAchievement] = useState([]); + const [constants] = useState([1.2, 1.375, 1.555, 1.725, 1.9]); + + useEffect(() => { + for (let i = 0; i < diet.length; i++) { + if (diet[i]) { + setAchievement(diet[i].achievement); + break; + } + } + },[diet]); + + const calculateBmr = () => { + if (user.user_gender === 1) { + return 66.47 + (13.75 * achievement.weight) + (5 * achievement.height) - (6.76 * user.user_birth); + } else { + return 655.1 + (9.56 * achievement.weight) + (1.85 * achievement.height) - (4.68 * user.user_birth); + } + }; + + useEffect(() => { + const calculatedBmr = calculateBmr(); + setBmr(Math.round(calculatedBmr)); + }); + + return ( + <React.Fragment> + <div className='BmrDisplay-container'> + <h1>{user.user_name}님의 식단 가이드</h1> + <div className="calorie-info"> + <div className="calorie-box"> + <p>기초대사량</p> + <span>{bmr}</span> + <p>kcal</p> + </div> + <div className="calorie-box"> + <p>총 대사량</p> + <span>{Math.round(constants[achievement.in_current_week] * bmr)}</span> + <p>kcal</p> + + </div> + </div> + </div> + <NutrientDisplay user={user} bmr={Math.round(constants[achievement.in_current_week] * bmr)} /> + </React.Fragment> + ); +} + +export default BmrDisplay; \ No newline at end of file diff --git a/front/src/pages/diet/Diet.css b/front/src/pages/diet/Diet.css new file mode 100644 index 0000000000000000000000000000000000000000..e468aa27320ebdb7e0b2c85bf3c5aa214d5b465b --- /dev/null +++ b/front/src/pages/diet/Diet.css @@ -0,0 +1,37 @@ +#Diet-container { + display: flex; + flex-direction: row; + align-items: center; + padding: 0px; + gap: 10px; + + position: relative; + width: 1325px; + height: 714px; + color: #181818; + background: #181818; +} + +#SideBar { + display: flex; + flex-direction: column; + align-items: center; + gap: 30px; + + width: 380px; + height: 626px; + + background: #FFFFFF; + border-radius: 30px; +} + +#Contents { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + gap: 10px; + + width: 850px; + height: 626px; +} \ No newline at end of file diff --git a/front/src/pages/diet/Diet.jsx b/front/src/pages/diet/Diet.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7988b295537d7ab38a6220da8f08ba01896eb0d3 --- /dev/null +++ b/front/src/pages/diet/Diet.jsx @@ -0,0 +1,59 @@ +import React, { useEffect, useState } from 'react'; +import BmrDisplay from './BmrDisplay'; +import WeightBar from './WeightBar'; +import WeightChart from './WeightChart'; +import MealRecord from './MealRecord'; +import { getDietData, getUserData } from './api'; +import "./Diet.css" + +function App() { + const [user, setUser] = useState([]); + const [diet, setDiet] = useState([]); + const [update, setUpdate] = useState(false); + + useEffect(() => { + const fetchDietData = async () => { + try { + const startDate = new Date(); + const endDate = new Date(); + endDate.setDate(startDate.getDate() - 14); + + const data = await getDietData(null, startDate, endDate); + setDiet(data); + setUpdate(false); + } catch (err) { + alert(err); + } + }; + + fetchDietData(); + }, [update]); + + useEffect(() => { + const fetchUser = async () => { + try { + const userData = await getUserData(['user_name', 'user_gender', 'user_birth']); + setUser(userData); + } catch (err) { + alert(err); + } + }; + + fetchUser(); + }, []); + + return ( + <div id='Diet-container'> + <div id='SideBar'> + <BmrDisplay user={user} diet={diet} /> + <WeightBar diet={diet} /> + </div> + <div id='Contents'> + <WeightChart diet={diet} /> + <MealRecord diet={diet} onClick={setUpdate(true)} /> + </div> + </div> + ); +} + +export default App; diff --git a/front/src/pages/diet/MealRecord.css b/front/src/pages/diet/MealRecord.css new file mode 100644 index 0000000000000000000000000000000000000000..d3cd881ce279fbd058850967b01dabc34f01a395 --- /dev/null +++ b/front/src/pages/diet/MealRecord.css @@ -0,0 +1,231 @@ +.MealRecord-container { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 0px; + + width: 960px; + height: 275px; + + background: #FFFFFF; + border-radius: 20px; +} + +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + color: #000000; + background: rgba(0, 0, 0, 0.6); + /* 반투명 어두운 배경 */ + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; +} + +.modal { + background: white; + padding: 40px; + border-radius: 5px; + text-align: center; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.mini { + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + border-radius: 1px; + width: 500px; + height: 450px; + top: 5%; + left: 30%; + padding: 80px; + background: white; + border: 1px solid #ddd; + + ul { + height: 200px; + width: 500px; + padding: 0px; + overflow-y: auto; + border: 2px solid #ddd; + list-style-type: none; + + li { + text-align: left; + border-radius: 0px; + padding-left: 0px; + border-bottom: 1px solid #ddd; + } + } + + ul::-webkit-scrollbar { + display: none; + } + + input { + border-radius: 8px; + width: 300px; + } + + button { + border-radius: 8px; + } +} + +.datelist { + overflow-y: auto; + -ms-overflow-style: none; + box-sizing: border-box; + max-Height: 400px; + + display: flex; + flex-direction: column; + align-items: center; + + width: 170px; + height: 240px; + + background: #ffffff; +} + +.datelist::-webkit-scrollbar { + display: none; +} + +.list { + padding: 5px; +} + +.list li { + border-radius: 5px; + padding: 5px; + cursor: pointer; +} + +.record { + background-color: black; + border-left: 1px solid #ddd; + + display: flex; + flex-direction: column; + justify-content: center; + + width: 350px; + height: 270px; + + background: #FFFFFF; +} + +.record span { + font-size: 16px; + font-weight: 600; + width: 330px; + margin-left: 10px; + margin-top: -20px; + margin-bottom: 5px; +} + + +.record table { + margin-bottom: -20px; + width: 350px; + height: 220px; +} + +.record th, +td { + width: 100px; + font-size: 13px; + text-align: center; + border-bottom: none; +} + +.mealtime { + width: 70px; + border-radius: 5px; +} + +.input { + background-color: black; + border-left: 1px solid #ddd; + + display: flex; + flex-direction: column; + justify-content: center; + + width: 420px; + height: 270px; + background: #FFFFFF; +} + +.input span { + font-size: 16px; + font-weight: 600; + width: 340px; + margin-left: 10px; + margin-top: 8px; + margin-bottom: 5px; +} + +.detail { + overflow-y: auto; + -ms-overflow-style: none; + width: 430px; + height: 300px; + +} + +.detail::-webkit-scrollbar { + display: none; +} + +.detail table { + margin-bottom: -20px; + width: 430px; +} + +.detail th { + font-size: 13px; +} + +.detail td { + font-size: 13px; + padding: 0px; + line-height: 20px; + padding: 5px 1px; + width: 71px; + height: 20px; +} + + +.input tbody button { + width: 40px; + height: 25px; + margin: 0px; + font-size: 12px; + padding: 3px 4px; + border-radius: 4px; + color: red; + cursor: pointer; +} + +.mealregi { + color: #007bff; + cursor: pointer; + border-radius: 4px; + width: 90px; + height: 20px; + font-size: 14px; +} + +.under{ + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/front/src/pages/diet/MealRecord.jsx b/front/src/pages/diet/MealRecord.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b745528f549a2bf76bea70342c39eecaff3e691d --- /dev/null +++ b/front/src/pages/diet/MealRecord.jsx @@ -0,0 +1,354 @@ +import React, { useState, useEffect } from 'react'; +import './MealRecord.css' +import { createDiet, deleteDiet } 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); + 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 handleSelectDate = (date) => setSelectedDate(date); + 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}`; + }; + + useEffect(() => { + if (diet && Array.isArray(diet)) { + const mealsData = diet.map(item => item.meals).flat(); + setData(mealsData); + } + }, [diet]); + + + const handleAddFood = async () => { + if (!selectedFood || foodWeight <= 0) { + alert("음식을 선택하고 적절한 무게를 입력하세요."); + return; + } + + try { + await createDiet(selectedDate, selectedMeal, selectedFood.food_id, foodWeight); + setIsModalOpen(false); + setSearchQuery(""); + setFoodWeight(100); + setSelectedFood(null); + } catch (err) { + alert(err); + } + }; + + const handleDeleteFood = (meal, index) => { + setData((prevData) => { + const updatedMeal = prevData[selectedDate][meal].filter((_, idx) => idx !== index); + return { + ...prevData, + [selectedDate]: { ...prevData[selectedDate], [meal]: updatedMeal }, + }; + }); + }; + + const calculateTotal = (meal, nutrient) => { + return data[selectedDate][meal].reduce((acc, item) => acc + item[nutrient], 0); + }; + + const calculateTotalSum = (nutrient) => { + let total = 0; + ["breakfast", "lunch", "dinner"].map((meal, index) => ( + total += calculateTotal(meal, nutrient) + )) + return total; + }; + + const filteredFoods = foodOptions.filter((food) => + food.name.includes(searchQuery) + ); + + // 음식 선택 + const handleSelectFood = (food) => { + setSelectedFood(food); + }; + + + return ( + <div className='MealRecord-container'> + <div className='datelist' style={{ overflowY: 'scroll' }}> + <ul className='list'> + {data && Object.keys(data).map((date) => ( + <li + key={date} + onClick={() => handleSelectDate(date)} + style={{ + backgroundColor: date === selectedDate ? '#eee' : 'transparent', + }} + > + {getTodayDate(data.date)} + </li> + ))} + </ul> + </div> + <div className='record'> + <span>{getTodayDate(selectedDate)} 식사 기록</span> + <table> + <thead> + <tr> + <th>식사시간</th> + <th>총 칼로리</th> + <th>탄수화물</th> + <th>단백질</th> + <th>지방</th> + </tr> + </thead> + {selectedDate && <tbody> + {["breakfast", "lunch", "dinner"].map((meal, index) => ( + <tr key={index} onClick={() => handleSelectMeal(meal)} style={{ cursor: 'pointer' }}> + <td className='mealtime' style={{ + backgroundColor: meal === selectedMeal ? '#eee' : 'transparent', + }}><strong>{meal === "breakfast" ? "아침" : meal === "lunch" ? "점심" : "저녁"}</strong></td> + <td>{calculateTotal(meal, 'calories').toFixed(1)} kcal</td> + <td>{calculateTotal(meal, 'carbs').toFixed(1)} g</td> + <td>{calculateTotal(meal, 'protein').toFixed(1)} g</td> + <td>{calculateTotal(meal, 'fat').toFixed(1)} g</td> + </tr> + ))} + <tr> + <td><strong>총합</strong></td> + <td> {calculateTotalSum('calories').toFixed(1)} kcal</td> + <td> {calculateTotalSum('carbs').toFixed(1)} g</td> + <td> {calculateTotalSum('protein').toFixed(1)} g</td> + <td> {calculateTotalSum('fat').toFixed(1)} g</td> + </tr> + </tbody> + } + </table> + </div> + + {(selectedDate && selectedMeal) && ( + <div className='input'> + <span>기록 하기</span> + <div className='detail'> + <table className=''> + <thead> + <tr> + <th>음식</th> + <th>칼로리</th> + <th>탄수화물</th> + <th>단백질</th> + <th>지방</th> + </tr> + </thead> + <tbody> + {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> + <button onClick={() => handleDeleteFood(selectedMeal, index)}>삭제</button> + </tr> + ))} + </tbody> + </table> + <button className='mealregi' onClick={() => setIsModalOpen(true)}>+ 음식 추가</button> + </div> + </div> + )} + {isModalOpen && ( + <div className="modal-backdrop"> + <div className="modal"> + <div className='mini'> + <h4>음식 검색</h4> + <input + type="text" + placeholder="음식 이름" + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + /> + <ul className="list"> + {filteredFoods.map((food, index) => ( + <li key={index} onClick={() => handleSelectFood(food)} style={{ cursor: 'pointer' }}> + {food.name} + </li> + ))} + </ul> + <div className='under'> + {selectedFood && ( + <div> + <p>선택: {selectedFood.name}</p> + <input + type="number" + placeholder="무게 (g)" + value={foodWeight} + onChange={(e) => setFoodWeight(Number(e.target.value))} + /> g + </div> + )} + <div> + {selectedFood && ( + <button onClick={handleAddFood}>추가</button> + )} + <button onClick={() => setIsModalOpen(false)}>닫기</button> + </div> + </div> + </div> + </div> + </div> + )} + + </div> + ); +} + +export default MealRecord; diff --git a/front/src/pages/diet/NutrientDisplay.css b/front/src/pages/diet/NutrientDisplay.css new file mode 100644 index 0000000000000000000000000000000000000000..85311ee54979e526a01adcd7e39d0895690409e5 --- /dev/null +++ b/front/src/pages/diet/NutrientDisplay.css @@ -0,0 +1,104 @@ +.NutrientDisplay-container { + display: flex; + flex-direction: column; + align-items: left; + padding: 0px; + gap: 15px; + margin-top: -35px; + margin-left: 10px; + width: 370px; + height: 130px; +} + +.NutrientDisplay-container h1 { + font-weight: 700; + font-size: 25px; + margin-bottom: -5px; +} + +.nutrient-goals { + display: flex; + flex-direction: row; + align-items: center; + padding: 0px 30px; + gap: 40px; + + width: 300px; + height: 90px; + background: #DFDFE4; + border-radius: 10px; + +} + +.side { + width: 100px; + + font-family: 'Roboto'; + font-style: normal; + font-weight: 700; + font-size: 11px; + line-height: 20px; + margin-left: -20px; + display: flex; + flex-direction: column; + color: #000000; + +} + +.side p { + margin: 0px; +} + +.nutrients { + display: flex; + flex-direction: row; + align-items: flex-end; + padding: 0px; + gap: 0px; + width: 210px; + height: 37px; +} + +.nutrients span { + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 0px; + margin-top: -20px; + margin-left: -7px; + width: 80px; + height: 36px; +} + +.nutrients div div { + display: flex; + flex-direction: row; +} + +.nutrients div { + margin-bottom: -3px; +} + +.carbs { + color: #E29165; + font-size: 13px; + margin-bottom: -4px; +} + +.protein { + color: #EA5858; + font-size: 13px; + margin-bottom: -4px; +} + +.fat { + color: #e579de; + font-size: 13px; + margin-bottom: -4px; +} + +.num { + font-size: 30px; + font-weight: 600; + margin-right: -28px; +} \ No newline at end of file diff --git a/front/src/pages/diet/NutrientDisplay.jsx b/front/src/pages/diet/NutrientDisplay.jsx new file mode 100644 index 0000000000000000000000000000000000000000..18008efff232ef11118ab212bcd9a4b956bcbd58 --- /dev/null +++ b/front/src/pages/diet/NutrientDisplay.jsx @@ -0,0 +1,22 @@ +import './NutrientDisplay.css'; + +function NutrientDisplay({ bmr }) { + return ( + <div className='NutrientDisplay-container'> + <h1>식단 가이드</h1> + <div className="nutrient-goals"> + <div className="side"> + <p><strong>끼니당</strong> 영양소</p> + <p> 섭취목표</p> + </div> + <div className="nutrients"> + <div><span className="carbs">탄수화물</span> <div><span className="num">{Math.round((bmr*0.4)/12)}</span>g</div></div> + <div><span className="protein">단백질</span> <div><span className="num">{Math.round((bmr*0.2)/12)}</span>g</div></div> + <div><span className="fat">지방</span> <div><span className="num">{Math.round((bmr*0.2)/27)}</span>g</div></div> + </div> + </div> + </div> + ); +} + +export default NutrientDisplay; \ No newline at end of file diff --git a/front/src/pages/diet/WeightBar.css b/front/src/pages/diet/WeightBar.css new file mode 100644 index 0000000000000000000000000000000000000000..3498ed778fa5f41fce0d47490158392d49413c40 --- /dev/null +++ b/front/src/pages/diet/WeightBar.css @@ -0,0 +1,75 @@ +.WeightBar-container { + display: flex; + flex-direction: column; + align-items: left; + padding: 0px; + gap: 15px; + margin-top: -35px; + margin-left: 10px; + + width: 370px; + height: 130px; +} + +.WeightBar-container h3 { + font-weight: 700; + font-size: 25px; + margin-bottom: -5px; +} + +.weight-bar { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0px; + + width: 360px; + height: 65px; + background: #DFDFE4; + border-radius: 10px; + +} + +.bar { + + height: 15px; + width: 300px; + border-radius: 10px; + background-color: #8C7DFF; +} + +.bar-progress { + height: 100%; + height: 15px; + width: 10px; + background-color: #B87EED; + border-radius: 10px; +} + +.suchi { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 0px; + gap: 240px; + font-size: 13px; + font-weight: 600; + width: 300px; + height: 20px; + +} + +.per { + width: 300px; + height: 20px; + font-family: 'Roboto'; + font-style: normal; + font-weight: 600; + font-size: 13px; + line-height: 20px; + align-items: right; + text-align: right; + color: #000000; +} \ No newline at end of file diff --git a/front/src/pages/diet/WeightBar.jsx b/front/src/pages/diet/WeightBar.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ad320d27e22e5bfcd86c4e6113188c0129750215 --- /dev/null +++ b/front/src/pages/diet/WeightBar.jsx @@ -0,0 +1,42 @@ +import React, { useState, useEffect } from 'react'; +import './WeightBar.css' + +function WeightBar({ diet }) { + const [percentage, setPercentage] = useState(0); + const [goalweight, setGoalWeight] = useState(0); + const [startweight, setStartWeight] = useState(0); + + useEffect(() => { + let goal_weight = -1; + let current_weight = -1; + let start_weight; + if (diet&&diet.array) { + diet.array.forEach(element => { + if (element.achievement.weight) start_weight = element.achievement.weight; + if (goal_weight == -1 && element.achievement.goal_weight) goal_weight = element.achievement.goal_weight; + if (current_weight == -1 && element.achievement.weight) current_weight = element.achievement.weight + setPercentage(((start_weight - current_weight) / (start_weight - goal_weight) * 100)); + }); + setGoalWeight(goal_weight); + setStartWeight(start_weight); + } + }, [diet]); + + return ( + <div className='WeightBar-container'> + <h3>체중 목표</h3> + <div className="weight-bar"> + <div className='per'>{percentage}%</div> + <div className="bar"> + <div className="bar-progress" style={{ width: `${percentage}%` }}></div> + </div> + <div className='suchi'> + <span>{startweight}kg</span> + <span>{goalweight}kg</span> + </div> + </div> + </div> + ); +} + +export default WeightBar; \ No newline at end of file diff --git a/front/src/pages/diet/WeightChart.css b/front/src/pages/diet/WeightChart.css new file mode 100644 index 0000000000000000000000000000000000000000..c2a8cacadfbd0b9be7b5b8bab0549b0646cf179a --- /dev/null +++ b/front/src/pages/diet/WeightChart.css @@ -0,0 +1,20 @@ +.WeightChart-container{ + display: flex; + align-items: center; + padding: 30px; + text-align: center; + width: 900px; + height: 280px; + border-radius: 20px; + color: #333; + background: #FFFFFF; +} + +.graph { + width: 100%; + height: 100%; + background: #FFFFFF; + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/front/src/pages/diet/WeightChart.jsx b/front/src/pages/diet/WeightChart.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8e8223829465cfb8b266f40d5b511cd01b32fc25 --- /dev/null +++ b/front/src/pages/diet/WeightChart.jsx @@ -0,0 +1,133 @@ +import { Line } from "react-chartjs-2"; +import React, { useState, useEffect } from 'react'; +import './WeightChart.css' + +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from "chart.js"; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +const options = { + responsive: true, + maintainAspectRatio: false, + interaction: { + intersect: false, + }, + scales: { + x: { + title: { + display: true, + text: "날짜 (YYYY/MM/DD)", + }, + grid: { + display: false, + }, + ticks: { + font: { + size: 9, // 가로 축 폰트 크기 조정 + }, + }, + }, + y: { + title: { + display: true, + text: "체중 (kg)", + }, + ticks: { + font: { + size: 9, // 가로 축 폰트 크기 조정 + }, + }, + } + }, + plugins: { + legend: { + display: false, + }, + }, + tooltip: { + callbacks: { + label: (context) => { + const label = context.dataset.label || ''; + const value = context.raw; + return `${label}: ${value} kg`; + }, + }, + }, +}; + + + +function WeightChart(diet) { + const [chartData, setChartData] = useState({ + labels: [], + datasets: [ + { + label: "Weight", + data: [], + backgroundColor: "#FFCA29", + borderColor: "#FFCA29", + }, + ], + }); + const [weight, setWeight] = useState(0); + + 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 getWeight = (w) => { + if(w!=undefined){ + setWeight(w); + return w; + }else return false; + } + + useEffect(() => { + if (diet && diet.array) { + const labels = diet.array.map(entry => getTodayDate(entry.date)); // 날짜 데이터 + const weights = diet.array.map(entry => getWeight(entry.achievement?.weight) || weight); // 체중 데이터 + 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} /> + </div> + </div> + ); +} + +export default WeightChart; diff --git a/front/src/pages/diet/api.js b/front/src/pages/diet/api.js new file mode 100644 index 0000000000000000000000000000000000000000..2805e94a53e796894dab92c728f480185c41e0e7 --- /dev/null +++ b/front/src/pages/diet/api.js @@ -0,0 +1,64 @@ +const URL = '/api/diet'; + +async function fetchWithOptions(url, options) { + try { + const response = await fetch(url, options); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `HTTP 오류! 상태 코드: ${response.status}`); + } + return await response.json(); + } catch (error) { + console.error('API 요청 중 에러 발생:', error.message); + throw error; + } +} + +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); + + return await fetchWithOptions(`${URL}?${queryParams.toString()}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); +}; + +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()}`), { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }; +} + +const createDiet = async ({ date, mealtime, food_id, grams }) => { + return await fetchWithOptions(URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ date, mealtime, food_id, grams }), + }); +}; + +const deleteDiet = async ({ date, mealtime, food_id }) => { + return await fetchWithOptions(URL, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ date, mealtime, food_id }), + }); +}; + +export { getDietData, createDiet, deleteDiet, getUserData }; \ No newline at end of file diff --git a/front/src/pages/index.css b/front/src/pages/index.css index 4eb09377d72022b0059de938f9eda97e6a6c950f..7463d62abd70769b31ad17e5e747922f88de2cde 100644 --- a/front/src/pages/index.css +++ b/front/src/pages/index.css @@ -124,4 +124,4 @@ input::placeholder{ color: white; width: 100%; margin: 10px 0; -} \ No newline at end of file +} diff --git a/up.sh b/up.sh index b7cd82d6516e0ffc9685ff90b4a1fc19303bc1a3..7984744cb373009416b636d3eab2d9e2e8d75744 100755 --- a/up.sh +++ b/up.sh @@ -1 +1 @@ -docker-compose down -v; docker-compose up -d --build \ No newline at end of file +docker-compose down -v; docker-compose up -d --build