-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] Docker Compose 기반 환경별 CI/CD 구축 #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
66a17f8
e48b658
03188c4
aaf316b
741d0b9
bdfedca
d6f6021
cc24f67
e664027
b946824
fc9c961
3cdeac8
d79b7ac
2d027cc
401ea95
0e53acb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,73 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: CD [Development] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| push: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| branches: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - develop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| paths: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 'src/**' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 'build.gradle.kts' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 'docker/**' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - 'cd.yml' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: 'Deploy environment' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| required: true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: choice | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| options: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - dev | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - prod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 1. 공통 빌드/테스트 워크플로우 호출 (ci.yml 재사용) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ci-and-build: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uses: ./.github/workflows/ci.yml | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| environment: ${{ inputs.environment }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| secrets: inherit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 2. Deploy Job | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deploy: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| needs: ci-and-build | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # 빌드 단계에서 결정된 환경 사용 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| environment: ${{ needs.ci-and-build.outputs.environment }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ENVIRONMENT: ${{ needs.ci-and-build.outputs.environment }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| REPO_OWNER: ${{ needs.ci-and-build.outputs.repo_owner }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IMAGE_TAG: ${{ needs.ci-and-build.outputs.image_tag }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: Checkout code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: SSH 접속 및 스크립트 실행 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uses: appleboy/ssh-action@v1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| env: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| APPLICATION_PROD: ${{ secrets.APPLICATION_PROD }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| APPLICATION_DEV: ${{ secrets.APPLICATION_DEV }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| host: ${{ secrets.SERVER_SSH_HOST }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: ${{ secrets.SERVER_SSH_USERNAME }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| port: ${{ secrets.SERVER_SSH_PORT }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| envs: APPLICATION_PROD,APPLICATION_DEV,APPLICATION_SECRET,ENVIRONMENT,IMAGE_TAG,REPO_OWNER,DOCKER_IMAGE_NAME,DOCKERHUB_TOKEN,DOCKERHUB_USERNAME | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| script_path: scripts/cd-dev.sh | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - name: SSH 접속 및 스크립트 실행 | |
| uses: appleboy/ssh-action@v1 | |
| env: | |
| APPLICATION_PROD: ${{ secrets.APPLICATION_PROD }} | |
| APPLICATION_DEV: ${{ secrets.APPLICATION_DEV }} | |
| APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| with: | |
| host: ${{ secrets.SERVER_SSH_HOST }} | |
| username: ${{ secrets.SERVER_SSH_USERNAME }} | |
| key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SERVER_SSH_PORT }} | |
| envs: APPLICATION_PROD,APPLICATION_DEV,APPLICATION_SECRET,ENVIRONMENT,IMAGE_TAG,REPO_OWNER,DOCKER_IMAGE_NAME,DOCKERHUB_TOKEN,DOCKERHUB_USERNAME | |
| script_path: scripts/cd-dev.sh | |
| - name: SSH 접속 및 스크립트 실행 | |
| uses: appleboy/ssh-action@v1 | |
| env: | |
| APPLICATION_PROD: ${{ secrets.APPLICATION_PROD }} | |
| APPLICATION_DEV: ${{ secrets.APPLICATION_DEV }} | |
| APPLICATION_SECRET: ${{ secrets.APPLICATION_SECRET }} | |
| DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} | |
| DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} | |
| DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE_NAME }} | |
| APP_DIR_PRODUCTION: ${{ secrets.APP_DIR_PRODUCTION }} | |
| APP_DIR_DEVELOPMENT: ${{ secrets.APP_DIR_DEVELOPMENT }} | |
| with: | |
| host: ${{ secrets.SERVER_SSH_HOST }} | |
| username: ${{ secrets.SERVER_SSH_USERNAME }} | |
| key: ${{ secrets.SERVER_SSH_PRIVATE_KEY }} | |
| port: ${{ secrets.SERVER_SSH_PORT }} | |
| envs: APPLICATION_PROD,APPLICATION_DEV,APPLICATION_SECRET,ENVIRONMENT,IMAGE_TAG,REPO_OWNER,DOCKER_IMAGE_NAME,DOCKERHUB_TOKEN,DOCKERHUB_USERNAME,APP_DIR_PRODUCTION,APP_DIR_DEVELOPMENT | |
| script_path: scripts/cd-dev.sh |
🤖 Prompt for AI Agents
In @.github/workflows/cd-dev.yml around lines 46 - 61, Add the missing
APP_DIR_PRODUCTION and APP_DIR_DEVELOPMENT environment variables to the SSH
action: define them in the env: block (e.g., APP_DIR_PRODUCTION: ${{
secrets.APP_DIR_PRODUCTION }} and APP_DIR_DEVELOPMENT: ${{
secrets.APP_DIR_DEVELOPMENT }}) and include their names in the with: envs
comma-separated list (alongside APPLICATION_PROD,APPLICATION_DEV,...). This
ensures the deployment script referenced by script_path: cd-dev.sh receives the
required APP_DIR_PRODUCTION and APP_DIR_DEVELOPMENT values that it validates.
Outdated
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: SSH 액션의 script_path 속성은 로컬 파일 경로를 원격 서버로 전송하는 용도입니다. 그러나 현재 구조에서는 scripts/cd-dev.sh 파일이 원격 서버에 존재해야 하는데, 이 파일을 원격 서버로 전송하는 단계가 없습니다.
다음 두 가지 방법 중 하나를 선택해야 합니다:
- script 속성을 사용하여 스크립트 내용을 직접 전달
- 또는 scp/rsync로 스크립트를 먼저 전송한 후 실행
script 속성 사용 예시:
script: |
bash << 'EOF'
$(cat scripts/cd-dev.sh의 내용)
EOF| script_path: scripts/cd-dev.sh | |
| script: | | |
| bash << 'EOF' | |
| # scripts/cd-dev.sh의 내용을 여기에 그대로 붙여넣습니다. | |
| # 예: 기존 scripts/cd-dev.sh 파일의 모든 쉘 명령들 | |
| EOF |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Shellcheck 스타일 경고를 개선할 수 있습니다.
Deployment Summary 스크립트에서 여러 개의 echo 명령이 같은 파일로 리다이렉션됩니다. 이를 하나의 블록으로 묶으면 더 효율적이고 가독성이 좋아집니다.
♻️ 제안 수정사항
- name: Deployment Summary
if: always()
run: |
- echo "### Deployment Summary :rocket:" >> $GITHUB_STEP_SUMMARY
- echo "" >> $GITHUB_STEP_SUMMARY
- echo "- **Environment:** ${{ env.ENVIRONMENT }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Branch:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Image:** ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.ENVIRONMENT }}-latest" >> $GITHUB_STEP_SUMMARY
- echo "- **Server:** ${{ secrets.SERVER_SSH_HOST }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Status:** ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
+ {
+ echo "### Deployment Summary :rocket:"
+ echo ""
+ echo "- **Environment:** ${{ env.ENVIRONMENT }}"
+ echo "- **Branch:** ${{ github.ref_name }}"
+ echo "- **Commit:** ${{ github.sha }}"
+ echo "- **Image:** ${{ secrets.DOCKER_IMAGE_NAME }}:${{ env.ENVIRONMENT }}-latest"
+ echo "- **Server:** ${{ secrets.SERVER_SSH_HOST }}"
+ echo "- **Status:** ${{ job.status }}"
+ } >> "$GITHUB_STEP_SUMMARY"🧰 Tools
🪛 actionlint (1.7.10)
65-65: shellcheck reported issue in this script: SC2086:info:1:43: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:2:12: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:3:53: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:4:48: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:5:43: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:6:87: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:7:56: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2086:info:8:43: Double quote to prevent globbing and word splitting
(shellcheck)
65-65: shellcheck reported issue in this script: SC2129:style:1:1: Consider using { cmd1; cmd2; } >> file instead of individual redirects
(shellcheck)
🤖 Prompt for AI Agents
In @.github/workflows/cd-dev.yml around lines 65 - 73, Multiple sequential echo
statements appending to $GITHUB_STEP_SUMMARY are inefficient and trigger
shellcheck style warnings; replace the repeated echo lines with a single
here-document that appends the entire Deployment Summary to $GITHUB_STEP_SUMMARY
(use an unquoted heredoc like cat <<EOF >> $GITHUB_STEP_SUMMARY so environment
variables like ${env.ENVIRONMENT} and ${github.sha} expand) to improve
readability and reduce subprocess calls, keeping the same content and ordering
as the original echo lines.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,185 @@ | ||||||||||||||||||||||||||||||||||
| name: CI | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| on: | ||||||||||||||||||||||||||||||||||
| pull_request: | ||||||||||||||||||||||||||||||||||
| branches: | ||||||||||||||||||||||||||||||||||
| - develop | ||||||||||||||||||||||||||||||||||
| - main | ||||||||||||||||||||||||||||||||||
kyeoungwoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||||||||||
| description: 'Deploy environment' | ||||||||||||||||||||||||||||||||||
| required: true | ||||||||||||||||||||||||||||||||||
| type: choice | ||||||||||||||||||||||||||||||||||
| options: | ||||||||||||||||||||||||||||||||||
| - dev | ||||||||||||||||||||||||||||||||||
| - prod | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| workflow_call: | ||||||||||||||||||||||||||||||||||
| inputs: | ||||||||||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||||||||||
| description: 'Deploy environment (optional)' | ||||||||||||||||||||||||||||||||||
| required: false | ||||||||||||||||||||||||||||||||||
| type: string | ||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||
| environment: | ||||||||||||||||||||||||||||||||||
| description: "Determined environment" | ||||||||||||||||||||||||||||||||||
| value: ${{ jobs.build-and-test.outputs.environment }} | ||||||||||||||||||||||||||||||||||
| repo_owner: | ||||||||||||||||||||||||||||||||||
| description: "Repository owner" | ||||||||||||||||||||||||||||||||||
| value: ${{ jobs.build-and-test.outputs.repo_owner }} | ||||||||||||||||||||||||||||||||||
| image_tag: | ||||||||||||||||||||||||||||||||||
| description: "Docker image tag" | ||||||||||||||||||||||||||||||||||
| value: ${{ jobs.build-and-test.outputs.image_tag }} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| concurrency: | ||||||||||||||||||||||||||||||||||
| group: ci-${{ github.workflow }}-${{ github.ref }} | ||||||||||||||||||||||||||||||||||
| cancel-in-progress: true # 이전 실행 취소 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| jobs: | ||||||||||||||||||||||||||||||||||
| # Job 1: 빌드 및 테스트 (한 번만 실행) | ||||||||||||||||||||||||||||||||||
| build-and-test: | ||||||||||||||||||||||||||||||||||
| runs-on: self-hosted | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| permissions: | ||||||||||||||||||||||||||||||||||
| contents: read | ||||||||||||||||||||||||||||||||||
| checks: write | ||||||||||||||||||||||||||||||||||
| pull-requests: write | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| outputs: | ||||||||||||||||||||||||||||||||||
| environment: ${{ steps.set-env.outputs.environment }} | ||||||||||||||||||||||||||||||||||
| repo_owner: ${{ steps.set-env.outputs.repo_owner }} | ||||||||||||||||||||||||||||||||||
| image_tag: ${{ steps.set-env.outputs.image_tag }} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| strategy: | ||||||||||||||||||||||||||||||||||
| matrix: | ||||||||||||||||||||||||||||||||||
| platform: [ linux/amd64, linux/arm64 ] | ||||||||||||||||||||||||||||||||||
| include: | ||||||||||||||||||||||||||||||||||
| - platform: linux/amd64 | ||||||||||||||||||||||||||||||||||
| tag-suffix: amd64 | ||||||||||||||||||||||||||||||||||
| - platform: linux/arm64 | ||||||||||||||||||||||||||||||||||
| tag-suffix: arm64 | ||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| steps: | ||||||||||||||||||||||||||||||||||
| - name: Checkout code | ||||||||||||||||||||||||||||||||||
| uses: actions/checkout@v4 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Set deployment environment | ||||||||||||||||||||||||||||||||||
| id: set-env | ||||||||||||||||||||||||||||||||||
| run: | | ||||||||||||||||||||||||||||||||||
| INPUT_ENV="${{ inputs.environment }}" | ||||||||||||||||||||||||||||||||||
| # PR일 때는 'test' 환경으로 설정 | ||||||||||||||||||||||||||||||||||
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | ||||||||||||||||||||||||||||||||||
| echo "Running in PR mode - Setting test environment for build" | ||||||||||||||||||||||||||||||||||
| ENVIRONMENT="test" | ||||||||||||||||||||||||||||||||||
| elif [[ -n "$INPUT_ENV" ]]; then | ||||||||||||||||||||||||||||||||||
| ENVIRONMENT="$INPUT_ENV" | ||||||||||||||||||||||||||||||||||
| # CD에서 호출한 경우 branch에 따라서 환경 결정 | ||||||||||||||||||||||||||||||||||
| elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then | ||||||||||||||||||||||||||||||||||
| ENVIRONMENT="prod" | ||||||||||||||||||||||||||||||||||
| elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then | ||||||||||||||||||||||||||||||||||
| ENVIRONMENT="dev" | ||||||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||||||
| ENVIRONMENT="test" | ||||||||||||||||||||||||||||||||||
kyeoungwoon marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+87
|
||||||||||||||||||||||||||||||||||
| echo "environment=${ENVIRONMENT}" >> $GITHUB_OUTPUT | ||||||||||||||||||||||||||||||||||
| REPO_OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') | ||||||||||||||||||||||||||||||||||
| echo "repo_owner=${REPO_OWNER}" >> $GITHUB_OUTPUT | ||||||||||||||||||||||||||||||||||
| SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7) | ||||||||||||||||||||||||||||||||||
| IMAGE_TAG="${ENVIRONMENT}-${SHORT_SHA}" | ||||||||||||||||||||||||||||||||||
| echo "image_tag=${IMAGE_TAG}" >> $GITHUB_OUTPUT | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 환경 결정 로직에 else 절이 누락되었습니다. Line 77-79의 조건문에 else 절이 없어, 특정 경우에 ♻️ 제안하는 수정안 # PR일 때는 'test' 환경으로 설정
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "Running in PR mode - Setting test environment for build"
ENVIRONMENT="test"
elif [[ -n "$INPUT_ENV" ]]; then
ENVIRONMENT="$INPUT_ENV"
-
- # CD에서 호출한 경우 branch에 따라서 환경 결정
elif [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
ENVIRONMENT="prod"
elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
ENVIRONMENT="dev"
else
ENVIRONMENT="test"
fi🧰 Tools🪛 actionlint (1.7.10)70-70: shellcheck reported issue in this script: SC2086:info:19:38: Double quote to prevent globbing and word splitting (shellcheck) 70-70: shellcheck reported issue in this script: SC2086:info:22:36: Double quote to prevent globbing and word splitting (shellcheck) 70-70: shellcheck reported issue in this script: SC2086:info:26:34: Double quote to prevent globbing and word splitting (shellcheck) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| - name: Setup JDK 21 | ||||||||||||||||||||||||||||||||||
| uses: actions/setup-java@v4 | ||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||
| java-version: '21' | ||||||||||||||||||||||||||||||||||
| distribution: 'corretto' | ||||||||||||||||||||||||||||||||||
| cache: gradle | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Grant execute permission for gradlew | ||||||||||||||||||||||||||||||||||
| run: chmod +x gradlew | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Compile Check | ||||||||||||||||||||||||||||||||||
| run: ./gradlew compileJava compileTestJava | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Run Tests | ||||||||||||||||||||||||||||||||||
| run: ./gradlew test | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Publish Test Results | ||||||||||||||||||||||||||||||||||
| uses: EnricoMi/publish-unit-test-result-action@v2 | ||||||||||||||||||||||||||||||||||
| if: always() | ||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||
| files: build/test-results/test/*.xml | ||||||||||||||||||||||||||||||||||
| check_name: "Backend Test Results" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Build JAR | ||||||||||||||||||||||||||||||||||
| run: ./gradlew bootJar | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Set up QEMU | ||||||||||||||||||||||||||||||||||
| uses: docker/setup-qemu-action@v3 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Set up Docker Buildx | ||||||||||||||||||||||||||||||||||
| uses: docker/setup-buildx-action@v3 | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Log in to Docker Hub | ||||||||||||||||||||||||||||||||||
| uses: docker/login-action@v3 | ||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||
| username: ${{ secrets.DOCKERHUB_USERNAME }} | ||||||||||||||||||||||||||||||||||
| password: ${{ secrets.DOCKERHUB_TOKEN }} | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| - name: Build and push Docker image | ||||||||||||||||||||||||||||||||||
| uses: docker/build-push-action@v6 | ||||||||||||||||||||||||||||||||||
| with: | ||||||||||||||||||||||||||||||||||
| context: . | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| context: . | |
| context: . | |
| # CI용 이미지 빌드를 위해 eclipse-temurin 기반 Dockerfile 사용 | |
| # (amazoncorretto 기반 docker/app/dockerfile은 런타임/기타 용도로 별도 관리) |
kyeoungwoon marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: PR 모드일 때 Docker 이미지를 빌드하고 푸시하는 것은 불필요한 리소스 사용입니다. PR은 코드 검증 목적이므로 테스트만 실행하고, 이미지 빌드와 푸시는 실제 배포가 필요한 경우에만 수행하는 것이 효율적입니다.
다음과 같이 조건부로 실행하도록 개선할 수 있습니다:
- name: Build and push Docker image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v6또는 PR의 경우 push: false로 설정하여 빌드만 검증하도록 할 수 있습니다.
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
kyeoungwoon marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Build Summary 단계가 잘못된 위치에 있습니다. steps.set-env는 build-and-test job에 속하지만, 현재 이 단계는 create-manifest job에 있습니다. steps.set-env.outputs는 다른 job에서 접근할 수 없으므로 항상 빈 값이 출력됩니다.
needs를 통해 전달받은 값을 사용해야 합니다:
echo "- **Environment:** ${{ needs.build-and-test.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
echo "- **Image Tag:** ${{ needs.build-and-test.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY| echo "- **Environment:** ${{ steps.set-env.outputs.environment }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image Tag:** ${{ steps.set-env.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Environment:** ${{ needs.build-and-test.outputs.environment }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image Tag:** ${{ needs.build-and-test.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Build Summary 단계에서 존재하지 않는 step 출력을 참조하고 있습니다.
steps.set-env.outputs를 참조하고 있지만, create-manifest 잡에는 set-env라는 id를 가진 step이 없습니다. build-and-test 잡의 출력을 참조해야 합니다.
🔧 수정 방법 (create-manifest 잡을 유지하는 경우)
- name: Build Summary
if: always()
run: |
echo "### Build Summary :package:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
- echo "- **Environment:** ${{ steps.set-env.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
- echo "- **Image Tag:** ${{ steps.set-env.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Environment:** ${{ needs.build-and-test.outputs.environment }}" >> $GITHUB_STEP_SUMMARY
+ echo "- **Image Tag:** ${{ needs.build-and-test.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARYStatic analysis 도구의 지적사항을 기반으로 함.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Build Summary | |
| if: always() | |
| run: | | |
| echo "### Build Summary :package:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Environment:** ${{ steps.set-env.outputs.environment }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image Tag:** ${{ steps.set-env.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY | |
| - name: Build Summary | |
| if: always() | |
| run: | | |
| echo "### Build Summary :package:" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Environment:** ${{ needs.build-and-test.outputs.environment }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image Tag:** ${{ needs.build-and-test.outputs.image_tag }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY |
🧰 Tools
🪛 actionlint (1.7.10)
186-186: property "set-env" is not defined in object type {}
(expression)
🤖 Prompt for AI Agents
In @.github/workflows/ci.yml around lines 184 - 191, The Build Summary step is
referencing nonexistent step outputs (steps.set-env.outputs.environment and
steps.set-env.outputs.image_tag); update the echo lines in the Build Summary
step to use the outputs from the build-and-test job instead
(needs.build-and-test.outputs.environment and
needs.build-and-test.outputs.image_tag), and ensure the create-manifest job
declares needs: build-and-test so those job outputs are available.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| FROM amazoncorretto:21-alpine-jre | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| LABEL maintainer="UMC Product Team" | ||||||||||||||||||||||||||||||||||||||||||||||
| LABEL description="UMC Product Team Official SpringBoot Backend" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # 작업 디렉토리 설정 | ||||||||||||||||||||||||||||||||||||||||||||||
| WORKDIR /app | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # curl 설치 (헬스체크용) + non-root 유저 생성 | ||||||||||||||||||||||||||||||||||||||||||||||
| RUN apk add --no-cache curl && \ | ||||||||||||||||||||||||||||||||||||||||||||||
| addgroup -S spring && \ | ||||||||||||||||||||||||||||||||||||||||||||||
| adduser -S spring -G spring && \ | ||||||||||||||||||||||||||||||||||||||||||||||
| mkdir -p /app/logs && \ | ||||||||||||||||||||||||||||||||||||||||||||||
| chown -R spring:spring /app | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 패키지 버전 고정을 고려하세요. curl 패키지가 버전 고정 없이 설치되고 있습니다. 빌드 재현성과 보안을 위해 특정 버전을 명시하는 것이 권장됩니다. ♻️ 제안하는 수정안-RUN apk add --no-cache curl && \
+RUN apk add --no-cache curl=8.5.0-r0 && \
addgroup -S spring && \
adduser -S spring -G spring && \
mkdir -p /app/logs && \
chown -R spring:spring /app참고: Alpine 버전에 따라 사용 가능한 curl 버전이 다를 수 있으므로 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Hadolint (2.14.0)[warning] 10-10: Pin versions in apk add. Instead of (DL3018) 🤖 Prompt for AI Agents
Comment on lines
+7
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curl 버전을 고정하고 중복 디렉토리 생성 제거 필요 이 섹션에는 두 가지 개선이 필요합니다:
🔧 제안하는 수정사항-RUN apk add --no-cache curl && \
+RUN apk add --no-cache curl=8.5.0-r0 && \
addgroup -S spring && \
adduser -S spring -G spring && \
mkdir -p /app/logs && \
chown -R spring:spring /app그리고 Line 18-19를 제거하세요 (중복): -# 로그 디렉토리 생성 및 권한 설정
-RUN mkdir -p /app/logs && chown -R spring:spring /app/logs참고: curl 버전은 Alpine 버전에 따라 다를 수 있으므로, 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Hadolint (2.14.0)[warning] 8-8: Pin versions in apk add. Instead of (DL3018) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # JAR 파일 복사 (GitHub Actions에서 빌드된 JAR) | ||||||||||||||||||||||||||||||||||||||||||||||
| # docker build 시 context에서 복사됨 | ||||||||||||||||||||||||||||||||||||||||||||||
| COPY --chown=spring:spring build/libs/*.jar app.jar | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # spring 유저로 전환 | ||||||||||||||||||||||||||||||||||||||||||||||
| USER spring:spring | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # 애플리케이션 포트 | ||||||||||||||||||||||||||||||||||||||||||||||
| EXPOSE 8080 | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| # Health check 설정 | ||||||||||||||||||||||||||||||||||||||||||||||
| # curl은 Alpine Linux에 기본 포함됨 | ||||||||||||||||||||||||||||||||||||||||||||||
| HEALTHCHECK --interval=30s \ | ||||||||||||||||||||||||||||||||||||||||||||||
| --timeout=3s \ | ||||||||||||||||||||||||||||||||||||||||||||||
| --start-period=40s \ | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| --start-period=40s \ | |
| --start-period=60s \ |
Outdated
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Health check URL이 /actuator/health로 하드코딩되어 있지만, 실제 애플리케이션에서 해당 엔드포인트가 활성화되어 있는지 확인이 필요합니다. Spring Boot Actuator가 의존성에 포함되어 있지 않으면 health check가 계속 실패하여 컨테이너가 unhealthy 상태로 표시됩니다.
다음 중 하나를 확인해야 합니다:
- build.gradle.kts에 spring-boot-starter-actuator 의존성이 있는지 확인
- 또는 health check를 주석 처리하거나 다른 엔드포인트로 변경
| # curl은 Alpine Linux에 기본 포함됨 | |
| HEALTHCHECK --interval=30s \ | |
| --timeout=3s \ | |
| --start-period=40s \ | |
| --retries=3 \ | |
| CMD curl -f http://localhost:8080/actuator/health || exit 1 | |
| # 주의: 아래 헬스체크는 Spring Boot Actuator의 /actuator/health 엔드포인트가 활성화되어 있어야만 동작합니다. | |
| # - build.gradle.kts에 spring-boot-starter-actuator 의존성이 포함되어 있는지 | |
| # - management.endpoints.web.exposure.include 등에 health 엔드포인트가 노출되어 있는지 | |
| # 를 확인한 뒤, 실제 애플리케이션의 헬스 엔드포인트에 맞게 수정 후 주석을 해제해 사용하세요. | |
| # 예시: | |
| # HEALTHCHECK --interval=30s \ | |
| # --timeout=3s \ | |
| # --start-period=40s \ | |
| # --retries=3 \ | |
| # CMD curl -f http://localhost:8080/actuator/health || exit 1 |
Outdated
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: JVM 메모리 설정이 하드코딩되어 있어 환경별 유연성이 떨어집니다. JAVA_OPTS를 ENV로 설정하면 런타임에 변경할 수 없으며, 개발/프로덕션 환경에서 다른 메모리 설정이 필요할 수 있습니다.
ARG를 사용하여 빌드 시점에 설정하거나, docker-compose.yml에서 환경 변수로 오버라이드할 수 있도록 변경하는 것을 권장합니다:
ENV JAVA_OPTS="-Xms1400m -Xmx1400m"
# docker-compose.yml에서 environment로 오버라이드 가능또는 kyeoungwoon dockerfile처럼 MaxRAMPercentage를 사용하여 컨테이너 메모리에 비례하도록 설정하는 것이 더 유연합니다.
| # 환경변수로 힙 메모리 설정 가능 (기본값: dev 환경) | |
| ENV JAVA_OPTS="-Xms1400m -Xmx1400m" | |
| # 기본 JVM 옵션: 컨테이너 메모리에 비례하여 힙 메모리 설정 | |
| ARG JAVA_OPTS="-XX:InitialRAMPercentage=30.0 -XX:MaxRAMPercentage=75.0" | |
| ENV JAVA_OPTS=${JAVA_OPTS} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # ================================= | ||
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| # Run stage only | ||
| # ================================= | ||
| FROM eclipse-temurin:21-jre-jammy | ||
| WORKDIR /app | ||
|
|
||
| LABEL maintainer="UMC PRODUCT TEAM SERVER TEAM" | ||
| LABEL description="UMC PRODUCT Official SpringBoot Backend Server" | ||
|
|
||
| # Create non-root user | ||
| RUN groupadd -r spring && useradd -r -g spring spring | ||
|
|
||
| # 이미 build된 jar 파일을 docker 안으로 복사 | ||
| COPY --chown=spring:spring build/libs/*.jar app.jar | ||
| RUN chmod 444 app.jar | ||
|
|
||
| # 로그 디렉토리 생성 및 권한 설정 | ||
| RUN mkdir -p /app/logs && chown -R spring:spring /app/logs | ||
|
|
||
| # Switch to non-root user | ||
| USER spring:spring | ||
|
|
||
| # Expose application port | ||
| EXPOSE 8080 | ||
|
|
||
| # JVM options | ||
| ENV JAVA_OPTS="-XX:+UseContainerSupport \ | ||
| -XX:MaxRAMPercentage=75.0 \ | ||
| -XX:+UseG1GC \ | ||
| -XX:+ExitOnOutOfMemoryError \ | ||
| -XX:+HeapDumpOnOutOfMemoryError \ | ||
| -XX:HeapDumpPath=/tmp/heapdump.hprof \ | ||
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| -Duser.timezone=Asia/Seoul \ | ||
| -Djava.security.egd=file:/dev/./urandom" | ||
|
|
||
| # Use exec form with sh -c so JAVA_OPTS is expanded and signals forwarded | ||
| ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar app.jar"] | ||
|
|
||
| # Optional healthcheck (uncomment if actuator/health endpoint exists) | ||
| # HEALTHCHECK --interval=30s --timeout=5s CMD curl -f http://localhost:8080/actuator/health || exit 1 | ||
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
kyeoungwoon marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.