Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
b0c17c5
[ICC-304] Cloudflare DDoS 방어를 위한 인프라 설정 변경
GulSauce Apr 1, 2026
ddb4969
[ICC-304] 배포 테스트 위한 브랜치명 변경
GulSauce Apr 1, 2026
224b48a
[ICC-304] ✨ feat: Google AI Studio에서 Vertex AI + GCS로 전환
GulSauce Apr 1, 2026
dde46a3
[ICC-304] 🔧 chore: GCP 인프라 테라폼 구성 추가
GulSauce Apr 1, 2026
e192a9a
[ICC-304] ♻️ refactor: 설정 파일 리팩토링 및 파일 검증 프로퍼티 분리
GulSauce Apr 1, 2026
f64b953
[ICC-304] 📝 docs: CLAUDE.md Vertex AI + GCS 전환 반영
GulSauce Apr 1, 2026
ff699f9
[ICC-304] 🔥 remove: 이름 변경된 설정/배포 파일의 원본 삭제
GulSauce Apr 1, 2026
445ff85
[ICC-304] 🐛 fix: OAuth2/JWT 설정이 q-asker prefix로 잘못 바인딩된 문제 수정
GulSauce Apr 1, 2026
7272afd
[ICC-304] 🐛 fix: OAuth2 client-id/secret을 각 프로필 섹션으로 이동
GulSauce Apr 1, 2026
f34d332
[ICC-304] 🐛 fix: OAuth2 client-id/secret을 공통 섹션으로 복원
GulSauce Apr 1, 2026
d21cbb1
[ICC-304] 🔧 chore: SSL 인증서를 Let's Encrypt로 전환
GulSauce Apr 1, 2026
623e35a
[ICC-304] ♻️ refactor: AWS/CloudFront 네이밍을 CDN으로 통일
GulSauce Apr 1, 2026
cb5d031
[ICC-304] 🔧 chore: CD 배포 트리거 브랜치를 main으로 복원
GulSauce Apr 1, 2026
c69cc6f
[ICC-304] 🔧 chore: SSL 인증서를 Let's Encrypt에서 Cloudflare Origin CA로 전환
GulSauce Apr 2, 2026
d78a3a1
[ICC-304] ✨ feat: OCI NSG Cloudflare 인바운드 규칙 Terraform 코드 추가
GulSauce Apr 2, 2026
fe4ffb9
[ICC-304] 🔧 chore: CRITICAL 티어 Rate Limit 완화 (5 → 10 req/min)
GulSauce Apr 2, 2026
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
47 changes: 0 additions & 47 deletions .github/resources/init-ssl.sh

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/cd-prod_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_USER }}
key: ${{ env.EC2_KEY }}
source: ".github/resources/*"
source: "infra/blue-green/*"
target: "/home/${{ env.EC2_USER }}/deploy"
strip_components: 2

Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ out/
.vscode/
.DS_Store

### Terraform ###
*.tfstate
*.tfstate.backup
.terraform/

### Custom ###
**/src/main/resources/application-**
!**/src/main/resources/application-test.yml
Expand All @@ -48,9 +53,9 @@ app/newrelic/newrelic.yml
monitor_downtime.sh

# Claude Code
CLAUDE.md
shrimp-rules.md
.mcp.json
.claude
shrimp_data
docs
.gstack/
160 changes: 160 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Q-Asker API

## 프로젝트 개요

문서(PDF, PPT, DOCX)를 업로드하면 Google Gemini AI가 자동으로 퀴즈를 생성하는 Spring Boot 기반 백엔드 API 서버. SSE를 통한 실시간 생성 스트리밍, 퀴즈 세트 관리, 풀이 히스토리 기록을 지원한다.

## 기술 스택

| 분류 | 기술 | 버전 |
|---|---|---|
| 언어 | Java | 21 |
| 프레임워크 | Spring Boot | 3.5.8 |
| AI | Spring AI (Google Gemini via Vertex AI) | 1.1.2 |
| ORM | Spring Data JPA + Hibernate | (Boot BOM) |
| DB | MySQL | - |
| 인증 | JWT (Auth0 java-jwt 4.5.0) + OAuth2 Client | - |
| 클라우드 | OCI Java SDK (Object Storage) + Cloudflare CDN + Google Cloud Storage | 3.80.3 |
| 문서변환 | JODConverter (LibreOffice) | 4.4.9 |
| 모니터링 | Micrometer + Prometheus + Actuator | (Boot BOM) |
| 장애격리 | Resilience4j (Circuit Breaker) | 2.3.0 |
| Rate Limiting | Bucket4j + Caffeine | 8.16.1 |
| API 문서 | SpringDoc OpenAPI (Swagger UI) | 2.8.8 |
| 암호화 | Jasypt | 3.0.5 |
| ID 난독화 | Hashids | 1.0.3 |
| 빌드 | Gradle (Groovy DSL) | 8.14.3 |
| 컨테이너 | Jib (Docker) | 3.4.0 |
| 포맷터 | Spotless + Google Java Format | 7.0.4 / 1.25.2 |
| 테스트 | JUnit 5 | (Boot BOM) |

