React + MySQL ๊ธฐ๋ฐ์ ๋ก์ปฌ ํตํฉ ๊ด๋ฆฌ ์น ์์คํ
์ด๋/์๋จ ๋ฃจํด ๊ฐ์ด๋์ ๋ฉํ ๋ง ๋งค์นญ ๊ธฐ๋ฅ์ ํฌํจํ ํ์ ์ค์ฌ ํฌ์ค์ฅ ํ๋ซํผ
- ์ด๋ ๊ธฐ๋ก: 5ํ๋ง๋ค 100P ์๋ ์ง๊ธ
- ์๋จ ๊ธฐ๋ก: 3ํ๋ง๋ค 50P ์๋ ์ง๊ธ
- ์ถ์ ์ฒดํฌ: 10ํ๋ง๋ค 200P ์๋ ์ง๊ธ
- ๋ชฉํ ์ค์ : 2๊ฐ๋ง๋ค 80P ์๋ ์ง๊ธ
- SQL ํธ๋ฆฌ๊ฑฐ์ ๋์ผํ ๋ก์ง์ ํ๋ก ํธ์๋์์ ์๋ฒฝ ๊ตฌํ
achievement_id์๋ ์ฐ๊ฒฐ ๋ฐ Toast ์๋ฆผ
- Class ํ์ด์ง์ ํ์ฌ ์ด์ฉ ์ค์ธ ํ์ ์ค์๊ฐ ํ์
- SQL VIEW (
view_current_users) ์๋ฎฌ๋ ์ด์ - Attendance ํ ์ด๋ธ ๊ธฐ๋ฐ ํผ์ก๋ ๊ณ์ฐ
- ํ์ฌ ์ธ์ / ๋จ์ ์๋ฆฌ ์๊ฐํ
- MyPage์ ์ถ์ ์ฒดํฌ ๋ฒํผ ์ถ๊ฐ
- ์ค๋ณต ์ถ์ ๋ฐฉ์ง (ํ๋ฃจ 1ํ)
- ์ถ์ 10ํ๋ง๋ค ์๋ ํฌ์ธํธ ์ง๊ธ
- Toast ์๋ฆผ์ผ๋ก ์ฆ๊ฐ ํผ๋๋ฐฑ
- ํฌ์ธํธ ์ง๊ธ ์กฐ๊ฑด ์์ธ ์๋ด
- ํฌ์ค์ฅ ์ด์ฉ ์๋ด
- ๋ฉํ ๋ง ์์คํ ์ค๋ช
- ๋คํฌ๋ชจ๋ ์ง์
- Goal ์ปดํฌ๋ํธ์ ๋ฐฐ์น ๋ณด์ ๋ก์ง ์ถ๊ฐ
- ๋ชฉํ 2๊ฐ ์ค์ ์ 80P ์๋ ์ง๊ธ
- localStorage ๊ธฐ๋ฐ ๋ฐ์ดํฐ ์์์ฑ
์ด ํ๋ก์ ํธ๋ ํฌ์ค์ฅ ํ์ ํตํฉ ๊ด๋ฆฌ ์์คํ
์ผ๋ก,
ํ์(Member)์ ์ค์ฌ์ผ๋ก ์ด๋ ๋ฃจํด, ์๋จ, ๋ฉํ ๋ง, ์ธ์ผํฐ๋ธ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ํตํฉ ๊ด๋ฆฌํ ์ ์๋๋ก ์ค๊ณ๋์์ต๋๋ค.
ํ๋ก ํธ์๋๋ React ๊ธฐ๋ฐ์ด๋ฉฐ,
๋ฐ์ดํฐ๋ ๋ก์ปฌ MySQL ์ฐ๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌ๋ฉ๋๋ค.
๋ฐฑ์๋ API ์๋ฒ๋ ๋ณ๋๋ก ๋์ง ์๊ณ , ํ๋ก ํธ ๋ด๋ถ์์ ์ง์ DB์ ์ฐ๊ฒฐ๋ฉ๋๋ค.
| ํญ๋ชฉ | ๋ด์ฉ |
|---|---|
| Frontend | React 18 (Create React App) |
| Styling | TailwindCSS, Framer Motion |
| Database | MySQL (๋ก์ปฌ ์ฐ๊ฒฐ ๋ฐฉ์) |
| State Management | Context API (MatchContext.jsx) |
| UI Framework | Responsive Design (TailwindCSS ๊ธฐ๋ฐ) |
| Animation | Framer Motion |
| Package Manager | npm |
# 1. ํจํค์ง ์ค์น
npm install
# 2. ๊ฐ๋ฐ ์๋ฒ ์คํ
npm start
# 3. ๋ธ๋ผ์ฐ์ ์์ ์ด๊ธฐ
http://localhost:3000
# ๐ ํ๋ก์ ํธ ๊ตฌ์กฐ
my-app/
โโโ public/
โ โโโ index.html
โ โโโ favicon.ico
โ โโโ logo192.png / logo512.png
โ โโโ manifest.json / robots.txt
โ โโโ videos/
โ โโโ gym.mp4 # ๋ฉ์ธ ์๊ฐ ์์
โ
โโโ src/
โ โโโ assets/
โ โ โโโ mentoring/
โ โ โโโ mentor.png # ๊ธฐ๋ณธ ๋ฉํ ์ด๋ฏธ์ง
โ โ โโโ mentee.png # ๊ธฐ๋ณธ ๋ฉํฐ ์ด๋ฏธ์ง
โ โ โโโ defaultProfile.png # ๊ธฐ๋ณธ ํ๋กํ ์ด๋ฏธ์ง
โ โ
โ โโโ components/
โ โ โโโ Navbar.jsx # ์๋จ ๋ค๋น๊ฒ์ด์
(๊ณตํต)
โ โ
โ โโโ context/
โ โ โโโ ThemeContext.jsx # ๐ ์ ์ญ ๋คํฌ๋ชจ๋ ํ
๋ง ๊ด๋ฆฌ
โ โ
โ โโโ pages/
โ โ โโโ guide/ # ๐ฅ ํฌ์ค ๊ฐ์ด๋ ๋ชจ๋
โ โ โ โโโ Guide.jsx # ๊ฐ์ด๋ ๋ฉ์ธ ํ์ด์ง
โ โ โ โโโ DietTab.jsx # ์๋จ ๊ฐ์ด๋ ํญ
โ โ โ โโโ RoutineTab.jsx # ์ด๋ ๋ฃจํด ํญ
โ โ โ โโโ PostCard.jsx # ๊ฐ์ด๋ ๊ฒ์๊ธ ์นด๋
โ โ โ โโโ NewPostModal.jsx # ๊ฐ์ด๋ ๊ฒ์๊ธ ์์ฑ ๋ชจ๋ฌ
โ โ โ
โ โ โโโ mentoring/ # ๐ค ๋ฉํ ๋ง ๋ชจ๋
โ โ โ โโโ Mentoring.jsx # ๋ฉํ ๋ง ๋ฉ์ธ ํญ (๋คํฌ๋ชจ๋/ํ๋กํ)
โ โ โ โโโ MentorRecruitTab.jsx# ๋ฉํ ๋ชจ์ง๊ธ ์์ฑ + ๋ฉํฐ ์ ์ฒญ ๊ด๋ฆฌ
โ โ โ โโโ MenteeRecruitTab.jsx# ๋ฉํฐ ๋ชจ์ง๊ธ ์์ฑ + ๋ฉํ ์ ์ฒญ ๊ด๋ฆฌ
โ โ โ โโโ MatchContext.jsx # ๋ฉํ /๋ฉํฐ/๋งค์นญ ์ํ ๊ด๋ฆฌ
โ โ โ โโโ MatchModal.jsx # ์ ์ฒญยท์๋ฝยทํ๊ธฐ ๋ชจ๋ฌ์ฐฝ
โ โ โ โโโ MentorCard.jsx # ๋ฉํ ๋ชฉ๋ก ์นด๋ ์ปดํฌ๋ํธ
โ โ โ โโโ Home.jsx # (์๋น) ๋ฉํ ๋ง ์๊ฐ ํ์ด์ง
โ โ โ
โ โ โโโ Home.jsx # ๋ฉ์ธ ํ ํ๋ฉด
โ โ โโโ MyPage.jsx # ๐ง ๊ฐ์ธ ์ ๋ณด ๋ฐ ๋งค์นญ ํํฉ (๋คํฌ๋ชจ๋ ์ง์)
โ โ โโโ Goal.jsx # ๐ฏ ๋ชฉํ ์ค์ ๋ฐ ์ด๋ ๋ฃจํด
โ โ โโโ Class.jsx # ๐ ๊ต์์์
์๊ฐํ ๋ฐ ํฌ์ค์ฅ ๊ฐ์ฉ์ฑ ํ์ธ
โ โ โโโ Notice.jsx # ๊ณต์ง์ฌํญ
โ โ โโโ App.css # ์ ์ญ ์คํ์ผ
โ โ
โ โโโ App.js # ๋ผ์ฐํ
๋ฐ ์ ์ญ Provider ์ค์
โ โโโ index.js # ReactDOM ์ง์
์
โ โโโ index.css # ๊ณตํต ์คํ์ผ
โ โโโ reportWebVitals.js
โ โโโ setupTests.js
โ
โโโ server/ # ๐ง ๋ฐฑ์๋ ์๋ฒ (Express.js + MySQL)
โ โโโ server.js # Express ์๋ฒ ๋ฐ REST API
โ โโโ sql/
โ โโโ HS_Health.sql # ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง
โ โโโ insert_class_data.sql # ๊ต์์์
๋๋ฏธ ๋ฐ์ดํฐ
โ โโโ README.md # SQL ์คํ ๊ฐ์ด๋
โ
โโโ package.json
โโโ tailwind.config.js
โโโ postcss.config.js
โโโ .gitignore
โโโ README.md
---
## ๐ ์ต์ ์
๋ฐ์ดํธ (gyu ๋ธ๋์น)
### ๐ **Class ํ์ด์ง - ๊ต์์์
์๊ฐํ ๋ฐ ํฌ์ค์ฅ ๊ฐ์ฉ์ฑ ํ์ธ**
ํ๊ต ๊ต์์์
์๊ฐํ๋ฅผ ํ์ธํ๊ณ , ํ์ฌ ํฌ์ค์ฅ ์ด์ฉ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ์ค์๊ฐ์ผ๋ก ๋ณด์ฌ์ฃผ๋ ํ์ด์ง์
๋๋ค.
#### ์ฃผ์ ๊ธฐ๋ฅ:
- **์ค์๊ฐ ํฌ์ค์ฅ ๊ฐ์ฉ์ฑ ์ฒดํฌ**: ํ์ฌ ๊ต์์์
์งํ ์ค์ด๋ฉด ์ธ์ ์ ํ ํ์ (์ฝ 30๋ช
)
- **ํ์ฌ ์งํ ์ค์ธ ์์
ํ์**: ์ง๊ธ ์งํ ์ค์ธ ๊ต์์์
์ ๋ณด ์ค์๊ฐ ํ์
- **๋ค์ ์์
๋ฏธ๋ฆฌ๋ณด๊ธฐ**: ๋ค์ ์์ ๋ ๊ต์์์
์๊ฐ ์๋ด
- **์ค์๊ฐ ์๊ณ**: 1๋ถ๋ง๋ค ์๋ ์
๋ฐ์ดํธ๋๋ ํ์ฌ ์๊ฐ
- **๋คํฌ๋ชจ๋ ์ง์**: ํ์ด์ง๋ณ ๋
๋ฆฝ์ ์ธ ๋คํฌ๋ชจ๋ ํ ๊ธ
- **๋ฐ์ํ ๋์์ธ**: Framer Motion ์ ๋๋ฉ์ด์
๊ณผ ํจ๊ป ๋ถ๋๋ฌ์ด UI/UX
#### ๊ธฐ์ ์คํ:
```javascript
// ์ค์๊ฐ ์๊ฐ ์
๋ฐ์ดํธ
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 60000); // 1๋ถ๋ง๋ค ์
๋ฐ์ดํธ
return () => clearInterval(timer);
}, []);
// ํ์ฌ ์งํ ์ค์ธ ์์
ํ์ธ
const getCurrentClass = () => {
const now = currentTime;
const currentDay = ['์ผ', '์', 'ํ', '์', '๋ชฉ', '๊ธ', 'ํ '][now.getDay()];
const currentTimeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:00`;
for (const cls of classes) {
const schedules = classSchedules[cls.class_id] || [];
for (const schedule of schedules) {
if (schedule.day_of_week === currentDay) {
if (currentTimeStr >= schedule.start_time && currentTimeStr < schedule.end_time) {
return { class: cls, schedule };
}
}
}
}
return null;
};- Context API๋ฅผ ํ์ฉํ ์ ์ญ ๋คํฌ๋ชจ๋ ์ํ ๊ด๋ฆฌ
- localStorage์ ํ ๋ง ์ค์ ์ ์ฅ์ผ๋ก ํ์ด์ง ์๋ก๊ณ ์นจ ์์๋ ์ ์ง
- HTML ๋ฃจํธ ์์์
darkํด๋์ค ์๋ ์ถ๊ฐ/์ ๊ฑฐ
๊ฐ ํ์ด์ง๋ง๋ค ๋ ๋ฆฝ์ ์ธ ๋คํฌ๋ชจ๋ ํ ๊ธ ๋ฒํผ์ ์ ๊ณตํฉ๋๋ค:
- Class ํ์ด์ง:
classPageThemelocalStorage ํค ์ฌ์ฉ - MyPage:
myPageThemelocalStorage ํค ์ฌ์ฉ - Mentoring ํ์ด์ง: ๊ธฐ์กด ๋ ๋ฆฝ ๋คํฌ๋ชจ๋ ์ ์ง
- Guide ํ์ด์ง: ๊ธฐ์กด ๋ ๋ฆฝ ๋คํฌ๋ชจ๋ ์ ์ง
// ํ์ด์ง๋ณ ๋
๋ฆฝ ๋คํฌ๋ชจ๋ ์์ (Class.jsx)
const [isDark, setIsDark] = useState(() => {
const saved = localStorage.getItem('classPageTheme');
return saved ? saved === 'dark' : true;
});
useEffect(() => {
localStorage.setItem('classPageTheme', isDark ? 'dark' : 'light');
}, [isDark]);- ๋คํฌ๋ชจ๋ ํ ๊ธ: ์ฐ์ธก ์๋จ์ ๋ ๋ฆฝ์ ์ธ ๋คํฌ๋ชจ๋ ์ค์์น ์ถ๊ฐ
- Goal ์ปดํฌ๋ํธ ํตํฉ: ๋ชฉํ ์ค์ ๋ฐ ์ด๋ ๋ฃจํด ๊ด๋ฆฌ ๊ธฐ๋ฅ ํตํฉ
- ๋คํฌ๋ชจ๋ ํ ๋ง ์ ๋ฌ: MyPage์ isDark ์ํ๋ฅผ Goal ์ปดํฌ๋ํธ๋ก ์ ๋ฌํ์ฌ ์ผ๊ด๋ ํ ๋ง ์ ์ง
- ๋ถ๋ชจ(MyPage)๋ก๋ถํฐ
isDarkprop์ ๋ฐ์ ํ ๋ง ๋๊ธฐํ - ์ด๋ ๋ชฉํ ์ค์ ๋ฐ ํธ๋ํน ๊ธฐ๋ฅ
- ๋คํฌ๋ชจ๋ ์คํ์ผ ์ง์
- ์๋ ๋๋ฏธ ๋ฐ์ดํฐ ์ด๊ธฐํ: ์๋ฒ ์์ ์ ๊ต์์์ ๋ฐ์ดํฐ ์๋ ์ฝ์
- ๊ต์์์
API ์๋ํฌ์ธํธ:
GET /api/classes: ๋ชจ๋ ๊ต์์์ ์กฐํGET /api/class-schedules/:classId: ํน์ ์์ ์ ์๊ฐํ ์กฐํPOST /api/classes: ์ ๊ต์์์ ์ถ๊ฐPUT /api/classes/:id: ์์ ์ ๋ณด ์์ DELETE /api/classes/:id: ์์ ์ญ์
- Class: ๊ต์์์ ์ ๋ณด (๊ณผ๋ชฉ๋ช , ๋ด๋น๊ต์, ํ์ ๋ฑ)
- Class_Schedule: ์์ ์๊ฐํ (์์ผ, ์์/์ข ๋ฃ ์๊ฐ)
{
"dependencies": {
"express": "^4.21.2",
"mysql2": "^3.11.5",
"cors": "^2.8.5",
"react-hot-toast": "^2.4.1"
}
}- express: REST API ์๋ฒ
- mysql2: MySQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ฐ๊ฒฐ
- cors: CORS ๋ฏธ๋ค์จ์ด
- react-hot-toast: ํ ์คํธ ์๋ฆผ UI
- Framer Motion ์ ๋๋ฉ์ด์ : ๋ถ๋๋ฌ์ด ํ์ด์ง ์ ํ ๋ฐ ์นด๋ ์ ๋๋ฉ์ด์
- ๋ฐ์ํ ๊ทธ๋ผ๋์ธํธ ๋ฐฐ๊ฒฝ: ๋คํฌ/๋ผ์ดํธ ๋ชจ๋๋ณ ์ต์ ํ๋ ๋ฐฐ๊ฒฝ์
- ์ค์๊ฐ ์ํ ํ์: ํ์ค ์ ๋๋ฉ์ด์ ์ผ๋ก ํ์ฌ ์ํ ๊ฐ์กฐ
- ์ผ๊ด๋ ๋์์ธ ์์คํ : Tailwind CSS ๊ธฐ๋ฐ ํต์ผ๋ ์คํ์ผ
ํ์์ ์ด๋/์๋จ/๊ฑด๊ฐ ๊ธฐ๋ก์ ์๊ฐํํ๊ณ ๊ด๋ฆฌํ ์ ์๋ ํตํฉ ๋์๋ณด๋๋ก ๊ฐ์ ๋์์ต๋๋ค.
- Line Chart๋ก ์ฒด์ค ๋ณํ ์ถ์ด ์๊ฐํ
- ๋ถ๋๋ฌ์ด ๊ณก์ ๊ณผ ๊ทธ๋ผ๋ฐ์ด์ ๋ฐฐ๊ฒฝ
- ๋ง์ฐ์ค ํธ๋ฒ ์ ์ ํํ ๊ฐ ํ์
- ๋คํฌ๋ชจ๋ ์ง์ ๋ฐ ๋ฐ์ํ ๋์์ธ
// WeightChart.jsx - Chart.js ์ค์
const data = {
labels: recentRecords.map((record, idx) => {
if (idx === 0) return '์์';
if (idx === recentRecords.length - 1) return 'ํ์ฌ';
return '';
}),
datasets: [{
label: '์ฒด์ค (kg)',
data: recentRecords.map(r => r.weight_kg),
borderColor: 'rgb(34, 197, 94)',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
fill: true,
tension: 0.4
}]
};- ์ค๋ฅธ์ชฝ ํ๋จ ๊ณ ์ ๋ฒํผ (๐ฏ)์ผ๋ก ์ ๊ทผ
- ์ค๋ฅธ์ชฝ์์ ์ฌ๋ผ์ด๋๋๋ ์ฌ์ด๋ ํจ๋
- Spring ์ ๋๋ฉ์ด์ ์ผ๋ก ๋ถ๋๋ฌ์ด ์ ํ
- ๋ฐฐ๊ฒฝ ์ค๋ฒ๋ ์ด๋ก ํฌ์ปค์ค ๊ฐ์กฐ
- ๋ชจ๋ฐ์ผ: ์ ์ฒด ํ๋ฉด / ๋ฐ์คํฌํฑ: 600-700px ๋๋น
// ์ฌ์ด๋ ํจ๋ ์ ๋๋ฉ์ด์
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
>
<Goal isDark={isDark} />
</motion.div>6๊ฐ์ ์ธ์ฌ์ดํธ ์นด๋๋ก ๊ตฌ์ฑ๋ ๋์๋ณด๋:
- ๐ฅ ์ต๊ทผ ํ๋ ์์ฝ: ์์ผ๋ณ ์ด๋ ๋น๋ ์ฐจํธ
- โค๏ธ ๋ง์ด ์ํํ ์ด๋ TOP 3: ๊ฐ์ฅ ๋ง์ด ํ ์ด๋ ์์
- ๐ ๋ง์ด ์ฑ์ฅํ ์ด๋ TOP 3: ์ฑ์ฅ๋ฅ ๊ธฐ์ค ์์ (307.8% ์ฆ๊ฐ ๋ฑ)
- ๐ ๋ถ์๋ณ ์ด๋ ๋ถ์: ๋๋ ์ฐจํธ๋ก ์ด๋ ๋ถ์ ๋ถํฌ ์๊ฐํ
- ๐ช ์ด๋ฒ๋ฌ ์์ฝ: ๊ฐ์ธํ๋ ํ์ดํ ๋ฐ ์ฑ์ทจ ์์ฝ
- โ๏ธ ์ฒด์ค ๋ณํ: Chart.js ๋ผ์ธ ์ฐจํธ๋ก ์ถ์ด ํ์
๋ ์ง ํด๋ฆญ ์ ํด๋น ๋ ์ง์ ๋ชจ๋ ๊ธฐ๋ก์ ์นด๋ ํ์์ผ๋ก ํ์:
- ์ด๋ ๊ธฐ๋ก: ํ๋์ ๊ทธ๋ผ๋ฐ์ด์ , ์๊ฐ ํ์, ๋ณด์ ํ๋ ์ฌ๋ถ
- ์๋จ ๊ธฐ๋ก: ์ด๋ก์ ๊ทธ๋ผ๋ฐ์ด์ , ์์ฌ ์๊ฐ๋ณ ๋ฐฐ์ง (์์นจ/์ ์ฌ/์ ๋ /๊ฐ์)
- ๊ฑด๊ฐ ๊ธฐ๋ก: ๋นจ๊ฐ์ ๊ทธ๋ผ๋ฐ์ด์ , ์ฒด์ค/๊ทผ์ก๋/์ฒด์ง๋ฐฉ/BMI ํ์
- ๊ฐ ์นด๋์ ํธ๋ฒ ํจ๊ณผ ๋ฐ ์ ๋๋ฉ์ด์ ์ ์ฉ
- ๋น ์ํ UI ๊ฐ์ (์ด๋ชจ์ง + ์๋ด ๋ฉ์์ง)
// ์์ฌ ์๊ฐ๋ณ ๋ฐฐ์ง ์์
const mealColors = {
'์์นจ': 'bg-yellow-500/20 text-yellow-400',
'์ ์ฌ': 'bg-orange-500/20 text-orange-400',
'์ ๋
': 'bg-purple-500/20 text-purple-400',
'๊ฐ์': 'bg-pink-500/20 text-pink-400'
};- ํ์ฌ ๋ ์ง ๊ฐ์กฐ: ์ค๋ ์ง-๋ ๋ ๊ทธ๋ผ๋ฐ์ด์ + ๋ ธ๋์ ๋ง
- ์ถ์ ๊ธฐ๋ก: ํ๋์-๋ณด๋ผ์ ๊ทธ๋ผ๋ฐ์ด์ + ์ด๋ก ์
- ์ ํ๋ ๋ ์ง: ํ๋์ ๋ง์ผ๋ก ๊ฐ์กฐ
- ๋ ์ง ํด๋ฆญ ์ ํด๋น ๋ ์ง์ ๋ชจ๋ ๊ธฐ๋ก ํ์
- 8๊ฐ ๋ฑ์ง ์ข ๋ฅ: ํฌ์ค ์ ๋ฌธ์, ์๋จ ๊ด๋ฆฌ์, ์ถ์์, ๊ทผ์ก ๋น๋ ๋ฑ
- ํ๋ํ ๋ฑ์ง: ์ปฌ๋ฌํ + ํ๋ ๋ ์ง ํ์
- ๋ฏธํ๋ ๋ฑ์ง: ํ๋ฐฑ + ์ ๊ธ ์์ด์ฝ
- ๋ฑ์ง ๋ชจ๋ฌ์์ ์ ์ฒด ์ปฌ๋ ์ ํ์ธ
์บ๋ฆฐ๋ ์๋ 3๊ฐ ๋ฒํผ์ผ๋ก ๋น ๋ฅธ ๊ธฐ๋ก ์ถ๊ฐ:
- ๐ช ์ด๋ ๊ธฐ๋ก: ์นดํ ๊ณ ๋ฆฌ๋ณ ํํฐ๋ง (๊ฐ์ด/๋ฑ/ํ์ฒด/์ด๊นจ/ํ/๋ณต๊ทผ/์ ์ฐ์)
- ๐ฝ๏ธ ์๋จ ๊ธฐ๋ก: ์นดํ ๊ณ ๋ฆฌ๋ณ ํํฐ๋ง (๋จ๋ฐฑ์ง/ํ์ํ๋ฌผ/์ฑ์/๊ณผ์ผ/์ ์ ํ/๋ณด์ถฉ์ /ํ์)
- โค๏ธ ๊ฑด๊ฐ ๊ธฐ๋ก: ํค/์ฒด์ค/๊ทผ์ก๋/์ฒด์ง๋ฐฉ ์ ๋ ฅ, BMI ์๋ ๊ณ์ฐ
-- ํฌ์ธํธ ์บ์ฑ ์ถ๊ฐ
ALTER TABLE Member ADD COLUMN total_points INT DEFAULT 0;
-- ๋ณด์ ์ฐ๊ฒฐ ์ถ๊ฐ
ALTER TABLE ExerciseLog ADD COLUMN achievement_id INT;
ALTER TABLE DietLog ADD COLUMN achievement_id INT;
ALTER TABLE Attendance ADD COLUMN achievement_id INT;
ALTER TABLE Goal ADD COLUMN achievement_id INT;
-- ํธ๋ฆฌ๊ฑฐ ์ถ๊ฐ: ์๋ ํฌ์ธํธ ์ฆ๊ฐ
CREATE TRIGGER TRG_Achievement_Point_Earn
AFTER INSERT ON AchievementLog
FOR EACH ROW
BEGIN
UPDATE Member SET total_points = total_points + NEW.points_earned
WHERE member_id = NEW.member_id;
END;- ์ด๋ ๋ฆฌ์คํธ: 40๊ฐ (์นดํ
๊ณ ๋ฆฌ๋ณ ๋ถ๋ฅ)
- ๊ฐ์ด 6๊ฐ, ๋ฑ 6๊ฐ, ํ์ฒด 6๊ฐ, ์ด๊นจ 5๊ฐ, ํ 5๊ฐ, ๋ณต๊ทผ 5๊ฐ, ์ ์ฐ์ 7๊ฐ
- ์์ ๋ฆฌ์คํธ: 45๊ฐ (์นดํ
๊ณ ๋ฆฌ๋ณ ๋ถ๋ฅ)
- ๋จ๋ฐฑ์ง 8๊ฐ, ํ์ํ๋ฌผ 7๊ฐ, ์ฑ์ 7๊ฐ, ๊ณผ์ผ 6๊ฐ, ์ ์ ํ 4๊ฐ, ๋ณด์ถฉ์ 4๊ฐ, ํ์ 6๊ฐ, ๊ฐ์ 3๊ฐ
- ์ด๋ ๋ก๊ทธ: 40๊ฐ (์ต๊ทผ 30์ผ)
- ์๋จ ๋ก๊ทธ: 50๊ฐ (์ต๊ทผ 30์ผ)
- ๊ฑด๊ฐ ๊ธฐ๋ก: 7๊ฐ (์ฃผ๊ฐ ์ธก์ )
- ์ถ์ ๊ธฐ๋ก: 7๊ฐ
- ๋ฑ์ง: 8๊ฐ
- ํ์ ๋ฑ์ง: 4๊ฐ ํ๋
src/components/
โโโ WeightChart.jsx # Chart.js ์ฒด์ค ๋ณํ ์ฐจํธ
โโโ DailyRecordCard.jsx # ์ผ์ผ ๊ธฐ๋ก ์นด๋ (์ด๋/์๋จ/๊ฑด๊ฐ)
โโโ Navbar.jsx # ๊ธฐ์กด ๋ค๋น๊ฒ์ด์
- ์ฌ์ฌ์ฉ์ฑ: WeightChart๋ ๋ค๋ฅธ ํ์ด์ง์์๋ ์ฌ์ฉ ๊ฐ๋ฅ
- ์ ์ง๋ณด์์ฑ: ๊ฐ ์ปดํฌ๋ํธ๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌ๋จ
- ์ฑ๋ฅ: ํ์ํ ์ปดํฌ๋ํธ๋ง ๋ ๋๋ง
- ํ ์คํธ: ๊ฐ๋ณ ์ปดํฌ๋ํธ ๋จ์ ํ ์คํธ ์ฉ์ด
{
"dependencies": {
"chart.js": "^4.4.1",
"react-chartjs-2": "^5.2.0"
}
}import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
);- Framer Motion์ผ๋ก ๋ชจ๋ ์นด๋์ ๋ถ๋๋ฌ์ด ๋ฑ์ฅ ์ ๋๋ฉ์ด์
- ํธ๋ฒ ์ scale, rotate ํจ๊ณผ
- ์ฌ์ด๋ ํจ๋ ์ฌ๋ผ์ด๋ ์ ๋๋ฉ์ด์ (Spring)
- ๋ชจ๋ฌ fade in/out ํจ๊ณผ
// ์นดํ
๊ณ ๋ฆฌ๋ณ ์์ ๊ตฌ๋ถ
const categoryColors = {
exercise: 'from-blue-500 to-purple-600', // ์ด๋: ํ๋-๋ณด๋ผ
diet: 'from-green-500 to-emerald-600', // ์๋จ: ์ด๋ก
health: 'from-red-500 to-pink-600', // ๊ฑด๊ฐ: ๋นจ๊ฐ-ํํฌ
badge: 'from-yellow-500 to-orange-600', // ๋ฑ์ง: ๋
ธ๋-์ฃผํฉ
point: 'from-blue-600 to-pink-600' // ํฌ์ธํธ: ํ๋-ํํฌ
};- ๋ชจ๋ฐ์ผ: 1์ด ๊ทธ๋ฆฌ๋
- ํ๋ธ๋ฆฟ: 2์ด ๊ทธ๋ฆฌ๋
- ๋ฐ์คํฌํฑ: 3์ด ๊ทธ๋ฆฌ๋
- ์ฌ์ด๋ ํจ๋: ๋ชจ๋ฐ์ผ ์ ์ฒด ํ๋ฉด, ๋ฐ์คํฌํฑ 600-700px
- ๋ชจ๋ ์นด๋์ backdrop-blur ํจ๊ณผ
- ๊ทธ๋ผ๋ฐ์ด์ ๋ฐฐ๊ฒฝ์ผ๋ก ๊น์ด๊ฐ ํํ
- ํ ์คํธ ๊ฐ๋ ์ฑ ์ต์ ํ
- ์ฐจํธ ์์ ๋คํฌ๋ชจ๋ ๋์
- Line Chart with gradient fill
- 8๊ฐ ๋ฐ์ดํฐ ํฌ์ธํธ (์ต๊ทผ ๊ธฐ๋ก)
- ์์์ ๊ณผ ํ์ฌ์ ๋ผ๋ฒจ ํ์
- ํดํ์ผ๋ก ์ ํํ ๊ฐ ํ์ธ
- ๋ฐ ์ฐจํธ ํ์
- ํ์์ผ, ๋ชฉ์์ผ ๊ฐ์กฐ (๊ฐ์ฅ ๋ง์ด ์ด๋)
- ๋์ด๋ก ๋น๋ ํํ
- ๋๋ ์ฐจํธ (SVG)
- ๋ฑ 24%, ํ์ฒด 20%, ํ 14%, ๋ณต๊ทผ 12%, ๊ธฐํ 31%
- ๋ฒ๋ก์ ํจ๊ป ํ์
# 1. MySQL ์ ์
mysql -u root -p
# 2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ
CREATE DATABASE hs_health CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hs_health;
# 3. ์คํค๋ง ์ ์ฉ
source my-app/server/sql/HS_Health.sql;
# 4. ๋๋ฏธ ๋ฐ์ดํฐ ์ฝ์
source my-app/server/sql/insert_dummy_data.sql;- Member: ํ์ ์ ๋ณด + ํฌ์ธํธ ์บ์ฑ
- ExerciseList: ์ด๋ ๋ชฉ๋ก (40๊ฐ, ์นดํ ๊ณ ๋ฆฌ๋ณ)
- FoodList: ์์ ๋ชฉ๋ก (45๊ฐ, ์นดํ ๊ณ ๋ฆฌ๋ณ)
- ExerciseLog: ์ด๋ ๊ธฐ๋ก + achievement_id
- DietLog: ์๋จ ๊ธฐ๋ก + achievement_id
- HealthRecord: ๊ฑด๊ฐ ๊ธฐ๋ก (์ฒด์ค/๊ทผ์ก๋/์ฒด์ง๋ฐฉ/BMI)
- Attendance: ์ถ์ ๊ธฐ๋ก + achievement_id
- AchievementLog: ์ฑ์ทจ ๋ก๊ทธ (ํฌ์ธํธ ํ๋)
- Badge: ๋ฑ์ง ๋ชฉ๋ก
- MemberBadge: ํ์ ๋ฑ์ง ํ๋ ๊ธฐ๋ก
- ํฐ MyPage.jsx๋ฅผ ์์ ์ปดํฌ๋ํธ๋ก ๋ถ๋ฆฌ
- ํ์ํ ๋ถ๋ถ๋ง ๋ฆฌ๋ ๋๋ง
// ํํฐ๋ง๋ ๋ชฉ๋ก ์บ์ฑ
const filteredExercises = useMemo(() =>
exerciseCategory === '์ ์ฒด'
? exerciseList.filter(e => e.status === 'APPROVED')
: exerciseList.filter(e => e.status === 'APPROVED' && e.category === exerciseCategory),
[exerciseCategory, exerciseList]
);- ๋ชจ๋ฌ์ ์ด๋ฆด ๋๋ง ๋ ๋๋ง (AnimatePresence)
- ์ฐจํธ๋ ๋ฐ์ดํฐ๊ฐ ์์ ๋๋ง ๋ ๋๋ง
- ํฐ์น ์ ์ค์ฒ ์ง์ (Framer Motion)
- ํฐ ํฐ์น ์์ญ (์ต์ 44x44px)
- ์ค์์ดํ๋ก ์ฌ์ด๋ ํจ๋ ๋ซ๊ธฐ
- ๋ฐ์ํ ํฐํธ ํฌ๊ธฐ
- ๋ชจ๋ฐ์ผ ๋ค๋น๊ฒ์ด์ ์ต์ ํ
- main: ํ๋ก๋์ ๋ธ๋์น
- gyu: Class ํ์ด์ง, ๋คํฌ๋ชจ๋, ๋ฐฑ์๋ ์๋ฒ ๊ฐ๋ฐ
- hong: gyu ๋ธ๋์น ๋ณ๊ฒฝ์ฌํญ ๋จธ์ง ์๋ฃ
- hongrecent: ๋ง์ดํ์ด์ง ๋ํญ ๊ฐ์ , Chart.js ์ ์ฉ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ฅ โญ NEW
feat: ๋ง์ดํ์ด์ง ๊ฐ์ ๋ฐ Chart.js ์ ์ฉ
- Chart.js๋ฅผ ์ฌ์ฉํ ์ฒด์ค ๋ณํ ์ฐจํธ ์ถ๊ฐ
- ๋ชฉํ ๊ด๋ฆฌ๋ฅผ ์ฌ์ด๋ ํจ๋๋ก ๋ณ๊ฒฝ (์ ์๋ค ํผ์น ์ ์์)
- DailyRecordCard ์ปดํฌ๋ํธ๋ก ๊ธฐ๋ก ํ์ ๊ฐ์
- ์ด๋/์๋จ/๊ฑด๊ฐ ๊ธฐ๋ก UI ๊ฐ์ (์นดํ
๊ณ ๋ฆฌ๋ณ ์์, ์ ๋๋ฉ์ด์
)
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ฏธ ๋ฐ์ดํฐ ์ถ๊ฐ (insert_dummy_data.sql)
- ์ด๋ ๋ฆฌ์คํธ 40๊ฐ, ์์ ๋ฆฌ์คํธ 45๊ฐ๋ก ํ์ฅ
- ์นดํ
๊ณ ๋ฆฌ๋ณ ํํฐ๋ง ๊ธฐ๋ฅ ์ถ๊ฐ
- ๋์ ์์ฝ ์น์
์ถ๊ฐ (ํ๋ ์์ฝ, TOP 3, ๋ถ์๋ณ ๋ถ์ ๋ฑ)
- ํ์ฌ ๋ ์ง ๊ฐ์กฐ ๊ธฐ๋ฅ ์ถ๊ฐ
- ๋ฑ์ง ์์คํ
๊ตฌํ
- WeightChart ์ปดํฌ๋ํธ ๋ถ๋ฆฌ
- React์์ Chart.js ์ฌ์ฉ๋ฒ
- ๋ฐ์ํ ์ฐจํธ ๊ตฌํ
- ๋คํฌ๋ชจ๋ ๋์ ์ฐจํธ ์คํ์ผ๋ง
- ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ปดํฌ๋ํธ ๋ถ๋ฆฌ
- Props๋ฅผ ํตํ ๋ฐ์ดํฐ ์ ๋ฌ
- ์ปดํฌ๋ํธ ๊ฐ ํต์
- Framer Motion ๊ณ ๊ธ ๊ธฐ๋ฒ
- Spring ์ ๋๋ฉ์ด์
- ์ฌ์ด๋ ํจ๋ ๊ตฌํ
- ์ ๊ทํ์ ๋น์ ๊ทํ (ํฌ์ธํธ ์บ์ฑ)
- ํธ๋ฆฌ๊ฑฐ๋ฅผ ํตํ ์๋ํ
- ์ธ๋ํค ๊ด๊ณ ์ค์
- ์นด๋ ๊ธฐ๋ฐ ๋ ์ด์์
- ๋ชจ๋ฌ๊ณผ ์ฌ์ด๋ ํจ๋
- ๋ฐ์ํ ๊ทธ๋ฆฌ๋ ์์คํ
- ์์ ์์คํ ์ค๊ณ
- ๋ฐฑ์๋ ์๋ฒ ๋ฏธ๊ตฌํ: ํ์ฌ๋ ๋๋ฏธ ๋ฐ์ดํฐ๋ง ์ฌ์ฉ
- ์ค์๊ฐ ์ ๋ฐ์ดํธ: WebSocket ๋ฏธ๊ตฌํ
- ์ด๋ฏธ์ง ์ ๋ก๋: ํ๋กํ ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ ์์
- ๊ฒ์ ๊ธฐ๋ฅ: ์ด๋/์์ ๊ฒ์ ๊ธฐ๋ฅ ๋ฏธ๊ตฌํ
- ๋ฐฑ์๋ API ์๋ฒ ๊ตฌํ (Express.js)
- ์ค์๊ฐ ์๋ฆผ ์์คํ (WebSocket)
- ์ด๋ฏธ์ง ์ ๋ก๋ ๊ธฐ๋ฅ
- ์ด๋/์์ ๊ฒ์ ๋ฐ ํํฐ๋ง ๊ณ ๋ํ
- ์์ ๊ธฐ๋ฅ (์น๊ตฌ, ๋ญํน)
- PWA ์ง์
- ๋ชจ๋ฐ์ผ ์ฑ (React Native)
- gyu: Class ํ์ด์ง, ๋ฐฑ์๋ ์๋ฒ
- hong: ๋ง์ดํ์ด์ง ๊ฐ์ , Chart.js ํตํฉ, ๋ฐ์ดํฐ๋ฒ ์ด์ค ํ์ฅ
์ด ํ๋ก์ ํธ๋ ๊ต์ก ๋ชฉ์ ์ผ๋ก ์ ์๋์์ต๋๋ค.