Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 149 additions & 9 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Korean Travel Guide Backend - A Spring Boot Kotlin application providing OAuth a

# Run specific test
./gradlew test --tests "WeatherServiceTest"

# Compile Kotlin only (faster than full build)
./gradlew compileKotlin
```

### Code Quality
Expand Down Expand Up @@ -54,6 +57,24 @@ open http://localhost:8080/h2-console
curl http://localhost:8080/actuator/health
```

### Local RabbitMQ Setup
```bash
# Start RabbitMQ with docker-compose (for WebSocket testing)
docker network create common
docker-compose up -d

# Check RabbitMQ status
docker logs rabbitmq-1

# Access RabbitMQ Management UI
open http://localhost:15672
# Username: admin
# Password: (from .env PASSWORD_1 or default: qwpokd153098)

# Stop RabbitMQ
docker-compose down
```

### Redis (Optional - for caching)
```bash
# Start Redis with Docker
Expand All @@ -73,7 +94,7 @@ The codebase follows Domain-Driven Design with clear separation:
```
com/back/koreaTravelGuide/
├── common/ # Shared infrastructure
│ ├── config/ # App, Security, Redis, AI configs
│ ├── config/ # App, Security, Redis, AI, DevConfig
│ ├── security/ # OAuth2, JWT, filters
│ ├── exception/ # Global exception handler
│ └── ApiResponse.kt # Standard API response wrapper
Expand All @@ -86,7 +107,8 @@ com/back/koreaTravelGuide/
│ │ └── tour/ # Tourism API integration
│ ├── userChat/ # WebSocket chat between Guest-Guide
│ │ ├── chatroom/ # Chat room management
│ │ └── chatmessage/ # Message persistence
│ │ ├── chatmessage/ # Message persistence & publishing
│ │ └── stomp/ # WebSocket config (Simple/Rabbit)
│ └── rate/ # Rating system for AI sessions & guides
```

Expand All @@ -95,15 +117,43 @@ com/back/koreaTravelGuide/
1. **Each domain is self-contained** with its own entity, repository, service, controller, and DTOs
2. **Common utilities live in `common/`** - never duplicate config or security logic
3. **AI Chat uses Spring AI** with function calling for weather/tour tools
4. **User Chat uses WebSocket** (STOMP) for real-time messaging
5. **Global exception handling** via `GlobalExceptionHandler.kt` - just throw exceptions, they're caught automatically
4. **User Chat uses WebSocket** with profile-based configuration:
- **Dev**: SimpleBroker (in-memory, single server)
- **Prod**: RabbitMQ STOMP Relay (scalable, multi-server)
5. **Port-Adapter pattern** for message publishing (`ChatMessagePublisher` interface with `SimpleChatMessagePublisher` and `RabbitChatMessagePublisher` implementations)
6. **Global exception handling** via `GlobalExceptionHandler.kt` - just throw exceptions, they're caught automatically

### Critical Configuration Files

- **build.gradle.kts**: Contains BuildConfig plugin that generates constants from YAML files (area-codes.yml, prompts.yml, etc.)
- **application.yml**: Dev config with H2, Redis optional, OAuth2 providers
- **application.yml**: Dev config with H2, Redis optional, OAuth2 providers, RabbitMQ for local testing
- **application-prod.yml**: Production config with PostgreSQL, Redis required, RabbitMQ with connection stability settings
- **SecurityConfig.kt**: Currently allows all requests for dev (MUST restrict for production)
- **AiConfig.kt**: Spring AI ChatClient with OpenRouter (uses OPENROUTER_API_KEY env var)
- **DevConfig.kt**: Auto-generates 2 dummy GUIDE users on startup (dev profile only)

### Profile-Based Configuration Strategy

The application uses Spring profiles (`@Profile` annotation) to switch implementations:

**Development Profile (`dev`):**
- H2 in-memory database
- `SimpleChatMessagePublisher` - uses Spring's SimpleBroker (no RabbitMQ needed)
- `UserChatSimpleWebSocketConfig` - basic WebSocket with in-memory broker
- Redis optional (session.store-type: none)
- Dummy guide data auto-generation

**Production Profile (`prod`):**
- PostgreSQL database
- `RabbitChatMessagePublisher` - publishes to RabbitMQ
- `UserChatRabbitWebSocketConfig` - STOMP Broker Relay to RabbitMQ
- Redis required (session.store-type: redis)
- Connection stability settings (timeouts, heartbeats)

When adding new features that differ between dev/prod, follow this pattern:
1. Create an interface in the domain layer
2. Create separate implementations with `@Profile("dev")` and `@Profile("prod")`
3. Inject via the interface, Spring will wire the correct implementation

## Working with Spring AI

Expand All @@ -121,6 +171,44 @@ fun getTourSpots(area: String): TourResponse

**Important**: System prompts are managed in `src/main/resources/prompts.yml` and compiled into BuildConfig at build time.

## WebSocket & Real-Time Messaging

### Architecture
User-to-user chat uses WebSocket with STOMP protocol. The implementation switches based on profile:

**Development**: Uses Spring's SimpleBroker (in-memory)
- Suitable for single-server development
- No external dependencies
- Messages stored in memory only

**Production**: Uses RabbitMQ STOMP Relay
- Scales across multiple server instances
- Messages persist in RabbitMQ
- Handles reconnection and failover

### Message Flow
1. Client connects to WebSocket endpoint: `/ws/userchat`
2. Client sends message to: `/pub/chat/send`
3. Server processes and publishes to: `/topic/chat/{roomId}`
4. `ChatMessagePublisher` interface abstracts the publishing mechanism
5. Messages are persisted to database via `ChatMessageService`

### RabbitMQ Configuration
Located in `application.yml` and `application-prod.yml`:
```yaml
spring:
rabbitmq:
host: ${RABBITMQ_HOST}
port: ${RABBITMQ_PORT}
username: ${RABBITMQ_USERNAME}
password: ${RABBITMQ_PASSWORD}
stomp-port: ${RABBITMQ_STOMP_PORT} # Default: 61613
```

RabbitMQ requires two plugins enabled:
- `rabbitmq_management` - Management UI (port 15672)
- `rabbitmq_stomp` - STOMP protocol support (port 61613)

## Testing Strategy

Tests are in `src/test/kotlin` mirroring the main structure:
Expand Down Expand Up @@ -152,7 +240,7 @@ Always run `./gradlew ktlintCheck` before committing - it's enforced by git hook

- **Development**: H2 in-memory (jdbc:h2:mem:testdb), resets on restart
- **Production**: PostgreSQL (configured in application-prod.yml)
- **JPA Strategy**: `ddl-auto: create-drop` in dev (wipes DB on restart)
- **JPA Strategy**: `ddl-auto: create-drop` in dev (wipes DB on restart), `update` in prod

Main entities:
- `User` - OAuth users with roles (GUEST/GUIDE/ADMIN)
Expand All @@ -166,7 +254,7 @@ Required `.env` file (copy from .env.example):
```bash
# AI (Required)
OPENROUTER_API_KEY=sk-or-v1-...
OPENROUTER_MODEL=anthropic/claude-3.5-sonnet
OPENROUTER_MODEL=z-ai/glm-4.5-air:free