## 명령어 (Scripts)

```bash
# 빌드
./gradlew build # 전체 빌드 (컴파일 + 테스트 + JAR)
./gradlew :app:bootJar # 실행 가능 JAR 생성
./gradlew :app:bootRun # 로컬 실행

# 테스트
./gradlew test # 전체 테스트
./gradlew :모듈명:test # 특정 모듈 테스트

# 포맷팅
./gradlew spotlessApply # 코드 포맷 적용 (Google Java Format)
./gradlew spotlessCheck # 포맷 위반 검증

# Docker
./gradlew jib # Docker 이미지 빌드 + 푸시
./gradlew jibDockerBuild # 로컬 Docker 이미지 빌드

# 유틸리티
./gradlew installGitHooks # Git hooks 경로 설정
./gradlew dependencyGraphStyled # 모듈 의존성 그래프 생성 (SVG)
```

## 아키텍처

### 멀티모듈 구조

의존 방향: `app` → `*-impl` → `*-api` → `global`

```
q-asker/api/
├── app/ # 진입점 (Spring Boot Application)
│ └── src/main/resources/
│ ├── application.yml # 설정 진입점 (config/ import)
│ ├── application-secrets.yml # 암호화된 시크릿
│ └── config/ # 분리된 설정 파일들
│ ├── server.yml # 서버, DB, JPA, 캐시
│ ├── ai.yml # Google Gemini AI 설정
│ ├── security.yml # JWT, OAuth2, CORS
│ ├── aws.yml # OCI Object Storage, CDN
│ ├── jodconverter.yml # LibreOffice 문서변환
│ ├── monitoring.yml # Actuator, Prometheus
│ ├── q-asker.yml # 앱 커스텀 설정
│ ├── resilience4j.yml # Circuit Breaker
│ └── springdoc.yml # Swagger/OpenAPI
├── modules/
│ ├── global/ # 공통 (BaseEntity, ApiResponse, GlobalExceptionHandler)
│ ├── auth/ (api + impl) # 인증 (JWT, OAuth2, RateLimitFilter)
│ ├── oci/ (api + impl) # OCI Object Storage 파일 업로드
│ ├── board/ (api + impl) # 게시판
│ ├── quiz-ai/ (api + impl) # AI 퀴즈 생성 (Gemini 호출, 메트릭)
│ ├── quiz-make/(api + impl) # 퀴즈 생성 흐름 (파일업로드, SSE, 생성결과)
│ ├── quiz-set/ (api + impl) # 퀴즈 세트 CRUD
│ ├── quiz-history/(api + impl) # 풀이 히스토리
│ └── util/ (api + impl) # 유틸리티 (문서변환)
├── infra/
│ ├── monitoring/ # Grafana Alloy 설정
│ ├── mysql/ # MySQL Docker 설정
│ ├── base-image/ # Docker 베이스 이미지
│ └── terraform/
│ ├── gcp/ # GCP 인프라 (GCS, IAM, Vertex AI)
│ └── oci/ # OCI 인프라 (NSG Cloudflare 인바운드 규칙)
├── docs/ # 문서, 분석 자료
├── .githooks/ # Git 훅 (pre-commit, pre-push, prepare-commit-msg)
└── .github/workflows/ # CI/CD
├── cd-prod_deploy.yml
├── ci-auto-version-bump.yml
├── ci-check-code-convention.yml
└── ci-update-api-docs.yml
```

## 환경 변수

