From 15db9eb11433678d41d35be7797fe12503a67af1 Mon Sep 17 00:00:00 2001 From: Hwa Young Yang <110398814+young0311@users.noreply.github.com> Date: Tue, 3 Nov 2026 19:21:04 +0900 Subject: [PATCH 1/6] =?UTF-8?q?chore:=20CodeRabbit=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 80 ++++++++++++++++++++++++++++++++ .github/pull_request_template.md | 17 ++++--- 2 files changed, 91 insertions(+), 6 deletions(-) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..c65ad03 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,80 @@ +language: ko-KR +early_access: true +enable_free_tier: true + +reviews: + profile: chill + request_changes_workflow: true + high_level_summary: true + high_level_summary_in_walkthrough: false + high_level_summary_placeholder: '@coderabbitai 요약' + poem: false + review_status: true + collapse_walkthrough: false + sequence_diagrams: false + changed_files_summary: true + + path_filters: + - "!**/README.md" + - "!**/build/**" # Gradle 빌드 산출물 + - "!**/logs/**" # 로그 파일 + - "!**/gradle/wrapper/**" # Gradle wrapper + - "!gradlew*" + - "!**/.idea/**" + - "!docs/**" # 문서 리뷰 제외 + +#pr 닫히면 리뷰 중단 + abort_on_close: true + +#자동 리뷰 활성화 + auto_review: + enabled: true + auto_incremental_review: true #변경부분만 + base_branches: + - dev + - main + + tools: + shellcheck: + enabled: true + markdownlint: + enabled: true + github-checks: + enabled: true + timeout_ms: 90000 + languagetool: + enabled: true + disabled_rules: + - EN_UNPAIRED_BRACKETS + - EN_UNPAIRED_QUOTES + enabled_only: false + level: default + hadolint: + enabled: true # member-service 등 Dockerfile + yamllint: + enabled: true # application-*.yaml, workflows + checkov: + enabled: true # docker-compose, nginx, workflows 검사 + gitleaks: + enabled: true # 시크릿 하드코딩 탐지 — env 설정 + ast-grep: + packages: [] + rule_dirs: [] + util_dirs: [] + essential_rules: true + #무관한 언어 + detekt: + enabled: false + ruff: + enabled: false + swiftlint: + enabled: false + phpstan: + enabled: false + golangci-lint: + enabled: false + biome: + enabled: false + +chat: + auto_reply: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e252bca..b5b6372 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,16 @@ -## 🔗 연관된 이슈 +## Jira Number / Link -> ex)Resolves(이슈가 해결되면 붙이기) #이슈번호, #이슈번호 +> ex) [FBE-7](https://framey.atlassian.net/browse/FBE-7?atlOrigin=eyJpIjoiYmUwMWRjNDBmZmM2NGM4Y2E0MDVkYjg3NjI4ZjkzMzgiLCJwIjoiaiJ9l) -## 📝 작업 내용 +## Summary -> 이번 PR에서 작업한 내용을 간략히 설명해주세요(이미지 첨부 가능) +> 이번 PR에서 작업한 내용 -## 💬 리뷰 요구사항(선택) +## Test Plan +> 구현 사항 검증 -> 리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요 +## Follow-up Tasks +>추후 작업 필요한 사항이 있다면 작성해주세요 (선택) + +## Summary by CodeRabbit +@coderabbitai 요약 From 482f058a053a69e00d8285bf2147bdd21e28de5c Mon Sep 17 00:00:00 2001 From: Hwa Young Yang <110398814+young0311@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:08:15 +0900 Subject: [PATCH 2/6] =?UTF-8?q?chore:=20=20CodeRabbit=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20*=20CodeRabbit=20Summary=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=EC=9C=BC=EB=A1=9C=20=EC=83=9D=EC=84=B1=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20*=20CodeRabbit=20=EC=9A=94?= =?UTF-8?q?=EC=95=BD=20=EA=B0=80=EC=9D=B4=EB=93=9C=EB=9D=BC=EC=9D=B8=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20*=20CodeRabbit=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=ED=8C=8C=EC=9D=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 42 +++++++++++++++++++++++--------- .github/pull_request_template.md | 1 - 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index c65ad03..bc863e7 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -5,28 +5,48 @@ enable_free_tier: true reviews: profile: chill request_changes_workflow: true + + # PR 전체 변경 사항 요약 high_level_summary: true high_level_summary_in_walkthrough: false + + #PR 본문에 요약 삽입 high_level_summary_placeholder: '@coderabbitai 요약' + + #PR 요약 내용 + high_level_summary_instructions: | + PR의 변경 사항을 분석하여 한국어로 요약해주세요. + + 다음 형식을 우선적으로 사용해주세요. + + ## New Features + - 새롭게 추가된 기능과 API를 설명합니다. + + ## Bug Fixes / Changes + - 버그 수정, 기존 로직 변경 및 성능 개선 사항을 설명합니다. + + ## Tests + - 추가되거나 수정된 테스트를 설명합니다. + + 작성 규칙: + - 실제 코드 변경 사항을 근거로 작성합니다. + - 변경 사항이 없는 항목은 생략합니다. + - 각 항목은 간결한 한국어로 작성합니다. + - 구현 세부 사항보다 기능과 동작의 변화를 우선 설명합니다. + - Redis, SSE, RabbitMQ 등 주요 기술은 명시합니다. + - 확인되지 않은 기능이나 테스트는 작성하지 않습니다. + - 불필요한 인사말과 마무리 문장은 작성하지 않습니다. + poem: false review_status: true collapse_walkthrough: false sequence_diagrams: false changed_files_summary: true - path_filters: - - "!**/README.md" - - "!**/build/**" # Gradle 빌드 산출물 - - "!**/logs/**" # 로그 파일 - - "!**/gradle/wrapper/**" # Gradle wrapper - - "!gradlew*" - - "!**/.idea/**" - - "!docs/**" # 문서 리뷰 제외 - -#pr 닫히면 리뷰 중단 + #pr 닫히면 리뷰 중단 abort_on_close: true -#자동 리뷰 활성화 + #자동 리뷰 활성화 auto_review: enabled: true auto_incremental_review: true #변경부분만 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b5b6372..b7cf073 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,5 +12,4 @@ ## Follow-up Tasks >추후 작업 필요한 사항이 있다면 작성해주세요 (선택) -## Summary by CodeRabbit @coderabbitai 요약 From bffc182d4ffdd9bbd3847125aa980624aefb5edf Mon Sep 17 00:00:00 2001 From: Hwa Young Yang <110398814+young0311@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:20:41 +0900 Subject: [PATCH 3/6] =?UTF-8?q?chore:=20CodeRabbit=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=ED=81=AC=EA=B8=B0=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 6 +++--- .github/pull_request_template.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index bc863e7..a606d6e 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -19,13 +19,13 @@ reviews: 다음 형식을 우선적으로 사용해주세요. - ## New Features + ### New Features - 새롭게 추가된 기능과 API를 설명합니다. - ## Bug Fixes / Changes + ### Bug Fixes / Changes - 버그 수정, 기존 로직 변경 및 성능 개선 사항을 설명합니다. - ## Tests + ### Tests - 추가되거나 수정된 테스트를 설명합니다. 작성 규칙: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b7cf073..cdb02ca 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,4 +12,5 @@ ## Follow-up Tasks >추후 작업 필요한 사항이 있다면 작성해주세요 (선택) +## Summary By CodeRabbit @coderabbitai 요약 From 4d9ad07ddd9331099677e681417cd5392fea7a3f Mon Sep 17 00:00:00 2001 From: Hwa Young Yang <110398814+young0311@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:43:08 +0900 Subject: [PATCH 4/6] =?UTF-8?q?docs:=20CLAUDE=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20*=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8?= =?UTF-8?q?=20=EA=B0=84=EB=9E=B5=20=EC=84=A4=EB=AA=85=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=20=EB=8B=A8=EA=B3=84=20=EC=A0=95=EC=9D=98=20?= =?UTF-8?q?*=20Dev=20=ED=99=98=EA=B2=BD=20=EA=B8=B0=EC=A4=80=20CICD=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=AC=B8=EC=84=9C=20=EB=82=B4=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=20*=20=ED=98=84=EC=9E=AC=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EC=9E=91=EC=97=85=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=B0=98=EB=93=9C=EC=8B=9C=20=EC=A7=80=EC=BC=9C=EC=95=BC=20?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B7=9C=EC=B9=99=EC=9D=84=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=EB=82=B4=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/commands/add-error-code.md | 0 CLAUDE.md | 99 ++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 .claude/commands/add-error-code.md create mode 100644 CLAUDE.md diff --git a/.claude/commands/add-error-code.md b/.claude/commands/add-error-code.md new file mode 100644 index 0000000..e69de29 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1ad3c9a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,99 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Summary +**framey** is a photo collage platform API built with Spring Boot 3, PostgreSQL. This API helps users easily capture and express the moments they wish to preserve by allowing them to select photos and a theme, from which it then generates a random collage. + +## Essential Commands + +### Build +- `./gradlew build` — 전체 모듈 빌드 (컴파일 + Checkstyle + 테스트) +- `./gradlew :framey-application:member-service:build` — 특정 서비스만 빌드 + +### Run +- `./gradlew :framey-application:member-service:bootRun` — member-service 실행 +- `./gradlew :framey-application:collage-service:bootRun` — collage-service 실행 +- `./gradlew :framey-application:archive-service:bootRun` — archive-service 실행 + +### Test +- `./gradlew test` — 전체 테스트 (JUnit 5) +- `./gradlew :framey-application:member-service:test` — 특정 모듈 테스트 +- `./gradlew :framey-application:member-service:test --tests "ClassName"` — 단일 테스트 클래스 + +### Code Style +- `./gradlew check` — Checkstyle 포함 전체 검증 (naver-checkstyle 규칙, violation 0건 요구) +- Checkstyle 리포트: 각 모듈 `build/reports/checkstyle/` 하위에 생성 + +### Local Infrastructure (Docker) +- `docker compose -f framey-infrastructure/docker/local/docker-compose.local.yaml up -d` — 로컬 인프라 기동 (DB, Redis 등) +- `docker compose -f framey-infrastructure/docker/local/docker-compose.test.yaml up -d` — 테스트용 인프라 +- `docker compose -f framey-infrastructure/docker/local/docker-compose.local.yaml down` — 종료 + + +## Tech Foundation +- **Language/Runtime**: Java 21 (Gradle toolchain, Temurin 21 in CI) +- **Framework**: Spring Boot (BOM managed via `io.spring.dependency-management`) +- **Build**: Gradle 멀티 모듈 ([settings.gradle](cci:7://file:///Users/yanghwayeong/Desktop/framey-server/server/settings.gradle:0:0-0:0) 참고), Spring Boot plugin은 `framey-application/*` 서비스에만 적용 +- **DB**: PostgreSQL (각 서비스 `runtimeOnly 'org.postgresql:postgresql'`) +- **API Docs**: springdoc-openapi 3.1.1 (`framey-web`, Swagger UI) +- **Validation**: spring-boot-starter-validation +- **Code Quality**: Checkstyle 10.26.1, Naver checkstyle 규칙 (`framey-common/checkstyle/naver-checkstyle-rules.xml`), `maxWarnings=0`, `maxErrors=0` — 위반 시 빌드 실패 +- **Test**: JUnit 5 (`useJUnitPlatform`), spring-boot-starter-test, `spring-boot-starter-webmvc-test` (MockMvc용) +- **Monitoring**: `framey-monitoring` 모듈 + Actuator (`/actuator/health`로 health check) + +## Architecture + +### Module Structure +- `framey-common` — 공통 응답(`ApiResponse`), 예외(`BusinessException`, `ErrorCode`), Checkstyle 규칙. 의존성 없는 최하위 모듈. +- `framey-web` — `framey-common`을 `api`로 의존. Web/Validation/Swagger/`CommonExceptionHandler` 공통 웹 계층. +- `framey-monitoring` — 모니터링·Actuator 공통 설정. +- `framey-application/{member,collage,archive}-service` — 각각 독립 실행 가능한 Spring Boot 앱. `framey-common`, `framey-web`을 `api`, `framey-monitoring`을 `implementation`으로 의존. +- `framey-infrastructure` — docker-compose(local/dev/test), nginx, swagger-dev 등 배포 인프라. Gradle 모듈 아님. + +### Dependency Rules +- 서비스 간 직접 의존 금지 — 서비스는 공유 모듈(`framey-common`, `framey-web`, `framey-monitoring`)만 의존. +- `framey-common`/`framey-web`/`framey-monitoring` 변경은 3개 서비스 모두 재배포 트리거 (CI paths-filter의 `shared` 필터). +- 새 코드는 서비스 모듈 내부에 두고, 2개 이상 서비스에서 필요할 때만 공유 모듈로 승격. + +### Error Handling +- 비즈니스 오류는 `BusinessException` + 도메인별 `ErrorCode` enum 사용. +- 공통 오류는 `CommonErrorCode` (`CM-xxx` 형식, 한국어 메시지 + HttpStatus). +- 응답은 `ApiResponse.fail(errorCode)`로 통일. 컨트롤러에서 `try-catch` 하지 않음 — `CommonExceptionHandler`(`@RestControllerAdvice`)가 처리. + +## CI/CD (dev 브랜치 기준) + +### PR CI ([.github/workflows/pr-ci.yaml](cci:7://file:///Users/yanghwayeong/Desktop/framey-server/server/.github/workflows/pr-ci.yaml:0:0-0:0)) +- **트리거**: `dev` 브랜치 대상 PR +- **실행**: `./gradlew clean test check --no-daemon` (Checkstyle + 테스트 통과 필수) +- **인프라**: postgres:16-alpine 서비스 컨테이너, `TEST_DB_URL`/`TEST_DB_USER`/`TEST_DB_PASSWORD` env로 테스트 DB 주입 + +### Dev Deploy ([.github/workflows/dev-deploy.yaml](cci:7://file:///Users/yanghwayeong/Desktop/framey-server/server/.github/workflows/dev-deploy.yaml:0:0-0:0)) +- **트리거**: `dev` 브랜치 push +- **변경 감지**: `dorny/paths-filter`로 서비스별 배포 여부 판단. 공유 모듈(`framey-common`, `framey-web`, `framey-monitoring`, 루트 gradle 파일) 변경 시 전 서비스 재배포. +- **배포**: SSH로 dev EC2 접속 → `git pull` → `docker compose -f framey-infrastructure/docker/dev/docker-compose.dev.yaml up -d --build --no-deps {변경된 서비스}` +- **Health Check**: `/actuator/health`가 `"status":"UP"` 될 때까지 최대 100초 폴링. 실패 시 `docker logs --tail=50` 출력 후 workflow 실패. +- **포트**: member 8081 / collage 8082 / archive 8083 + +### Branch Flow +- feature/* → dev (PR + CI) → dev push 시 자동 dev 배포 +- `main`은 배포 워크플로우 없음 — 프로덕션 배포 정책은 별도 정의 필요 + + +## Language Conventions +- **Checkstyle 준수**: Naver checkstyle 규칙이 빌드에 강제됨. 수정 후 `./gradlew check` 로컬 통과 필수. +- **Lombok 사용**: `@Getter`, `@RequiredArgsConstructor` 등은 이미 허용 패턴. 단 `@Data`, `@Setter`는 사용하지 않는 것을 기본으로. +- **DI**: 생성자 주입 (`@RequiredArgsConstructor` + `final` 필드). 필드/세터 주입 금지. +- **주석/문서**: 기존 코드베이스와 동일하게 한국어 주석 허용. Checkstyle Javadoc 규칙 준수. +- **응답 형식**: 컨트롤러 반환은 `ApiResponse`로 감싸기. raw entity를 직접 노출하지 않음. +- **예외 메시지**: `ErrorCode` enum의 한국어 메시지 사용 — 문자열 하드코딩 예외 메시지 지양. + +## Fix-Feedback Loop +- 빌드/테스트/Checkstyle 실패 시: 실패 원인을 **첫 번째 에러부터** 분석하고, 증상이 아닌 근본 원인을 수정한다. 실패한 작업만 재실행해 확인한다. +- CI(`./gradlew clean test check`)와 로컬 결과가 다르면 환경 차이(DB env, 테스트용 docker-compose)를 먼저 확인한다. +- 같은 유형의 실패가 반복되면 원인을 이 파일의 규칙 섹션에 추가해 재발을 방지한다. + +## Compounding Layer +- 이 문서는 누적 업데이트 대상이다. 리뷰에서 반복 지적된 패턴, CI에서 자주 깨지는 규칙, 새로 도입된 기술(예: Redis, RabbitMQ)은 발견 즉시 해당 섹션에 추가한다. +- 일회성 지시(특정 PR에만 해당하는 맥락)는 여기 적지 않는다 — 반복 적용 가능한 규칙만 추가한다. +- 규칙 추가 시 **근거(실패한 CI 로그, 리뷰 코멘트, 아키텍처 결정)**를 함께 기록한다. From fa744039fb1a4515c5ce4b9eff2c849814f77f59 Mon Sep 17 00:00:00 2001 From: Hwa Young Yang <110398814+young0311@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:43:36 +0900 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20Claude=20AI=20=EC=96=B4=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=84=B4=ED=8A=B8=EC=9A=A9=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=20=EA=B0=80=EC=9D=B4=EB=93=9C=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=8F=20=EC=9E=90=EB=8F=99=ED=99=94=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix-Feedback Loop / Compounding Layer: 반복 오류의 규칙화 절차를 CLAUDE.md에 작성 * .claude/skills 내에는 Controller 규칙, domain 규칙, Event 규칙 등 정리 * .claude/commands/ 내에는 예외처리와 dev 브렌치 기준의 depoly를 정리 --- .claude/commands/add-error-code.md | 36 ++++ .claude/commands/deploy-check.md | 34 ++++ .claude/commands/new-api.md | 77 ++++++++ .claude/skills/api-design/SKILL.md | 94 ++++++++++ .claude/skills/domain-design/SKILL.md | 17 ++ .../inter-service-communication/SKILL.md | 175 ++++++++++++++++++ .claude/skills/persistence/SKILL.md | 16 ++ CLAUDE.md | 91 +++++++-- .../framey/common/response/ApiResponse.java | 13 +- 9 files changed, 532 insertions(+), 21 deletions(-) create mode 100644 .claude/commands/deploy-check.md create mode 100644 .claude/commands/new-api.md create mode 100644 .claude/skills/api-design/SKILL.md create mode 100644 .claude/skills/domain-design/SKILL.md create mode 100644 .claude/skills/inter-service-communication/SKILL.md create mode 100644 .claude/skills/persistence/SKILL.md diff --git a/.claude/commands/add-error-code.md b/.claude/commands/add-error-code.md index e69de29..3db4157 100644 --- a/.claude/commands/add-error-code.md +++ b/.claude/commands/add-error-code.md @@ -0,0 +1,36 @@ +--- +description: Add a domain-specific ErrorCode to the target FRAMEY service following project conventions. +argument-hint: " " +allowed-tools: Read, Grep, Glob, Edit, Write +--- + +# Add ErrorCode + +$ARGUMENTS에 지정된 도메인의 ErrorCode를 추가한다. + +## 절차 + +### 1. 대상 확인 +- 대상 서비스(member/collage/archive)를 확인한다. +- 추가할 오류 상황과 적절한 HttpStatus를 파악한다. +- 불명확하면 사용자에게 질문한다. + +### 2. 기존 코드 분석 +- 대상 서비스의 기존 `{Domain}ErrorCode` enum이 있는지 확인한다 — 있으면 항목만 추가한다. +- 형식 참조: `framey-common/framey/common/exception/CommonErrorCode.java` +- 동일한 의미의 기존 코드가 있는지 확인해 중복 생성을 방지한다. + +### 3. ErrorCode 생성 또는 추가 +- 위치: 해당 서비스의 exception 패키지 (`framey.{domain}.exception`) +- `framey.common.response.ErrorCode` 인터페이스 구현 +- 코드 형식: `{도메인 약자}-###` (예: `MB-001`, `CL-001`, `AR-001`) +- 한국어 메시지 + `HttpStatus` 지정 +- `@Getter` + `@RequiredArgsConstructor` 패턴 사용 + +### 4. 금지 사항 +- `framey-common`의 `CommonErrorCode`에 도메인 전용 코드를 추가하지 않는다 — 공통 모듈 수정은 사용자 승인 필요. +- 문자열 하드코딩 예외 메시지를 코드에 직접 쓰지 않는다. + +### 5. 결과 보고 +- 생성/수정한 파일 +- 추가한 에러 코드 목록 (코드, 메시지, HttpStatus) diff --git a/.claude/commands/deploy-check.md b/.claude/commands/deploy-check.md new file mode 100644 index 0000000..1402c93 --- /dev/null +++ b/.claude/commands/deploy-check.md @@ -0,0 +1,34 @@ +--- +description: Analyze which services will be redeployed on dev push based on changed files and paths-filter rules. +argument-hint: "(optional) " +allowed-tools: Read, Grep, Glob, Bash +--- + +# Deploy Check + +현재 변경 사항이 dev 배포 시 어떤 서비스를 재배포하는지 분석한다. + +## 절차 + +### 1. 변경 파일 수집 +- `git status` / `git diff --name-only`로 변경된 파일 목록을 확인한다. + +### 2. 배포 트리거 판별 +`.github/workflows/dev-deploy.yaml`의 paths-filter 규칙으로 판별한다. + +- **공유 모듈 변경 시 전 서비스 재배포**: + - `framey-common/**`, `framey-web/**`, `framey-monitoring/**` + - `build.gradle`, `settings.gradle`, `gradle/**` +- **서비스별 트리거**: + - `framey-application/member-service/**` → member-dev (포트 8081) + - `framey-application/collage-service/**` → collage-dev (포트 8082) + - `framey-application/archive-service/**` → archive-dev (포트 8083) +- **인프라 트리거**: + - `framey-infrastructure/swagger-dev/**` → swagger-dev + - `framey-infrastructure/nginx/**` → nginx + +### 3. 결과 보고 +- 재배포 대상 서비스 목록 +- 공유 모듈 변경 여부 (변경 시 전체 재배포 경고) +- 배포 후 `/actuator/health` 헬스체크 대상 포트 +- 주의 사항: `main`은 배포 워크플로우 없음 — `dev` push 시에만 배포됨 diff --git a/.claude/commands/new-api.md b/.claude/commands/new-api.md new file mode 100644 index 0000000..90c5c89 --- /dev/null +++ b/.claude/commands/new-api.md @@ -0,0 +1,77 @@ +--- +description: Implement a new API in the specified FRAMEY service following project architecture conventions, including tests and validation. +argument-hint: " " +allowed-tools: Read, Grep, Glob, Edit, Write, Bash +--- + +# New API Implementation + +$ARGUMENTS에 지정된 API를 구현한다. + +## 절차 + +### 1. 대상 확인 +- 대상 서비스(member/collage/archive)를 확인한다. +- 구현할 API의 요구사항을 분석한다. +- 대상 서비스 또는 요구사항이 불명확하면 사용자에게 질문한다. + +### 2. 기존 코드 분석 +- CLAUDE.md의 아키텍처 규칙을 확인한다. +- 대상 서비스의 기존 패키지 구조와 구현 패턴을 분석한다. +- 재사용 가능한 Entity, DTO, Repository, Service를 확인한다. +- 기존 코드의 중복 구현을 방지한다. +- 다른 서비스 및 공유 모듈을 임의로 수정하지 않는다. + +### 3. 관련 Skill 적용 +다음 Skill의 규칙을 확인하고 적용한다. + +필수: +- `.claude/skills/api-design/SKILL.md` + - Controller, Service, DTO 구현 규칙 + +필요한 경우: +- `.claude/skills/domain-design/SKILL.md` + - Entity 생성 또는 변경이 필요한 경우 +- `.claude/skills/persistence/SKILL.md` + - Repository 구현이 필요한 경우 +- `.claude/skills/inter-service-communication/SKILL.md` + - 외부 또는 도메인 간 통신이 필요한 경우 + +### 4. 구현 계획 수립 +- API의 HTTP Method와 Endpoint를 확인한다. +- Request 및 Response 구조를 정의한다. +- 생성 및 수정이 필요한 파일을 식별한다. +- 기존 코드와의 중복 여부를 검토한다. +- 아키텍처 변경 또는 다른 모듈 수정이 필요한 경우 사용자 승인을 받는다. +- 요구사항이 명확하고 기존 구조 내에서 구현 가능한 경우 바로 진행한다. + +### 5. API 구현 +1. 기존 코드 재사용 여부 판단 +2. 필요한 DTO 정의 +3. 필요한 경우 Domain 구현 또는 수정 +4. 데이터 접근이 필요한 경우 Repository Port 및 Adapter 구현 +5. Service 비즈니스 로직 구현 +6. Controller 구현 +7. 도메인 ErrorCode 및 BusinessException 적용 +8. 기존 프로젝트 규칙에 따른 API 문서화 + +### 6. 테스트 +- Controller 테스트 작성 +- Service 테스트 작성 +- 필요한 경우 Repository 통합 테스트 작성 +- 성공 및 실패 시나리오 검증 +- 기존 테스트에 영향을 주는 변경 사항 확인 + +### 7. 검증 +- 대상 서비스의 Gradle check 작업을 실행한다. +- 검증에 실패하면 원인을 분석하고 수정 후 재검증한다. +- 동일한 오류가 반복되거나 작업 범위 밖의 문제가 발견되면 사용자에게 보고한다. +- 테스트를 실행하지 못한 경우 성공했다고 보고하지 않는다. +- 검증을 위해 다른 서비스나 공유 모듈을 임의로 수정하지 않는다. + +### 8. 결과 보고 +- 생성 및 수정한 파일 목록 +- 구현한 API 목록 (HTTP Method, Endpoint) +- 작성한 테스트 및 실행 결과 +- 아키텍처 규칙 준수 여부 +- 해결되지 않은 문제 및 추가 검토 사항 diff --git a/.claude/skills/api-design/SKILL.md b/.claude/skills/api-design/SKILL.md new file mode 100644 index 0000000..2025fe8 --- /dev/null +++ b/.claude/skills/api-design/SKILL.md @@ -0,0 +1,94 @@ +--- +name: api-design +description: Controller, Service, DTO implementation rules for FRAMEY services. Apply when implementing API endpoints. +--- + +# API Design + +## Controller +- `@RestController` + `@RequestMapping` 사용 +- 반환형은 `ResponseEntity>` — raw Entity, `Map` 직접 반환 금지 +- 비즈니스 로직 작성 금지 — Service로 위임 +- `try-catch`로 예외 응답을 만들지 않는다 — `CommonExceptionHandler`가 처리 +- 입력 검증은 `@Valid` + DTO 제약 어노테이션 사용 +- 인증된 사용자 정보는 `@AuthUser`로 주입 +- Swagger 어노테이션(`@Operation`, `@ApiResponse` 등)은 Controller에 직접 붙이지 않고 별도 문서화 인터페이스(예: `{Domain}ApiDocs`)에 선언하고 Controller가 구현하게 하여 Controller를 깔끔하게 유지한다. + +### Controller 작성 규칙 +- 성공 응답은 `ApiResponse`의 static 메서드를 static import한다. **데이터 없음**: `success(SuccessCode)`, **데이터 있음**: `successWithData(SuccessCode, data)`. `SuccessCode`는 `framey.common.response.SuccessCode` enum (OK, CREATED, UPDATED, DELETED). +- 메서드 파라미터는 `final`로 선언한다. +- PathVariable/RequestBody/AuthUser 순서는 기존 패턴을 따른다. + +예시: +```java +@DeleteMapping("/{productId}") +public ResponseEntity> deleteProduct( + @AuthUser final Long memberId, + @PathVariable final Long productId +) { + productService.deleteProduct(memberId, productId); + return ResponseEntity.ok(success(SuccessCode.DELETED)); +} + +@GetMapping("/{productId}") +public ResponseEntity> getMemberProduct( + @AuthUser final Long memberId, + @PathVariable final Long productId +) { + GetMemberProductResponse product = productService.findMemberProduct(memberId, productId); + return ResponseEntity.ok(successWithData(SuccessCode.OK, product)); +} +``` + +## Service +- `@Service` + `@RequiredArgsConstructor` 생성자 주입 (`final` 필드) +- 조회 전용 메서드는 `@Transactional(readOnly = true)` +- 데이터 변경 메서드는 쓰기 가능한 `@Transactional` +- 오류는 `BusinessException` + 도메인 `ErrorCode`로 던진다 +- 조회 결과가 없을 때는 `orElseThrow(() -> new BusinessException(XxxErrorCode.NOT_FOUND))` 패턴 사용 +- Entity → Result 변환은 `map(XxxResult::from)` 등 Result의 static factory로 수행 + +예시: +```java +public MemberInfo getProfile(final Long memberId) { + return memberRepository.findById(memberId) + .map(MemberInfo::from) + .orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND)); +} +``` + +## DTO Mapping +- Controller는 Request DTO를 Application Command로 변환한다. +- Service는 Application Command를 입력받아 비즈니스 로직을 수행한다. +- Service는 Application Result를 반환한다. +- Controller는 Application Result를 Response DTO로 변환한다. +- Service는 Controller 전용 DTO에 의존하지 않는다. +- Entity를 Controller에 전달하거나 API 응답으로 직접 반환하지 않는다. + +### Response DTO 작성 규칙 +- Java `record` 사용. +- DTO 필드에 `@Schema(description = ..., requiredMode = Schema.RequiredMode.REQUIRED)`를 붙여 Swagger 스키마를 정의한다 (DTO 수준의 `@Schema`는 허용 — Controller의 운영 어노테이션과는 구분). +- Result → Response 변환은 DTO 내부의 static factory `from(final XxxResult result)`으로 수행한다. + +예시: +```java +@Schema(description = "사용자 정보 응답") +public record MemberResponse( + @Schema(description = "사용자 ID", requiredMode = Schema.RequiredMode.REQUIRED) Long memberId, + @Schema(description = "닉네임", requiredMode = Schema.RequiredMode.REQUIRED) String nickname +) { + public static MemberResponse from(final MemberResult result) { + return new MemberResponse(result.memberId(), result.nickname()); + } +} +``` + +## Exception +- 비즈니스 오류는 `BusinessException` + 도메인별 `ErrorCode` enum으로 던진다. +- 도메인 `ErrorCode`는 각 서비스 모듈의 exception 패키지에 생성한다 (`{도메인 약자}-###`, `framey.common.response.ErrorCode` 구현). 형식 참조: `CommonErrorCode.java`. +- Controller/Service에서 `try-catch`로 응답을 만들지 않는다 — `CommonExceptionHandler`가 처리한다. + +## 참조 +- 응답 래퍼: `framey-common/framey/common/response/ApiResponse.java` +- 예외: `framey-common/framey/common/exception/BusinessException.java` +- 공통 에러 코드 형식: `framey-common/framey/common/exception/CommonErrorCode.java` diff --git a/.claude/skills/domain-design/SKILL.md b/.claude/skills/domain-design/SKILL.md new file mode 100644 index 0000000..87da76b --- /dev/null +++ b/.claude/skills/domain-design/SKILL.md @@ -0,0 +1,17 @@ +--- +name: domain-design +description: Entity and domain model rules for FRAMEY services. Apply when creating or modifying entities. +--- + +# Domain Design + +## Entity +- 프로젝트 `BaseEntity`를 상속한다 (없으면 생성 전 사용자에게 확인) +- Entity 간 불필요한 양방향 연관관계를 생성하지 않는다 — 필요한 방향만 단방향 +- 다른 도메인(서비스)의 Entity/Repository를 직접 참조하지 않는다 — 다른 도메인 식별자는 ID 값으로만 참조 +- `@Setter` / `@Data` 사용 금지 — 상태 변경은 의도 있는 도메인 메서드로 수행 +- 생성은 생성자 또는 정적 팩터리 메서드로 제한한다 + +## 패키지 +- 새 Entity는 해당 서비스(`framey.{domain}`)의 domain 패키지에 배치 +- 도메인별 `ErrorCode` enum은 서비스 내 exception 패키지에 배치 diff --git a/.claude/skills/inter-service-communication/SKILL.md b/.claude/skills/inter-service-communication/SKILL.md new file mode 100644 index 0000000..8acf4c7 --- /dev/null +++ b/.claude/skills/inter-service-communication/SKILL.md @@ -0,0 +1,175 @@ +--- +name: inter-service-communication +description: Apply when implementing HTTP communication or RabbitMQ events between FRAMEY services or external systems. +--- + +# Inter-Service Communication + +## 절차 + +### 1. Purpose +FRAMEY 서비스 간 통신 및 외부 시스템 연동 시 계층 간 의존성을 분리하고, 통신 계약을 일관되게 유지한다. + +이 Skill은 다음 작업에 적용한다. +- 서비스 간 HTTP 통신 +- 외부 API 연동 +- RabbitMQ 이벤트 발행 +- RabbitMQ 이벤트 수신 +- 이벤트 DTO 생성 및 변경 +- 메시지 처리 실패 및 재시도 정책 구현 + +### 2. Dependency Rules +- 서비스 간 직접적인 Gradle 모듈 의존을 금지한다. +- 다른 서비스의 Entity, Repository, Service를 직접 참조하지 않는다. +- 통신은 Application Port를 통해 수행한다. +- 통신 기술 구현은 Infrastructure 계층에 배치한다. +- Application 및 Domain 계층에서 HTTP Client 또는 RabbitMQ 구현 클래스를 직접 사용하지 않는다. +- 다른 서비스의 통신 DTO를 직접 import하지 않는다. + +### 3. HTTP Communication +#### Structure + +```text +application/ +└── port/ + └── MemberClientPort.java + +infrastructure/ +└── client/ + ├── MemberHttpClientAdapter.java + └── dto/ + ├── MemberClientRequest.java + └── MemberClientResponse.java +``` + +#### Rules +- 외부 시스템 및 동기 서비스 간 통신은 HTTP를 사용한다. +- HTTP Client 구현은 `infrastructure/client`에 배치한다. +- HTTP Request/Response DTO는 `infrastructure/client/dto`에 배치한다. +- HTTP 응답 DTO를 Application 또는 Domain 계층에 직접 노출하지 않는다. +- Client Adapter에서 HTTP 응답 DTO를 Application 모델로 변환한다. +- Application Service는 HTTP Client Adapter가 아닌 Port에 의존한다. +- HTTP Client에는 연결 및 응답 Timeout을 설정한다. +- 외부 API 오류는 Application 계층에서 처리 가능한 예외로 변환한다. +- 재시도는 멱등성이 보장되는 요청에 한해 적용한다. +- 외부 API 응답 DTO를 Domain Entity로 직접 사용하지 않는다. + +### 4. RabbitMQ Communication +#### Structure + +```text +application/ +└── port/ + └── CollageEventPort.java + +infrastructure/ +└── event/ + ├── dto/ + │ └── CollageCreatedEventV1.java + ├── publisher/ + │ └── CollageEventPublisher.java + └── handler/ + └── CollageCreatedEventHandler.java +``` + +필요한 역할만 구현하며, 사용하지 않는 패키지는 미리 생성하지 않는다. + +#### Event DTO +- RabbitMQ 이벤트 DTO는 `infrastructure/event/dto`에 배치한다. +- 이벤트 DTO는 Java Record로 작성한다. +- 이벤트 Payload에 JPA Entity를 직접 포함하지 않는다. +- 이벤트 계약의 소유권은 발행 서비스에 있다. +- 발행 서비스는 이벤트 이름, 버전, Payload 스키마를 정의한다. +- 구독 서비스는 해당 계약에 맞는 수신 DTO를 자체적으로 정의한다. +- 이벤트 DTO는 서비스별로 독립적으로 관리하지만 동일한 메시지 계약을 준수한다. +- 이벤트 이름과 버전을 명시한다. +- 이벤트 스키마 변경 시 기존 구독자와의 호환성을 검토한다. + +#### Publisher +- 이벤트 발행 구현은 `infrastructure/event/publisher`에 배치한다. +- Application 계층은 이벤트 발행 Port에 의존한다. +- Publisher는 해당 Port를 구현한다. +- Application Service에서 `RabbitTemplate`을 직접 사용하지 않는다. +- 메시지 발행에 필요한 Exchange, Routing Key 등은 Infrastructure 설정에서 관리한다. +- 발행 실패 시 처리 정책을 명확히 한다. + +#### Handler +- 이벤트 수신 Handler는 `infrastructure/event/handler`에 배치한다. +- Handler는 메시지 수신 및 DTO 변환을 담당한다. +- 비즈니스 로직은 Application Service에 위임한다. +- Handler에서 Repository를 직접 호출하지 않는다. +- Handler에서 Entity를 직접 생성하거나 수정하지 않는다. +- Handler는 수신한 이벤트 DTO를 Application Command로 변환한다. +- Application Service는 RabbitMQ 메시지 DTO에 직접 의존하지 않는다. + +### 5. Event Contract +이벤트 계약은 발행 서비스가 소유한다. +예시: + +```json +{ + "eventType": "collage.created", + "eventVersion": 1, + "collageId": 100, + "memberId": 10 +} +``` + +#### Rules +- 이벤트 이름은 명확한 과거형 사실을 표현한다. +- 이벤트 버전을 관리한다. +- 이벤트 Payload는 필요한 데이터만 포함한다. +- 이벤트 계약 변경 시 구독 서비스와의 호환성을 검토한다. +- 호환되지 않는 변경은 새로운 이벤트 버전 도입을 검토한다. +- 도메인별 이벤트 DTO를 `framey-common`에 추가하지 않는다. + +### 6. Transaction & Event Publishing + +- DB 트랜잭션과 RabbitMQ 메시지 발행은 기본적으로 하나의 원자적 트랜잭션이 아니다. +- 트랜잭션 커밋 전에 이벤트를 발행하면 롤백된 데이터를 다른 서비스가 처리하게 되므로, 커밋 후 발행(예: `@TransactionalEventListener(phase = AFTER_COMMIT)`)을 기본으로 한다. +- DB 변경과 이벤트 발행의 일관성이 필요한 경우 Transactional Outbox 패턴을 검토한다. +- 트랜잭션 커밋 이후 이벤트를 발행하더라도 메시지 전달이 보장되는 것은 아니다. +- 이벤트 발행 실패 시 복구 방법을 검토한다. + +### 7. Reliability + +#### Idempotency +- RabbitMQ 메시지는 중복 전달될 수 있다고 가정한다. +- 메시지 처리 로직은 멱등성을 고려하여 구현한다. +- 동일 이벤트를 여러 번 수신해도 데이터가 중복 생성되지 않도록 한다. +- 필요한 경우 이벤트 ID를 사용하여 중복 처리를 방지한다. + +#### Retry & DLQ +- 메시지 처리 실패 시 재시도 횟수와 간격을 정의한다. +- 반복 실패한 메시지는 DLQ로 이동하도록 구성한다. +- 무한 재시도로 메시지가 반복 처리되지 않도록 한다. +- 재시도 대상 오류와 재시도하지 않을 오류를 구분한다. + +#### ACK / NACK +- 메시지 처리 성공 여부에 따라 ACK/NACK 정책을 정의한다. +- 메시지 처리 실패를 단순히 로그만 남기고 정상 처리로 간주하지 않는다. +- ACK/NACK 정책은 Listener Container 설정과 일치해야 한다. + +### 8. Modification Rules +- 다른 서비스의 이벤트 DTO를 직접 import하지 않는다. +- 이벤트 스키마 변경 시 발행자와 구독자의 호환성을 검토한다. +- 이벤트 계약 변경이 필요한 경우 사용자에게 먼저 알린다. +- 공유 모듈에 도메인별 이벤트 DTO를 추가하지 않는다. +- 새로운 통신 기술 도입이 필요한 경우 사용자 승인을 받는다. +- 기존 메시지 계약을 변경할 때는 영향받는 서비스를 확인한다. + +### 9. Validation +HTTP 통신 구현 시: +- 정상 응답을 검증한다. +- Timeout 상황을 검증한다. +- 외부 API 오류 응답을 검증한다. +- DTO 변환 결과를 검증한다. + +**RabbitMQ 구현 시:** +- 이벤트 직렬화 및 역직렬화를 검증한다. +- Publisher의 메시지 발행 동작을 검증한다. +- Handler의 Application Service 위임을 검증한다. +- 중복 메시지 처리 시나리오를 검증한다. +- 재시도 및 DLQ 설정이 존재하는 경우 실패 시나리오를 검증한다. + +검증 결과는 실제 실행 여부와 함께 보고한다. diff --git a/.claude/skills/persistence/SKILL.md b/.claude/skills/persistence/SKILL.md new file mode 100644 index 0000000..50ead7b --- /dev/null +++ b/.claude/skills/persistence/SKILL.md @@ -0,0 +1,16 @@ +--- +name: persistence +description: Repository Port/Adapter rules for FRAMEY services. Apply when implementing data access. +--- + +# Persistence + +## Repository (Port/Adapter) +- **Port**: 도메인이 필요로 하는 조회/저장 인터페이스 — 도메인 측에 정의 +- **Adapter**: Port 구현체 + JPA Repository — 인프라 측에 배치 +- Service는 JPA Repository가 아닌 Port에 의존한다 +- 다른 서비스의 Port/Adapter에 접근하지 않는다 + +## 규칙 +- 조회 전용 작업은 `@Transactional(readOnly = true)` 경계(Service)에서 관리 +- 다른 도메인의 데이터가 필요하면 Repository 직접 참조 대신 통신 인터페이스를 사용한다 diff --git a/CLAUDE.md b/CLAUDE.md index 1ad3c9a..7c8c2f8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Summary -**framey** is a photo collage platform API built with Spring Boot 3, PostgreSQL. This API helps users easily capture and express the moments they wish to preserve by allowing them to select photos and a theme, from which it then generates a random collage. +**framey** is a photo collage platform API built with Spring Boot 4, PostgreSQL. This API helps users easily capture and express the moments they wish to preserve by allowing them to select photos and a theme, from which it then generates a random collage. ## Essential Commands @@ -26,15 +26,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Checkstyle 리포트: 각 모듈 `build/reports/checkstyle/` 하위에 생성 ### Local Infrastructure (Docker) -- `docker compose -f framey-infrastructure/docker/local/docker-compose.local.yaml up -d` — 로컬 인프라 기동 (DB, Redis 등) +- `docker compose -f framey-infrastructure/docker/local/docker-compose.local.yaml up -d` — 로컬 인프라 기동 (PostgreSQL 16, host 5433→container 5432) - `docker compose -f framey-infrastructure/docker/local/docker-compose.test.yaml up -d` — 테스트용 인프라 - `docker compose -f framey-infrastructure/docker/local/docker-compose.local.yaml down` — 종료 ## Tech Foundation - **Language/Runtime**: Java 21 (Gradle toolchain, Temurin 21 in CI) -- **Framework**: Spring Boot (BOM managed via `io.spring.dependency-management`) -- **Build**: Gradle 멀티 모듈 ([settings.gradle](cci:7://file:///Users/yanghwayeong/Desktop/framey-server/server/settings.gradle:0:0-0:0) 참고), Spring Boot plugin은 `framey-application/*` 서비스에만 적용 +- **Framework**: Spring Boot 4.1.0 (BOM managed via `io.spring.dependency-management`) +- **Build**: Gradle 멀티 모듈 (`settings.gradle` 참고), Spring Boot plugin은 `framey-application/*` 서비스에만 적용 - **DB**: PostgreSQL (각 서비스 `runtimeOnly 'org.postgresql:postgresql'`) - **API Docs**: springdoc-openapi 3.1.1 (`framey-web`, Swagger UI) - **Validation**: spring-boot-starter-validation @@ -54,21 +54,75 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ### Dependency Rules - 서비스 간 직접 의존 금지 — 서비스는 공유 모듈(`framey-common`, `framey-web`, `framey-monitoring`)만 의존. - `framey-common`/`framey-web`/`framey-monitoring` 변경은 3개 서비스 모두 재배포 트리거 (CI paths-filter의 `shared` 필터). -- 새 코드는 서비스 모듈 내부에 두고, 2개 이상 서비스에서 필요할 때만 공유 모듈로 승격. +- 새 코드는 해당 서비스 모듈 내부에 작성한다. +- 2개 이상 서비스에서 사용하는 기술적 공통 기능만 공유 모듈 승격을 검토한다. +- 도메인별 Entity, DTO, Event는 공유 모듈로 이동하지 않는다. +- 공유 모듈 변경은 사용자 승인 후 진행한다. + +### Domain +- JPA Entity는 프로젝트 BaseEntity를 상속한다. +- Entity 간 불필요한 양방향 연관관계를 생성하지 않는다. +- 다른 서비스의 Entity/Repository를 직접 참조하지 않는다. +- 다른 서비스의 Entity와 JPA 연관관계를 생성하지 않는다. +- 다른 서비스의 식별자는 ID 값으로 참조한다. +- 동일 서비스 내부의 도메인 간 의존성은 해당 서비스의 도메인 경계에 따른다. + +### Application +- Controller에 비즈니스 로직을 작성하지 않는다. +- Service는 `@Service` + `@RequiredArgsConstructor` 생성자 주입을 사용한다. +- 조회 전용 작업에는 `@Transactional(readOnly = true)`, 변경 작업에는 쓰기 트랜잭션을 사용한다. +- Controller용 DTO와 Service용 DTO를 분리한다. Entity를 응답으로 반환하지 않는다. + +### Inter-service Communication + +- 서비스 간 직접적인 코드 의존을 금지한다. +- 다른 서비스의 Entity, Repository, Service를 직접 참조하지 않는다. +- 서비스 간 통신은 정의된 통신 계약을 통해 수행한다. +- 외부 시스템 및 동기 서비스 간 통신은 HTTP를 사용한다. +- 서비스 간 비동기 이벤트 전달은 RabbitMQ를 사용한다. + +### Infrastructure Structure + +각 서비스는 자신의 Infrastructure 구현을 독립적으로 관리한다. + +- `infrastructure/persistence`: Repository Adapter 및 JPA 구현 +- `infrastructure/client`: HTTP Client 구현 +- `infrastructure/client/dto`: HTTP 통신 전용 DTO +- `infrastructure/event/dto`: RabbitMQ 이벤트 Payload DTO +- `infrastructure/event/publisher`: 이벤트 발행 Adapter +- `infrastructure/event/handler`: 이벤트 수신 Handler + +### Event Rules + +- 이벤트 DTO는 각 서비스 내부에서 관리한다. +- 도메인별 이벤트 DTO를 framey-common에 추가하지 않는다. +- 이벤트 Payload에 Entity를 직접 포함하지 않는다. +- 이벤트 계약은 발행자와 구독자가 동일하게 해석할 수 있어야 한다. +- 이벤트 스키마 변경 시 기존 구독자와의 호환성을 검토한다. +- 이벤트 Handler에서는 비즈니스 로직을 작성하지 않는다. +- Handler는 이벤트 DTO를 Application DTO로 변환하여 Service에 위임한다. +- 이벤트 발행은 Application Port를 통해 수행한다. +- RabbitMQ 관련 구현은 Infrastructure 계층에 둔다. +- 메시지 중복 전달을 가정하고 멱등성을 고려한다. 실패 시 재시도/DLQ 정책을 정의한다. +- 이벤트 발행은 트랜잭션 커밋 후에 수행한다 — 상세는 `inter-service-communication` skill 참조. + +### 현재 상태 +- 각 서비스는 아직 `Application` 클래스만 있는 초기 상태. 기존 패턴이 없으면 `.claude/skills/`의 규칙을 기준 패턴으로 삼는다. ### Error Handling - 비즈니스 오류는 `BusinessException` + 도메인별 `ErrorCode` enum 사용. -- 공통 오류는 `CommonErrorCode` (`CM-xxx` 형식, 한국어 메시지 + HttpStatus). -- 응답은 `ApiResponse.fail(errorCode)`로 통일. 컨트롤러에서 `try-catch` 하지 않음 — `CommonExceptionHandler`(`@RestControllerAdvice`)가 처리. +- 도메인 `ErrorCode`는 각 서비스 모듈에 생성한다 (`{도메인 약자}-###` 형식, `framey-common`의 `ErrorCode` 인터페이스 구현). `framey-common`에는 공통 코드(`CommonErrorCode`, `CM-xxx`)만 둔다. +- 컨트롤러에서 `try-catch`로 예외 응답을 만들지 않는다 — `CommonExceptionHandler`(`@RestControllerAdvice`)가 처리한다. +- 응답은 `ApiResponse.fail(errorCode)`로 통일. 성공 응답: 데이터 없으면 `success(SuccessCode)`, 데이터 있으면 `successWithData(SuccessCode, data)` (`SuccessCode` enum: OK, CREATED, UPDATED, DELETED). ## CI/CD (dev 브랜치 기준) -### PR CI ([.github/workflows/pr-ci.yaml](cci:7://file:///Users/yanghwayeong/Desktop/framey-server/server/.github/workflows/pr-ci.yaml:0:0-0:0)) +### PR CI (`.github/workflows/pr-ci.yaml`) - **트리거**: `dev` 브랜치 대상 PR - **실행**: `./gradlew clean test check --no-daemon` (Checkstyle + 테스트 통과 필수) - **인프라**: postgres:16-alpine 서비스 컨테이너, `TEST_DB_URL`/`TEST_DB_USER`/`TEST_DB_PASSWORD` env로 테스트 DB 주입 -### Dev Deploy ([.github/workflows/dev-deploy.yaml](cci:7://file:///Users/yanghwayeong/Desktop/framey-server/server/.github/workflows/dev-deploy.yaml:0:0-0:0)) +### Dev Deploy (`.github/workflows/dev-deploy.yaml`) - **트리거**: `dev` 브랜치 push - **변경 감지**: `dorny/paths-filter`로 서비스별 배포 여부 판단. 공유 모듈(`framey-common`, `framey-web`, `framey-monitoring`, 루트 gradle 파일) 변경 시 전 서비스 재배포. - **배포**: SSH로 dev EC2 접속 → `git pull` → `docker compose -f framey-infrastructure/docker/dev/docker-compose.dev.yaml up -d --build --no-deps {변경된 서비스}` @@ -85,15 +139,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Lombok 사용**: `@Getter`, `@RequiredArgsConstructor` 등은 이미 허용 패턴. 단 `@Data`, `@Setter`는 사용하지 않는 것을 기본으로. - **DI**: 생성자 주입 (`@RequiredArgsConstructor` + `final` 필드). 필드/세터 주입 금지. - **주석/문서**: 기존 코드베이스와 동일하게 한국어 주석 허용. Checkstyle Javadoc 규칙 준수. -- **응답 형식**: 컨트롤러 반환은 `ApiResponse`로 감싸기. raw entity를 직접 노출하지 않음. +- **응답 형식**: 컨트롤러 반환은 `ResponseEntity>`. raw entity를 직접 노출하지 않음. +- **DTO**: Java Record 사용, Request/Response 분리. 인증된 사용자 정보는 `@AuthUser`로 주입. +- **API 문서**: Swagger/springdoc 어노테이션은 별도 문서화 인터페이스에 분리하여 Controller를 깔끔하게 유지한다. - **예외 메시지**: `ErrorCode` enum의 한국어 메시지 사용 — 문자열 하드코딩 예외 메시지 지양. ## Fix-Feedback Loop -- 빌드/테스트/Checkstyle 실패 시: 실패 원인을 **첫 번째 에러부터** 분석하고, 증상이 아닌 근본 원인을 수정한다. 실패한 작업만 재실행해 확인한다. -- CI(`./gradlew clean test check`)와 로컬 결과가 다르면 환경 차이(DB env, 테스트용 docker-compose)를 먼저 확인한다. -- 같은 유형의 실패가 반복되면 원인을 이 파일의 규칙 섹션에 추가해 재발을 방지한다. +- 빌드/테스트/Checkstyle 실패 시 첫 번째 오류부터 분석한다. +- 실패 원인을 수정한 뒤 관련 검증을 재실행한다. +- 작업 범위 밖의 오류가 발생하면 임의로 다른 모듈을 수정하지 않는다. +- 반복되는 오류는 원인과 해결 방법을 결과 보고에 기록한다. ## Compounding Layer -- 이 문서는 누적 업데이트 대상이다. 리뷰에서 반복 지적된 패턴, CI에서 자주 깨지는 규칙, 새로 도입된 기술(예: Redis, RabbitMQ)은 발견 즉시 해당 섹션에 추가한다. -- 일회성 지시(특정 PR에만 해당하는 맥락)는 여기 적지 않는다 — 반복 적용 가능한 규칙만 추가한다. -- 규칙 추가 시 **근거(실패한 CI 로그, 리뷰 코멘트, 아키텍처 결정)**를 함께 기록한다. +- 반복적으로 발생하는 문제는 재사용 가능한 규칙으로 정리한다. +- CLAUDE.md 및 Skill 수정이 필요한 경우 변경안을 제안한다. +- 사용자 승인 없이 공통 규칙을 변경하지 않는다. +- 승인된 변경은 PR 리뷰를 통해 반영한다. +- 일회성 작업 맥락은 공통 규칙에 추가하지 않는다. diff --git a/framey-common/src/main/java/framey/common/response/ApiResponse.java b/framey-common/src/main/java/framey/common/response/ApiResponse.java index dd9267f..92e5571 100644 --- a/framey-common/src/main/java/framey/common/response/ApiResponse.java +++ b/framey-common/src/main/java/framey/common/response/ApiResponse.java @@ -10,13 +10,15 @@ public class ApiResponse { private String code; - private String message; @JsonInclude(JsonInclude.Include.NON_NULL) private T data; - public static ApiResponse success(final SuccessCode successCode) { + //공통 + public static ApiResponse success( + final SuccessCode successCode + ) { return new ApiResponse<>( successCode.getValue(), successCode.getMessage(), @@ -24,13 +26,14 @@ public static ApiResponse success(final SuccessCode successCode) { ); } - public static ApiResponse success( + //data + public static ApiResponse successWithData( final SuccessCode successCode, final T data ) { return new ApiResponse<>( - successCode.getValue(), - successCode.getMessage(), + successCode.getValue(), //code + successCode.getMessage(), //message data ); } From 0ff8abeabbdfd09694df448e8ea000a93197aad3 Mon Sep 17 00:00:00 2001 From: Hwa Young Yang <110398814+young0311@users.noreply.github.com> Date: Sun, 8 Nov 2026 04:24:26 +0900 Subject: [PATCH 6/6] =?UTF-8?q?deploy:=20=EB=88=84=EC=A0=81=20SHA=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20dev=20=EB=B0=B0=ED=8F=AC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=ED=94=84=EB=9D=BC=EC=9D=B8=20=EC=9E=AC=EA=B5=AC?= =?UTF-8?q?=EC=84=B1=20*=20=EB=B0=B0=ED=8F=AC=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20workflow=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=EC=84=9C=20EC2=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=202=EB=8B=A8=EA=B3=84=EB=A1=9C=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EB=B3=80=EA=B2=BD=20=EA=B0=90=EC=A7=80?= =?UTF-8?q?=C2=B7=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=A0=9C=EC=96=B4=C2=B7?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A4=80=EC=9C=BC=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20*=20=20EC2=20=EA=B3=A0=EC=A0=95=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=8B=A4=ED=96=89=EA=B8=B0=20deploy-dispatch.sh=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20*=20certbot=20=EA=B0=B1=EC=8B=A0=EC=9D=84?= =?UTF-8?q?=20systemd=20timer=EB=A1=9C=20=EC=A0=84=ED=99=98=20*=20=20CLAUD?= =?UTF-8?q?E.md=20dev=20=EB=B0=B0=ED=8F=AC=20=EA=B5=AC=EC=A1=B0=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EA=B0=B1=EC=8B=A0=20*=20Nginx=20default.c?= =?UTF-8?q?onf=20=ED=94=84=EB=A1=9D=EC=8B=9C=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=ED=99=94=20=EB=B0=8F=20HTTPS=20/health=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .githooks/pre-commit | 68 +++ .githooks/pre-push | 52 ++ .github/workflows/dev-deploy.yaml | 136 +----- CLAUDE.md | 23 +- .../docker/dev/docker-compose.dev.yaml | 23 +- framey-infrastructure/nginx/dev/default.conf | 105 ++--- framey-infrastructure/scripts/deploy-dev.sh | 445 ++++++++++++++++++ .../scripts/deploy-dispatch.sh | 60 +++ .../systemd/framey-certbot-renew.service | 9 + .../systemd/framey-certbot-renew.timer | 12 + 10 files changed, 757 insertions(+), 176 deletions(-) create mode 100644 .githooks/pre-commit create mode 100644 .githooks/pre-push create mode 100755 framey-infrastructure/scripts/deploy-dev.sh create mode 100755 framey-infrastructure/scripts/deploy-dispatch.sh create mode 100644 framey-infrastructure/systemd/framey-certbot-renew.service create mode 100644 framey-infrastructure/systemd/framey-certbot-renew.timer diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..fbe8695 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,68 @@ +#!/bin/bash +#set -euo pipefail +#ROOT_DIR="$(git rev-parse --show-toplevel)" +#cd "$ROOT_DIR" + +echo "📸 FRAMEY pre-commit 실행" + +echo "🚀 스테이징된 Java파일만 확인 중.. " +mapfile -t CHANGED_FILES < <( + git diff --cached --name-only --diff-filter=ACMR | + grep '\.java$' || true +) + +if [ ${#CHANGED_FILES[@]} -eq 0 ]; then + echo "➡️ 파일 변경 사항 없어 검사 생략.." + exit 0 +fi + +# 중복 Gradle Task 방지 +declare -A TASK_MAP + +for file in "${CHANGED_FILES[@]}"; do + case "$file" in + framey-common/*) + TASK_MAP[":framey-common:checkstyleMain"]=1 + ;; + framey-web/*) + TASK_MAP[":framey-web:checkstyleMain"]=1 + ;; + framey-monitoring/*) + TASK_MAP[":framey-monitoring:checkstyleMain"]=1 + ;; + framey-application/member-service/*) + TASK_MAP[":framey-application:member-service:checkstyleMain"]=1 + ;; + framey-application/collage-service/*) + TASK_MAP[":framey-application:collage-service:checkstyleMain"]=1 + ;; + framey-application/archive-service/*) + TASK_MAP[":framey-application:archive-service:checkstyleMain"]=1 + ;; + esac +done + +if [ ${#TASK_MAP[@]} -eq 0 ]; then + echo "☑️ 검사 대상 모듈 없음 ..." + exit 0 +fi + +TASKS=("${!TASK_MAP[@]}") + +echo "⚠️ Checkstyle 검사 대상:" +printf ' %s\n' "${TASKS[@]}" + +./gradlew "${TASKS[@]}" --no-daemon || { + echo "⛔️Checkstyle 실패 — 커밋 중단 (상세: build/reports/checkstyle/)" + exit 1 +} +echo "🔥 pre-commit 통과" +# test-code +#case "$file" in + # */src/main/java/*) + # TASK_MAP[":${module}:checkstyleMain"]=1 + # ;; + # */src/test/java/*) + # TASK_MAP[":${module}:checkstyleTest"]=1 + # ;; +# esac diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..7c39596 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,52 @@ +#!/bin/bash +#set -euo pipefail +#ROOT_DIR="$(git rev-parse --show-toplevel)" +#cd "$ROOT_DIR" + +echo "📸 FRAMEY pre-push 실행" + +FULL_CHECK=false +while read -r local_ref local_sha remote_ref remote_sha; do + + # 브랜치 삭제는 검사 생략 + if [[ "$local_sha" =~ ^0+$ ]]; then + continue + fi + case "$remote_ref" in + refs/heads/main) + echo "⚠️ main push 감지" + FULL_CHECK=true + ;; + refs/heads/dev) + echo "⚠️ dev push 감지" + FULL_CHECK=true + ;; + refs/heads/*) + branch="${remote_ref#refs/heads/}" + echo " feature/기타 브랜치 push: $branch" + ;; + + *) + echo "‼️ 브랜치 이외의 Ref — 별도 검사 생략" + continue + ;; + esac + +done +if [ "$FULL_CHECK" = true ]; then + echo "🚀 전체 Clean Check 실행" + + ./gradlew clean check --no-daemon || { + echo "✗ 전체 검증 실패 — Push 중단" + exit 1 + } +else + echo "➡️ Check 실행" + ./gradlew check --no-daemon || { + echo "⛔️ 검증 실패 — Push 중단" + exit 1 + } + +fi +echo "✅ pre-push 통과" +exit 0 diff --git a/.github/workflows/dev-deploy.yaml b/.github/workflows/dev-deploy.yaml index d5c7f54..afe7a6e 100644 --- a/.github/workflows/dev-deploy.yaml +++ b/.github/workflows/dev-deploy.yaml @@ -2,134 +2,38 @@ name: Dev Deploy on: push: - branches: [dev] + branches: + - dev -jobs: - detect-changes: - runs-on: ubuntu-latest - outputs: - member: ${{ steps.filter.outputs.member }} - collage: ${{ steps.filter.outputs.collage }} - archive: ${{ steps.filter.outputs.archive }} - swagger: ${{ steps.filter.outputs.swagger }} - nginx: ${{ steps.filter.outputs.nginx }} - steps: - - uses: actions/checkout@v4 - - - uses: dorny/paths-filter@v3 - id: filter - with: - filters: | - shared: &shared - - 'framey-common/**' - - 'framey-web/**' - - 'framey-monitoring/**' - - 'build.gradle' - - 'settings.gradle' - - 'gradle/**' - member: - - *shared - - 'framey-application/member-service/**' - collage: - - *shared - - 'framey-application/collage-service/**' - archive: - - *shared - - 'framey-application/archive-service/**' - swagger: - - 'framey-infrastructure/swagger-dev/**' - nginx: - - 'framey-infrastructure/nginx/**' +concurrency: + group: dev-deploy + cancel-in-progress: false +jobs: deploy: - needs: detect-changes + name: Deploy to DEV EC2 runs-on: ubuntu-latest - if: | - needs.detect-changes.outputs.member == 'true' || - needs.detect-changes.outputs.collage == 'true' || - needs.detect-changes.outputs.archive == 'true' || - needs.detect-changes.outputs.swagger == 'true' || - needs.detect-changes.outputs.nginx == 'true' - steps: - - name: Build target services list - id: targets - run: | - SERVICES="" - if [ "${{ needs.detect-changes.outputs.member }}" == "true" ]; then - SERVICES="$SERVICES member-dev" - fi - if [ "${{ needs.detect-changes.outputs.collage }}" == "true" ]; then - SERVICES="$SERVICES collage-dev" - fi - if [ "${{ needs.detect-changes.outputs.archive }}" == "true" ]; then - SERVICES="$SERVICES archive-dev" - fi - if [ "${{ needs.detect-changes.outputs.swagger }}" == "true" ]; then - SERVICES="$SERVICES swagger-dev" - fi - if [ "${{ needs.detect-changes.outputs.nginx }}" == "true" ]; then - SERVICES="$SERVICES nginx" - fi - echo "services=$SERVICES" >> $GITHUB_OUTPUT - - name: Deploy to DEV EC2 uses: appleboy/ssh-action@v1 - with: - host: ${{ secrets.DEV_EC2_HOST }} - username: ${{ secrets.DEV_EC2_USER }} - key: ${{ secrets.DEV_EC2_SSH_KEY }} - script: | - cd /home/ec2-user/Framey-Server - git pull origin dev - - export DEV_DB_URL="${{ secrets.DEV_DB_URL }}" - export DEV_DB_USER="${{ secrets.DEV_DB_USER }}" - export DEV_DB_PASSWORD="${{ secrets.DEV_DB_PASSWORD }}" + env: + DEV_DB_URL: ${{ secrets.DEV_DB_URL }} + DEV_DB_USER: ${{ secrets.DEV_DB_USER }} + DEV_DB_PASSWORD: ${{ secrets.DEV_DB_PASSWORD }} - docker compose \ - -f framey-infrastructure/docker/dev/docker-compose.dev.yaml \ - up -d --build --no-deps ${{ steps.targets.outputs.services }} - - - name: Health Check - uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEV_EC2_HOST }} username: ${{ secrets.DEV_EC2_USER }} key: ${{ secrets.DEV_EC2_SSH_KEY }} - script: | - sleep 30 - - SERVICES="${{ steps.targets.outputs.services }}" - - check_health() { - local name=$1 - local port=$2 - for i in $(seq 1 20); do - if docker exec framey-dev-${name} \ - wget -qO- http://localhost:${port}/actuator/health 2>/dev/null \ - | grep -q '"status":"UP"'; then - echo "✅ ${name} is healthy" - return 0 - fi - sleep 5 - done - echo "❌ ${name} health check failed" - docker logs --tail=50 framey-dev-${name} - return 1 - } - FAILED=0 + envs: DEV_DB_URL,DEV_DB_USER,DEV_DB_PASSWORD - if echo "$SERVICES" | grep -q "member-dev"; then - check_health "member" 8081 || FAILED=1 - fi - if echo "$SERVICES" | grep -q "collage-dev"; then - check_health "collage" 8082 || FAILED=1 - fi - if echo "$SERVICES" | grep -q "archive-dev"; then - check_health "archive" 8083 || FAILED=1 - fi - - exit $FAILED + command_timeout: 30m + script: | + set -e + echo "=======================================" + echo "FRAMEY DEV DEPLOYMENT" + echo "Commit: ${{ github.sha }}" + echo "=======================================" + /opt/framey/bin/deploy-dispatch.sh ${{ github.sha }} diff --git a/CLAUDE.md b/CLAUDE.md index 7c8c2f8..ed20ebe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `docker compose -f framey-infrastructure/docker/local/docker-compose.test.yaml up -d` — 테스트용 인프라 - `docker compose -f framey-infrastructure/docker/local/docker-compose.local.yaml down` — 종료 +### Git Hooks (`.githooks/`) +- `git config core.hooksPath .githooks` — 최초 1회 설정 (팀원 온보딩 필수, 로컬 설정이라 자동 전파 안 됨) +- `pre-commit`: staged `.java` 파일이 속한 모듈만 `checkstyleMain` 실행 (test 코드는 검사 안 함). **bash 4+ 문법 사용 (`mapfile`, `declare -A`) — macOS는 `brew install bash` 필요** +- `pre-push`: `dev`/`main` push 시 `./gradlew clean check` 전체 검증, 기타 브랜치는 `check` +- 우회: `git commit --no-verify` / `git push --no-verify` (긴급 시에만) + ## Tech Foundation - **Language/Runtime**: Java 21 (Gradle toolchain, Temurin 21 in CI) @@ -123,17 +129,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **인프라**: postgres:16-alpine 서비스 컨테이너, `TEST_DB_URL`/`TEST_DB_USER`/`TEST_DB_PASSWORD` env로 테스트 DB 주입 ### Dev Deploy (`.github/workflows/dev-deploy.yaml`) -- **트리거**: `dev` 브랜치 push -- **변경 감지**: `dorny/paths-filter`로 서비스별 배포 여부 판단. 공유 모듈(`framey-common`, `framey-web`, `framey-monitoring`, 루트 gradle 파일) 변경 시 전 서비스 재배포. -- **배포**: SSH로 dev EC2 접속 → `git pull` → `docker compose -f framey-infrastructure/docker/dev/docker-compose.dev.yaml up -d --build --no-deps {변경된 서비스}` -- **Health Check**: `/actuator/health`가 `"status":"UP"` 될 때까지 최대 100초 폴링. 실패 시 `docker logs --tail=50` 출력 후 workflow 실패. +- **트리거**: `dev` 브랜치 push → SSH로 dev EC2 접속 후 `/opt/framey/bin/deploy-dispatch.sh ${{ github.sha }}` 실행 (`command_timeout: 30m`). dispatcher는 repo 밖 고정 경로에 설치 — 서버 최초 설정: `sudo mkdir -p /opt/framey/bin && sudo cp framey-infrastructure/scripts/deploy-dispatch.sh /opt/framey/bin/ && sudo chmod +x /opt/framey/bin/deploy-dispatch.sh` +- **실행 구조**: `deploy-dispatch.sh`(EC2 고정 진입점) — `flock` 획득 → `git fetch` → TARGET이 `origin/dev`의 ancestor인지 검증 → **checkout 전에** 구형 배포 skip(TARGET이 마지막 성공 SHA의 ancestor면 조기 종료) → 서버 tracked 변경 감지 시 거부 → detached HEAD로 TARGET checkout → **해당 커밋의** `deploy-dev.sh` 실행. checkout과 배포 실행이 모두 같은 잠금 범위 안에 있어 동시 배포 충돌 방지. `deploy-dev.sh` 직접 실행 시 HEAD!=TARGET 가드로 차단됨. `FORCE_FULL=true` 환경변수로 수동 full deploy 가능. +- **변경 감지**: `deploy-dev.sh`가 `.git/framey-dev-success`(마지막 성공 SHA)와 대상 SHA의 `git diff`로 누적 변경분 판단. 공유 모듈(`framey-common`, `framey-web`, `framey-monitoring`, 루트 gradle 파일, `framey-infrastructure/docker/dev|scripts|systemd`, workflow 자체) 변경 시 전체 재배포. 베이스 SHA 유실·비선형 이력이면 full deploy. 어떤 규칙에도 매칭되지 않는 변경 파일은 WARNING으로 출력 (새 모듈/루트 설정 누락 감지용). +- **동시 배포 제어**: `concurrency: dev-deploy`(workflow 직렬화) + `deploy-dispatch.sh`의 `flock`(`.git/framey-dev.lock`, 최대 20분 대기). 이미 배포된 SHA로의 구형 배포 요청은 자동 skip. +- **배포**: checkout된 커밋의 `deploy-dev.sh`가 변경 서비스만 `docker compose build` → NGINX 변경 시 새 conf를 임시 컨테이너(`framey-dev` 네트워크, letsencrypt ro 마운트)에서 `nginx -t` 사전 검증 — 실패 시 컨테이너 재생성 전에 배포 중단 → `up -d --no-build --no-deps` → 검증 전부 통과 시에만 성공 SHA 기록 (실패 시 state 미갱신 → 다음 배포가 누적 diff 재계산) +- **검증**: 앱 `/actuator/health` `"UP"` 폴링(최대 200s) → nginx·swagger 컨테이너 미실행 시 ERROR로 즉시 실패(DEV는 상시 실행 환경 전제) → `nginx -t`는 항상, reload는 NGINX 변경 시에만 → `/health` + swagger 응답 → `https://dev-swagger.framey.kr/` 외부 검증 → OpenAPI 검증(nginx/swagger 변경 시 3개 앱 전부, 앱만 변경 시 해당 앱만). 변경 파일이 있으나 배포 대상이 없으면 NOTICE로 파일 목록 출력. +- **부분 배포 실패**: 성공 SHA 미기록은 "다음 배포가 누적 diff를 재시도"하게 만들 뿐, 이미 뜬 컨테이너를 이전 상태로 되돌리지는 않는다(이전 이미지 태그를 보관하지 않으므로 롤백 불가). 실패 시 `docker compose ps -a` + 대상 서비스 로그가 출력되고, 수정 후 다음 배포가 동일 서비스를 다시 빌드·검증한다. - **포트**: member 8081 / collage 8082 / archive 8083 +- **SSL**: 호스트 certbot + systemd timer로 갱신 (`framey-infrastructure/systemd/framey-certbot-renew.{service,timer}` — 하루 2회, deploy-hook으로 nginx reload). 서버 최초 설정: + - `sudo cp framey-infrastructure/systemd/framey-certbot-renew.{service,timer} /etc/systemd/system/` + - `sudo systemctl daemon-reload && sudo systemctl enable --now framey-certbot-renew.timer` + - 검증: `systemctl list-timers framey-certbot-renew` (활성 확인), `sudo certbot renew --dry-run` (갱신 dry-run) +- **Container Health**: 5개 서비스 모두 compose `healthcheck` 보유 — 앱 3개는 actuator, swagger는 `GET :8080/`, nginx는 `GET /health`(`default.conf` 정의) ### Branch Flow - feature/* → dev (PR + CI) → dev push 시 자동 dev 배포 - `main`은 배포 워크플로우 없음 — 프로덕션 배포 정책은 별도 정의 필요 + ## Language Conventions - **Checkstyle 준수**: Naver checkstyle 규칙이 빌드에 강제됨. 수정 후 `./gradlew check` 로컬 통과 필수. - **Lombok 사용**: `@Getter`, `@RequiredArgsConstructor` 등은 이미 허용 패턴. 단 `@Data`, `@Setter`는 사용하지 않는 것을 기본으로. diff --git a/framey-infrastructure/docker/dev/docker-compose.dev.yaml b/framey-infrastructure/docker/dev/docker-compose.dev.yaml index 5290652..4106bb2 100644 --- a/framey-infrastructure/docker/dev/docker-compose.dev.yaml +++ b/framey-infrastructure/docker/dev/docker-compose.dev.yaml @@ -87,6 +87,16 @@ services: restart: unless-stopped networks: - framey-dev + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q -O /dev/null http://localhost:8080/ || exit 1" + ] + interval: 15s + timeout: 5s + retries: 10 + start_period: 20s nginx: image: nginx:1.27-alpine container_name: framey-dev-nginx @@ -106,9 +116,20 @@ services: archive-dev: condition: service_healthy swagger-dev: - condition: service_started + condition: service_healthy networks: - framey-dev + # /health 엔드포인트는 nginx/dev/default.conf에 정의됨 + healthcheck: + test: + [ + "CMD-SHELL", + "wget -q -O /dev/null http://localhost/health || exit 1" + ] + interval: 15s + timeout: 5s + retries: 10 + start_period: 10s networks: framey-dev: diff --git a/framey-infrastructure/nginx/dev/default.conf b/framey-infrastructure/nginx/dev/default.conf index 534e17b..c2abab0 100644 --- a/framey-infrastructure/nginx/dev/default.conf +++ b/framey-infrastructure/nginx/dev/default.conf @@ -1,122 +1,117 @@ -# HTTP → HTTPS +# ================================================== +# HTTP +# ================================================== + server { listen 80; server_name dev-swagger.framey.kr; - location /.well-known/acme-challenge/ { + # Certbot ACME Challenge + location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; } + # Docker Compose Health Check + location = /health { + default_type text/plain; + return 200 "ok\n"; + } + + # Redirect HTTP to HTTPS location / { return 301 https://$host$request_uri; } } +# ================================================== # HTTPS +# ================================================== + server { listen 443 ssl; http2 on; server_name dev-swagger.framey.kr; + # SSL Certificate ssl_certificate /etc/letsencrypt/live/dev-swagger.framey.kr/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/dev-swagger.framey.kr/privkey.pem; - # ========================= - # OpenAPI - # ========================= + # -------------------------------------------------- + # Common Proxy Headers + # -------------------------------------------------- + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # -------------------------------------------------- + # HTTPS Health Check + # -------------------------------------------------- + + location = /health { + default_type text/plain; + return 200 "ok\n"; + } + + # -------------------------------------------------- + # OpenAPI Documents + # -------------------------------------------------- + location = /api-docs/member { proxy_pass http://member-dev:8081/v3/api-docs; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; } location = /api-docs/collage { proxy_pass http://collage-dev:8082/v3/api-docs; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; } location = /api-docs/archive { proxy_pass http://archive-dev:8083/v3/api-docs; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; } - # ========================= + # -------------------------------------------------- # Member Service - # ========================= + # -------------------------------------------------- + location = /api/v1/members { proxy_pass http://member-dev:8081; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ^~ /api/v1/members/ { proxy_pass http://member-dev:8081; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - # ========================= + # -------------------------------------------------- # Collage Service - # ========================= + # -------------------------------------------------- + location = /api/v1/collages { proxy_pass http://collage-dev:8082; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ^~ /api/v1/collages/ { proxy_pass http://collage-dev:8082; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - # ========================= + # -------------------------------------------------- # Archive Service - # ========================= + # -------------------------------------------------- + location = /api/v1/archives { proxy_pass http://archive-dev:8083; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location ^~ /api/v1/archives/ { proxy_pass http://archive-dev:8083; - - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } - # ========================= + # -------------------------------------------------- # Swagger UI - # ========================= + # -------------------------------------------------- location / { proxy_pass http://swagger-dev:8080; - - proxy_set_header Host $host; - proxy_set_header X-Forwarded-Proto $scheme; } } diff --git a/framey-infrastructure/scripts/deploy-dev.sh b/framey-infrastructure/scripts/deploy-dev.sh new file mode 100755 index 0000000..3a86bc7 --- /dev/null +++ b/framey-infrastructure/scripts/deploy-dev.sh @@ -0,0 +1,445 @@ +#!/usr/bin/env bash +# FRAMEY DEV 환경 배포 실행 스크립트 + +set -Eeuo pipefail + +ROOT=/home/ec2-user/Framey-Server +cd "$ROOT" + +STATE="$ROOT/.git/framey-dev-success" +TARGET=${1:?Deployment SHA required} + +COMPOSE=framey-infrastructure/docker/dev/docker-compose.dev.yaml +BASE_URL=https://dev-swagger.framey.kr + +SERVICES=() +BUILD=() +APPS=() +VERIFY_APPS=() +UNMAPPED=() +CHANGED=() + +MEMBER=false +COLLAGE=false +ARCHIVE=false +SWAGGER=false +NGINX=false + +FULL=false +BASE='' + +compose() { + docker compose -f "$COMPOSE" "$@" +} + +running() { + [[ $(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null) == true ]] +} + +failed() { + local code=$? + + trap - ERR + + echo "=======================================" >&2 + echo "⛔️ framey dev deployment failed" >&2 + echo "Target SHA: $TARGET" >&2 + echo "Exit Code: $code" >&2 + echo "=======================================" >&2 + + echo "⚠️ WARNING: Some containers may already be running the new version." >&2 + echo "☑️ Last successful deployment SHA remains unchanged." >&2 + + echo "🐳 Docker Compose Status:" >&2 + compose ps -a || true + + if ((${#SERVICES[@]})); then + echo "🚀 Container Logs:" >&2 + + compose logs \ + --no-color \ + --tail=80 \ + "${SERVICES[@]}" || true + fi + + exit "$code" +} + +trap failed ERR + +echo "⭐️ 배포 대상 커밋 검증" + +[[ "$TARGET" =~ ^[0-9a-f]{40}$ ]] || { #SHA 형식 검증 + echo "Invalid deployment SHA: $TARGET" >&2 + exit 1 +} + +HEAD=$(git rev-parse HEAD) #현재 EC2 작업 디렉터리의 Git HEAD가 가리키는 커밋 가져옴 + +if [[ "$HEAD" != "$TARGET" ]]; then + echo "HEAD ($HEAD) != TARGET ($TARGET)" >&2 + echo "Run deployment through deploy-dispatch.sh" >&2 + exit 1 +fi + +echo "Deployment target: $TARGET" + + +if [[ -f "$STATE" ]]; then + BASE=$(cat "$STATE") +fi + +echo "Last successful deployment: ${BASE:-none}" + +echo "✅ 전체 서비스 배포 여부 필요 여부 체크" +if [[ "${FORCE_FULL:-false}" == "true" ]]; then + + echo "FORCE_FULL enabled" + FULL=true + +elif [[ -z "$BASE" ]] || \ + ! git cat-file -e "$BASE^{commit}" 2>/dev/null; then + + echo "❓이전 배포 찾을수 없음" + FULL=true + +elif ! git merge-base --is-ancestor "$BASE" "$TARGET"; then + + if git merge-base --is-ancestor "$TARGET" "$BASE"; then + echo "⏭️ 유효하지않은 배포 건너띔" + exit 0 + fi + + echo "🎋 배포 이력 갈라짐" + FULL=true +fi + +echo "✅ 변경된 파일 확인" +if [[ "$FULL" == true ]]; then + + echo "Full deployment selected" + + MEMBER=true + COLLAGE=true + ARCHIVE=true + SWAGGER=true + NGINX=true + +else + + while IFS= read -r -d '' file; do + + CHANGED+=("$file") + matched=true + + case "$file" in + + # Shared modules / build configuration + framey-common/*|\ + framey-web/*|\ + framey-monitoring/*|\ + build.gradle|\ + settings.gradle|\ + gradle.properties|\ + gradle/*|\ + gradlew|\ + gradlew.bat|\ + .dockerignore) + + MEMBER=true + COLLAGE=true + ARCHIVE=true + ;; + + # Member Service + framey-application/member-service/*) + + MEMBER=true + ;; + + # Collage Service + framey-application/collage-service/*) + + COLLAGE=true + ;; + + # Archive Service + framey-application/archive-service/*) + + ARCHIVE=true + ;; + + # Swagger + framey-infrastructure/swagger-dev/*) + + SWAGGER=true + ;; + + # Nginx + framey-infrastructure/nginx/*) + + NGINX=true + ;; + + # Deployment infrastructure + framey-infrastructure/docker/dev/*|\ + framey-infrastructure/scripts/*|\ + framey-infrastructure/systemd/*|\ + .github/workflows/dev-deploy.yaml) + + MEMBER=true + COLLAGE=true + ARCHIVE=true + SWAGGER=true + NGINX=true + ;; + + # Files that do not affect deployment + .github/*|\ + .claude/*|\ + docs/*|\ + CLAUDE.md|\ + README.md|\ + .coderabbit.yaml|\ + .gitattributes|\ + .gitignore|\ + .githooks/*|\ + framey-infrastructure/docker/local/*) + + ;; + + # Unmapped files + *) + + matched=false + ;; + esac + + if [[ "$matched" == false ]]; then + UNMAPPED+=("$file") + fi + + done < <( + git diff \ + --name-only \ + --no-renames \ + -z \ + "$BASE" \ + "$TARGET" + ) + +fi + +if ((${#UNMAPPED[@]})); then + echo "⚠️ WARNING: ${#UNMAPPED[@]} changed file(s) matched no deployment rule:" >&2 + printf ' - %s\n' "${UNMAPPED[@]}" >&2 +fi + +echo "🎯 배포할 서비스 결정" +for app in member collage archive; do + + case "$app" in + member) selected=$MEMBER ;; + collage) selected=$COLLAGE ;; + archive) selected=$ARCHIVE ;; + esac + + if [[ "$selected" == true ]]; then + + SERVICES+=("$app-dev") + BUILD+=("$app-dev") + APPS+=("$app") + + fi + +done + +if [[ "$SWAGGER" == true ]]; then + + SERVICES+=(swagger-dev) + BUILD+=(swagger-dev) + +fi + +if [[ "$NGINX" == true ]]; then + + SERVICES+=(nginx) + +fi + +if ((${#CHANGED[@]})) && ((${#SERVICES[@]} == 0)); then + echo "📝 NOTICE: Files changed but no deployable service selected:" >&2 + printf ' - %s\n' "${CHANGED[@]}" >&2 +fi + +echo "➡️ Docker 이미지 빌드 및 컨테이너 실행 - 실제 배포중 " +if ((${#SERVICES[@]})); then + + echo "=======================================" + echo "FRAMEY DEV DEPLOYMENT" + echo "BASE: ${BASE:-none}" + echo "TARGET: $TARGET" + echo "SERVICES: ${SERVICES[*]}" + echo "=======================================" + + compose config --quiet + + if ((${#BUILD[@]})); then + + echo "Building: ${BUILD[*]}" + compose build "${BUILD[@]}" + fi + + # nginx 설정 사전 검증 — 새 conf를 임시 컨테이너에 마운트해 nginx -t 실행. + # compose up 전에 검증해야 잘못된 설정으로 nginx 컨테이너 재생성 실패를 막을 수 있음. + # upstream(member-dev 등) DNS 해석을 위해 framey-dev 네트워크에 붙이고, + # ssl_certificate 경로 검증을 위해 letsencrypt 디렉터리도 ro 마운트. + if [[ "$NGINX" == true ]]; then + + echo "Validating nginx config (pre-deploy)" + + docker run --rm \ + --network framey-dev \ + -v "$ROOT/framey-infrastructure/nginx/dev/default.conf:/etc/nginx/conf.d/default.conf:ro" \ + -v /etc/letsencrypt:/etc/letsencrypt:ro \ + nginx:1.27-alpine \ + nginx -t + + fi + + echo "Starting: ${SERVICES[*]}" + + compose up \ + -d \ + --no-build \ + --no-deps \ + "${SERVICES[@]}" + + echo "✅ 배포한 Spring Boot 서비스만 대상 검사 " + for app in "${APPS[@]}"; do + + case "$app" in + member) port=8081 ;; + collage) port=8082 ;; + archive) port=8083 ;; + esac + + echo "Checking health: $app" + + healthy=false + + for attempt in {1..40}; do + + if docker exec "framey-dev-$app" \ + wget -T 5 -qO- \ + "http://127.0.0.1:$port/actuator/health" \ + | grep -Eq '"status"[[:space:]]*:[[:space:]]*"UP"'; then + healthy=true + break + fi + sleep 5 + done + + if [[ "$healthy" != true ]]; then + echo "Health Check failed: $app" >&2 + false + fi + echo "Health Check passed: $app" + done + + echo "✅ Nginx 및 Swagger 검증" + + if ! running framey-dev-nginx; then + echo "ERROR: Nginx container is not running" >&2 + false + fi + + if ! running framey-dev-swagger; then + echo "ERROR: Swagger container is not running" >&2 + false + fi + + docker exec framey-dev-nginx nginx -t + + if [[ "$NGINX" == true ]]; then + echo "Reloading Nginx" + docker exec framey-dev-nginx nginx -s reload + fi + + docker exec framey-dev-nginx \ + wget -T 5 -qO- \ + http://127.0.0.1/health > /dev/null + + docker exec framey-dev-swagger \ + wget -T 5 -qO- \ + http://127.0.0.1:8080/ > /dev/null + + echo "✅ 외부 HTTPS 검증" + + curl \ + --fail \ + --silent \ + --show-error \ + --retry 12 \ + --retry-all-errors \ + --retry-delay 5 \ + --max-time 10 \ + "$BASE_URL/" > /dev/null + + echo "✅ OpenAPi 검증 대상 결정" + if [[ "$NGINX" == true || "$SWAGGER" == true ]]; then + VERIFY_APPS=(member collage archive) + else + VERIFY_APPS=("${APPS[@]}") + fi + + for app in "${VERIFY_APPS[@]}"; do + + echo "Checking OpenAPI: $app" + + response=$(mktemp) + + curl \ + --fail \ + --silent \ + --show-error \ + --retry 12 \ + --retry-all-errors \ + --retry-delay 5 \ + --max-time 10 \ + "$BASE_URL/api-docs/$app" > "$response" + + python3 - "$response" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as source: + document = json.load(source) + +assert document.get("openapi"), "Missing OpenAPI version" + +assert isinstance(document.get("paths"), dict), "Missing OpenAPI paths" + +for server in document.get("servers", []): + assert server["url"] in ( + "/", + "https://dev-swagger.framey.kr" + ), server +PY + + rm -f "$response" + + echo "OpenAPI verification passed: $app" + + done + +fi + +printf '%s\n' "$TARGET" > "$STATE.tmp" + +mv "$STATE.tmp" "$STATE" + +echo "=======================================" +echo "FRAMEY DEV DEPLOYMENT SUCCESSFUL" +echo "SHA: $TARGET" +echo "=======================================" diff --git a/framey-infrastructure/scripts/deploy-dispatch.sh b/framey-infrastructure/scripts/deploy-dispatch.sh new file mode 100755 index 0000000..3c92bf2 --- /dev/null +++ b/framey-infrastructure/scripts/deploy-dispatch.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# EC2 고정 배포 실행기 — workflow가 직접 호출하는 유일한 진입점. +# flock 획득 → fetch → TARGET 검증 → checkout → 해당 커밋의 deploy-dev.sh 실행. +# checkout과 스크립트 실행이 모두 같은 잠금 범위 안에 있어 동시 배포 충돌을 방지 +set -Eeuo pipefail + +ROOT=/home/ec2-user/Framey-Server +STATE="$ROOT/.git/framey-dev-success" + +TARGET=${1:?Deployment SHA required} + +[[ "$TARGET" =~ ^[0-9a-f]{40}$ ]] || { + echo "Invalid SHA: $TARGET" >&2 + exit 1 +} + +cd "$ROOT" + +exec 9>"$ROOT/.git/framey-dev.lock" +flock -w 1200 9 + +git fetch origin \ + +refs/heads/dev:refs/remotes/origin/dev + +if [[ $(git rev-parse --is-shallow-repository) == true ]]; then + git fetch --unshallow origin +fi + +git cat-file -e "$TARGET^{commit}" + +git merge-base --is-ancestor "$TARGET" origin/dev + +if [[ -f "$STATE" ]]; then + + BASE=$(cat "$STATE") + + if git cat-file -e "$BASE^{commit}" 2>/dev/null; then + + if [[ "$TARGET" != "$BASE" ]] && \ + git merge-base --is-ancestor "$TARGET" "$BASE"; then + + echo "Skipping obsolete deployment: $TARGET" + exit 0 + + fi + + fi + +fi + +if [[ -n $(git status --porcelain --untracked-files=no) ]]; then + + echo "Tracked changes detected. Aborting deployment." >&2 + exit 1 + +fi + +git checkout --detach "$TARGET" + +exec "$ROOT/framey-infrastructure/scripts/deploy-dev.sh" "$TARGET" diff --git a/framey-infrastructure/systemd/framey-certbot-renew.service b/framey-infrastructure/systemd/framey-certbot-renew.service new file mode 100644 index 0000000..1eb401c --- /dev/null +++ b/framey-infrastructure/systemd/framey-certbot-renew.service @@ -0,0 +1,9 @@ +[Unit] +Description=Renew Let's Encrypt certificates for dev-swagger.framey.kr +After=network.target docker.service +Requires=docker.service + +[Service] +Type=oneshot +# webroot 방식 — nginx의 /.well-known/acme-challenge/ 경로가 /var/www/certbot 을 서빙 +ExecStart=/usr/bin/certbot renew --webroot -w /var/www/certbot --quiet --deploy-hook "/usr/bin/docker exec framey-dev-nginx nginx -s reload" diff --git a/framey-infrastructure/systemd/framey-certbot-renew.timer b/framey-infrastructure/systemd/framey-certbot-renew.timer new file mode 100644 index 0000000..230d03f --- /dev/null +++ b/framey-infrastructure/systemd/framey-certbot-renew.timer @@ -0,0 +1,12 @@ +[Unit] +Description=Daily Let's Encrypt renewal check (certbot은 만료 30일 전부터만 실제 갱신) + +[Timer] +# Let's Encrypt 권장: 하루 2회 체크, 부트 후 10분 뒤 첫 실행 +OnCalendar=*-*-* 00,12:00:00 +OnBootSec=10min +RandomizedDelaySec=1h +Persistent=true + +[Install] +WantedBy=timers.target