# OAuth (Required for auth)
GOOGLE_CLIENT_ID=...
Expand All @@ -183,7 +271,14 @@ TOUR_API_KEY=...
# JWT (Required for production)
CUSTOM__JWT__SECRET_KEY=...

# Redis (Optional - caching)
# RabbitMQ (Required for prod WebSocket)
RABBITMQ_HOST=localhost
RABBITMQ_PORT=5672
RABBITMQ_USERNAME=admin
RABBITMQ_PASSWORD=qwpokd153098
RABBITMQ_STOMP_PORT=61613

# Redis (Optional in dev, required in prod)
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
Expand Down Expand Up @@ -217,6 +312,33 @@ Common exceptions mapped to HTTP status:
5. **Commit**: `{type}(scope): summary` (e.g., `feat(be): Add weather caching`)
6. **PR title**: `{type}(scope): summary (#{issue})` (e.g., `feat(be): Add weather caching (#42)`)

## Infrastructure & Deployment

### Terraform Infrastructure (`infra/main.tf`)
EC2 instance setup with:
1. Docker & docker-compose installation
2. Container network (`common`)
3. Nginx Proxy Manager (ports 80, 443, 81)
4. Redis (port 6379)
5. PostgreSQL 16 (port 5432)
6. RabbitMQ with management & STOMP plugins (ports 5672, 15672, 61613)