- 민감한 값은 `application-secrets.yml`에 Jasypt `ENC()`로 암호화하여 관리
- Jasypt 복호화 키: `JASYPT_PASSWORD` 환경변수 또는 JVM 옵션으로 전달
- 프로파일: `local` (개발), `prod` (운영)
- Actuator 포트: 9090 (서비스 포트와 분리)
- Virtual Threads 활성화 (`spring.threads.virtual.enabled: true`)
- OCI Object Storage: `~/.oci/config` 파일 기반 인증, `OCI_NAMESPACE`/`OCI_BUCKET_NAME` 환경변수
- Google Cloud: Vertex AI + GCS (ADC 인증)
- `spring.ai.google.genai.project-id`: GCP 프로젝트 ID
- `spring.ai.google.genai.location`: GCP 엔드포인트 (현재: `global`)
- `GCS_BUCKET_NAME`: GCS 버킷 이름 (기본값: `q-asker-ai-files`)
- 로컬: `gcloud auth application-default login`, 프로덕션: 서비스 계정
- DDoS 방어: Cloudflare Free (`api.q-asker.com`만 프록시 활성화)
- SSL/HTTPS: Cloudflare (Universal SSL) → Nginx (Origin CA TLS), Full (Strict) 모드
- Origin 인증서: Cloudflare Origin CA (15년 유효)
- 인증서 경로: `/etc/ssl/cloudflare/api.q-asker.com.pem`, `.key`
- OCI NSG: 80/443 인바운드 Cloudflare IP 대역만 허용

## 개발 도구 및 설정

- **빌드**: Gradle 8.14.3 (Groovy DSL)
- **JDK**: 21 (Gradle Toolchain 자동 관리)
- **포맷터**: Spotless + Google Java Format 1.25.2
- `./gradlew spotlessApply` — 포맷 적용
- `./gradlew spotlessCheck` — 포맷 검증
- **Git Hooks** (`.githooks/`)
- `prepare-commit-msg` — 브랜치에서 JIRA 티켓(`[A-Z]+-[0-9]+`) 감지하여 커밋 메시지 접두사 자동 추가
- `pre-commit` — `spotlessCheck` 실행, 위반 시 커밋 차단
- `pre-push` — `spotlessCheck` 실행, 위반 시 푸시 차단
- **CI/CD**: GitHub Actions
- `ci-check-code-convention.yml` — PR 포맷 검증
- `ci-auto-version-bump.yml` — 자동 버전 범프
- `ci-update-api-docs.yml` — OpenAPI 스펙 자동 갱신
- `cd-prod_deploy.yml` — 운영 배포

## gstack

- 모든 웹 브라우징은 gstack의 `/browse` 스킬을 사용한다. `mcp__claude-in-chrome__*` 도구는 사용하지 않는다.
- 사용 가능한 스킬: `/office-hours`, `/plan-ceo-review`, `/plan-eng-review`, `/plan-design-review`, `/design-consultation`, `/design-shotgun`, `/design-html`, `/review`, `/ship`, `/land-and-deploy`, `/canary`, `/benchmark`, `/browse`, `/connect-chrome`, `/qa`, `/qa-only`, `/design-review`, `/setup-browser-cookies`, `/setup-deploy`, `/retro`, `/investigate`, `/document-release`, `/codex`, `/cso`, `/autoplan`, `/careful`, `/freeze`, `/guard`, `/unfreeze`, `/gstack-upgrade`, `/learn`

## Skill routing

When the user's request matches an available skill, ALWAYS invoke it using the Skill
tool as your FIRST action. Do NOT answer directly, do NOT use other tools first.
The skill has specialized workflows that produce better results than ad-hoc answers.

