diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..d851b3408
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,37 @@
+# Build output
+build/
+!build/libs/
+
+# IDE files
+.idea/
+*.iml
+*.iws
+
+# OS files
+.DS_Store
+Thumbs.db
+
+# Version control
+.git/
+.gitignore
+
+# Documentation
+README.md
+HELP.md
+
+# Other directories
+admin/
+.github/
+
+# Gradle cache
+.gradle/
+
+# Test results
+test-results/
+
+# Temporary files
+*.tmp
+*.log
+
+.env
+discodeit.env
\ No newline at end of file
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 000000000..b944f5644
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,106 @@
+name: Deploy to AWS ECS
+
+on:
+ push:
+ branches: [ release ]
+
+jobs:
+ build-and-push:
+ name: Build and Push Docker Image
+ runs-on: ubuntu-latest
+ outputs:
+ image: ${{ steps.build-image.outputs.image }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'temurin'
+ cache: gradle
+
+ - name: Configure AWS credentials
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: us-east-1
+
+ - name: Login to Amazon ECR (Public)
+ id: login-ecr
+ uses: aws-actions/amazon-ecr-login@v1
+ with:
+ registry-type: public
+
+ - name: Build, tag and push image to Amazone ECR
+ id: build-image
+ env:
+ ECR_REPOSITORY_URI: ${{ vars.ECR_REPOSITORY_URI }}
+ IMAGE_TAG: ${{ github.sha }}
+ run: |
+ chmod +x ./gradlew
+ ./gradlew bootJar
+
+ # Docker 이미지 빌드 (x86_64 아키텍처)
+ docker build --platform linux/amd64 \
+ -t $ECR_REPOSITORY_URI:$IMAGE_TAG \
+ -t $ECR_REPOSITORY_URI:latest .
+
+ # 이미지 푸시
+ docker push $ECR_REPOSITORY_URI:$IMAGE_TAG
+ docker push $ECR_REPOSITORY_URI:latest
+
+ echo "image=$ECR_REPOSITORY_URI:$IMAGE_TAG" >> $GITHUB_OUTPUT
+
+ deploy:
+ name: Deploy to ECS
+ runs-on: ubuntu-latest
+ needs: build-and-push
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Configure AWS credentials for ECS
+ uses: aws-actions/configure-aws-credentials@v4
+ with:
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
+ aws-region: ${{ vars.AWS_REGION }}
+
+ - name: Fill in the new image ID in the Amazon ECS task definition
+ id: task-def
+ uses: aws-actions/amazon-ecs-render-task-definition@v1
+ with:
+ task-definition: task-definition.json
+ container-name: discodeit-app
+ image: ${{ needs.build-and-push.outputs.image }}
+
+ - name: Stop existing ECS service
+ run: |
+ aws ecs update-service \
+ --cluster ${{ vars.ECS_CLUSTER }} \
+ --service ${{ vars.ECS_SERVICE }} \
+ --desired-count 0
+
+ aws ecs wait services-stable \
+ --cluster ${{ vars.ECS_CLUSTER }} \
+ --services ${{ vars.ECS_SERVICE }}
+
+ - name: Deploy to Amazon ECS
+ uses: aws-actions/amazon-ecs-deploy-task-definition@v1
+ with:
+ task-definition: ${{ steps.task-def.outputs.task-definition }}
+ cluster: ${{ vars.ECS_CLUSTER }}
+ service: ${{ vars.ECS_SERVICE }}
+ wait-for-service-stability: true
+
+ - name: Start ECS service
+ run: |
+ aws ecs update-service \
+ --cluster ${{ vars.ECS_CLUSTER }} \
+ --service ${{ vars.ECS_SERVICE }}\
+ --desired-count 1
\ No newline at end of file
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 000000000..6be7617cc
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,51 @@
+name: Java CI (Gradle + Codecov)
+
+on:
+ # main 브랜치 대상으로 만든 PR 에서만 실행
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ env:
+ SPRING_PROFILES_ACTIVE: test
+
+ steps:
+ # 1. 소스 체크아웃
+ - name: Source CheckOut
+ uses: actions/checkout@v4
+
+ # 2. JDK 17 설치 + Gradle 캐시
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: 17
+ cache: gradle # Gradle 종속성 캐싱 설정
+
+ # 3. Gradle wrapper 실행 권한 부여 (Linux/macOS용)
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+
+ # 4. GitHub Actions 시크릿 및 변수를 통한 .env 생성
+ - name: Generate .env file
+ run: |
+ echo "AWS_S3_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }}" >> .env
+ echo "AWS_S3_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }}" >> .env
+ echo "AWS_S3_BUCKET=${{ secrets.AWS_S3_BUCKET }}" >> .env
+ echo "AWS_S3_REGION=${{ vars.AWS_REGION }}" >> .env
+ echo "AWS_S3_PRESIGNED_URL_EXPIRATION=600" >> .env
+
+ # 5. 단위 테스트 + JaCoCo 커버리지 리포트 생성
+ - name: Run tests
+ run: |
+ ./gradlew clean test jacocoTestReport
+
+ # 6. Codecov 로 커버리지 업로드
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ files: build/reports/jacoco/test/jacocoTestReport.xml
+ fail_ci_if_error: true # 업로드 실패 시 워크플로 실패 처리
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index a88dda34a..e75c630ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,4 +45,22 @@ bin/
.storage
### Logs ###
-logs
\ No newline at end of file
+logs
+
+# Environment variables
+.env
+.env.local
+.env.*.local
+discodeit.env
+
+# Docker
+docker-compose.override.yml
+docker-compose.yml
+
+### AWS 관련 민감 파일들 ###
+*.pem
+*.key
+aws-credentials.txt
+.aws/
+credentials
+config
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index b018b2fc9..0ead08afa 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,14 +12,6 @@
-
-
-
-
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..1d5831c71
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,32 @@
+# === 1단계: 빌드 전용 이미지 ===
+FROM amazoncorretto:17 AS builder
+
+WORKDIR /app
+
+# Gradle Wrapper 및 프로젝트 소스 복사
+COPY gradlew gradlew.bat ./
+COPY gradle ./gradle
+COPY build.gradle settings.gradle ./
+COPY src ./src
+
+RUN chmod +x ./gradlew
+RUN ./gradlew clean build -x test
+
+# === 2단계: 실행 전용 이미지 ===
+FROM amazoncorretto:17
+
+WORKDIR /app
+
+# 실행에 필요한 jar 파일만 복사
+ARG PROJECT_NAME=discodeit
+ARG PROJECT_VERSION=1.2-M8
+COPY --from=builder /app/build/libs/${PROJECT_NAME}-${PROJECT_VERSION}.jar app.jar
+
+# 포트 노출
+EXPOSE 80
+
+# 환경변수 설정
+ENV JVM_OPTS=""
+
+# 애플리케이션 실행
+CMD ["sh", "-c", "java $JVM_OPTS -jar app.jar --spring.profiles.active=prod --server.port=80"]
diff --git a/README.md b/README.md
index 914230f9c..5f5954268 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Pure - Discord Clone Application
+# Pure - Discord Clone Application [](https://codecov.io/gh/pureod/3-sprint-mission)
Spring Boot 기반의 실시간 채팅 애플리케이션입니다. Discord와 유사한 기능들을 제공하는 RESTful API 서버입니다.
diff --git a/admin/.gitattributes b/admin/.gitattributes
deleted file mode 100644
index 8af972cde..000000000
--- a/admin/.gitattributes
+++ /dev/null
@@ -1,3 +0,0 @@
-/gradlew text eol=lf
-*.bat text eol=crlf
-*.jar binary
diff --git a/admin/.gitignore b/admin/.gitignore
deleted file mode 100644
index c2065bc26..000000000
--- a/admin/.gitignore
+++ /dev/null
@@ -1,37 +0,0 @@
-HELP.md
-.gradle
-build/
-!gradle/wrapper/gradle-wrapper.jar
-!**/src/main/**/build/
-!**/src/test/**/build/
-
-### STS ###
-.apt_generated
-.classpath
-.factorypath
-.project
-.settings
-.springBeans
-.sts4-cache
-bin/
-!**/src/main/**/bin/
-!**/src/test/**/bin/
-
-### IntelliJ IDEA ###
-.idea
-*.iws
-*.iml
-*.ipr
-out/
-!**/src/main/**/out/
-!**/src/test/**/out/
-
-### NetBeans ###
-/nbproject/private/
-/nbbuild/
-/dist/
-/nbdist/
-/.nb-gradle/
-
-### VS Code ###
-.vscode/
diff --git a/admin/build.gradle b/admin/build.gradle
deleted file mode 100644
index c6b7f29aa..000000000
--- a/admin/build.gradle
+++ /dev/null
@@ -1,38 +0,0 @@
-plugins {
- id 'java'
- id 'org.springframework.boot' version '3.5.3'
- id 'io.spring.dependency-management' version '1.1.7'
-}
-
-group = 'com.sprint.mission'
-version = '0.0.1-SNAPSHOT'
-
-java {
- toolchain {
- languageVersion = JavaLanguageVersion.of(17)
- }
-}
-
-repositories {
- mavenCentral()
-}
-
-ext {
- set('springBootAdminVersion', "3.5.0")
-}
-
-dependencies {
- implementation 'de.codecentric:spring-boot-admin-starter-server'
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
- testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
-}
-
-dependencyManagement {
- imports {
- mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
- }
-}
-
-tasks.named('test') {
- useJUnitPlatform()
-}
diff --git a/admin/gradle/wrapper/gradle-wrapper.jar b/admin/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index 1b33c55ba..000000000
Binary files a/admin/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/admin/gradle/wrapper/gradle-wrapper.properties b/admin/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index ff23a68d7..000000000
--- a/admin/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
-networkTimeout=10000
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/admin/gradlew b/admin/gradlew
deleted file mode 100644
index 23d15a936..000000000
--- a/admin/gradlew
+++ /dev/null
@@ -1,251 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015-2021 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# SPDX-License-Identifier: Apache-2.0
-#
-
-##############################################################################
-#
-# Gradle start up script for POSIX generated by Gradle.
-#
-# Important for running:
-#
-# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-# noncompliant, but you have some other compliant shell such as ksh or
-# bash, then to run this script, type that shell name before the whole
-# command line, like:
-#
-# ksh Gradle
-#
-# Busybox and similar reduced shells will NOT work, because this script
-# requires all of these POSIX shell features:
-# * functions;
-# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-# * compound commands having a testable exit status, especially «case»;
-# * various built-in commands including «command», «set», and «ulimit».
-#
-# Important for patching:
-#
-# (2) This script targets any POSIX shell, so it avoids extensions provided
-# by Bash, Ksh, etc; in particular arrays are avoided.
-#
-# The "traditional" practice of packing multiple parameters into a
-# space-separated string is a well documented source of bugs and security
-# problems, so this is (mostly) avoided, by progressively accumulating
-# options in "$@", and eventually passing that to Java.
-#
-# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-# see the in-line comments for details.
-#
-# There are tweaks for specific operating systems such as AIX, CygWin,
-# Darwin, MinGW, and NonStop.
-#
-# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
-# within the Gradle project.
-#
-# You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
- APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
- [ -h "$app_path" ]
-do
- ls=$( ls -ld "$app_path" )
- link=${ls#*' -> '}
- case $link in #(
- /*) app_path=$link ;; #(
- *) app_path=$APP_HOME$link ;;
- esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
- echo "$*"
-} >&2
-
-die () {
- echo
- echo "$*"
- echo
- exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in #(
- CYGWIN* ) cygwin=true ;; #(
- Darwin* ) darwin=true ;; #(
- MSYS* | MINGW* ) msys=true ;; #(
- NONSTOP* ) nonstop=true ;;
-esac
-
-CLASSPATH="\\\"\\\""
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD=$JAVA_HOME/jre/sh/java
- else
- JAVACMD=$JAVA_HOME/bin/java
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD=java
- if ! command -v java >/dev/null 2>&1
- then
- die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
- case $MAX_FD in #(
- max*)
- # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- MAX_FD=$( ulimit -H -n ) ||
- warn "Could not query maximum file descriptor limit"
- esac
- case $MAX_FD in #(
- '' | soft) :;; #(
- *)
- # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
- # shellcheck disable=SC2039,SC3045
- ulimit -n "$MAX_FD" ||
- warn "Could not set maximum file descriptor limit to $MAX_FD"
- esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-# * args from the command line
-# * the main class name
-# * -classpath
-# * -D...appname settings
-# * --module-path (only if needed)
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
- APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
-
- JAVACMD=$( cygpath --unix "$JAVACMD" )
-
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- for arg do
- if
- case $arg in #(
- -*) false ;; # don't mess with options #(
- /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
- [ -e "$t" ] ;; #(
- *) false ;;
- esac
- then
- arg=$( cygpath --path --ignore --mixed "$arg" )
- fi
- # Roll the args list around exactly as many times as the number of
- # args, so each arg winds up back in the position where it started, but
- # possibly modified.
- #
- # NB: a `for` loop captures its iteration list before it begins, so
- # changing the positional parameters here affects neither the number of
- # iterations, nor the values presented in `arg`.
- shift # remove old arg
- set -- "$@" "$arg" # push replacement arg
- done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-# and any embedded shellness will be escaped.
-# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-# treated as '${Hostname}' itself on the command line.
-
-set -- \
- "-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
- "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
- die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-# set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
- printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
- xargs -n1 |
- sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
- tr '\n' ' '
- )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/admin/gradlew.bat b/admin/gradlew.bat
deleted file mode 100644
index db3a6ac20..000000000
--- a/admin/gradlew.bat
+++ /dev/null
@@ -1,94 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem SPDX-License-Identifier: Apache-2.0
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-goto fail
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=
-
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
-
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/admin/settings.gradle b/admin/settings.gradle
deleted file mode 100644
index e83a19cde..000000000
--- a/admin/settings.gradle
+++ /dev/null
@@ -1 +0,0 @@
-rootProject.name = 'admin'
diff --git a/admin/src/main/java/com/sprint/mission/admin/AdminApplication.java b/admin/src/main/java/com/sprint/mission/admin/AdminApplication.java
deleted file mode 100644
index 9245ccc8a..000000000
--- a/admin/src/main/java/com/sprint/mission/admin/AdminApplication.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.sprint.mission.admin;
-
-import de.codecentric.boot.admin.server.config.EnableAdminServer;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-
-@SpringBootApplication
-@EnableAdminServer
-public class AdminApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(AdminApplication.class, args);
- }
-
-}
diff --git a/admin/src/main/resources/application.yaml b/admin/src/main/resources/application.yaml
deleted file mode 100644
index 0e146222d..000000000
--- a/admin/src/main/resources/application.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-spring:
- application:
- name: admin
-server:
- port: 9090
diff --git a/admin/src/test/java/com/sprint/mission/admin/AdminApplicationTests.java b/admin/src/test/java/com/sprint/mission/admin/AdminApplicationTests.java
deleted file mode 100644
index 490b85e74..000000000
--- a/admin/src/test/java/com/sprint/mission/admin/AdminApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.sprint.mission.admin;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class AdminApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
diff --git a/build.gradle b/build.gradle
index c7bd6091f..3c702bdc9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,7 +18,7 @@ jacocoTestReport {
}
group = 'com.sprint.mission'
-version = '0.0.1-SNAPSHOT'
+version = '1.2-M8'
java {
toolchain {
@@ -58,8 +58,7 @@ dependencies {
implementation 'org.slf4j:slf4j-api'
- implementation 'de.codecentric:spring-boot-admin-starter-client:3.4.5'
-
+ implementation 'software.amazon.awssdk:s3:2.31.7'
}
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
diff --git a/src/main/java/com/sprint/mission/discodeit/storage/s3/S3BinaryContentStorage.java b/src/main/java/com/sprint/mission/discodeit/storage/s3/S3BinaryContentStorage.java
new file mode 100644
index 000000000..030693a6f
--- /dev/null
+++ b/src/main/java/com/sprint/mission/discodeit/storage/s3/S3BinaryContentStorage.java
@@ -0,0 +1,149 @@
+package com.sprint.mission.discodeit.storage.s3;
+
+import com.sprint.mission.discodeit.dto.data.BinaryContentDto;
+import com.sprint.mission.discodeit.storage.BinaryContentStorage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.time.Duration;
+import java.util.NoSuchElementException;
+import java.util.UUID;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
+import software.amazon.awssdk.services.s3.model.PutObjectResponse;
+import software.amazon.awssdk.services.s3.model.S3Exception;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+
+@Log4j2
+@Component
+@ConditionalOnProperty(name = "discodeit.storage.type", havingValue = "s3")
+public class S3BinaryContentStorage implements BinaryContentStorage {
+
+ private final String accessKey;
+ private final String secretKey;
+ private final String region;
+ private final String bucket;
+
+ public S3BinaryContentStorage(
+ @Value("${discodeit.storage.s3.access-key}") String accessKey,
+ @Value("${discodeit.storage.s3.secret-key}") String secretKey,
+ @Value("${discodeit.storage.s3.region}") String region,
+ @Value("${discodeit.storage.s3.bucket}") String bucket
+ ) {
+ this.accessKey = accessKey;
+ this.secretKey = secretKey;
+ this.region = region;
+ this.bucket = bucket;
+ }
+
+ @Override
+ public UUID put(UUID binaryContentId, byte[] bytes) {
+
+ try (S3Client s3Client = getS3Client()) {
+ PutObjectResponse response = s3Client.putObject(
+ PutObjectRequest.builder()
+ .bucket(bucket)
+ .key(binaryContentId.toString())
+ .build(),
+ RequestBody.fromBytes(bytes)
+ );
+
+ if (response.eTag() == null || response.eTag().isEmpty()) {
+ throw new RuntimeException("Failed to upload file to S3");
+ }
+ }
+
+ return binaryContentId;
+
+ }
+
+ @Override
+ public InputStream get(UUID binaryContentId) {
+
+ try {
+ return getS3Client().getObject(GetObjectRequest.builder()
+ .bucket(bucket)
+ .key(binaryContentId.toString())
+ .build());
+
+ } catch (S3Exception s3e) {
+ if ("NoSuchKey".equals(s3e.awsErrorDetails().errorCode())) {
+ throw new NoSuchElementException(
+ "S3 object not found: " + binaryContentId, s3e);
+ }
+ throw s3e;
+ }
+
+ }
+
+ @Override
+ public ResponseEntity download(BinaryContentDto metaData) {
+
+ try (InputStream ignored = get(metaData.id())) {
+ log.info("S3 filed exist - fileId: {}", metaData.id());
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to verify S3 object", e);
+ }
+
+ String presignedUrl = generatePresignedUrl(metaData.id().toString(),
+ metaData.contentType());
+
+ log.info("S3 download - presignedUrl: {}", presignedUrl);
+
+ return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT)
+ .header(HttpHeaders.LOCATION, presignedUrl)
+ .build();
+
+ }
+
+ private S3Client getS3Client() {
+
+ AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
+
+ return S3Client.builder()
+ .region(Region.of(region))
+ .credentialsProvider(StaticCredentialsProvider.create(credentials))
+ .build();
+ }
+
+ private String generatePresignedUrl(String key, String contentType) {
+
+ AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey);
+
+ try (S3Presigner presigner = S3Presigner.builder()
+ .region(Region.of(region))
+ .credentialsProvider(StaticCredentialsProvider.create(credentials))
+ .build()) {
+
+ GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
+ .signatureDuration(Duration.ofSeconds(600))
+ .getObjectRequest(GetObjectRequest.builder()
+ .bucket(bucket)
+ .key(key)
+ .responseContentType(contentType)
+ .build())
+ .build();
+
+ URI presignedUri = presigner.presignGetObject(presignRequest).url().toURI();
+ return presignedUri.toString();
+
+ } catch (Exception e) {
+ throw new RuntimeException("Error generating presigned URL", e);
+ }
+ }
+
+}
diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml
index 619e2175d..f59af14db 100644
--- a/src/main/resources/application-dev.yaml
+++ b/src/main/resources/application-dev.yaml
@@ -2,11 +2,6 @@ spring:
application:
name: discodeit
- boot:
- admin:
- client:
- url: http://localhost:9090
-
config:
activate:
on-profile: dev
diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml
index 2ddac9ad6..19596603d 100644
--- a/src/main/resources/application-prod.yaml
+++ b/src/main/resources/application-prod.yaml
@@ -2,19 +2,14 @@ spring:
application:
name: discodeit
- boot:
- admin:
- client:
- url: ${SPRING_BOOT_ADMIN_CLIENT_URL}
-
config:
activate:
on-profile: prod
datasource:
- url: jdbc:postgresql://localhost:5432/discodeit
- username: discodeit_user
- password: discodeit1234
+ url: ${SPRING_DATASOURCE_URL}
+ username: ${SPRING_DATASOURCE_USERNAME}
+ password: ${SPRING_DATASOURCE_PASSWORD}
driver-class-name: org.postgresql.Driver
jpa:
@@ -41,4 +36,4 @@ logging:
org.hibernate.orm.jdbc.bind: off
server:
- port: 8081
+ port: 80
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 055e76565..866ef64e8 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -2,12 +2,6 @@ spring:
application:
name: discodeit
- boot:
- admin:
- client:
- instance:
- name: discodeit
-
profiles:
active: prod # 기본은 dev, 운영 시 prod로 교체
@@ -29,13 +23,15 @@ spring:
discodeit:
storage:
- type: local
+ type: ${STORAGE_TYPE:local}
local:
- root-path: ./.storage
-
-storage:
- type: local
- path: ./uploads
+ root-path: ${STORAGE_LOCAL_ROOT_PATH:./.storage}
+ s3:
+ access-key: ${AWS_S3_ACCESS_KEY}
+ secret-key: ${AWS_S3_SECRET_KEY}
+ region: ${AWS_S3_REGION}
+ bucket: ${AWS_S3_BUCKET}
+ presigned-url-expiration: ${AWS_S3_PRESIGNED_URL_EXPIRATION}
logging:
level:
diff --git a/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java b/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java
index 3a987a214..527261a22 100644
--- a/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java
+++ b/src/test/java/com/sprint/mission/discodeit/DiscodeitApplicationTests.java
@@ -2,12 +2,14 @@
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
@SpringBootTest
+@ActiveProfiles("test")
class DiscodeitApplicationTests {
- @Test
- void contextLoads() {
- }
+ @Test
+ void contextLoads() {
+ }
}
diff --git a/src/test/java/com/sprint/mission/discodeit/storage/s3/AWSS3Test.java b/src/test/java/com/sprint/mission/discodeit/storage/s3/AWSS3Test.java
new file mode 100644
index 000000000..6cda4e44d
--- /dev/null
+++ b/src/test/java/com/sprint/mission/discodeit/storage/s3/AWSS3Test.java
@@ -0,0 +1,136 @@
+package com.sprint.mission.discodeit.storage.s3;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.time.Duration;
+import java.util.Properties;
+
+import org.junit.jupiter.api.*;
+
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
+import software.amazon.awssdk.core.ResponseInputStream;
+import software.amazon.awssdk.core.sync.RequestBody;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.s3.model.*;
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class AWSS3Test {
+
+ private S3Client s3;
+ private S3Presigner presigner;
+ private String bucket;
+
+ private static final String KEY = "sampleTest-ljy.txt";
+
+ /**
+ * .env 로드 및 클라이언트 생성
+ */
+ @BeforeAll
+ void setUp() throws IOException {
+ Properties env = new Properties();
+ env.load(Files.newBufferedReader(Path.of(".env")));
+
+ AwsBasicCredentials credentials = AwsBasicCredentials.create(
+ env.getProperty("AWS_S3_ACCESS_KEY"),
+ env.getProperty("AWS_S3_SECRET_KEY"));
+
+ Region region = Region.of(env.getProperty("AWS_S3_REGION"));
+ this.bucket = env.getProperty("AWS_S3_BUCKET");
+
+ this.s3 = S3Client.builder()
+ .region(region)
+ .credentialsProvider(StaticCredentialsProvider.create(credentials))
+ .build();
+
+ this.presigner = S3Presigner.builder()
+ .region(region)
+ .credentialsProvider(StaticCredentialsProvider.create(credentials))
+ .build();
+ }
+
+ /**
+ * 리소스 정리
+ */
+ @AfterAll
+ void tearDown() {
+ if (s3 != null) {
+ s3.close();
+ }
+ if (presigner != null) {
+ presigner.close();
+ }
+ }
+
+ /**
+ * 1) 업로드
+ */
+ @Test
+ @DisplayName("S3 파일 업로드 성공")
+ void upload() throws IOException {
+ Path tmp = Files.writeString(Files.createTempFile("s3-upload", ".txt"),
+ "JUnit5 S3 upload test");
+
+ PutObjectResponse response = s3.putObject(
+ PutObjectRequest.builder()
+ .bucket(bucket)
+ .key(KEY)
+ .contentType("text/plain")
+ .build(),
+ RequestBody.fromFile(tmp));
+
+ assertThat(response.eTag()).isNotBlank();
+ Files.deleteIfExists(tmp);
+ }
+
+ /**
+ * 2) 다운로드
+ */
+ @Test
+ @DisplayName("S3 파일 다운로드 성공")
+ void download() throws IOException {
+ Path target = Files.createTempFile("s3-download", ".txt");
+
+ try (ResponseInputStream obj =
+ s3.getObject(GetObjectRequest.builder()
+ .bucket(bucket)
+ .key(KEY)
+ .build())) {
+ obj.transferTo(Files.newOutputStream(target));
+ }
+
+ String body = Files.readString(target);
+ System.out.println("target = " + target);
+ assertThat(body).contains("JUnit5 S3 upload test");
+ Files.deleteIfExists(target);
+ }
+
+ /**
+ * 3) Presigned URL 생성
+ */
+ @Test
+ @DisplayName("Presigned URL 생성 성공")
+ void presignedUrl() throws URISyntaxException {
+ URI url = presigner.presignGetObject(
+ GetObjectPresignRequest.builder()
+ .signatureDuration(Duration.ofMinutes(10))
+ .getObjectRequest(
+ GetObjectRequest.builder()
+ .bucket(bucket)
+ .key(KEY)
+ .build())
+ .build())
+ .url().toURI();
+
+ System.out.println("Presigned URL: " + url);
+ assertThat(url.toString()).startsWith("https://");
+ }
+}
diff --git a/src/test/java/com/sprint/mission/discodeit/storage/s3/S3BinaryContentStorageTest.java b/src/test/java/com/sprint/mission/discodeit/storage/s3/S3BinaryContentStorageTest.java
new file mode 100644
index 000000000..0bf4ce8b3
--- /dev/null
+++ b/src/test/java/com/sprint/mission/discodeit/storage/s3/S3BinaryContentStorageTest.java
@@ -0,0 +1,90 @@
+package com.sprint.mission.discodeit.storage.s3;
+
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
+import static org.assertj.core.api.Fail.fail;
+
+import com.sprint.mission.discodeit.dto.data.BinaryContentDto;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class S3BinaryContentStorageTest {
+
+ private S3BinaryContentStorage storage;
+ private UUID testId;
+ private byte[] testData;
+
+ @BeforeAll
+ void setUp() throws IOException {
+ Properties env = new Properties();
+ env.load(Files.newBufferedReader(Path.of(".env")));
+
+ storage = new S3BinaryContentStorage(
+ env.getProperty("AWS_S3_ACCESS_KEY"),
+ env.getProperty("AWS_S3_SECRET_KEY"),
+ env.getProperty("AWS_S3_REGION"),
+ env.getProperty("AWS_S3_BUCKET")
+ );
+
+ testId = UUID.randomUUID();
+ testData = "S3BinaryContentStorage 테스트 데이터".getBytes();
+ }
+
+ @Test
+ @DisplayName("파일 업로드 성공")
+ void put_success() {
+ UUID result = storage.put(testId, testData);
+ System.out.println("upload file id = " + result);
+ assertThat(result).isEqualTo(testId);
+ }
+
+ @Test
+ @DisplayName("파일 조회 성공")
+ void get_success() {
+ UUID testId = UUID.randomUUID();
+ storage.put(testId, testData);
+
+ try (InputStream inputStream = storage.get(testId)) {
+ byte[] retrievedData = inputStream.readAllBytes();
+ assertThat(retrievedData).isEqualTo(testData);
+
+ } catch (IOException e) {
+ fail("파일 조회 중 예외 발생: " + e.getMessage(), e);
+ }
+ }
+
+ @Test
+ @DisplayName("다운로드 - PresignedUrl 리다이렉트")
+ void download_success() {
+ UUID testId = UUID.randomUUID();
+ storage.put(testId, testData);
+
+ BinaryContentDto binaryContentDto = new BinaryContentDto(
+ testId,
+ "test-file.txt",
+ (long) testData.length,
+ "text/plain"
+ );
+
+ ResponseEntity response = storage.download(binaryContentDto);
+
+ System.out.println(
+ "response.getHeaders().getLocation() = " + response.getHeaders().getLocation());
+
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TEMPORARY_REDIRECT);
+ assertThat(response.getHeaders().getLocation()).isNotNull();
+
+ }
+
+}
diff --git a/task-definition.json b/task-definition.json
new file mode 100644
index 000000000..da72d5f91
--- /dev/null
+++ b/task-definition.json
@@ -0,0 +1,59 @@
+{
+ "containerDefinitions": [
+ {
+ "name": "discodeit-app",
+ "image": "public.ecr.aws/j1s9e0a6/discodeit:1.2-M8",
+ "cpu": 256,
+ "memory": 512,
+ "memoryReservation": 256,
+ "portMappings": [
+ {
+ "name": "discodeit-app-80-tcp",
+ "containerPort": 80,
+ "hostPort": 80,
+ "protocol": "tcp",
+ "appProtocol": "http"
+ }
+ ],
+ "essential": true,
+ "environment": [],
+ "environmentFiles": [
+ {
+ "value": "arn:aws:s3:::discodeit-binary-content-storage-ljy/discodeit.env",
+ "type": "s3"
+ }
+ ],
+ "mountPoints": [],
+ "volumesFrom": [],
+ "ulimits": [],
+ "logConfiguration": {
+ "logDriver": "awslogs",
+ "options": {
+ "awslogs-group": "/ecs/discodeit-task",
+ "awslogs-create-group": "true",
+ "awslogs-region": "ap-northeast-2",
+ "awslogs-stream-prefix": "ecs"
+ },
+ "secretOptions": []
+ },
+ "systemControls": []
+ }
+ ],
+ "family": "discodeit-task",
+ "executionRoleArn": "arn:aws:iam::318911662229:role/ecsTaskExecutionRole",
+ "networkMode": "bridge",
+ "volumes": [],
+ "placementConstraints": [],
+ "compatibilities": [
+ "EC2"
+ ],
+ "requiresCompatibilities": [
+ "EC2"
+ ],
+ "cpu": "256",
+ "memory": "512",
+ "runtimePlatform": {
+ "cpuArchitecture": "X86_64",
+ "operatingSystemFamily": "LINUX"
+ }
+}
\ No newline at end of file