**Order matters**: Docker → docker-compose → network → containers

### CI/CD Pipeline (`.github/workflows/deploy.yml`)
Blue-Green deployment strategy:
1. Build on GitHub Actions runner
2. Transfer JAR to EC2
3. Deploy to blue/green container based on availability
4. Health check before switching traffic
5. Environment variables injected via `docker run -e`

### Docker Compose Files
- `docker-compose.yml` (root) - Local development RabbitMQ
- `infra/rabbitmq-docker-compose.yml` - EC2 production RabbitMQ template

Both require `common` network to be pre-created: `docker network create common`

## Common Issues & Solutions

### Build fails with "BuildConfig not found"
Expand All @@ -228,20 +350,33 @@ Common exceptions mapped to HTTP status:
- Start Redis: `docker run -d -p 6379:6379 --name redis redis:alpine`
- Check: `docker logs redis`

### RabbitMQ connection issues
- In dev: RabbitMQ is optional (SimpleBroker used instead)
- In prod: RabbitMQ is required for WebSocket
- Start local RabbitMQ: `docker-compose up -d`
- Check logs: `docker logs rabbitmq-1`
- Verify plugins: `rabbitmq_management` and `rabbitmq_stomp` must be enabled

### ktlint failures
- Auto-fix: `./gradlew ktlintFormat`
- Pre-commit hook enforces this - setup via `./setup-git-hooks.sh`

### Spring AI errors
- Verify `OPENROUTER_API_KEY` in .env
- Check model name matches OpenRouter API (currently: anthropic/claude-3.5-sonnet)
- Check model name matches OpenRouter API (currently: z-ai/glm-4.5-air:free)
- Logs show AI requests: `logging.level.org.springframework.ai: DEBUG`

### OAuth login fails
- Ensure all OAuth credentials in .env
- Check redirect URIs match OAuth provider settings
- Dev: `http://localhost:8080/login/oauth2/code/{provider}`

### WebSocket connection fails
- Check profile: dev uses SimpleBroker, prod uses RabbitMQ
- In prod: Ensure RabbitMQ is running and accessible
- Verify STOMP port (61613) is open and reachable
- Check `UserChatStompAuthChannelInterceptor` for authentication issues

## Important Notes