Key routing rules:
- Product ideas, "is this worth building", brainstorming → invoke office-hours
- Bugs, errors, "why is this broken", 500 errors → invoke investigate
- Ship, deploy, push, create PR → invoke ship
- QA, test the site, find bugs → invoke qa
- Code review, check my diff → invoke review
- Update docs after shipping → invoke document-release
- Weekly retro → invoke retro
- Design system, brand → invoke design-consultation
- Visual audit, design polish → invoke design-review
- Architecture review → invoke plan-eng-review
23 changes: 13 additions & 10 deletions app/src/main/resources/application-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ spring:
ai:
google:
genai:
api-key: ENC(QT6BgyfUJpU8eMQ1HeNsDMAO2F4vbCYclqSof8EFxzSqnuuH+rc74ep469JuWspP2N6lHjQvjROZlA6l2gXET1m3hsaLoVt0Rknj0tUTUlk=)
project-id: ENC(M5//c8RRJMLe3mL8W0/4oHNV4xvjE345unvzaasi0rEbEfBEja7BmNlE7/H9LKSSjC4hsexV2z5UNAC0+qTskg==)
location: ENC(At/NbaopVE34W3e3iNNvBTYFHYUYwkjBotCZlYjJo+b8VDIwaRqQFmQnMQO+1XQB)
security:
oauth2:
client:
Expand All @@ -19,21 +20,23 @@ spring:
jwt:
secret: ENC(Jeu3MuAaMOI3X+lLvscvqJCIYwjGacOoWze0XmTWV89W3F8QU2yucQO4kfXWuQgi)

aws:
cloudfront:
base-url: ENC(9+YmILh4Tu8KExN3af3xft6fqJBfi8THL8eIQE1RhDJfau54OHutWOvDpKhSHn5i02liSEoDGu6Ybcj6HiXGQA==)

oci:
object-storage:
namespace: ENC(5t4tYpNdUIeyvrKDePas63mev1aNO0kcv+E5l1THH3UWXpsa0IkYw9FZrOVAd5Ew)
bucket-name: ENC(/eDdNp9IQdDEH+YUrSNAUzQfONKmxq86HGlnw+8rJtPyakHknvh4WR54mblmC7HXJJvReuCAKgNBRVwspoFyxA==)

q-asker:
ai:
gcs:
bucket-name: ENC(O65crsFZNuUzdCrd5FI3w75/yb5YQ8gvNDErIbGgVmoR2Pn9+j0KQgSBTJ5fi4sEnh63lY3VFcsInB5mOQdEgQ==)
slack:
webhook-url-notify: ENC(9gKy67mFwk2AbeisSdGxQQI6j/DIiKUZoWGzik3QCRvEQa49e6bQsJ69VmfI0cD5kR93TTg1rFl+0f5bnx3SBVKoVHhUUMRVxcN61iyUEMJe0svSRd71VT6Pz5193SJqm1OcULxDd6A8Ifqf0QwlGUULYcVMmTraugoHwvpB1e8=)
hashid:
salt: ENC(Y+PWwkcYm1pmuHBHW6O4MGw4sS3l/GZAkTxSoJ8Pi3Cp8buxGKyGD1QztMHnVScvHeqTbQJDC9UlKFaNcJ49LEy33dFYEQznm0U4AUCGyuU=)

cdn:
base-url: ENC(9+YmILh4Tu8KExN3af3xft6fqJBfi8THL8eIQE1RhDJfau54OHutWOvDpKhSHn5i02liSEoDGu6Ybcj6HiXGQA==)

oci:
object-storage:
namespace: ENC(5t4tYpNdUIeyvrKDePas63mev1aNO0kcv+E5l1THH3UWXpsa0IkYw9FZrOVAd5Ew)
bucket-name: ENC(/eDdNp9IQdDEH+YUrSNAUzQfONKmxq86HGlnw+8rJtPyakHknvh4WR54mblmC7HXJJvReuCAKgNBRVwspoFyxA==)

---
spring:
config:
Expand Down
15 changes: 4 additions & 11 deletions app/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ spring:
ai:
google:
genai:
api-key: ci-dummy-key
project-id: ci-dummy-project
location: us-central1
chat:
options:
model: gemini-3-flash-preview
Expand All @@ -58,17 +59,9 @@ q-asker:

web:
frontend-deploy-url: http://localhost:3000
frontend-dev-url: http://localhost:3000

aws:
s3:
bucket-name: ci-dummy
access-key: ci-dummy
secret-key: ci-dummy
allowed-extensions: application/pdf

cloudfront:
base-url: http://localhost
cdn:
base-url: http://localhost

jodconverter:
local:
Expand Down
16 changes: 8 additions & 8 deletions app/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ spring:
name: q-asker
config:
import:
- config/server.yml
- config/ai.yml
- config/security.yml
- config/oci.yml
- config/database-config.yml
- config/ai-setting.yml
- config/spring-security.yml
- config/oci-bucket-config.yml
- config/jodconverter.yml
- config/monitoring.yml
- config/q-asker.yml
- config/resilience4j.yml
- config/springdoc.yml
- config/actuator.yml
- config/app-common.yml
- config/resilience.yml
- config/spring-doc.yml
- application-secrets.yml

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# AI (Google Gemini) 설정
spring:
ai:
retry:
max-attempts: 1
google:
genai:
chat:
Expand All @@ -11,10 +13,7 @@ spring:

q-asker:
ai:
chat-timeout-ms: 90000
equalization-model: gemini-2.5-flash-lite
chunk:
max-count-variants: 10, 5
file-client:
base-url: https://generativelanguage.googleapis.com
connect-timeout-ms: 5000
read-timeout-ms: 30000
Loading
Loading