운동 기록과 신체 변화를 사용자별로 관리하는 Spring Boot 기반 REST API 서버입니다.
직접 사용할 운동 기록 서비스를 만들면서 Java와 Spring Boot, JPA, 인증·인가, 데이터 모델링을 학습하는 개인 프로젝트입니다. 현재 회원가입·로그인, 체중 기록 CRUD, 운동 종류·운동 기록·인바디 기록의 저장 및 조회, 프로필 수정, 눈바디 사진 업로드를 구현했습니다.
| 기능 | 현재 구현 범위 |
|---|---|
| 회원 인증 | 이메일 회원가입, BCrypt 비밀번호 해싱, 로그인 및 JWT 발급 |
| 체중 기록 | 체중·기록 날짜 저장, 본인 목록 조회, 수정, 삭제 |
| 운동 종류 | 사용자가 직접 운동명·설명을 등록하고 본인 목록 조회 |
| 운동 기록 | 운동 종류·날짜·소모 칼로리·운동 시간·세트 수·반복 횟수 저장, 본인 목록 조회 |
| 인바디 기록 | 체중·골격근량·체지방량·체지방률·기초대사량·내장지방 레벨·측정일 저장 및 조회 |
| BMI | 인바디 저장 시 프로필에 키가 있으면 계산하여 함께 저장 |
| 프로필 | 닉네임·키 수정 |
| 눈바디 사진 | 사진과 촬영 날짜 업로드, 본인 사진의 날짜·저장 경로 목록 조회 |
현재는 백엔드 API를 제공하며, Swagger UI에서 요청을 보내 확인할 수 있습니다. 기간별 조회, 통계, 세트별 중량 기록, 사진 파일 다운로드·삭제, 루틴·식단 추천은 향후 개발 범위입니다.
| 구분 | 기술 |
|---|---|
| 언어 | Java 21 |
| 프레임워크 | Spring Boot 3.5.15, Spring Web MVC |
| 데이터 접근 | Spring Data JPA, Hibernate |
| 데이터베이스 | MySQL |
| 인증 | Spring Security, JJWT 0.12.6 |
| API 문서 | SpringDoc OpenAPI 2.8.14 |
| 빌드 | Gradle Wrapper 8.14.5 |
| 테스트 | JUnit 5, Spring Boot Test |
| 개발 도구 | Lombok, Git, GitHub |
버전은 build.gradle과 gradle/wrapper/gradle-wrapper.properties를 기준으로 합니다.
src/main/java/com/github/minuk1749/workout_tracker/
├── config/ # Swagger 설정
├── controller/ # HTTP 요청과 응답
├── domain/ # JPA 엔티티 및 상태 변경
├── dto/ # 요청·응답 DTO
├── exception/ # API 예외 및 공통 오류 응답
├── repository/ # 데이터 조회·저장
├── security/ # JWT 인증, 보안 설정, 현재 사용자 조회
├── service/ # 기능 처리 및 소유권 확인
└── WorkoutTrackerApplication.java
src/main/resources/
└── application.properties
src/test/java/com/github/minuk1749/workout_tracker/
└── WorkoutTrackerApplicationTests.java
Controller는 요청을 받고, Service가 현재 사용자와 업무 규칙을 확인한 뒤 Repository를 통해 데이터를 처리합니다. 응답에는 DTO를 사용합니다.
erDiagram
User ||--o{ WeightRecord : owns
User ||--o{ Exercise : owns
User ||--o{ WorkoutRecord : owns
User ||--o{ Inbody : owns
User ||--o{ BodyPhoto : owns
Exercise ||--o{ WorkoutRecord : referenced_by
| 엔티티 | 테이블 | 주요 데이터 |
|---|---|---|
User |
users |
이메일, 해싱된 비밀번호, 닉네임, 키, 생년월일, 성별 |
WeightRecord |
weight_record |
사용자, 체중, 기록 날짜 |
Exercise |
exercise |
사용자, 운동명, 설명 |
WorkoutRecord |
workout_record |
사용자, 운동 종류, 날짜, 소모 칼로리, 운동 시간, 세트 수, 반복 횟수 |
Inbody |
inbody |
사용자, 신체 측정값, BMI, 측정일 |
BodyPhoto |
body_photo |
사용자, 촬영 날짜, 파일 저장 경로 |
- 이메일에는 중복 가입을 방지하기 위한 유일 제약을 설정했습니다.
- 각 기록은 사용자와 다대일 관계이며, 운동 기록은 운동 종류도 참조합니다.
- 인바디의 BMI는 저장 시점의 키와 체중으로 계산하며, 이후 프로필의 키를 변경해도 기존 BMI를 다시 계산하지 않습니다.
- 체중 기록과 프로필 수정은
@Transactional안에서 엔티티를 변경하고 JPA의 변경 감지를 사용합니다.
로그인에 성공하면 유효기간이 24시간인 JWT를 발급합니다. 인증이 필요한 요청에는 다음 헤더를 보냅니다.
Authorization: Bearer <발급받은 JWT>JwtFilter가 토큰을 검증해 인증 정보를 등록하고, CurrentUserProvider가 인증 정보의 이메일로 현재 사용자를 조회합니다. 세션은 사용하지 않습니다.
- 회원가입·로그인과 Swagger 문서 경로는 인증 없이 접근할 수 있습니다.
- 그 외 API는 인증이 필요합니다.
- 목록 조회에는 현재 사용자 조건을 적용합니다.
- 체중 기록 수정·삭제는
findByIdAndUser로 본인 소유의 기록만 찾습니다. - 운동 기록 저장 시에도
findByIdAndUser로 본인 소유의 운동 종류인지 확인합니다. - 다른 사용자의 대상과 존재하지 않는 대상은 위 소유권 조회에서 모두
404로 처리합니다.
현재 토큰 재발급과 서버 측 로그아웃·토큰 폐기 기능은 구현하지 않았습니다.
기본 주소는 http://localhost:8080입니다. 아래에서 인증이 필요한 API는 Bearer 토큰을 사용합니다.
| Method | Endpoint | 기능 | 인증 |
|---|---|---|---|
| POST | /api/auth/signup |
회원가입 | 불필요 |
| POST | /api/auth/login |
로그인 및 JWT 발급 | 불필요 |
| GET | /api/weights |
본인 체중 기록 목록 | 필요 |
| POST | /api/weights |
체중 기록 저장 | 필요 |
| PATCH | /api/weights/{id} |
체중 기록 수정 | 필요 |
| DELETE | /api/weights/{id} |
체중 기록 삭제 | 필요 |
| GET | /api/exercises |
본인 운동 종류 목록 | 필요 |
| POST | /api/exercises |
운동 종류 등록 | 필요 |
| GET | /api/workouts |
본인 운동 기록 목록 | 필요 |
| POST | /api/workouts |
운동 기록 저장 | 필요 |
| GET | /api/inbody |
본인 인바디 기록 목록 | 필요 |
| POST | /api/inbody |
인바디 기록 저장 | 필요 |
| PATCH | /api/users/profile |
닉네임·키 수정 | 필요 |
| GET | /api/body-photos |
본인 사진의 날짜·경로 목록 | 필요 |
| POST | /api/body-photos |
사진·촬영 날짜 업로드 | 필요 |
사진 업로드는 multipart/form-data 형식으로 file과 date를 전달합니다. 날짜는 YYYY-MM-DD 형식을 사용합니다.
회원가입: POST /api/auth/signup
{
"email": "demo@example.com",
"password": "ChangeMe123!",
"nickname": "fitlog-user",
"gender": true,
"birthDate": "2000-01-01"
}현재 성별은 Boolean으로 저장하며 코드에서 true를 남성으로 정의합니다.
로그인: POST /api/auth/login
{
"email": "demo@example.com",
"password": "ChangeMe123!"
}로그인 응답의 token을 이후 요청에 사용합니다.
{
"token": "<발급받은 JWT>"
}체중 기록 저장: POST /api/weights
{
"weight": 72.5,
"recordedAt": "2026-10-27"
}체중 수정에는 weight와 recordedAt을, 프로필 수정에는 nickname과 height를 함께 보내야 합니다. 현재 PATCH 구현은 요청에서 생략한 필드에도 null을 대입하므로 일부 필드만 보내면 기존 값이 지워지거나 DB 제약 오류가 발생할 수 있습니다. 부분 수정 정책은 개선할 예정입니다.
ApiException으로 처리하는 오류와 인증 실패는 다음 형태로 응답합니다.
{
"status": 401,
"message": "인증이 필요합니다."
}| 상황 | 상태 코드 |
|---|---|
| 보호된 API에 토큰 없이 접근하거나 유효하지 않은 토큰 사용 | 401 |
| 로그인 정보 불일치 | 401 |
| 체중 수정·삭제 또는 운동 종류 참조 시 본인 소유의 대상이 없음 | 404 |
| 회원가입 시 이미 등록된 이메일을 사전 조회로 확인 | 409 |
잘못된 JSON, 입력값 검증, DB 제약 위반 등까지 같은 오류 형식으로 처리하는 작업은 남아 있습니다.
- JDK 21
- 실행 중인 MySQL 서버
- Git
- 저장소에 대한 접근 권한
Gradle은 저장소의 Wrapper를 사용하므로 별도로 설치할 필요가 없습니다.
git clone https://github.com/minuk1749/FitLog.git
cd FitLogMySQL에 접속해 개발용 데이터베이스를 생성합니다.
CREATE DATABASE IF NOT EXISTS workout_db CHARACTER SET utf8mb4;현재 기본 연결 설정은 다음과 같습니다.
| 항목 | 값 |
|---|---|
| 호스트·포트 | localhost:3306 |
| 데이터베이스 | workout_db |
| 사용자 | root |
| 비밀번호 | 환경변수 DB_PASSWORD |
로컬 MySQL 설정이 다르면 src/main/resources/application.properties의 연결 정보를 변경하거나 SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME 환경변수로 재정의합니다.
현재 spring.jpa.hibernate.ddl-auto=update를 사용하므로 애플리케이션 기동 시 테이블 구조를 자동 반영합니다. 별도 마이그레이션 스크립트는 아직 없습니다.
| 변수 | 용도 | 형식 |
|---|---|---|
DB_PASSWORD |
MySQL 접속 비밀번호 | 로컬 DB 비밀번호 |
JWT_SECRET |
JWT 서명·검증 키 | 최소 32바이트의 랜덤 값을 Base64로 인코딩한 문자열 |
Windows PowerShell에서 설정하는 예시입니다. 서버도 같은 터미널에서 실행합니다.
$env:DB_PASSWORD = '로컬_MySQL_비밀번호'
# 최초 로컬 실행용 JWT 키 생성
$fitlogKeyBytes = New-Object byte[] 32
$fitlogRng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$fitlogRng.GetBytes($fitlogKeyBytes)
$fitlogRng.Dispose()
$env:JWT_SECRET = [Convert]::ToBase64String($fitlogKeyBytes)환경변수는 현재 터미널 세션에 적용됩니다. 재시작 후 기존 토큰을 계속 사용하려면 같은 JWT_SECRET을 설정해야 합니다. 키를 새로 생성하면 이전 키로 발급한 토큰은 검증되지 않습니다. DB 비밀번호와 JWT 키는 저장소에 커밋하지 않습니다.
Windows PowerShell:
.\gradlew.bat bootRunmacOS·Linux에서는 같은 DB 설정과 환경변수를 준비한 뒤 실행합니다.
bash ./gradlew bootRun- Swagger UI에 접속합니다.
/api/auth/signup으로 회원가입합니다./api/auth/login으로 로그인하고 응답의token을 복사합니다.- Authorize에
Bearer접두어 없이 토큰 값만 입력합니다. - 체중 기록을 저장하고 목록을 조회합니다.
OpenAPI 문서는 API Docs에서 확인할 수 있습니다.
현재 자동 테스트는 @SpringBootTest 기반의 contextLoads() 1개입니다. 애플리케이션 컨텍스트 기동을 확인하며, API 동작·인증·소유권을 검증하는 테스트는 추가할 예정입니다.
별도 테스트 프로필이나 테스트 전용 DB 설정이 없으므로 현재 테스트에도 MySQL과 DB_PASSWORD, JWT_SECRET이 필요합니다. 데이터베이스 URL을 별도 테스트 DB로 지정하면 개발 DB와 분리할 수 있습니다.
먼저 MySQL에서 테스트용 데이터베이스를 생성합니다.
CREATE DATABASE IF NOT EXISTS workout_test_db CHARACTER SET utf8mb4;환경변수를 설정한 PowerShell에서 테스트를 실행합니다. 아래 URL 변경은 현재 터미널 세션에 적용됩니다.
$env:SPRING_DATASOURCE_URL = 'jdbc:mysql://localhost:3306/workout_test_db?serverTimezone=Asia/Seoul&characterEncoding=UTF-8'
.\gradlew.bat test테스트를 포함한 전체 빌드:
.\gradlew.bat build테스트 보고서는 build/reports/tests/test/index.html, 빌드 결과는 build/libs/에 생성됩니다. 개발 서버를 다시 실행할 때는 SPRING_DATASOURCE_URL을 개발 DB 주소로 변경해야 합니다.
- 요청 DTO 검증과 일관된 입력 오류 응답
- PATCH에서 생략한 필드의 처리 정책 정리
- 체중·운동·인바디 기록의 기간별 조회, 정렬, 페이지 처리
- 운동·인바디 기록의 수정·삭제
- 데이터베이스 마이그레이션과 조회 조건에 맞는 인덱스 검토
현재 업로드한 파일은 실행 디렉터리 기준 uploads/body-photos/에 저장하며 DB에는 파일 경로를 기록합니다. 목록의 filePath는 서버 파일 경로이며 이미지 접근 URL이 아닙니다.
- 소유권을 확인하는 사진 파일 조회·삭제 API
- 파일 종류·크기·이름 검증
- 파일 저장과 DB 저장 중 실패가 발생했을 때 정리 처리
- 서비스·Repository·API 테스트
- JWT 인증과 타인 데이터 접근 차단 테스트
- 테스트 환경 분리
- Docker·Compose 실행 환경
- GitHub Actions 기반 테스트·빌드 자동화
- 배포 환경 구성과 운영 확인
현재 저장소에는 Docker·Compose 및 CI/CD 설정이 없습니다.
- 운동 통계와 체중 변화 분석
- 운동 세트별 중량·반복 횟수 기록
- 목표 설정과 루틴 관리
- 식단 기록 및 외부 AI API를 활용한 추천 기능
- 요구사항을 API 계약과 데이터 모델로 구체화하기
- 핵심 서비스 로직과 조회 조건을 직접 구현하기
- 인증과 데이터 소유권 검사의 역할 구분하기
- JPA 연관관계, 트랜잭션, 변경 감지 이해하기
- 정상·경계·실패 상황을 테스트로 검증하기
- 기능별 브랜치와 PR에 설계 이유·검증 결과 남기기
- 실행 환경을 문서화하고 배포까지 연결하기