- **Never commit .env** - it's gitignored, contains secrets
Expand All @@ -250,3 +385,8 @@ Common exceptions mapped to HTTP status:
- **Global config in common/** - don't duplicate security/config in domains
- **BuildConfig is generated** - don't edit manually, modify YAML sources
- **Redis is optional in dev** - required for production caching/sessions
- **RabbitMQ is optional in dev** - required for production WebSocket scaling
- **Profile-based beans** - use `@Profile` to switch implementations between dev/prod
- **Dummy data in dev** - 2 guide users auto-generated on startup (DevConfig.kt)
- **Docker network required** - `common` network must exist before running docker-compose
- **Terraform order matters** - Docker installation before docker-compose, containers after both
16 changes: 11 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ services:
rabbit:
image: rabbitmq:3-management # 버전 명시 권장 (예: 3.13-management)
container_name: rabbitmq-1
networks:
- common # 이미 존재하는 공용 네트워크 사용
ports:
- "5672:5672" # AMQP (Spring ↔ RabbitMQ)
- "61613:61613" # STOMP (Relay가 여기에 붙음)
- "15672:15672" # Management UI (로컬에서만)
environment:
TZ: Asia/Seoul # 타임존 설정
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: admin
volumes: # 영속화가 필요할 때만 유지
- ./dockerProjects/rabbitmq-1/volumes/etc/rabbitmq:/etc/rabbitmq
- ./dockerProjects/rabbitmq-1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq
- ./dockerProjects/rabbitmq-1/volumes/var/log/rabbitmq:/var/log/rabbitmq
RABBITMQ_DEFAULT_PASS: "$PASSWORD_1"
volumes:
- ./volumes/etc/rabbitmq:/etc/rabbitmq
- ./volumes/var/lib/rabbitmq:/var/lib/rabbitmq
- ./volumes/var/log/rabbitmq:/var/log/rabbitmq
command: >
sh -c "
rabbitmq-plugins enable rabbitmq_management &&
rabbitmq-plugins enable rabbitmq_stomp &&
rabbitmq-server
"
networks:
common:
external: true # 이미 만들어진 네트워크를 사용
76 changes: 49 additions & 27 deletions infra/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ yum install docker -y
systemctl enable docker
systemctl start docker

# docker-compose 설치
echo "docker-compose 설치 중..."
curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
docker-compose --version

# 도커 네트워크 생성
docker network create common

Expand Down Expand Up @@ -263,33 +270,48 @@ CREATE DATABASE \"${var.app_1_db_name}\" OWNER team11;
GRANT ALL PRIVILEGES ON DATABASE \"${var.app_1_db_name}\" TO team11;
"

# rabbitmq 설치
docker run -d \
--name rabbitmq_1 \
--restart unless-stopped \
--network common \
-p 5672:5672 \
-p 61613:61613 \
-p 15672:15672 \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=${var.password_1} \
-e TZ=Asia/Seoul \
-v /dockerProjects/rabbitmq_1/volumes/data:/var/lib/rabbitmq \
rabbitmq:3-management

# RabbitMQ가 준비될 때까지 대기
echo "RabbitMQ가 기동될 때까지 대기 중..."
until docker exec rabbitmq_1 rabbitmqctl status &> /dev/null; do
echo "RabbitMQ가 아직 준비되지 않음. 5초 후 재시도..."
sleep 5
done
echo "RabbitMQ가 준비됨. STOMP 플러그인 활성화 중..."

# RabbitMQ STOMP 플러그인 활성화
docker exec rabbitmq_1 rabbitmq-plugins enable rabbitmq_stomp
docker exec rabbitmq_1 rabbitmq-plugins enable rabbitmq_management

echo "RabbitMQ 설치 및 설정 완료!"
# RabbitMQ docker-compose.yml 생성
mkdir -p /dockerProjects/rabbitmq_1
cat > /dockerProjects/rabbitmq_1/docker-compose.yml <<'RABBITMQ_COMPOSE'
version: "3.8"

services:
rabbitmq:
image: rabbitmq:3-management
container_name: rabbitmq_1
restart: unless-stopped
networks:
- common
ports:
- "5672:5672"
- "61613:61613"
- "15672:15672"
environment:
TZ: Asia/Seoul
RABBITMQ_DEFAULT_USER: admin
RABBITMQ_DEFAULT_PASS: \${PASSWORD_1}
volumes:
- /dockerProjects/rabbitmq_1/volumes/etc/rabbitmq:/etc/rabbitmq
- /dockerProjects/rabbitmq_1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq
- /dockerProjects/rabbitmq_1/volumes/var/log/rabbitmq:/var/log/rabbitmq
command: >
sh -c "
rabbitmq-plugins enable rabbitmq_management &&
rabbitmq-plugins enable rabbitmq_stomp &&
rabbitmq-server
"

networks:
common:
external: true
RABBITMQ_COMPOSE

# RabbitMQ 시작
echo "RabbitMQ 시작 중..."
cd /dockerProjects/rabbitmq_1
docker-compose up -d

echo "RabbitMQ docker-compose 설치 완료!"

echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin

Expand Down
Loading