diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b24dcf3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,53 @@ +# ============================================================================= +# CODEOWNERS - 코드 소유자 설정 +# ============================================================================= +# 이 파일은 주석처리된 상태로 생성되었습니다. +# v1 완료 후 필요한 줄의 주석을 해제하여 사용하세요. +# +# 문법: +# <파일/폴더 패턴> @사용자1 @사용자2 ... +# +# 참고: +# - 파일 패턴은 .gitignore와 동일한 형식을 사용합니다. +# - 마지막에 매칭된 규칙이 적용됩니다 (순서 중요). +# - CODEOWNERS에 지정된 사용자는 자동으로 리뷰어로 요청됩니다. +# ============================================================================= + +# ----------------------------------------------------------------------------- +# 전체 코드 기본 리뷰어 +# ----------------------------------------------------------------------------- +# * @team-lead @tech-lead + +# ----------------------------------------------------------------------------- +# GitHub Actions 및 CI/CD 설정 +# ----------------------------------------------------------------------------- +# .github/ @devops-team +# Dockerfile @devops-team +# docker-compose*.yml @devops-team + +# ----------------------------------------------------------------------------- +# 빌드 설정 +# ----------------------------------------------------------------------------- +# build.gradle* @backend-lead +# settings.gradle* @backend-lead +# gradle/ @backend-lead + +# ----------------------------------------------------------------------------- +# 애플리케이션 설정 +# ----------------------------------------------------------------------------- +# src/main/resources/application*.yml @backend-lead +# src/main/resources/application*.properties @backend-lead + +# ----------------------------------------------------------------------------- +# 도메인별 담당자 (예시) +# ----------------------------------------------------------------------------- +# src/main/java/**/domain/user/ @user-domain-owner +# src/main/java/**/domain/order/ @order-domain-owner +# src/main/java/**/domain/payment/ @payment-domain-owner + +# ----------------------------------------------------------------------------- +# 인프라/공통 모듈 +# ----------------------------------------------------------------------------- +# src/main/java/**/common/ @backend-lead +# src/main/java/**/config/ @backend-lead +# src/main/java/**/security/ @security-lead diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 811ec35..9689679 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,7 @@ -## 🎫 지라 티켓 - +## 🍀 이슈 번호 + +- #이슈번호 --- @@ -9,6 +10,17 @@ +--- + +## 📋 체크리스트 + + + +- [ ] 코드가 정상적으로 빌드됩니다. +- [ ] 관련 테스트 코드를 작성했습니다. +- [ ] 기존 테스트가 모두 통과합니다. +- [ ] 코드 스타일(Spotless, Checkstyle)을 준수합니다. + --- ## ⌨ 기타 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8fdae00..106d5ab 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,7 +1,18 @@ # ============================================================================ # Backend Deploy Workflow # ============================================================================ -# 이 파일을 backend 레포지토리의 .github/workflows/deploy.yml 로 저장하세요. +# 역할: +# - Integrate workflow 성공 후 자동 실행 +# - integrate에서 생성된 JAR artifact 사용 (빌드 재수행 없음) +# - Docker 이미지 빌드 및 ECR 푸시 +# - ECS 서비스 배포 +# +# Job 구조: +# prepare ──> build-image ──> deploy +# +# 트리거: +# - Integrate Backend workflow가 main 브랜치에서 성공적으로 완료된 후 +# - 수동 실행 (workflow_dispatch) - 최근 성공한 integrate run의 artifact 사용 # # GitHub Secrets (Settings > Secrets and variables > Actions > Secrets): # - AWS_ACCESS_KEY_ID: AWS IAM Access Key @@ -16,7 +27,10 @@ name: Deploy Backend to ECS on: - push: + workflow_run: + workflows: ["Integrate Backend"] + types: + - completed branches: - main workflow_dispatch: @@ -25,14 +39,104 @@ env: AWS_REGION: ap-northeast-2 jobs: - deploy: - name: Build and Deploy + # ========================================================================== + # Prepare Job - Artifact 준비 + # ========================================================================== + prepare: + name: Prepare runs-on: ubuntu-latest + # workflow_run 트리거일 경우 integrate 성공 및 push 이벤트일 때만 실행 + if: > + github.event_name == 'workflow_dispatch' || + (github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push') + + outputs: + run_id: ${{ steps.get-run-info.outputs.run_id }} + head_sha: ${{ steps.get-run-info.outputs.head_sha }} steps: - name: Checkout code uses: actions/checkout@v4 + - name: Get workflow run info + id: get-run-info + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + echo "Manual trigger detected. Finding latest successful integrate run..." + + # 최근 성공한 Integrate Backend workflow run 조회 + RUN_INFO=$(gh run list \ + --workflow "Integrate Backend" \ + --branch main \ + --status success \ + --event push \ + --limit 1 \ + --json databaseId,headSha) + + RUN_ID=$(echo "$RUN_INFO" | jq -r '.[0].databaseId') + HEAD_SHA=$(echo "$RUN_INFO" | jq -r '.[0].headSha') + + if [ "$RUN_ID" == "null" ] || [ -z "$RUN_ID" ]; then + echo "::error::No successful integrate workflow run found" + exit 1 + fi + + echo "Found run ID: $RUN_ID, commit: $HEAD_SHA" + else + echo "workflow_run trigger detected" + RUN_ID="${{ github.event.workflow_run.id }}" + HEAD_SHA="${{ github.event.workflow_run.head_sha }}" + fi + + echo "run_id=$RUN_ID" >> $GITHUB_OUTPUT + echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT + + - name: Download artifact from Integrate workflow + uses: actions/download-artifact@v4 + with: + name: spring-boot-app + path: build/libs + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ steps.get-run-info.outputs.run_id }} + + - name: Verify and upload artifact + run: | + echo "Downloaded artifacts:" + ls -la build/libs/ + JAR_FILE=$(ls build/libs/*.jar | head -1) + echo "JAR file: $JAR_FILE" + + - name: Upload artifact for next jobs + uses: actions/upload-artifact@v4 + with: + name: deploy-artifact + path: build/libs/*.jar + retention-days: 1 + + # ========================================================================== + # Build Image Job - Docker 빌드 및 ECR 푸시 + # ========================================================================== + build-image: + name: Build Image + runs-on: ubuntu-latest + needs: [prepare] + + outputs: + image_tag: ${{ needs.prepare.outputs.head_sha }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: deploy-artifact + path: build/libs + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: @@ -51,13 +155,30 @@ jobs: uses: docker/build-push-action@v5 with: context: . + file: Dockerfile.deploy push: true tags: | - ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:${{ github.sha }} + ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:${{ needs.prepare.outputs.head_sha }} ${{ steps.login-ecr.outputs.registry }}/${{ vars.ECR_REPOSITORY }}:latest cache-from: type=gha cache-to: type=gha,mode=max + # ========================================================================== + # Deploy Job - ECS 배포 + # ========================================================================== + deploy: + name: Deploy + runs-on: ubuntu-latest + needs: [prepare, build-image] + + steps: + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + - name: Deploy to ECS run: | aws ecs update-service \ @@ -76,7 +197,9 @@ jobs: - name: Deployment Summary run: | echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "- **Image Tag**: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Image Tag**: ${{ needs.prepare.outputs.head_sha }}" >> $GITHUB_STEP_SUMMARY + echo "- **Source Run ID**: ${{ needs.prepare.outputs.run_id }}" >> $GITHUB_STEP_SUMMARY echo "- **ECS Cluster**: ${{ vars.ECS_CLUSTER }}" >> $GITHUB_STEP_SUMMARY echo "- **ECS Service**: ${{ vars.ECS_SERVICE }}" >> $GITHUB_STEP_SUMMARY echo "- **Region**: ${{ env.AWS_REGION }}" >> $GITHUB_STEP_SUMMARY + echo "- **Triggered by**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index fd98273..7cb764c 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -5,6 +5,11 @@ # - 소스 코드 정적 분석 (Spotless, Checkstyle) # - 테스트 수행 및 JaCoCo 커버리지 생성 # - SonarCloud 코드 품질/커버리지/보안 분석 +# - JAR artifact 생성 (deploy workflow에서 사용) +# +# Job 구조: +# lint ──┬──> analyze ──> build +# test ──┘ # # GitHub Secrets: # - SONAR_TOKEN : SonarCloud Token @@ -27,21 +32,16 @@ env: JAVA_VERSION: '17' jobs: - integrate: - name: Backend Integration Pipeline + # ========================================================================== + # Lint Job - 코드 스타일 검사 + # ========================================================================== + lint: + name: Lint runs-on: ubuntu-latest - permissions: - contents: read - actions: write - checks: write - pull-requests: write - steps: - name: Checkout source uses: actions/checkout@v4 - with: - fetch-depth: 0 # SonarCloud 분석을 위해 전체 히스토리 필요 - name: Set up JDK uses: actions/setup-java@v4 @@ -59,16 +59,114 @@ jobs: - name: Run Checkstyle run: ./gradlew checkstyleMain checkstyleTest + # ========================================================================== + # Test Job - 테스트 및 커버리지 + # ========================================================================== + test: + name: Test + runs-on: ubuntu-latest + + permissions: + contents: read + checks: write + pull-requests: write + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + cache: gradle + + - name: Grant execution to Gradle wrapper + run: chmod +x gradlew + - name: Run Tests with JaCoCo run: ./gradlew test jacocoTestReport + - name: Upload test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: test-results + path: | + build/test-results/ + build/reports/jacoco/ + + # ========================================================================== + # Analyze Job - SonarCloud 분석 + # ========================================================================== + analyze: + name: Analyze + runs-on: ubuntu-latest + needs: [lint, test] + + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 # SonarCloud 분석을 위해 전체 히스토리 필요 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + cache: gradle + + - name: Cache SonarQube packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Download test results + uses: actions/download-artifact@v4 + with: + name: test-results + path: build/ + + - name: Grant execution to Gradle wrapper + run: chmod +x gradlew + - name: SonarCloud Scan env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} SONAR_PROJECT: ${{ secrets.SONAR_PROJECT }} SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} - run: ./gradlew sonar -Dsonar.host.url=$SONAR_HOST_URL + run: ./gradlew sonar -Dsonar.host.url=$SONAR_HOST_URL --info + + # ========================================================================== + # Build Job - JAR 빌드 및 Artifact 업로드 + # ========================================================================== + build: + name: Build + runs-on: ubuntu-latest + needs: [analyze] + + permissions: + contents: read + actions: write + + steps: + - name: Checkout source + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: temurin + cache: gradle + + - name: Grant execution to Gradle wrapper + run: chmod +x gradlew - name: Build Spring Boot JAR run: ./gradlew bootJar diff --git a/.github/workflows/pr-approval-check.yml b/.github/workflows/pr-approval-check.yml new file mode 100644 index 0000000..0f20410 --- /dev/null +++ b/.github/workflows/pr-approval-check.yml @@ -0,0 +1,64 @@ +name: Approval Check + +on: + pull_request: + types: [opened, synchronize, reopened] + branches: [main, develop] + pull_request_review: + types: [submitted, dismissed] + +jobs: + check-approval: + runs-on: ubuntu-latest + name: Approval Check + + steps: + - name: Check PR Approval Status + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request?.number || context.payload.review?.pull_request?.number; + const prAuthor = context.payload.pull_request?.user?.login || context.payload.review?.pull_request?.user?.login; + + if (!prNumber) { + core.setFailed('PR 번호를 찾을 수 없습니다.'); + return; + } + + console.log(`📋 PR #${prNumber} 승인 상태 확인 중...`); + console.log(` 작성자: ${prAuthor}`); + + // PR의 모든 리뷰 가져오기 + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + // 각 리뷰어의 최신 리뷰 상태만 확인 + const latestReviews = {}; + for (const review of reviews) { + const reviewer = review.user.login; + // PR 작성자의 리뷰는 무시 + if (reviewer.toLowerCase() === prAuthor.toLowerCase()) { + continue; + } + // 더 최신 리뷰로 덮어쓰기 + if (!latestReviews[reviewer] || new Date(review.submitted_at) > new Date(latestReviews[reviewer].submitted_at)) { + latestReviews[reviewer] = review; + } + } + + // APPROVED 상태인 리뷰 수 계산 + const approvals = Object.values(latestReviews).filter(r => r.state === 'APPROVED'); + + console.log(` 승인 수: ${approvals.length}`); + if (approvals.length > 0) { + console.log(` 승인자: ${approvals.map(r => r.user.login).join(', ')}`); + } + + if (approvals.length >= 1) { + console.log('✅ 필요한 승인을 받았습니다.'); + } else { + core.setFailed('❌ 최소 1명의 승인이 필요합니다. (PR 작성자 제외)'); + } diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..747b4c6 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,101 @@ +name: PR Size Labeler + +on: + pull_request: + types: [opened, synchronize] + +jobs: + size-label: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Label PR by Size + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + + // PR의 변경 사항 가져오기 + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + const additions = pr.additions; + const deletions = pr.deletions; + const totalChanges = additions + deletions; + + console.log(`📊 PR #${prNumber} 변경 통계`); + console.log(` 추가: +${additions}`); + console.log(` 삭제: -${deletions}`); + console.log(` 총 변경: ${totalChanges} lines`); + + // 크기별 라벨 정의 + const sizeLabels = { + 'size/XS': { max: 10, color: '3CBF00' }, + 'size/S': { max: 50, color: '5D9801' }, + 'size/M': { max: 200, color: 'FBCA04' }, + 'size/L': { max: 500, color: 'FFA500' }, + 'size/XL': { max: Infinity, color: 'D93F0B' } + }; + + // 적절한 라벨 선택 + let selectedLabel = 'size/XL'; + for (const [label, config] of Object.entries(sizeLabels)) { + if (totalChanges <= config.max) { + selectedLabel = label; + break; + } + } + + console.log(`🏷️ 선택된 라벨: ${selectedLabel}`); + + // 기존 size 라벨 제거 + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + for (const label of currentLabels) { + if (label.name.startsWith('size/')) { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: label.name + }); + } + } + + // 라벨이 존재하는지 확인하고 없으면 생성 + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: selectedLabel + }); + } catch (error) { + if (error.status === 404) { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: selectedLabel, + color: sizeLabels[selectedLabel].color, + description: `PR size: ${selectedLabel.replace('size/', '')}` + }); + } + } + + // 새 라벨 추가 + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [selectedLabel] + }); + + console.log(`✅ 라벨 적용 완료: ${selectedLabel}`); diff --git a/.github/workflows/pr-reviewer.yml b/.github/workflows/pr-reviewer.yml new file mode 100644 index 0000000..4bf6ae6 --- /dev/null +++ b/.github/workflows/pr-reviewer.yml @@ -0,0 +1,53 @@ +name: Auto Assign Reviewer + +on: + pull_request: + types: [opened, ready_for_review] + branches: [main, develop] + +jobs: + assign-reviewer: + runs-on: ubuntu-latest + # Draft PR은 건너뛰기 + if: github.event.pull_request.draft == false + + steps: + - name: Auto Assign Reviewer + uses: actions/github-script@v7 + with: + script: | + // ============================================================ + // 리뷰어 목록 설정 + // scripts/github-config.env의 REVIEWERS 값과 동기화하세요 + // ============================================================ + const reviewers = ['arkchive', 'arlen02-01', 'k0081915', 'paul0755', 'starboxxxx', 'swthewhite']; + + const prAuthor = context.payload.pull_request.user.login; + const prNumber = context.payload.pull_request.number; + + // PR 작성자를 제외한 리뷰어 후보 + const candidates = reviewers.filter(r => r.toLowerCase() !== prAuthor.toLowerCase()); + + if (candidates.length === 0) { + console.log('⚠️ 할당 가능한 리뷰어가 없습니다.'); + return; + } + + // 랜덤으로 1명 선택 + const selectedReviewer = candidates[Math.floor(Math.random() * candidates.length)]; + + console.log(`📌 PR #${prNumber} - 리뷰어 할당: ${selectedReviewer}`); + + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + reviewers: [selectedReviewer] + }); + + console.log(`✅ 리뷰어 할당 완료: ${selectedReviewer}`); + } catch (error) { + console.log(`❌ 리뷰어 할당 실패: ${error.message}`); + // 실패해도 워크플로우는 성공으로 처리 (리뷰어가 collaborator가 아닐 수 있음) + } diff --git a/.github/workflows/stale-pr.yml b/.github/workflows/stale-pr.yml new file mode 100644 index 0000000..bca1c55 --- /dev/null +++ b/.github/workflows/stale-pr.yml @@ -0,0 +1,44 @@ +name: Stale PR Check + +on: + schedule: + # 매일 오전 9시 (KST) 실행 = UTC 0시 + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Mark Stale PRs + uses: actions/stale@v9 + with: + # PR 설정 + stale-pr-message: | + ⚠️ 이 PR은 **1일** 동안 활동이 없어 stale로 표시되었습니다. + + 리뷰가 필요하거나 작업 중인 경우 코멘트를 남겨주세요. + 더 이상 필요하지 않은 경우 PR을 닫아주세요. + + stale-pr-label: 'stale' + days-before-pr-stale: 1 + days-before-pr-close: -1 # 자동 close 비활성화 + + # Issue는 처리하지 않음 + days-before-issue-stale: -1 + days-before-issue-close: -1 + + # stale 라벨 해제 조건 + remove-stale-when-updated: true + + # 제외할 라벨 + exempt-pr-labels: 'wip,blocked,do-not-close' + + # Draft PR 제외 + exempt-draft-pr: true + + # 처리할 PR 수 + operations-per-run: 30 diff --git a/Dockerfile.deploy b/Dockerfile.deploy new file mode 100644 index 0000000..78a420f --- /dev/null +++ b/Dockerfile.deploy @@ -0,0 +1,17 @@ +# ============================================================================ +# Dockerfile for CI/CD Pipeline (Pre-built JAR) +# ============================================================================ +# 이 Dockerfile은 CI 파이프라인에서 이미 빌드된 JAR 파일을 사용합니다. +# 로컬 개발 시에는 기존 Dockerfile (멀티스테이지 빌드)을 사용하세요. +# ============================================================================ + +FROM eclipse-temurin:17-jre + +WORKDIR /app + +# CI에서 빌드된 JAR 파일 복사 +COPY build/libs/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 61340b8..65586b7 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,12 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-actuator' + // Monitoring & Observability + implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'net.logstash.logback:logstash-logback-encoder:7.4' + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + implementation 'io.opentelemetry:opentelemetry-exporter-otlp' + // Database implementation 'org.springframework.boot:spring-boot-starter-data-jpa' runtimeOnly 'org.postgresql:postgresql' @@ -85,7 +91,7 @@ jacocoTestCoverageVerification { violationRules { rule { limit { - minimum = 0.75 // 75% 미만이면 빌드 실패 + minimum = 0.8 } } } @@ -105,7 +111,7 @@ jacocoTestCoverageVerification { // Checkstyle (Code Style) // ============================================================================ checkstyle { - toolVersion = '10.18.2' + toolVersion = '10.21.4' configFile = file("${rootDir}/config/checkstyle/checkstyle.xml") ignoreFailures = false maxWarnings = 0 diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml index e1d2f63..9568b84 100644 --- a/config/checkstyle/checkstyle.xml +++ b/config/checkstyle/checkstyle.xml @@ -1,73 +1,444 @@ + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> + Checkstyle configuration that checks the Google coding conventions from Google Java Style + that can be found at https://google.github.io/styleguide/javaguide.html + + Checkstyle is very configurable. Be sure to read the documentation at + http://checkstyle.org (or in your downloaded distribution). + + To completely disable a check, just comment it out or delete it from the file. + To suppress certain violations please review suppression filters. + + Authors: Max Vetrenko, Mauryan Kansara, Ruslan Diachenko, Roman Ivanov. + --> + - - - - - - - + - - - - - + - - - - - - - - - - - - - - + + + + + + - - - - + - - - - - - - + + + + + - - - - - - - - - + + + + + + - - - - - + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/project/core/controller/dto/request/ChangeEmailRequest.java b/src/main/java/com/project/core/controller/dto/request/ChangeEmailRequest.java index a2029fc..fcaebeb 100644 --- a/src/main/java/com/project/core/controller/dto/request/ChangeEmailRequest.java +++ b/src/main/java/com/project/core/controller/dto/request/ChangeEmailRequest.java @@ -1,6 +1,3 @@ package com.project.core.controller.dto.request; -public record ChangeEmailRequest( - String emailEnc -) { -} \ No newline at end of file +public record ChangeEmailRequest(String emailEnc) {} diff --git a/src/main/java/com/project/core/controller/dto/request/ChangeGradeRequest.java b/src/main/java/com/project/core/controller/dto/request/ChangeGradeRequest.java index 6fbcda3..c7fc510 100644 --- a/src/main/java/com/project/core/controller/dto/request/ChangeGradeRequest.java +++ b/src/main/java/com/project/core/controller/dto/request/ChangeGradeRequest.java @@ -2,6 +2,4 @@ import com.project.core.infra.entity.customer.enums.Grade; -public record ChangeGradeRequest(Grade grade) { - -} +public record ChangeGradeRequest(Grade grade) {} diff --git a/src/main/java/com/project/core/controller/dto/request/PlanChangeRequest.java b/src/main/java/com/project/core/controller/dto/request/PlanChangeRequest.java index 179ef48..bbe6d8d 100644 --- a/src/main/java/com/project/core/controller/dto/request/PlanChangeRequest.java +++ b/src/main/java/com/project/core/controller/dto/request/PlanChangeRequest.java @@ -1,6 +1,3 @@ package com.project.core.controller.dto.request; -public record PlanChangeRequest( - Long subId, - Long planId -) {} +public record PlanChangeRequest(Long subId, Long planId) {} diff --git a/src/main/java/com/project/core/controller/dto/request/SaveExampleRequest.java b/src/main/java/com/project/core/controller/dto/request/SaveExampleRequest.java index 404b2c5..a14a344 100644 --- a/src/main/java/com/project/core/controller/dto/request/SaveExampleRequest.java +++ b/src/main/java/com/project/core/controller/dto/request/SaveExampleRequest.java @@ -1,7 +1,3 @@ package com.project.core.controller.dto.request; -public record SaveExampleRequest( - String exampleName, - String exampleContent -) { -} +public record SaveExampleRequest(String exampleName, String exampleContent) {} diff --git a/src/main/java/com/project/core/controller/dto/response/ChangeEmailResponse.java b/src/main/java/com/project/core/controller/dto/response/ChangeEmailResponse.java index 82dee1f..0ee4374 100644 --- a/src/main/java/com/project/core/controller/dto/response/ChangeEmailResponse.java +++ b/src/main/java/com/project/core/controller/dto/response/ChangeEmailResponse.java @@ -1,5 +1,3 @@ package com.project.core.controller.dto.response; -public record ChangeEmailResponse(String emailEnc) { - -} +public record ChangeEmailResponse(String emailEnc) {} diff --git a/src/main/java/com/project/core/controller/dto/response/ChangeGradeResponse.java b/src/main/java/com/project/core/controller/dto/response/ChangeGradeResponse.java index 76a0b9f..31d5cd6 100644 --- a/src/main/java/com/project/core/controller/dto/response/ChangeGradeResponse.java +++ b/src/main/java/com/project/core/controller/dto/response/ChangeGradeResponse.java @@ -2,6 +2,4 @@ import com.project.core.infra.entity.customer.enums.Grade; -public record ChangeGradeResponse(Grade grade) { - -} +public record ChangeGradeResponse(Grade grade) {} diff --git a/src/main/java/com/project/core/infra/entity/customer/Customer.java b/src/main/java/com/project/core/infra/entity/customer/Customer.java index 8f71ff4..03083d2 100644 --- a/src/main/java/com/project/core/infra/entity/customer/Customer.java +++ b/src/main/java/com/project/core/infra/entity/customer/Customer.java @@ -1,12 +1,7 @@ package com.project.core.infra.entity.customer; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; - -import com.project.core.infra.entity.subscription.Subscription; import com.project.core.infra.entity.customer.enums.Grade; - +import com.project.core.infra.entity.subscription.Subscription; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -17,6 +12,9 @@ import jakarta.persistence.Id; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -26,38 +24,38 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "customer") public class Customer { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "customer_id") - private Long customerId; - - @Column(name = "name", nullable = false) - private String name; - - @Column(name = "contact_enc", nullable = false) //암호화된 전화번호 - private String contactEnc; - - @Column(name = "email_enc", nullable = false) //암호화된 이메일 - private String emailEnc; - - @Enumerated(EnumType.STRING) - @Column(name = "grade", nullable = false, length = 20) - private Grade grade; - - @Column(name = "created_at", nullable = false) - private LocalDateTime createdAt; - - @Column(name = "is_deleted", nullable = false) - private Boolean isDeleted; - - @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) - private List subscriptionHistory = new ArrayList<>(); - - - public void changeEmailEnc(String emailEnc) { - this.emailEnc = emailEnc; - } - public void changeGrade(Grade grade) { - this.grade = grade; - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "customer_id") + private Long customerId; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "contact_enc", nullable = false) // 암호화된 전화번호 + private String contactEnc; + + @Column(name = "email_enc", nullable = false) // 암호화된 이메일 + private String emailEnc; + + @Enumerated(EnumType.STRING) + @Column(name = "grade", nullable = false, length = 20) + private Grade grade; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "is_deleted", nullable = false) + private Boolean isDeleted; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL) + private List subscriptionHistory = new ArrayList<>(); + + public void changeEmailEnc(String emailEnc) { + this.emailEnc = emailEnc; + } + + public void changeGrade(Grade grade) { + this.grade = grade; + } } diff --git a/src/main/java/com/project/core/infra/entity/customer/enums/Grade.java b/src/main/java/com/project/core/infra/entity/customer/enums/Grade.java index de56891..6e385f2 100644 --- a/src/main/java/com/project/core/infra/entity/customer/enums/Grade.java +++ b/src/main/java/com/project/core/infra/entity/customer/enums/Grade.java @@ -1,7 +1,7 @@ package com.project.core.infra.entity.customer.enums; public enum Grade { - GENERAL // 일반 - ,VIP - ,VVIP + GENERAL, // 일반 + VIP, + VVIP } diff --git a/src/main/java/com/project/core/infra/entity/plan/Plan.java b/src/main/java/com/project/core/infra/entity/plan/Plan.java index b4ee04c..55f66ef 100644 --- a/src/main/java/com/project/core/infra/entity/plan/Plan.java +++ b/src/main/java/com/project/core/infra/entity/plan/Plan.java @@ -1,7 +1,12 @@ package com.project.core.infra.entity.plan; import com.project.core.infra.entity.plan.enums.AllowancePeriod; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -12,20 +17,20 @@ @Table(name = "plan") public class Plan { - @Id - @Column(name = "plan_id") - private Long planId; + @Id + @Column(name = "plan_id") + private Long planId; - @Column(name = "plan_name", nullable = false, length = 30) - private String planName; + @Column(name = "plan_name", nullable = false, length = 30) + private String planName; - @Column(name = "plan_base_fee", nullable = false) - private Integer planBaseFee; + @Column(name = "plan_base_fee", nullable = false) + private Integer planBaseFee; - @Column(name = "allowance_amount", nullable = false) - private Long allowanceAmount; // MB 단위, -1은 무제한 + @Column(name = "allowance_amount", nullable = false) + private Long allowanceAmount; // MB 단위, -1은 무제한 - @Enumerated(EnumType.STRING) - @Column(name = "allowance_period", nullable = false, length = 10) - private AllowancePeriod allowancePeriod; // MONTH / DAY + @Enumerated(EnumType.STRING) + @Column(name = "allowance_period", nullable = false, length = 10) + private AllowancePeriod allowancePeriod; // MONTH / DAY } diff --git a/src/main/java/com/project/core/infra/entity/plan/SubscriptionPlan.java b/src/main/java/com/project/core/infra/entity/plan/SubscriptionPlan.java index 09790e4..df00a53 100644 --- a/src/main/java/com/project/core/infra/entity/plan/SubscriptionPlan.java +++ b/src/main/java/com/project/core/infra/entity/plan/SubscriptionPlan.java @@ -1,53 +1,60 @@ package com.project.core.infra.entity.plan; import com.project.core.infra.entity.subscription.Subscription; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.time.LocalDateTime; - @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "subscription_plan") public class SubscriptionPlan { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "sp_id") - private Long spId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "sub_id", nullable = false) - private Subscription subscription; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "plan_id", nullable = false) - private Plan plan; - - @Column(name = "cost", nullable = false) - private Integer cost; - - @Column(name = "created_date", nullable = false) - private LocalDateTime createdDate; - - @Column(name = "left_date", nullable = false) - private LocalDateTime leftDate; - - @Builder - public SubscriptionPlan(Subscription subscription, Plan plan) { - this.subscription = subscription; - this.plan = plan; - this.cost = plan.getPlanBaseFee(); // 요금제 가격을 스냅샷으로 저장 - this.createdDate = LocalDateTime.now(); - this.leftDate = LocalDateTime.of(9999, 12, 31, 23, 59, 59); - } - - // 요금제 해지(만료) 처리 메소드 - public void expire() { - this.leftDate = LocalDateTime.now(); - } + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "sp_id") + private Long spId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sub_id", nullable = false) + private Subscription subscription; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "plan_id", nullable = false) + private Plan plan; + + @Column(name = "cost", nullable = false) + private Integer cost; + + @Column(name = "created_date", nullable = false) + private LocalDateTime createdDate; + + @Column(name = "left_date", nullable = false) + private LocalDateTime leftDate; + + @Builder + public SubscriptionPlan(Subscription subscription, Plan plan) { + this.subscription = subscription; + this.plan = plan; + this.cost = plan.getPlanBaseFee(); // 요금제 가격을 스냅샷으로 저장 + this.createdDate = LocalDateTime.now(); + this.leftDate = LocalDateTime.of(9999, 12, 31, 23, 59, 59); + } + + // 요금제 해지(만료) 처리 메소드 + public void expire() { + this.leftDate = LocalDateTime.now(); + } } diff --git a/src/main/java/com/project/core/infra/entity/plan/enums/AllowancePeriod.java b/src/main/java/com/project/core/infra/entity/plan/enums/AllowancePeriod.java index 3bf79c1..ab95503 100644 --- a/src/main/java/com/project/core/infra/entity/plan/enums/AllowancePeriod.java +++ b/src/main/java/com/project/core/infra/entity/plan/enums/AllowancePeriod.java @@ -1,6 +1,6 @@ package com.project.core.infra.entity.plan.enums; public enum AllowancePeriod { - MONTH, - DAY + MONTH, + DAY } diff --git a/src/main/java/com/project/core/infra/entity/subscription/enums/SubscriptionStatus.java b/src/main/java/com/project/core/infra/entity/subscription/enums/SubscriptionStatus.java index af88641..9a14d82 100644 --- a/src/main/java/com/project/core/infra/entity/subscription/enums/SubscriptionStatus.java +++ b/src/main/java/com/project/core/infra/entity/subscription/enums/SubscriptionStatus.java @@ -1,7 +1,7 @@ package com.project.core.infra.entity.subscription.enums; public enum SubscriptionStatus { - ACTIVE, // 사용중 - SUSPENDED, // 정지 - TERMINATED // 해지됨 + ACTIVE, // 사용중 + SUSPENDED, // 정지 + TERMINATED // 해지됨 } diff --git a/src/main/java/com/project/core/infra/entity/vas/SubscriptionVas.java b/src/main/java/com/project/core/infra/entity/vas/SubscriptionVas.java index 2b358a8..72f4c41 100644 --- a/src/main/java/com/project/core/infra/entity/vas/SubscriptionVas.java +++ b/src/main/java/com/project/core/infra/entity/vas/SubscriptionVas.java @@ -1,18 +1,24 @@ package com.project.core.infra.entity.vas; import com.project.core.infra.entity.subscription.Subscription; - -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; @Entity public class SubscriptionVas { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "sv_id") - private Long svId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "sub_id", nullable = false) - private Subscription subscription; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "sv_id") + private Long svId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sub_id", nullable = false) + private Subscription subscription; } diff --git a/src/main/java/com/project/core/infra/repository/plan/PlanRepository.java b/src/main/java/com/project/core/infra/repository/plan/PlanRepository.java index 005feb3..7416b59 100644 --- a/src/main/java/com/project/core/infra/repository/plan/PlanRepository.java +++ b/src/main/java/com/project/core/infra/repository/plan/PlanRepository.java @@ -3,5 +3,4 @@ import com.project.core.infra.entity.plan.Plan; import org.springframework.data.jpa.repository.JpaRepository; -public interface PlanRepository extends JpaRepository { -} +public interface PlanRepository extends JpaRepository {} diff --git a/src/main/java/com/project/core/infra/repository/plan/SubscriptionPlanRepository.java b/src/main/java/com/project/core/infra/repository/plan/SubscriptionPlanRepository.java index fbe1bae..8e84dee 100644 --- a/src/main/java/com/project/core/infra/repository/plan/SubscriptionPlanRepository.java +++ b/src/main/java/com/project/core/infra/repository/plan/SubscriptionPlanRepository.java @@ -1,15 +1,16 @@ package com.project.core.infra.repository.plan; import com.project.core.infra.entity.plan.SubscriptionPlan; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.Optional; - public interface SubscriptionPlanRepository extends JpaRepository { - // 해당 회선이 현재 사용 중인 요금제 조회 (leftDate가 현재보다 미래인 것) - @Query("SELECT sp from SubscriptionPlan sp WHERE sp.subscription.subId = :subId AND sp.leftDate > CURRENT_TIMESTAMP") - Optional findActivePlanBySubId(@Param("subId") Long subId); + // 해당 회선이 현재 사용 중인 요금제 조회 (leftDate가 현재보다 미래인 것) + @Query( + "SELECT sp from SubscriptionPlan sp " + + "WHERE sp.subscription.subId = :subId AND sp.leftDate > CURRENT_TIMESTAMP") + Optional findActivePlanBySubId(@Param("subId") Long subId); } diff --git a/src/main/java/com/project/global/config/RedisConfig.java b/src/main/java/com/project/global/config/RedisConfig.java index 0d51ea2..d0df546 100644 --- a/src/main/java/com/project/global/config/RedisConfig.java +++ b/src/main/java/com/project/global/config/RedisConfig.java @@ -1,23 +1,23 @@ package com.project.global.config; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -//import org.springframework.data.redis.connection.RedisConnectionFactory; -//import org.springframework.data.redis.core.RedisTemplate; -//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -//import org.springframework.data.redis.serializer.StringRedisSerializer; + +// import org.springframework.data.redis.connection.RedisConnectionFactory; +// import org.springframework.data.redis.core.RedisTemplate; +// import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +// import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { -// @Bean -// public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { -// RedisTemplate template = new RedisTemplate<>(); -// template.setConnectionFactory(connectionFactory); -// template.setKeySerializer(new StringRedisSerializer()); -// template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); -// template.setHashKeySerializer(new StringRedisSerializer()); -// template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); -// return template; -// } + // @Bean + // public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + // RedisTemplate template = new RedisTemplate<>(); + // template.setConnectionFactory(connectionFactory); + // template.setKeySerializer(new StringRedisSerializer()); + // template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + // template.setHashKeySerializer(new StringRedisSerializer()); + // template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); + // return template; + // } } diff --git a/src/main/java/com/project/global/exception/ApplicationException.java b/src/main/java/com/project/global/exception/ApplicationException.java index 85c8d56..38acf76 100644 --- a/src/main/java/com/project/global/exception/ApplicationException.java +++ b/src/main/java/com/project/global/exception/ApplicationException.java @@ -4,7 +4,7 @@ public class ApplicationException extends BaseException { - public ApplicationException(BaseErrorCode code) { - super(code); - } + public ApplicationException(BaseErrorCode code) { + super(code); + } } diff --git a/src/main/java/com/project/global/exception/BaseException.java b/src/main/java/com/project/global/exception/BaseException.java index 207343d..9f50515 100644 --- a/src/main/java/com/project/global/exception/BaseException.java +++ b/src/main/java/com/project/global/exception/BaseException.java @@ -6,18 +6,18 @@ @Getter public abstract class BaseException extends RuntimeException { - private final BaseErrorCode code; + private final BaseErrorCode code; - protected BaseException(BaseErrorCode code) { - super(code.getMessage()); - this.code = code; - } + protected BaseException(BaseErrorCode code) { + super(code.getMessage()); + this.code = code; + } - public static T from(BaseErrorCode code, Class exceptionClass) { - try { - return exceptionClass.getConstructor(BaseErrorCode.class).newInstance(code); - } catch (Exception e) { - throw new RuntimeException("Could not create exception instance", e); - } + public static T from(BaseErrorCode code, Class exceptionClass) { + try { + return exceptionClass.getConstructor(BaseErrorCode.class).newInstance(code); + } catch (Exception e) { + throw new RuntimeException("Could not create exception instance", e); } + } } diff --git a/src/main/java/com/project/global/exception/ExceptionAdvice.java b/src/main/java/com/project/global/exception/ExceptionAdvice.java index 6aa53d6..7705467 100644 --- a/src/main/java/com/project/global/exception/ExceptionAdvice.java +++ b/src/main/java/com/project/global/exception/ExceptionAdvice.java @@ -4,47 +4,41 @@ import com.project.global.exception.code.domain.GlobalErrorCode; import jakarta.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ProblemDetail; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -import org.springframework.http.ProblemDetail; - @Slf4j @RestControllerAdvice public class ExceptionAdvice extends ResponseEntityExceptionHandler { - @ExceptionHandler(BaseException.class) - public ResponseEntity handleBaseException(BaseException e, HttpServletRequest request) { - BaseErrorCode code = e.getCode(); - log.error("[BaseException] {} - {}", code.name(), code.getMessage()); + @ExceptionHandler(BaseException.class) + public ResponseEntity handleBaseException(BaseException e, HttpServletRequest request) { + BaseErrorCode code = e.getCode(); + log.error("[BaseException] {} - {}", code.name(), code.getMessage()); - ProblemDetail problem = ProblemDetail.forStatus(code.getHttpStatus()); - problem.setTitle(code.name()); - problem.setDetail(code.getMessage()); - problem.setProperty("code", code.getCustomCode()); + ProblemDetail problem = ProblemDetail.forStatus(code.getHttpStatus()); + problem.setTitle(code.name()); + problem.setDetail(code.getMessage()); + problem.setProperty("code", code.getCustomCode()); - return ResponseEntity - .status(code.getHttpStatus()) - .body(problem); - } + return ResponseEntity.status(code.getHttpStatus()).body(problem); + } - @ExceptionHandler(Exception.class) - public ResponseEntity handleUnhandledException(Exception e, WebRequest request) { - log.error("[Exception] Unhandled", e); + @ExceptionHandler(Exception.class) + public ResponseEntity handleUnhandledException(Exception e, WebRequest request) { + log.error("[Exception] Unhandled", e); - GlobalErrorCode code = GlobalErrorCode.INTERNAL_SERVER_ERROR; + GlobalErrorCode code = GlobalErrorCode.INTERNAL_SERVER_ERROR; - ProblemDetail problem = ProblemDetail.forStatus(code.getHttpStatus()); - problem.setTitle(code.name()); - problem.setDetail(code.getMessage()); - problem.setProperty("code", code.getCustomCode()); + ProblemDetail problem = ProblemDetail.forStatus(code.getHttpStatus()); + problem.setTitle(code.name()); + problem.setDetail(code.getMessage()); + problem.setProperty("code", code.getCustomCode()); - return ResponseEntity - .status(code.getHttpStatus()) - .body(problem); - } + return ResponseEntity.status(code.getHttpStatus()).body(problem); + } } diff --git a/src/main/java/com/project/global/exception/code/domain/BaseErrorCode.java b/src/main/java/com/project/global/exception/code/domain/BaseErrorCode.java index cb78e09..f0dafbe 100644 --- a/src/main/java/com/project/global/exception/code/domain/BaseErrorCode.java +++ b/src/main/java/com/project/global/exception/code/domain/BaseErrorCode.java @@ -3,8 +3,11 @@ import org.springframework.http.HttpStatus; public interface BaseErrorCode { - String name(); // ⭐ 핵심 - HttpStatus getHttpStatus(); - String getMessage(); - String getCustomCode(); + String name(); // ⭐ 핵심 + + HttpStatus getHttpStatus(); + + String getMessage(); + + String getCustomCode(); } diff --git a/src/main/java/com/project/global/exception/code/domain/GlobalErrorCode.java b/src/main/java/com/project/global/exception/code/domain/GlobalErrorCode.java index 9690929..66c4dc6 100644 --- a/src/main/java/com/project/global/exception/code/domain/GlobalErrorCode.java +++ b/src/main/java/com/project/global/exception/code/domain/GlobalErrorCode.java @@ -1,19 +1,17 @@ package com.project.global.exception.code.domain; -import org.springframework.http.HttpStatus; - import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; @Getter @RequiredArgsConstructor -public enum GlobalErrorCode implements BaseErrorCode{ - - EXAMPLE_NOT_FOUND(HttpStatus.BAD_REQUEST, "EXAMPLE_001", "Example을 찾을 수 없습니다"), - INTERNAL_SERVER_ERROR(HttpStatus.BAD_REQUEST, "EXAMPLE_001", "Example을 찾을 수 없습니다"), - ; +public enum GlobalErrorCode implements BaseErrorCode { + EXAMPLE_NOT_FOUND(HttpStatus.BAD_REQUEST, "EXAMPLE_001", "Example을 찾을 수 없습니다"), + INTERNAL_SERVER_ERROR(HttpStatus.BAD_REQUEST, "EXAMPLE_001", "Example을 찾을 수 없습니다"), + ; - private final HttpStatus httpStatus; - private final String customCode; - private final String message; + private final HttpStatus httpStatus; + private final String customCode; + private final String message; } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ab7b6af..18428bf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,7 +1,14 @@ +project: + name: API Core + version: 1.0.0 + spring: profiles: include: secret + application: + name: api-core + datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5433/${POSTGRES_DB:app_db} @@ -23,7 +30,37 @@ spring: format_sql: true open-in-view: false +server: + port: 8080 + +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics,loggers + endpoint: + health: + show-details: always + probes: + enabled: true + prometheus: + enabled: true + prometheus: + metrics: + export: + enabled: true + metrics: + tags: + application: ${spring.application.name} + tracing: + sampling: + probability: ${OTEL_SAMPLING_PROBABILITY:1.0} + otlp: + tracing: + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318/v1/traces} + logging: level: - org.hibernate.SQL: debug - org.hibernate.orm.jdbc.bind: trace + root: INFO + org.springframework.web: INFO + org.hibernate.SQL: WARN diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..bbe9bf8 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,39 @@ + + + + + + + + + + ts + level + logger + msg + + {"service":"${APP_NAME}","env":"${APP_ENV}"} + traceId + spanId + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + +