- π Overview
- β¨ Key Features
- π Tech Stack
- π Architecture
- π Project Structure
- π‘ API Endpoints
- π Authentication Flow
- π§ Technical Challenges & Solutions
- π Getting Started
- π Roadmap
- π Related
- π€ Author
ZeroGravity Backend is a Spring Boot REST API that powers the emotion tracking and personal wellness platform. It provides secure authentication, emotion recording, analytics, and AI-powered insights.
π Part of the ZeroGravity full-stack project. Refactored from an incomplete collaborative Spring Boot project into a production-ready API. Frontend Repository | Original Vue Version
- π Secure Authentication - JWT integration with NextAuth, supporting Google & Kakao OAuth
- π Analytics Engine - Timezone-aware emotion statistics and chart data
- π€ AI-Powered Insights - Google Gemini API for emotion prediction and period analysis
- π Zero-Downtime Deploy - Build-first strategy with automatic rollback
| Feature | Description | Tech |
|---|---|---|
| π JWT Authentication | NextAuth integration with 15-min access / 30-day refresh tokens | jjwt, Spring Security |
| π€ User Management | Profile, consent tracking, GDPR-compliant data deletion | MyBatis, Snowflake ID |
| π Emotion Analytics | Daily/Moment records, level/count/reason statistics | MySQL, CONVERT_TZ |
| π€ AI Predictions | Emotion prediction from diary, period analysis | Google Gemini API |
| π Zero-Downtime Deploy | Build-first strategy with auto-rollback | Docker, GitHub Actions |
| π API Security | Rate limiting, caching, security headers | Nginx, Spring Security |
| Category | Technologies |
|---|---|
| Framework | Spring Boot 3.2.5, Java 17 |
| Database | MySQL 8.0, MyBatis 3.0.3 |
| Authentication | JWT (jjwt 0.12.5), Spring Security, NextAuth Integration |
| AI | Google Gemini API |
| Infrastructure | Docker, Docker Compose, Nginx |
| Cloud | OCI (Ampere A1 ARM64, Flexible Load Balancer, Object Storage) |
| IaC | Terraform (Networking, Compute, LB, Monitoring modules) |
| CI/CD | GitHub Actions (Zero-Downtime, Auto-Rollback) |
| DNS/SSL | AWS Route53, Let's Encrypt (ACME, TLS 1.3) |
| Monitoring | OCI Monitoring (CPU/Memory/Container Health Alarms, Email Alerts) |
| Documentation | SpringDoc OpenAPI (Swagger) |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OCI Cloud (Terraform-Managed) β
β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β VCN (Virtual Cloud Network) β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β β β Public Subnet β β β
β β β β β β
β β β βββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β β β Load Balancer β β Compute (Ampere A1 - ARM64) β β β β
β β β β (Flexible) β β β β β β
β β β β β β βββββββββββββββββββββββββββββββββββββββββ β β β β
β β β β - TLS 1.3 ββββββββΆβ β Nginx β β β β β
β β β β - Let's Encrypt β :80 β β (Reverse Proxy) β β β β β
β β β β - Health Check β β β Rate Limit: 500 req/min β β β β β
β β β βββββββββββββββββββββ β βββββββββββββββββββ¬ββββββββββββββββββββββ β β β β
β β β β β β β β β
β β β β βββββββββββββββ΄ββββββββββββββ β β β β
β β β β β β β β β β
β β β β βΌ βΌ β β β β
β β β β zerogv.com api.zerogv.com β β β β
β β β β dev.zerogv.com api-dev.zerogv.com β β β β
β β β β β β β β β β
β β β β βΌ βΌ β β β β
β β β β βββββββββββββββββββββββββββββββββββββββ β β β β
β β β β β Docker Containers β β β β β
β β β β β β β β β β
β β β β β βββββββββββββββ ββββββββββββββββ β β β β β
β β β β β β Frontend β β Backend β β β β β β
β β β β β β (Docker) β β (Docker) β β β β β β
β β β β β β β β β β β β β β
β β β β β β Next.js 15 β β Spring Boot β β β β β β
β β β β β β standalone β β 3.2.5 β β β β β β
β β β β β β β β β β β β β β
β β β β β β prod :3000 β β prod :8080 β β β β β β
β β β β β β dev :3001 β β dev :8081 β β β β β β
β β β β β βββββββββββββββ ββββββββ¬ββββββββ β β β β β
β β β β β β β β β β β
β β β β β ββββββββ΄ββββββββ β β β β β
β β β β β β MySQL β β β β β β
β β β β β β (Docker) β β β β β β
β β β β β β β β β β β β
β β β β β β prod :3306 β β β β β β
β β β β β β dev :3307 β β β β β β
β β β β β ββββββββββββββββ β β β β β
β β β β βββββββββββββββββββββββββββββββββββββββ β β β β
β β β β β β β β
β β β βββββββββββββββββββββββββββββββββββββββββββββββ β β β
β β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Object Storage β β OCI Monitoring β β
β β (Static Files) β β CPU/Memory Alarms (>80%) Β· Container Health Β· Email Alert β β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
External Services:
βββ AWS Route53 (DNS: zerogv.com, api.zerogv.com, dev.zerogv.com, api-dev.zerogv.com)
βββ Google Gemini API (AI emotion prediction & period analysis)
βββ OAuth Providers: Google, Kakao
CI/CD: GitHub Actions β SSH β Docker Build β Zero-Downtime Deploy (+ Auto-Rollback)
zerogravity/src/main/java/com/zerogravity/myapp/
βββ common/ # Shared infrastructure
β βββ config/ # DB, Swagger, Web, Jackson configs
β βββ security/ # JWT, @AuthUserId annotation
β βββ exception/ # Global exception handler
β βββ util/ # TimezoneUtil
β
βββ auth/ # Authentication domain
β βββ controller/ # OAuth2 endpoints
β βββ dto/ # AuthResponse
β
βββ user/ # User management domain
β βββ controller/ # User profile, consent
β βββ service/ # UserService
β βββ dao/ # MyBatis mapper
β
βββ emotion/ # Core emotion tracking domain
β βββ controller/ # Emotion records CRUD
β βββ service/ # EmotionService, EmotionRecordService
β βββ dao/ # MyBatis mappers
β
βββ chart/ # Analytics domain
β βββ controller/ # Statistics endpoints
β βββ service/ # ChartService
β
βββ ai/ # AI features domain
βββ controller/ # AI prediction endpoints
βββ service/ # Gemini API integration
The Challenge: The original Spring Boot project had a traditional layered architecture with all controllers, services, and DAOs grouped by technical concern. This made it difficult to understand the business logic and maintain feature boundaries.
The Refactoring: Instead of a complete rewrite, I analyzed the existing codebase, preserved the working layer structure, and reorganized code by business domain while adding new features like NextAuth integration and AI capabilities.
The Result:
common/: Shared infrastructure (security, config, exceptions, utilities)auth/: Authentication & JWT token managementuser/: User profile and consent managementemotion/: Core emotion tracking (records, emotions)chart/: Analytics and statisticsai/: Gemini-powered predictions and analysis
This organization allows each domain to evolve independently while sharing common infrastructure.
Base URL: https://api.zerogv.com
| Method | Endpoint | Description |
|---|---|---|
| POST | /auth/verify |
Verify OAuth token and issue JWT |
| POST | /auth/refresh |
Refresh access token |
| DELETE | /auth/logout |
Logout and invalidate tokens |
| Method | Endpoint | Description |
|---|---|---|
| GET | /user/me |
Get current user profile |
| DELETE | /user/me |
Delete account |
| POST | /user/consent |
Update consent status |
| Method | Endpoint | Description |
|---|---|---|
| GET | /emotions/records |
Get emotion records (with filters) |
| POST | /emotions/records |
Create new emotion record |
| PUT | /emotions/records/{id} |
Update emotion record |
| DELETE | /emotions/records/{id} |
Delete emotion record |
| Method | Endpoint | Description |
|---|---|---|
| GET | /chart/level |
Get emotion level statistics |
| GET | /chart/count |
Get emotion count by type |
| GET | /chart/reason |
Get emotion reasons statistics |
| Method | Endpoint | Description |
|---|---|---|
| POST | /ai/emotion-predictions |
Predict emotion from diary |
| GET | /ai/period-analyses |
Get AI analysis for period |
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β Client βββββΆβ NextAuth βββββΆβ Backend βββββΆβ MySQL β
β(Frontend)β β (OAuth) β β (JWT) β β (User) β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β β β β
β 1. OAuth β β β
ββββββββββββββββΆβ β β
β β 2. Verify β β
β ββββββββββββββββΆβ β
β β β 3. Upsert β
β β ββββββββββββββββΆβ
β β β 4. User β
β β βββββββββββββββββ
β β 5. JWT β β
β βββββββββββββββββ β
β 6. Session β β β
βββββββββββββββββ β β
Problem: Sending all emotion records to Gemini API causes token overflow and high costs (Year period = 365+ records)
Solution:
- Statistical Representative Sampling: Select best-matching record per time bucket
- Year: 365 β 12 records (1 per month)
- Month: ~31 β 4 records (1 per week)
- Week: 7 β 7 records (1 per day)
- Smart Matching Algorithm: 60% emotion level + 40% reason matching
- Daily 1.5x Weighting: Daily records weighted higher (more representative than moment)
- Tie-breaking: score β diary length β reason count β recency
- Prompt Design: JSON-only response, emotion level mapping (0-6), predefined reasons
Outcome: 97% token reduction for year period (365β12), accurate AI analysis maintained
// Select best matching record per bucket using weighted scoring
private double calculateMatchScore(EmotionRecord record, Double targetLevel, String topReason) {
// Daily records weighted 1.5x (more representative)
double recordLevel = record.getEmotionId() *
(record.getEmotionRecordType() == EmotionRecord.Type.DAILY ? 1.5 : 1.0);
double levelScore = 1.0 - (Math.abs(recordLevel - targetLevel) / 9.0);
// Reason matching
double reasonScore = record.getEmotionReasons().contains(topReason) ? 1.0 : 0.0;
// 60% level, 40% reason
return (levelScore * 0.6) + (reasonScore * 0.4);
}Problem: Chart grouping showed incorrect data due to server timezone (Asia/Seoul) vs user timezone mismatch
Why This Architecture:
- Global User Support: Users can access the app from anywhere (Korea β US travel scenario)
- UTC Storage: MySQL stores all timestamps in UTC for consistency
- Automatic Detection: Browser detects timezone via
Intl.DateTimeFormat().resolvedOptions().timeZone - No User Selection: Timezone changes automatically when user travels to a different timezone
Solution:
- X-Timezone Header: Frontend sends browser-detected timezone (e.g.,
Asia/Seoul,America/New_York) - SQL-level CONVERT_TZ: For grouped data (charts) - converts UTC to user timezone in query
- Java-level Conversion: For raw timestamps - avoids JDBC double conversion issue
// Frontend axios interceptor - automatic browser timezone detection
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
config.headers['X-Timezone'] = timezone;-- Chart grouping with user timezone (SQL-level conversion)
SELECT DATE_FORMAT(
CONVERT_TZ(created_time, '+00:00', #{timezoneOffset}),
'%Y-%m-%d'
) as label
FROM emotion_recordsLessons Learned (PR #62):
- JDBC Double Conversion Issue: When JDBC driver reads MySQL timestamp, it automatically converts to JVM timezone
- Problem Scenario: UTC in DB β JDBC converts to Asia/Seoul β Java CONVERT_TZ applies again = wrong time
- Solution: Use SQL
CONVERT_TZonly for grouped/aggregated data, use JavaZonedDateTimeconversion for raw timestamps
// Raw timestamp conversion (Java-level to avoid JDBC double conversion)
ZonedDateTime userTime = utcTime
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(userTimezone));Outcome: Correct chart grouping for users in any timezone, automatic adaptation when traveling
Problem: Integrate NextAuth frontend with Spring Boot backend, supporting multiple OAuth providers
Solution:
- Provider-based user lookup (providerId + provider combination)
- Snowflake ID generation for distributed unique user IDs
- JWT tokens with 15-min access / 30-day refresh lifecycle
- Custom @AuthUserId annotation with ArgumentResolver
Outcome: Seamless OAuth login with Google/Kakao, secure JWT-based session
// @AuthUserId annotation extracts user ID from JWT
@GetMapping("/me")
public ResponseEntity<UserResponse> getMe(@AuthUserId Long userId) {
return ResponseEntity.ok(userService.getUserById(userId));
}Problem: 502 errors during deployments when new container failed to start
Solution:
- Build-first strategy: Build new image while old container runs
- Image-based backup and instant rollback
- 150-second health check (30 attempts Γ 5 seconds)
- Auto-rollback on health check failure
Outcome: Old container keeps running if build fails, instant rollback from backup image
# Build new image (old container still running)
docker build -t zerogv-backend:${ENV}-new .
# Backup current image
docker tag zerogv-backend:${ENV} zerogv-backend:${ENV}-backup
# Swap only after successful build
docker compose down && docker compose up -dProblem: Token rotation caused concurrent request failures and false security alerts
Solution:
- Initially: Token rotation with reuse detection and 5-second grace period
- Final: Simplified validation (no rotation) for stability
- Hourly cleanup of expired/revoked tokens
- Specific error codes (REFRESH_TOKEN_EXPIRED, REFRESH_TOKEN_INVALID)
Outcome: Stable token refresh without concurrent request errors
Problem: Database stores lowercase ('daily', 'moment') but Java uses uppercase (DAILY, MOMENT)
Solution: Custom TypeHandler extending BaseTypeHandler for case-insensitive conversion
Outcome: Seamless enum mapping between DB and Java layers
public class EmotionRecordTypeHandler extends BaseTypeHandler<EmotionRecord.Type> {
@Override
public EmotionRecord.Type getNullableResult(ResultSet rs, String col) {
String value = rs.getString(col);
return value != null
? EmotionRecord.Type.valueOf(value.toUpperCase())
: null;
}
}- Java 17+
- Maven 3.9+
- Docker & Docker Compose
- MySQL 8.0
# Clone the repository
git clone https://github.com/zerogravity-project/zerogravity-backend.git
cd zerogravity-backend
# Set up environment variables
cp .env.example .env# Local development
cd zerogravity
./mvnw spring-boot:run
# Docker deployment
docker-compose up -d
# API Documentation
open http://localhost:8080/swagger-ui.html# Database
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/zerogravity
SPRING_DATASOURCE_USERNAME=user
SPRING_DATASOURCE_PASSWORD=password
# JWT
SPRING_JWT_SECRET=your-256-bit-secret
# AI
GEMINI_API_KEY=your-gemini-api-key- Advanced AI insights with trend prediction
- WebSocket for real-time sync
- Rate limiting per user (not just IP)
- Prometheus/Grafana monitoring
- Multi-language support (i18n)
Minuk Hwang - Fullstack Developer
Made with β€οΈ and β