From 8e27c98641b9e6ab665b061454ddbc15108b872c Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:19:57 +0900 Subject: [PATCH 01/19] [Fix] Update build-test.yml (#64) --- .github/workflows/build-test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 913376b..2bb4246 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,6 +8,10 @@ on: jobs: build: runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: dummy-key + AWS_SECRET_ACCESS_KEY: dummy-secret + AWS_DEFAULT_REGION: dummy-region steps: - name: ✔️ 리포지토리 가져오기 @@ -26,4 +30,4 @@ jobs: run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test - name: ✔️ Gradle test - run: ./gradlew --info test -Dspring.profiles.active=test \ No newline at end of file + run: ./gradlew --info test -Dspring.profiles.active=test From f7e2dbc2d8979c2c968c3c705f930a36385bbdf6 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:35:08 +0900 Subject: [PATCH 02/19] Revert "[Fix] Update build-test.yml (#64)" (#66) This reverts commit 8e27c98641b9e6ab665b061454ddbc15108b872c. --- .github/workflows/build-test.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 2bb4246..913376b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,10 +8,6 @@ on: jobs: build: runs-on: ubuntu-latest - env: - AWS_ACCESS_KEY_ID: dummy-key - AWS_SECRET_ACCESS_KEY: dummy-secret - AWS_DEFAULT_REGION: dummy-region steps: - name: ✔️ 리포지토리 가져오기 @@ -30,4 +26,4 @@ jobs: run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test - name: ✔️ Gradle test - run: ./gradlew --info test -Dspring.profiles.active=test + run: ./gradlew --info test -Dspring.profiles.active=test \ No newline at end of file From 665a13df4458d650f95a77f529082c5e2a4de508 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:42:35 +0900 Subject: [PATCH 03/19] [Fix] Update build-test.yml (#65) --- .github/workflows/build-test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 913376b..080b83c 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -8,6 +8,12 @@ on: jobs: build: runs-on: ubuntu-latest + env: + AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} + AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION} + S3_BUCKET: ${S3_BUCKET} + JWT_SECRET: ${JWT_SECRET} steps: - name: ✔️ 리포지토리 가져오기 From 3e35219b64ce7ec84791609b3efe06aca06c4252 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:53:38 +0900 Subject: [PATCH 04/19] [Fix] Update build-test.yml (#67) --- .github/workflows/build-test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 080b83c..71f978b 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,11 +9,11 @@ jobs: build: runs-on: ubuntu-latest env: - AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} - AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION} - S3_BUCKET: ${S3_BUCKET} - JWT_SECRET: ${JWT_SECRET} + AWS_ACCESS_KEY_ID: ${secrets.AWS_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${secrets.AWS_SECRET_ACCESS_KEY} + AWS_DEFAULT_REGION: ${secrets.AWS_DEFAULT_REGION} + S3_BUCKET: ${secrets.S3_BUCKET} + JWT_SECRET: ${secrets.JWT_SECRET} steps: - name: ✔️ 리포지토리 가져오기 @@ -32,4 +32,4 @@ jobs: run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test - name: ✔️ Gradle test - run: ./gradlew --info test -Dspring.profiles.active=test \ No newline at end of file + run: ./gradlew --info test -Dspring.profiles.active=test From 1921f9dcb57afc7eac68ce24f10be194a2afa446 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:56:49 +0900 Subject: [PATCH 05/19] [Fix] Update build-test.yml --- .github/workflows/build-test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 71f978b..5f0530d 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,11 +9,11 @@ jobs: build: runs-on: ubuntu-latest env: - AWS_ACCESS_KEY_ID: ${secrets.AWS_ACCESS_KEY_ID} - AWS_SECRET_ACCESS_KEY: ${secrets.AWS_SECRET_ACCESS_KEY} - AWS_DEFAULT_REGION: ${secrets.AWS_DEFAULT_REGION} - S3_BUCKET: ${secrets.S3_BUCKET} - JWT_SECRET: ${secrets.JWT_SECRET} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} steps: - name: ✔️ 리포지토리 가져오기 From a472e6eba6c0f77f641a8c37e16f8dbd324cf6a4 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:21:27 +0900 Subject: [PATCH 06/19] =?UTF-8?q?[Feat]=20S3=20setting=20=EB=B0=8F=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Feat] client 요청시 cors 전체 허용으로 설정 (#59) * [Feat] .gitignore에 aws 인증 정보 추가 * [refactor] 환경 변수 사용 코드 리펙토링 * [Chore] gradle 설정 변경 * [Chore] gradle 설정 변경 * [Fix] github action mock 환경변수 추가 * [refactor] health check api 스웨거에서 숨기기 --- .gitignore | 5 +- build.gradle | 3 + docker/deploy/deploy.sh | 19 ++++ docker/deploy/docker-compose.yml | 5 -- env/local-db.env | 5 -- env/local-spring.env | 13 --- env/prod-db.env | 5 -- env/prod-spring.env | 14 --- .../apiPayLoad/code/status/ErrorStatus.java | 4 +- .../java/umc/codeplay/config/AWSConfig.java | 34 ++++++++ .../umc/codeplay/config/SecurityConfig.java | 1 + .../umc/codeplay/controller/HealthCheck.java | 24 ++++- src/main/java/umc/codeplay/jwt/JwtUtil.java | 7 +- .../java/umc/codeplay/service/S3Service.java | 87 +++++++++++++++++++ src/main/resources/application-prod.yml | 15 +++- src/main/resources/application.yml | 15 +++- src/test/resources/application-test.yml | 2 + 17 files changed, 204 insertions(+), 54 deletions(-) delete mode 100644 env/local-db.env delete mode 100644 env/local-spring.env delete mode 100644 env/prod-db.env delete mode 100644 env/prod-spring.env create mode 100644 src/main/java/umc/codeplay/config/AWSConfig.java create mode 100644 src/main/java/umc/codeplay/service/S3Service.java diff --git a/.gitignore b/.gitignore index 4b6f09b..2f77959 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,7 @@ out/ # General .DS_Store .AppleDouble -.LSOverride \ No newline at end of file +.LSOverride + +### aws 및 환경변수 정보 ### +env/ diff --git a/build.gradle b/build.gradle index 5f5c061..0341b86 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,9 @@ dependencies { // validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + // s3 + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' } tasks.named('test') { diff --git a/docker/deploy/deploy.sh b/docker/deploy/deploy.sh index 32e0e1c..e14332f 100644 --- a/docker/deploy/deploy.sh +++ b/docker/deploy/deploy.sh @@ -20,4 +20,23 @@ fi echo "dangling 이미지 삭제" docker image prune -f +echo "멈춘 container 삭제" +docker container prune -f + +for i in {1..10}; do + if [ "$i" -eq 10 ]; then + echo "Health check failed" + docker compose down + exit 1 + fi + + if curl "http://localhost:8080/health"; then + echo "컨테이너가 정상적으로 실행되었습니다..." + break + fi + + echo "spring boot application health check 중..." + sleep 15 +done + echo "모든 작업이 완료되었습니다." \ No newline at end of file diff --git a/docker/deploy/docker-compose.yml b/docker/deploy/docker-compose.yml index bac4dc5..930290a 100644 --- a/docker/deploy/docker-compose.yml +++ b/docker/deploy/docker-compose.yml @@ -30,11 +30,6 @@ services: restart: always depends_on: - database - healthcheck: - test: [ "CMD", "curl", "-f", "http://localhost:8080/health" ] - interval: 20s - timeout: 10s - retries: 5 networks: umc_code_play: diff --git a/env/local-db.env b/env/local-db.env deleted file mode 100644 index d578ea8..0000000 --- a/env/local-db.env +++ /dev/null @@ -1,5 +0,0 @@ -MYSQL_DATABASE=codeplay_local -MYSQL_USER=codeplay -MYSQL_PASSWORD=QWER1234QWER1234XCVBNMASDFGH -MYSQL_ROOT_PASSWORD=QWER1234QWER1234XCVBNMASDFGH -TZ=Asia/Seoul diff --git a/env/local-spring.env b/env/local-spring.env deleted file mode 100644 index d071728..0000000 --- a/env/local-spring.env +++ /dev/null @@ -1,13 +0,0 @@ -# MySQL 설정 -DB_URL=jdbc:mysql://localhost:3306/codeplay_local -MYSQL_USERNAME=codeplay -MYSQL_PASSWORD=QWER1234QWER1234XCVBNMASDFGH - -# Spring Boot 설정 -JWT_SECRET=qwertyuiopokjhSDFGHJKIUYTREDCVBNMKIJKJHGFHYTRFCVBGFDSXCVBHH - -# AWS S3 settings - -# Kakao settings - -# Google settings \ No newline at end of file diff --git a/env/prod-db.env b/env/prod-db.env deleted file mode 100644 index 5bc2231..0000000 --- a/env/prod-db.env +++ /dev/null @@ -1,5 +0,0 @@ -MYSQL_DATABASE=codeplay_prod -MYSQL_USER=codeplay -MYSQL_PASSWORD=QWER1234QWER1234XCVBNMASDFGH -MYSQL_ROOT_PASSWORD=QWER1234QWER1234XCVBNMASDFGH -TZ=Asia/Seoul \ No newline at end of file diff --git a/env/prod-spring.env b/env/prod-spring.env deleted file mode 100644 index b335105..0000000 --- a/env/prod-spring.env +++ /dev/null @@ -1,14 +0,0 @@ -# MySQL 설정 -DB_URL=jdbc:mysql://codeplay-db:3306/codeplay_prod?useSSL=false&allowPublicKeyRetrieval=true -MYSQL_USERNAME=codeplay -MYSQL_PASSWORD=QWER1234QWER1234XCVBNMASDFGH - -# Spring Boot 설정 -SPRING_PROFILES_ACTIVE=prod -JWT_SECRET=qwertyuiopokjhSDFGHJKIUYTREDCVBNMKIJKJHGFHYTRFCVBGFDSXCVBHH - -# AWS S3 settings - -# Kakao settings - -# Google settings \ No newline at end of file diff --git a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java index df8f9a0..c215100 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -23,7 +23,9 @@ public enum ErrorStatus implements BaseErrorCode { NOT_AUTHORIZED(HttpStatus.BAD_REQUEST, "AUTH400", "인증되지 않은 요청입니다."), ID_OR_PASSWORD_WRONG(HttpStatus.BAD_REQUEST, "AUTH401", "아이디 혹은 비밀번호가 잘못되었습니다."), - INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH402", "유효하지 않은 리프레시 토큰입니다."); + INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH402", "유효하지 않은 리프레시 토큰입니다."), + + AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/umc/codeplay/config/AWSConfig.java b/src/main/java/umc/codeplay/config/AWSConfig.java new file mode 100644 index 0000000..f521913 --- /dev/null +++ b/src/main/java/umc/codeplay/config/AWSConfig.java @@ -0,0 +1,34 @@ +package umc.codeplay.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +@Configuration +public class AWSConfig { + + @Value("${cloud.aws.credentials.accessKey}") + private String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + private String secretKey; + + @Value("${cloud.aws.region.static}") + private String region; + + @Bean + public AmazonS3Client amazonS3Client() { + final BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) + AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } +} diff --git a/src/main/java/umc/codeplay/config/SecurityConfig.java b/src/main/java/umc/codeplay/config/SecurityConfig.java index fca2218..412c049 100644 --- a/src/main/java/umc/codeplay/config/SecurityConfig.java +++ b/src/main/java/umc/codeplay/config/SecurityConfig.java @@ -61,6 +61,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // 로그인, 회원가입 등 토큰 없이 접근해야 하는 API 허용 .requestMatchers( "/health", + "/health/s3", "/auth/refresh", "/auth/signup", "/auth/login", diff --git a/src/main/java/umc/codeplay/controller/HealthCheck.java b/src/main/java/umc/codeplay/controller/HealthCheck.java index c8f7bcc..0b9da00 100644 --- a/src/main/java/umc/codeplay/controller/HealthCheck.java +++ b/src/main/java/umc/codeplay/controller/HealthCheck.java @@ -1,14 +1,32 @@ package umc.codeplay.controller; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import lombok.RequiredArgsConstructor; + +import io.swagger.v3.oas.annotations.Hidden; +import umc.codeplay.service.S3Service; + +@Hidden @RestController +@RequestMapping("/health") +@RequiredArgsConstructor public class HealthCheck { - @GetMapping("/health") + private final S3Service s3Service; + + // 연결 확인 용 + @GetMapping("") public ResponseEntity healthCheck() { return ResponseEntity.ok("UMC 7th CodePlay Well Connected!"); } + + // s3 업로드 테스트 용 + @PostMapping("/s3") + public ResponseEntity s3HealthCheck(@RequestPart(value = "file") MultipartFile file) { + final String s3Url = s3Service.uploadFile(file); + return ResponseEntity.ok("S3 FIle is uploaded! : " + s3Url); + } } diff --git a/src/main/java/umc/codeplay/jwt/JwtUtil.java b/src/main/java/umc/codeplay/jwt/JwtUtil.java index 466e4fb..abd5290 100644 --- a/src/main/java/umc/codeplay/jwt/JwtUtil.java +++ b/src/main/java/umc/codeplay/jwt/JwtUtil.java @@ -18,11 +18,8 @@ @Component public class JwtUtil { - private final String SECRET_KEY; - - public JwtUtil(@Value("${JWT_SECRET}") String secretKey) { - this.SECRET_KEY = secretKey; - } + @Value("${jwt.secret}") + private String SECRET_KEY; private Key getSigningKey() { return Keys.hmacShaKeyFor(SECRET_KEY.getBytes()); diff --git a/src/main/java/umc/codeplay/service/S3Service.java b/src/main/java/umc/codeplay/service/S3Service.java new file mode 100644 index 0000000..1c58580 --- /dev/null +++ b/src/main/java/umc/codeplay/service/S3Service.java @@ -0,0 +1,87 @@ +package umc.codeplay.service; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import lombok.RequiredArgsConstructor; + +import com.amazonaws.AmazonClientException; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.util.IOUtils; +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; + +@Service +@RequiredArgsConstructor +@Transactional +public class S3Service { + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + private final AmazonS3Client amazonS3Client; + + /* + s3에 파일 업로드 + */ + public String uploadFile(MultipartFile file) { + if (file.getOriginalFilename() == null || file.getOriginalFilename().isEmpty()) { + throw new GeneralHandler(ErrorStatus._BAD_REQUEST); + } + + final String fileName = newFileName(file.getOriginalFilename()); + + ObjectMetadata objectMetadata = new ObjectMetadata(); + try { + objectMetadata.setContentLength(file.getSize()); + objectMetadata.setContentType(file.getContentType()); + + /* + TODO: 용량 문제가 생긴다면 아래 ByteArrayInputStream 방식을 변경해야함 + 1. 임시파일 방식으로 변경 + 2. 가능한 용량 limit 설정 + */ + byte[] bytes = IOUtils.toByteArray(file.getInputStream()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + + PutObjectRequest putObjectRequest = + new PutObjectRequest(bucket, fileName, inputStream, objectMetadata); + amazonS3Client.putObject(putObjectRequest); + + inputStream.close(); + + } catch (AmazonClientException | IOException e) { + throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE); + } + + return amazonS3Client.getUrl(bucket, fileName).toString(); + } + + /* + s3 업로드시 파일 이름 변경 + */ + private String newFileName(String fileName) { + + final String FILE_EXTENSION_SEPARATOR = "."; + final String now = String.valueOf(System.currentTimeMillis()); + + int fileExtensionIndex = fileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); + if (fileExtensionIndex == -1) { + return fileName + + "_" + + System.currentTimeMillis(); // No extension found, just add timestamp + } + + final String fileExtension = fileName.substring(fileExtensionIndex); + final String originalFileName = fileName.substring(0, fileExtensionIndex); + + return originalFileName + "_" + now + fileExtension; + } +} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e1c85a5..38f8888 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -26,4 +26,17 @@ spring: show_sql: true format_sql: true use_sql_comments: true - default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화) \ No newline at end of file + default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화) + +cloud: + aws: + s3: + bucket: ${S3_BUCKET} + stack.auto: false + region.static: ${AWS_DEFAULT_REGION} + credentials: + accessKey: ${AWS_ACCESS_KEY_ID} + secretKey: ${AWS_SECRET_ACCESS_KEY} + +jwt: + secret: ${JWT_SECRET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 30d35b5..2baf714 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,4 +26,17 @@ spring: show_sql: true format_sql: true use_sql_comments: true - default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화) \ No newline at end of file + default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화) + +cloud: + aws: + s3: + bucket: ${S3_BUCKET} + stack.auto: false + region.static: ${AWS_DEFAULT_REGION} + credentials: + accessKey: ${AWS_ACCESS_KEY_ID} + secretKey: ${AWS_SECRET_ACCESS_KEY} + +jwt: + secret: ${JWT_SECRET} \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index ae60a01..efefcfb 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -2,11 +2,13 @@ spring: h2: console: enabled: true + datasource: url: jdbc:h2:mem:testdb;MODE=MYSQL driver-class-name: org.h2.Driver username: sa password: + jpa: show-sql: true properties: From 5980d6ef95bc25972e4167f459d0e9b4575fa481 Mon Sep 17 00:00:00 2001 From: SeoJimin1234 <113419021+SeoJimin1234@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:20:45 +0900 Subject: [PATCH 07/19] [Feat] Create Domain Table (#71) --- .../java/umc/codeplay/domain/Harmony.java | 38 +++++++++++++++++++ src/main/java/umc/codeplay/domain/Member.java | 11 ++++-- src/main/java/umc/codeplay/domain/Music.java | 28 ++++++++++++++ src/main/java/umc/codeplay/domain/Track.java | 35 +++++++++++++++++ .../codeplay/domain/common/BaseEntity.java | 20 ++++++++++ .../codeplay/domain/mapping/MusicLike.java | 28 ++++++++++++++ 6 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 src/main/java/umc/codeplay/domain/Harmony.java create mode 100644 src/main/java/umc/codeplay/domain/Music.java create mode 100644 src/main/java/umc/codeplay/domain/Track.java create mode 100644 src/main/java/umc/codeplay/domain/common/BaseEntity.java create mode 100644 src/main/java/umc/codeplay/domain/mapping/MusicLike.java diff --git a/src/main/java/umc/codeplay/domain/Harmony.java b/src/main/java/umc/codeplay/domain/Harmony.java new file mode 100644 index 0000000..51018ee --- /dev/null +++ b/src/main/java/umc/codeplay/domain/Harmony.java @@ -0,0 +1,38 @@ +package umc.codeplay.domain; + +import jakarta.persistence.*; + +import lombok.*; + +import umc.codeplay.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Harmony extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String title; + + private String harmonyKey; + + private String scale; + + private String chord; + + private Integer bpm; + + private Integer soundPressure; + + @Column(columnDefinition = "TEXT") + private String harmonyUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "music_id") + private Music music; +} diff --git a/src/main/java/umc/codeplay/domain/Member.java b/src/main/java/umc/codeplay/domain/Member.java index 8d7d6ed..e99c00f 100644 --- a/src/main/java/umc/codeplay/domain/Member.java +++ b/src/main/java/umc/codeplay/domain/Member.java @@ -1,9 +1,6 @@ package umc.codeplay.domain; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -35,4 +32,10 @@ public class Member { public void encodePassword(String password) { this.password = password; } + + @Column(columnDefinition = "TEXT") + private String profileUrl; + + // @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + // private List likeList = new ArrayList<>(); } diff --git a/src/main/java/umc/codeplay/domain/Music.java b/src/main/java/umc/codeplay/domain/Music.java new file mode 100644 index 0000000..a978655 --- /dev/null +++ b/src/main/java/umc/codeplay/domain/Music.java @@ -0,0 +1,28 @@ +package umc.codeplay.domain; + +import jakarta.persistence.*; + +import lombok.*; + +import umc.codeplay.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Music extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + // @OneToMany(mappedBy = "music", cascade = CascadeType.ALL) + // private List likeList = new ArrayList<>(); +} diff --git a/src/main/java/umc/codeplay/domain/Track.java b/src/main/java/umc/codeplay/domain/Track.java new file mode 100644 index 0000000..a211b86 --- /dev/null +++ b/src/main/java/umc/codeplay/domain/Track.java @@ -0,0 +1,35 @@ +package umc.codeplay.domain; + +import jakarta.persistence.*; + +import lombok.*; + +import umc.codeplay.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Track extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 50) + private String title; + + @Column(columnDefinition = "TEXT") + private String guitarUrl; + + @Column(columnDefinition = "TEXT") + private String drumUrl; + + @Column(columnDefinition = "TEXT") + private String keyboardUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "music_id") + private Music music; +} diff --git a/src/main/java/umc/codeplay/domain/common/BaseEntity.java b/src/main/java/umc/codeplay/domain/common/BaseEntity.java new file mode 100644 index 0000000..844f213 --- /dev/null +++ b/src/main/java/umc/codeplay/domain/common/BaseEntity.java @@ -0,0 +1,20 @@ +package umc.codeplay.domain.common; + +import java.time.LocalDateTime; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import lombok.Getter; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@Getter +public abstract class BaseEntity { + @CreatedDate private LocalDateTime createdAt; + + @LastModifiedDate private LocalDateTime updatedAt; +} diff --git a/src/main/java/umc/codeplay/domain/mapping/MusicLike.java b/src/main/java/umc/codeplay/domain/mapping/MusicLike.java new file mode 100644 index 0000000..1a438d4 --- /dev/null +++ b/src/main/java/umc/codeplay/domain/mapping/MusicLike.java @@ -0,0 +1,28 @@ +package umc.codeplay.domain.mapping; + +import jakarta.persistence.*; + +import lombok.*; + +import umc.codeplay.domain.Member; +import umc.codeplay.domain.Music; +import umc.codeplay.domain.common.BaseEntity; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class MusicLike extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "music_id") + private Music music; +} From 628b62b612d4c8667c86aaf5e010b7497d07c4b4 Mon Sep 17 00:00:00 2001 From: Minhyung Kim Date: Sat, 25 Jan 2025 18:39:05 +0900 Subject: [PATCH 08/19] =?UTF-8?q?[Feat]=20=EA=B5=AC=EA=B8=80=20/=20?= =?UTF-8?q?=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAuth=20=EA=B5=AC=ED=98=84=20(#73?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../apiPayLoad/code/status/ErrorStatus.java | 4 + .../umc/codeplay/config/SecurityConfig.java | 5 +- .../properties/BaseOAuthProperties.java | 28 ++++ .../properties/GoogleOAuthProperties.java | 10 ++ .../properties/KakaoOAuthProperties.java | 10 ++ .../codeplay/controller/AuthController.java | 12 +- .../codeplay/controller/OAuthController.java | 149 ++++++++++++++++++ .../codeplay/converter/MemberConverter.java | 5 +- src/main/java/umc/codeplay/domain/Member.java | 12 +- .../codeplay/domain/enums/SocialStatus.java | 7 + .../umc/codeplay/dto/MemberRequestDTO.java | 3 - .../umc/codeplay/service/MemberService.java | 31 ++++ src/main/resources/application.yml | 24 ++- 14 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 src/main/java/umc/codeplay/config/properties/BaseOAuthProperties.java create mode 100644 src/main/java/umc/codeplay/config/properties/GoogleOAuthProperties.java create mode 100644 src/main/java/umc/codeplay/config/properties/KakaoOAuthProperties.java create mode 100644 src/main/java/umc/codeplay/controller/OAuthController.java create mode 100644 src/main/java/umc/codeplay/domain/enums/SocialStatus.java diff --git a/build.gradle b/build.gradle index 0341b86..dc91a43 100644 --- a/build.gradle +++ b/build.gradle @@ -61,6 +61,9 @@ dependencies { // s3 implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + + // oauth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { diff --git a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java index c215100..73aac45 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -24,6 +24,10 @@ public enum ErrorStatus implements BaseErrorCode { NOT_AUTHORIZED(HttpStatus.BAD_REQUEST, "AUTH400", "인증되지 않은 요청입니다."), ID_OR_PASSWORD_WRONG(HttpStatus.BAD_REQUEST, "AUTH401", "아이디 혹은 비밀번호가 잘못되었습니다."), INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "AUTH402", "유효하지 않은 리프레시 토큰입니다."), + OAUTH_TOKEN_REQUEST_FAILED(HttpStatus.BAD_REQUEST, "AUTH403", "외부인증 토큰 요청에 실패했습니다."), + OAUTH_USERINFO_REQUEST_FAILED(HttpStatus.BAD_REQUEST, "AUTH404", "외부인증 유저 정보 요청에 실패했습니다."), + AUTHORIZATION_METHOD_ERROR(HttpStatus.BAD_REQUEST, "AUTH405", "인증 방식이 잘못되었습니다."), + INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH406", "유효하지 않은 OAuth 제공자입니다."), AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."); diff --git a/src/main/java/umc/codeplay/config/SecurityConfig.java b/src/main/java/umc/codeplay/config/SecurityConfig.java index 412c049..7e54f03 100644 --- a/src/main/java/umc/codeplay/config/SecurityConfig.java +++ b/src/main/java/umc/codeplay/config/SecurityConfig.java @@ -60,11 +60,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti auth // 로그인, 회원가입 등 토큰 없이 접근해야 하는 API 허용 .requestMatchers( + "/oauth/**", "/health", "/health/s3", - "/auth/refresh", - "/auth/signup", - "/auth/login", + "/auth/**", "/v2/api-docs", "/v3/api-docs", "/v3/api-docs/**", diff --git a/src/main/java/umc/codeplay/config/properties/BaseOAuthProperties.java b/src/main/java/umc/codeplay/config/properties/BaseOAuthProperties.java new file mode 100644 index 0000000..369bf57 --- /dev/null +++ b/src/main/java/umc/codeplay/config/properties/BaseOAuthProperties.java @@ -0,0 +1,28 @@ +package umc.codeplay.config.properties; + +import lombok.Data; + +@Data +public class BaseOAuthProperties { + + private String clientId; + private String clientSecret; + private String redirectUri; + private String scope; + private String authorizationUri; + private String tokenUri; + private String userInfoUri; + private String additionalParameters; + + public String getUrl() { + return authorizationUri + + "?client_id=" + + clientId + + "&redirect_uri=" + + redirectUri + + "&response_type=code" + + "&scope=" + + scope + + additionalParameters; + } +} diff --git a/src/main/java/umc/codeplay/config/properties/GoogleOAuthProperties.java b/src/main/java/umc/codeplay/config/properties/GoogleOAuthProperties.java new file mode 100644 index 0000000..88ea754 --- /dev/null +++ b/src/main/java/umc/codeplay/config/properties/GoogleOAuthProperties.java @@ -0,0 +1,10 @@ +package umc.codeplay.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "google.oauth2") +public class GoogleOAuthProperties extends BaseOAuthProperties { + // BaseOAuthProperties 의 필드를 그대로 상속받아 사용. +} diff --git a/src/main/java/umc/codeplay/config/properties/KakaoOAuthProperties.java b/src/main/java/umc/codeplay/config/properties/KakaoOAuthProperties.java new file mode 100644 index 0000000..544213b --- /dev/null +++ b/src/main/java/umc/codeplay/config/properties/KakaoOAuthProperties.java @@ -0,0 +1,10 @@ +package umc.codeplay.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "kakao.oauth2") +public class KakaoOAuthProperties extends BaseOAuthProperties { + // BaseOAuthProperties 의 필드를 그대로 상속받아 사용. +} diff --git a/src/main/java/umc/codeplay/controller/AuthController.java b/src/main/java/umc/codeplay/controller/AuthController.java index a082dae..7458a3e 100644 --- a/src/main/java/umc/codeplay/controller/AuthController.java +++ b/src/main/java/umc/codeplay/controller/AuthController.java @@ -13,12 +13,12 @@ import lombok.RequiredArgsConstructor; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; import umc.codeplay.apiPayLoad.ApiResponse; import umc.codeplay.apiPayLoad.code.status.ErrorStatus; import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; import umc.codeplay.converter.MemberConverter; import umc.codeplay.domain.Member; +import umc.codeplay.domain.enums.SocialStatus; import umc.codeplay.dto.MemberRequestDTO; import umc.codeplay.dto.MemberResponseDTO; import umc.codeplay.jwt.JwtUtil; @@ -37,6 +37,10 @@ public class AuthController { @PostMapping("/login") public ApiResponse login( @RequestBody MemberRequestDTO.LoginDto request) { + if (memberService.getSocialStatus(request.getEmail()) != SocialStatus.NONE) { + throw new GeneralHandler(ErrorStatus.AUTHORIZATION_METHOD_ERROR); + } + // 아이디/비밀번호를 사용해 AuthenticationToken 생성 UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword()); @@ -98,10 +102,4 @@ public ApiResponse refresh( throw new GeneralHandler(ErrorStatus.INVALID_REFRESH_TOKEN); } } - - @SecurityRequirement(name = "JWT TOKEN") - @GetMapping("/test") - public ApiResponse test() { - return ApiResponse.onSuccess("test"); - } } diff --git a/src/main/java/umc/codeplay/controller/OAuthController.java b/src/main/java/umc/codeplay/controller/OAuthController.java new file mode 100644 index 0000000..e2758b2 --- /dev/null +++ b/src/main/java/umc/codeplay/controller/OAuthController.java @@ -0,0 +1,149 @@ +package umc.codeplay.controller; + +import java.util.List; +import java.util.Map; + +import org.springframework.http.*; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.view.RedirectView; + +import lombok.RequiredArgsConstructor; + +import umc.codeplay.apiPayLoad.ApiResponse; +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; +import umc.codeplay.config.properties.BaseOAuthProperties; +import umc.codeplay.config.properties.GoogleOAuthProperties; +import umc.codeplay.config.properties.KakaoOAuthProperties; +import umc.codeplay.domain.Member; +import umc.codeplay.domain.enums.SocialStatus; +import umc.codeplay.dto.MemberResponseDTO; +import umc.codeplay.jwt.JwtUtil; +import umc.codeplay.service.MemberService; + +@RestController +@RequestMapping("/oauth") +@RequiredArgsConstructor +public class OAuthController { + + private final JwtUtil jwtUtil; + private final RestTemplate restTemplate = new RestTemplate(); + private final GoogleOAuthProperties googleOAuthProperties; + private final KakaoOAuthProperties kakaoOAuthProperties; + private final MemberService memberService; + + @GetMapping("/authorize/{provider}") + public RedirectView redirectToOAuth(@PathVariable("provider") String provider) { + // CSRF 방어용 state, PKCE(code_challenge)..는 굳이 + BaseOAuthProperties properties = + switch (provider) { + case "google" -> googleOAuthProperties; + case "kakao" -> kakaoOAuthProperties; + default -> throw new GeneralHandler(ErrorStatus.INVALID_OAUTH_PROVIDER); + }; + + String url = properties.getUrl(); + + RedirectView redirectView = new RedirectView(); + redirectView.setUrl(url); + return redirectView; + } + + @GetMapping("/callback/{provider}") + public ApiResponse OAuthCallback( + @RequestParam("code") String code, @PathVariable("provider") String provider) { + BaseOAuthProperties properties = + switch (provider) { + case "google" -> googleOAuthProperties; + case "kakao" -> kakaoOAuthProperties; + default -> throw new GeneralHandler(ErrorStatus.INVALID_OAUTH_PROVIDER); + }; + // (1) 받은 code 로 구글 토큰 엔드포인트에 Access/ID Token 교환 + Map tokenResponse = requestOAuthToken(code, properties); + + // (2) 받아온 Access Token(or ID Token)을 통해 사용자 정보 가져오기 + // String idToken = (String) tokenResponse.get("id_token"); // OIDC + String accessToken = (String) tokenResponse.get("access_token"); + Map userInfo = requestOAuthUserInfo(accessToken, properties); + String email = null; + String name = null; + switch (provider) { + case "google" -> { + // (3-a) 구글 UserInfo Endpoint 로 이메일, 프로필 등 조회 + email = (String) userInfo.get("email"); + name = (String) userInfo.get("name"); + } + case "kakao" -> { + // (3-b) 카카오 UserInfo Endpoint 로 이메일, 프로필 등 조회 + Map kakaoAccount = + (Map) userInfo.get("kakao_account"); + Map kakaoProperties = + (Map) userInfo.get("properties"); + email = (String) kakaoAccount.get("email"); + name = (String) kakaoProperties.get("nickname"); + } + } + + // (4) 우리 DB에서 회원 조회 or 생성 + Member member = + memberService.findOrCreateOAuthMember( + email, name, SocialStatus.valueOf(provider.toUpperCase())); + + // (5) JWTUtil 이용해서 Access/Refresh 토큰 발급 + var authorities = List.of(new SimpleGrantedAuthority("ROLE_" + member.getRole().name())); + + String serviceAccessToken = jwtUtil.generateToken(email, authorities); + String serviceRefreshToken = jwtUtil.generateRefreshToken(email, authorities); + + // (6) 최종적으로 JWT(액세스/리프레시)를 프론트에 응답 + return ApiResponse.onSuccess( + MemberResponseDTO.LoginResultDTO.builder() + .email(email) + .token(serviceAccessToken) + .refreshToken(serviceRefreshToken) + .build()); + } + + private Map requestOAuthToken(String code, BaseOAuthProperties properties) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", properties.getClientId()); + params.add("client_secret", properties.getClientSecret()); + params.add("redirect_uri", properties.getRedirectUri()); + params.add("code", code); + + HttpEntity> request = new HttpEntity<>(params, headers); + + ResponseEntity response = + restTemplate.postForEntity(properties.getTokenUri(), request, Map.class); + + if (response.getStatusCode() == HttpStatus.OK) { + return response.getBody(); + } + throw new GeneralHandler(ErrorStatus.OAUTH_TOKEN_REQUEST_FAILED); + } + + private Map requestOAuthUserInfo( + String accessToken, BaseOAuthProperties properties) { + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + + HttpEntity request = new HttpEntity<>(headers); + + ResponseEntity response = + restTemplate.exchange( + properties.getUserInfoUri(), HttpMethod.GET, request, Map.class); + + if (response.getStatusCode() == HttpStatus.OK) { + return response.getBody(); + } + throw new GeneralHandler(ErrorStatus.OAUTH_USERINFO_REQUEST_FAILED); + } +} diff --git a/src/main/java/umc/codeplay/converter/MemberConverter.java b/src/main/java/umc/codeplay/converter/MemberConverter.java index 7c5c0b8..70f43d3 100644 --- a/src/main/java/umc/codeplay/converter/MemberConverter.java +++ b/src/main/java/umc/codeplay/converter/MemberConverter.java @@ -1,6 +1,8 @@ package umc.codeplay.converter; import umc.codeplay.domain.Member; +import umc.codeplay.domain.enums.Role; +import umc.codeplay.domain.enums.SocialStatus; import umc.codeplay.dto.MemberRequestDTO; import umc.codeplay.dto.MemberResponseDTO; @@ -12,7 +14,8 @@ public static Member toMember(MemberRequestDTO.JoinDto request) { .name(request.getName()) .email(request.getEmail()) .password(request.getPassword()) - .role(request.getRole()) + .role(Role.USER) + .socialStatus(SocialStatus.NONE) .build(); } diff --git a/src/main/java/umc/codeplay/domain/Member.java b/src/main/java/umc/codeplay/domain/Member.java index e99c00f..c27a3ce 100644 --- a/src/main/java/umc/codeplay/domain/Member.java +++ b/src/main/java/umc/codeplay/domain/Member.java @@ -2,16 +2,14 @@ import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import umc.codeplay.domain.enums.Role; +import umc.codeplay.domain.enums.SocialStatus; @Entity @Getter +@Setter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @@ -27,8 +25,12 @@ public class Member { private String email; + @Enumerated(EnumType.STRING) private Role role; + @Enumerated(EnumType.STRING) + private SocialStatus socialStatus; + public void encodePassword(String password) { this.password = password; } diff --git a/src/main/java/umc/codeplay/domain/enums/SocialStatus.java b/src/main/java/umc/codeplay/domain/enums/SocialStatus.java new file mode 100644 index 0000000..9a4015d --- /dev/null +++ b/src/main/java/umc/codeplay/domain/enums/SocialStatus.java @@ -0,0 +1,7 @@ +package umc.codeplay.domain.enums; + +public enum SocialStatus { + GOOGLE, + KAKAO, + NONE +} diff --git a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java index b2b28eb..4d5a3e1 100644 --- a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java @@ -2,8 +2,6 @@ import lombok.Getter; -import umc.codeplay.domain.enums.Role; - public class MemberRequestDTO { @Getter @@ -11,7 +9,6 @@ public static class JoinDto { String name; String email; String password; - Role role; } @Getter diff --git a/src/main/java/umc/codeplay/service/MemberService.java b/src/main/java/umc/codeplay/service/MemberService.java index 48d1bc4..dda2090 100644 --- a/src/main/java/umc/codeplay/service/MemberService.java +++ b/src/main/java/umc/codeplay/service/MemberService.java @@ -9,6 +9,8 @@ import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; import umc.codeplay.converter.MemberConverter; import umc.codeplay.domain.Member; +import umc.codeplay.domain.enums.Role; +import umc.codeplay.domain.enums.SocialStatus; import umc.codeplay.dto.MemberRequestDTO; import umc.codeplay.repository.MemberRepository; @@ -29,4 +31,33 @@ public Member joinMember(MemberRequestDTO.JoinDto request) { newMember.encodePassword(passwordEncoder.encode(request.getPassword())); return memberRepository.save(newMember); } + + public Member findOrCreateOAuthMember(String email, String name, SocialStatus socialStatus) { + + Member member = memberRepository.findByEmail(email).orElse(null); + + if (member == null) { + member = + Member.builder() + .email(email) + .name(name) + .role(Role.USER) + .socialStatus(socialStatus) + .build(); + return memberRepository.save(member); + } else if (member.getSocialStatus() != socialStatus) { + throw new GeneralHandler(ErrorStatus.AUTHORIZATION_METHOD_ERROR); + } else { + return member; + } + } + + public SocialStatus getSocialStatus(String email) { + Member member = memberRepository.findByEmail(email).orElse(null); + if (member == null) { + return SocialStatus.NONE; + } else { + return member.getSocialStatus(); + } + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 2baf714..10fe9b2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -39,4 +39,26 @@ cloud: secretKey: ${AWS_SECRET_ACCESS_KEY} jwt: - secret: ${JWT_SECRET} \ No newline at end of file + secret: ${JWT_SECRET} + +google: + oauth2: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + redirect-uri: ${GOOGLE_REDIRECT_URI} + scope: "openid email profile" + authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth" + token-uri: "https://oauth2.googleapis.com/token" + user-info-uri: "https://openidconnect.googleapis.com/v1/userinfo" + additional-parameters: "&access_type=offline&prompt=consent" # refresh token / 동의화면 매번 요청 + +kakao: + oauth2: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + scope: "profile_nickname,account_email" + authorization-uri: "https://kauth.kakao.com/oauth/authorize" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" + additional-parameters: "" \ No newline at end of file From 4dad1900651ef96d913be3e63aa6e425a8fcdd99 Mon Sep 17 00:00:00 2001 From: chan0831 <116000778+chan0831@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:54:05 +0900 Subject: [PATCH 09/19] =?UTF-8?q?[Feat]=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=B6=94=EA=B0=80/=EC=82=AD=EC=A0=9C=20(#36)=20(#7?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이찬우 --- .../apiPayLoad/code/status/ErrorStatus.java | 6 +- .../codeplay/controller/LikeController.java | 46 ++++++++++++++ .../converter/MusicLikeConverter.java | 27 ++++++++ src/main/java/umc/codeplay/domain/Member.java | 8 ++- src/main/java/umc/codeplay/domain/Music.java | 7 ++- .../java/umc/codeplay/dto/LikeRequestDTO.java | 17 ++++++ .../umc/codeplay/dto/LikeResponseDTO.java | 30 +++++++++ .../repository/MusicLikeRepository.java | 13 ++++ .../codeplay/repository/MusicRepository.java | 7 +++ .../umc/codeplay/service/LikeService.java | 61 +++++++++++++++++++ 10 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 src/main/java/umc/codeplay/controller/LikeController.java create mode 100644 src/main/java/umc/codeplay/converter/MusicLikeConverter.java create mode 100644 src/main/java/umc/codeplay/dto/LikeRequestDTO.java create mode 100644 src/main/java/umc/codeplay/dto/LikeResponseDTO.java create mode 100644 src/main/java/umc/codeplay/repository/MusicLikeRepository.java create mode 100644 src/main/java/umc/codeplay/repository/MusicRepository.java create mode 100644 src/main/java/umc/codeplay/service/LikeService.java diff --git a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java index 73aac45..bd22056 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -29,7 +29,11 @@ public enum ErrorStatus implements BaseErrorCode { AUTHORIZATION_METHOD_ERROR(HttpStatus.BAD_REQUEST, "AUTH405", "인증 방식이 잘못되었습니다."), INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH406", "유효하지 않은 OAuth 제공자입니다."), - AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."); + AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."), + + MUSIC_NOT_FOUND(HttpStatus.BAD_REQUEST, "MUSIC400", "음원을 찾을 수 없습니다."), + + LIKE_NOT_FOUND(HttpStatus.BAD_REQUEST, "LIKE400", "해당 좋아요를 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/umc/codeplay/controller/LikeController.java b/src/main/java/umc/codeplay/controller/LikeController.java new file mode 100644 index 0000000..5fda8c8 --- /dev/null +++ b/src/main/java/umc/codeplay/controller/LikeController.java @@ -0,0 +1,46 @@ +package umc.codeplay.controller; + +import jakarta.validation.Valid; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; + +import umc.codeplay.apiPayLoad.ApiResponse; +import umc.codeplay.converter.MusicLikeConverter; +import umc.codeplay.domain.Music; +import umc.codeplay.domain.mapping.MusicLike; +import umc.codeplay.dto.LikeRequestDTO; +import umc.codeplay.dto.LikeResponseDTO; +import umc.codeplay.service.LikeService; + +@RestController +@RequiredArgsConstructor +public class LikeController { + + private final LikeService likeService; + + @PostMapping("/like/add") + public ApiResponse addLike( + @RequestBody @Valid LikeRequestDTO.addLikeRequestDTO request) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + + MusicLike like = likeService.addLike(username, request); + // 로그인 한 username, request 받아서 MusicLike 추가하고 해당 musicId 반환 + + return ApiResponse.onSuccess(MusicLikeConverter.toLikeResponseDTO(like)); + } + + @PostMapping("/like/remove") + public ApiResponse removeLike( + @RequestBody @Valid LikeRequestDTO.removeLikeRequestDTO request) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + + Music music = likeService.removeLike(username, request); + + return ApiResponse.onSuccess(MusicLikeConverter.toRemoveLikeResponseDTO(music)); + } +} diff --git a/src/main/java/umc/codeplay/converter/MusicLikeConverter.java b/src/main/java/umc/codeplay/converter/MusicLikeConverter.java new file mode 100644 index 0000000..b0f5e31 --- /dev/null +++ b/src/main/java/umc/codeplay/converter/MusicLikeConverter.java @@ -0,0 +1,27 @@ +package umc.codeplay.converter; + +import umc.codeplay.domain.Member; +import umc.codeplay.domain.Music; +import umc.codeplay.domain.mapping.MusicLike; +import umc.codeplay.dto.LikeResponseDTO; + +public class MusicLikeConverter { + + public static MusicLike toMusicLike(Member member, Music music) { + + return MusicLike.builder().member(member).music(music).build(); + } + + public static LikeResponseDTO.addLikeResponseDTO toLikeResponseDTO(MusicLike like) { + + return LikeResponseDTO.addLikeResponseDTO + .builder() + .musicId(like.getMusic().getId()) + .like(like) + .build(); + } + + public static LikeResponseDTO.removeLikeResponseDTO toRemoveLikeResponseDTO(Music music) { + return LikeResponseDTO.removeLikeResponseDTO.builder().musicId(music.getId()).build(); + } +} diff --git a/src/main/java/umc/codeplay/domain/Member.java b/src/main/java/umc/codeplay/domain/Member.java index c27a3ce..43da145 100644 --- a/src/main/java/umc/codeplay/domain/Member.java +++ b/src/main/java/umc/codeplay/domain/Member.java @@ -1,12 +1,16 @@ package umc.codeplay.domain; +import java.util.ArrayList; +import java.util.List; import jakarta.persistence.*; import lombok.*; import umc.codeplay.domain.enums.Role; +import umc.codeplay.domain.mapping.MusicLike; import umc.codeplay.domain.enums.SocialStatus; + @Entity @Getter @Setter @@ -38,6 +42,6 @@ public void encodePassword(String password) { @Column(columnDefinition = "TEXT") private String profileUrl; - // @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) - // private List likeList = new ArrayList<>(); + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List likeList = new ArrayList<>(); } diff --git a/src/main/java/umc/codeplay/domain/Music.java b/src/main/java/umc/codeplay/domain/Music.java index a978655..1985e24 100644 --- a/src/main/java/umc/codeplay/domain/Music.java +++ b/src/main/java/umc/codeplay/domain/Music.java @@ -1,10 +1,13 @@ package umc.codeplay.domain; +import java.util.ArrayList; +import java.util.List; import jakarta.persistence.*; import lombok.*; import umc.codeplay.domain.common.BaseEntity; +import umc.codeplay.domain.mapping.MusicLike; @Entity @Getter @@ -23,6 +26,6 @@ public class Music extends BaseEntity { @JoinColumn(name = "member_id") private Member member; - // @OneToMany(mappedBy = "music", cascade = CascadeType.ALL) - // private List likeList = new ArrayList<>(); + @OneToMany(mappedBy = "music", cascade = CascadeType.ALL) + private List likeList = new ArrayList<>(); } diff --git a/src/main/java/umc/codeplay/dto/LikeRequestDTO.java b/src/main/java/umc/codeplay/dto/LikeRequestDTO.java new file mode 100644 index 0000000..a11802e --- /dev/null +++ b/src/main/java/umc/codeplay/dto/LikeRequestDTO.java @@ -0,0 +1,17 @@ +package umc.codeplay.dto; + +import lombok.Getter; + +public class LikeRequestDTO { + + @Getter + public static class addLikeRequestDTO { + + Long musicId; + } + + @Getter + public static class removeLikeRequestDTO { + Long musicId; + } +} diff --git a/src/main/java/umc/codeplay/dto/LikeResponseDTO.java b/src/main/java/umc/codeplay/dto/LikeResponseDTO.java new file mode 100644 index 0000000..138060b --- /dev/null +++ b/src/main/java/umc/codeplay/dto/LikeResponseDTO.java @@ -0,0 +1,30 @@ +package umc.codeplay.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import umc.codeplay.domain.mapping.MusicLike; + +public class LikeResponseDTO { + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class addLikeResponseDTO { + + Long musicId; + MusicLike like; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class removeLikeResponseDTO { + + Long musicId; + } +} diff --git a/src/main/java/umc/codeplay/repository/MusicLikeRepository.java b/src/main/java/umc/codeplay/repository/MusicLikeRepository.java new file mode 100644 index 0000000..569d5ab --- /dev/null +++ b/src/main/java/umc/codeplay/repository/MusicLikeRepository.java @@ -0,0 +1,13 @@ +package umc.codeplay.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import umc.codeplay.domain.Member; +import umc.codeplay.domain.Music; +import umc.codeplay.domain.mapping.MusicLike; + +public interface MusicLikeRepository extends JpaRepository { + Optional findByMemberAndMusic(Member member, Music music); +} diff --git a/src/main/java/umc/codeplay/repository/MusicRepository.java b/src/main/java/umc/codeplay/repository/MusicRepository.java new file mode 100644 index 0000000..4ecef4e --- /dev/null +++ b/src/main/java/umc/codeplay/repository/MusicRepository.java @@ -0,0 +1,7 @@ +package umc.codeplay.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import umc.codeplay.domain.Music; + +public interface MusicRepository extends JpaRepository {} diff --git a/src/main/java/umc/codeplay/service/LikeService.java b/src/main/java/umc/codeplay/service/LikeService.java new file mode 100644 index 0000000..db9008d --- /dev/null +++ b/src/main/java/umc/codeplay/service/LikeService.java @@ -0,0 +1,61 @@ +package umc.codeplay.service; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.GeneralException; +import umc.codeplay.converter.MusicLikeConverter; +import umc.codeplay.domain.Member; +import umc.codeplay.domain.Music; +import umc.codeplay.domain.mapping.MusicLike; +import umc.codeplay.dto.LikeRequestDTO; +import umc.codeplay.repository.MemberRepository; +import umc.codeplay.repository.MusicLikeRepository; +import umc.codeplay.repository.MusicRepository; + +@Service +@RequiredArgsConstructor +public class LikeService { + + private final MusicRepository musicRepository; + private final MemberRepository memberRepository; + private final MusicLikeRepository musicLikeRepository; + + public MusicLike addLike(String username, LikeRequestDTO.addLikeRequestDTO request) { + + Music music = + musicRepository + .findById(request.getMusicId()) + .orElseThrow(() -> new GeneralException(ErrorStatus.MUSIC_NOT_FOUND)); + + Member member = + memberRepository + .findByEmail(username) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + + MusicLike newLike = MusicLikeConverter.toMusicLike(member, music); + return musicLikeRepository.save(newLike); + } + + public Music removeLike(String username, LikeRequestDTO.removeLikeRequestDTO request) { + Music music = + musicRepository + .findById(request.getMusicId()) + .orElseThrow(() -> new GeneralException(ErrorStatus.MUSIC_NOT_FOUND)); + + Member member = + memberRepository + .findByEmail(username) + .orElseThrow(() -> new GeneralException(ErrorStatus.MEMBER_NOT_FOUND)); + + MusicLike musicLike = + musicLikeRepository + .findByMemberAndMusic(member, music) + .orElseThrow(() -> new GeneralException(ErrorStatus.LIKE_NOT_FOUND)); + + musicLikeRepository.delete(musicLike); + return music; + } +} From 3c8e091cdd93ca67ef1d0527d1effd5cef0e6a40 Mon Sep 17 00:00:00 2001 From: Minhyung Kim Date: Sat, 25 Jan 2025 21:24:02 +0900 Subject: [PATCH 10/19] [Feat] exception manage (#76) --- .../umc/codeplay/controller/AuthController.java | 10 ++++++---- .../umc/codeplay/controller/OAuthController.java | 2 ++ src/main/java/umc/codeplay/domain/Member.java | 3 +-- .../java/umc/codeplay/dto/MemberRequestDTO.java | 13 +++++++++++++ src/main/resources/application.yml | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/umc/codeplay/controller/AuthController.java b/src/main/java/umc/codeplay/controller/AuthController.java index 7458a3e..3dcb448 100644 --- a/src/main/java/umc/codeplay/controller/AuthController.java +++ b/src/main/java/umc/codeplay/controller/AuthController.java @@ -2,6 +2,8 @@ import java.util.Collection; import java.util.stream.Collectors; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -36,7 +38,7 @@ public class AuthController { @PostMapping("/login") public ApiResponse login( - @RequestBody MemberRequestDTO.LoginDto request) { + @Validated @RequestBody MemberRequestDTO.LoginDto request) { if (memberService.getSocialStatus(request.getEmail()) != SocialStatus.NONE) { throw new GeneralHandler(ErrorStatus.AUTHORIZATION_METHOD_ERROR); } @@ -66,7 +68,7 @@ public ApiResponse login( @PostMapping("/signup") public ApiResponse join( - @RequestBody MemberRequestDTO.JoinDto request) { + @Validated @RequestBody MemberRequestDTO.JoinDto request) { Member member = memberService.joinMember(request); MemberResponseDTO.JoinResultDTO newJoinResult = MemberConverter.toJoinResultDTO(member); @@ -75,8 +77,8 @@ public ApiResponse join( @PostMapping("/refresh") public ApiResponse refresh( - @RequestHeader("Refresh-Token") String refreshToken, - @RequestParam("email") String email) { + @RequestHeader("Refresh-Token") @NotNull(message = "리프레시 토큰은 필수 헤더입니다.") String refreshToken, + @Validated @RequestParam("email") @NotBlank(message = "이메일은 필수 입력값입니다.") String email) { // 리프레시 토큰 유효성 검사 if (jwtUtil.validateToken(refreshToken) && (jwtUtil.getTypeFromToken(refreshToken).equals("refresh"))) { diff --git a/src/main/java/umc/codeplay/controller/OAuthController.java b/src/main/java/umc/codeplay/controller/OAuthController.java index e2758b2..a1e3552 100644 --- a/src/main/java/umc/codeplay/controller/OAuthController.java +++ b/src/main/java/umc/codeplay/controller/OAuthController.java @@ -7,6 +7,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.view.RedirectView; @@ -28,6 +29,7 @@ @RestController @RequestMapping("/oauth") @RequiredArgsConstructor +@Validated public class OAuthController { private final JwtUtil jwtUtil; diff --git a/src/main/java/umc/codeplay/domain/Member.java b/src/main/java/umc/codeplay/domain/Member.java index 43da145..0972b1c 100644 --- a/src/main/java/umc/codeplay/domain/Member.java +++ b/src/main/java/umc/codeplay/domain/Member.java @@ -7,9 +7,8 @@ import lombok.*; import umc.codeplay.domain.enums.Role; -import umc.codeplay.domain.mapping.MusicLike; import umc.codeplay.domain.enums.SocialStatus; - +import umc.codeplay.domain.mapping.MusicLike; @Entity @Getter diff --git a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java index 4d5a3e1..e2d4214 100644 --- a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java @@ -1,19 +1,32 @@ package umc.codeplay.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + import lombok.Getter; public class MemberRequestDTO { @Getter public static class JoinDto { + @NotBlank(message = "이름은 필수 입력값입니다.") String name; + + @NotBlank(message = "이메일은 필수 입력값입니다.") + @Email(message = "이메일 형식이 아닙니다.") String email; + + @NotBlank(message = "비밀번호는 필수 입력값입니다.") String password; } @Getter public static class LoginDto { + @NotBlank(message = "이메일은 필수 입력값입니다.") + @Email(message = "이메일 형식이 아닙니다.") String email; + + @NotBlank(message = "비밀번호는 필수 입력값입니다.") String password; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 10fe9b2..7d2b15d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -18,7 +18,7 @@ spring: jpa: hibernate: - ddl-auto: update # Hibernate 엔티티 스키마 자동 업데이트 + ddl-auto: create # Hibernate 엔티티 스키마 자동 업데이트 properties: jakarta.persistence.sharedCache.mode: ALL hibernate: From 2c7d60c63f441c0ceb29502b51df1e00b9d600c1 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:09:39 +0900 Subject: [PATCH 11/19] Update build-test.yml (#78) --- .github/workflows/build-test.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5f0530d..54af745 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,6 +14,15 @@ jobs: AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} S3_BUCKET: ${{ secrets.S3_BUCKET }} JWT_SECRET: ${{ secrets.JWT_SECRET }} + MYSQL_USERNAME: ${{ secrets.MYSQL_USERNAME }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + DB_URL: ${{ secrets.DB_URL }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} + KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} + KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }} steps: - name: ✔️ 리포지토리 가져오기 @@ -28,6 +37,23 @@ jobs: - name: ✔️ Gradle 명령 실행 권한 설정 run: chmod +x gradlew + - name: 🔍 환경변수 로드 확인 + run: | + echo "🔍 AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:+SET}" + echo "🔍 AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:+SET}" + echo "🔍 AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:+SET}" + echo "🔍 S3_BUCKET: ${S3_BUCKET:+SET}" + echo "🔍 JWT_SECRET: ${JWT_SECRET:+SET}" + echo "🔍 MYSQL_USERNAME: ${MYSQL_USERNAME:+SET}" + echo "🔍 MYSQL_PASSWORD: ${MYSQL_PASSWORD:+SET}" + echo "🔍 DB_URL: ${DB_URL:+SET}" + echo "🔍 GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:+SET}" + echo "🔍 GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:+SET}" + echo "🔍 GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:+SET}" + echo "🔍 KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID:+SET}" + echo "🔍 KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET:+SET}" + echo "🔍 KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI:+SET}" + - name: ✔️ Gradle build run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test From 7036b491d656f56e3fb3e53974c8ea870ea2ceb0 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 15:39:55 +0900 Subject: [PATCH 12/19] =?UTF-8?q?[Fix]=20=EB=B0=B0=ED=8F=AC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-prod.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 38f8888..8518a34 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -40,3 +40,25 @@ cloud: jwt: secret: ${JWT_SECRET} + +google: + oauth2: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + redirect-uri: ${GOOGLE_REDIRECT_URI} + scope: "openid email profile" + authorization-uri: "https://accounts.google.com/o/oauth2/v2/auth" + token-uri: "https://oauth2.googleapis.com/token" + user-info-uri: "https://openidconnect.googleapis.com/v1/userinfo" + additional-parameters: "&access_type=offline&prompt=consent" # refresh token / 동의화면 매번 요청 + +kakao: + oauth2: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + scope: "profile_nickname,account_email" + authorization-uri: "https://kauth.kakao.com/oauth/authorize" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" + additional-parameters: "" From 68f681fd9c7b4521edfa346e43fe079c85c05464 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:20:13 +0900 Subject: [PATCH 13/19] Update build-test.yml (#81) --- .github/workflows/build-test.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 54af745..1c3a507 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -37,22 +37,22 @@ jobs: - name: ✔️ Gradle 명령 실행 권한 설정 run: chmod +x gradlew - - name: 🔍 환경변수 로드 확인 + - name: ✔️ Debugging Environment Variables run: | - echo "🔍 AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:+SET}" - echo "🔍 AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:+SET}" - echo "🔍 AWS_DEFAULT_REGION: ${AWS_DEFAULT_REGION:+SET}" - echo "🔍 S3_BUCKET: ${S3_BUCKET:+SET}" - echo "🔍 JWT_SECRET: ${JWT_SECRET:+SET}" - echo "🔍 MYSQL_USERNAME: ${MYSQL_USERNAME:+SET}" - echo "🔍 MYSQL_PASSWORD: ${MYSQL_PASSWORD:+SET}" - echo "🔍 DB_URL: ${DB_URL:+SET}" - echo "🔍 GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:+SET}" - echo "🔍 GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:+SET}" - echo "🔍 GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:+SET}" - echo "🔍 KAKAO_CLIENT_ID: ${KAKAO_CLIENT_ID:+SET}" - echo "🔍 KAKAO_CLIENT_SECRET: ${KAKAO_CLIENT_SECRET:+SET}" - echo "🔍 KAKAO_REDIRECT_URI: ${KAKAO_REDIRECT_URI:+SET}" + echo "🔍 AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID" + echo "🔍 AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" + echo "🔍 AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION" + echo "🔍 S3_BUCKET: $S3_BUCKET" + echo "🔍 JWT_SECRET: $JWT_SECRET" + echo "🔍 MYSQL_USERNAME: $MYSQL_USERNAME" + echo "🔍 MYSQL_PASSWORD: $MYSQL_PASSWORD" + echo "🔍 DB_URL: $DB_URL" + echo "🔍 GOOGLE_CLIENT_ID: $GOOGLE_CLIENT_ID" + echo "🔍 GOOGLE_CLIENT_SECRET: $GOOGLE_CLIENT_SECRET" + echo "🔍 GOOGLE_REDIRECT_URI: $GOOGLE_REDIRECT_URI" + echo "🔍 KAKAO_CLIENT_ID: $KAKAO_CLIENT_ID" + echo "🔍 KAKAO_CLIENT_SECRET: $KAKAO_CLIENT_SECRET" + echo "🔍 KAKAO_REDIRECT_URI: $KAKAO_REDIRECT_URI" - name: ✔️ Gradle build run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test From c85fae64ffa797684243422694e954b6765ebc8e Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:27:27 +0900 Subject: [PATCH 14/19] =?UTF-8?q?[Style]=20=EA=B0=84=EA=B2=A9=20=EB=B0=8F?= =?UTF-8?q?=20=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/umc/codeplay/controller/HealthCheck.java | 15 +++------------ .../java/umc/codeplay/dto/LikeRequestDTO.java | 1 - .../java/umc/codeplay/dto/LikeResponseDTO.java | 2 -- .../java/umc/codeplay/dto/MemberRequestDTO.java | 2 ++ 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/main/java/umc/codeplay/controller/HealthCheck.java b/src/main/java/umc/codeplay/controller/HealthCheck.java index 0b9da00..ff66ff8 100644 --- a/src/main/java/umc/codeplay/controller/HealthCheck.java +++ b/src/main/java/umc/codeplay/controller/HealthCheck.java @@ -1,13 +1,13 @@ package umc.codeplay.controller; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; import io.swagger.v3.oas.annotations.Hidden; -import umc.codeplay.service.S3Service; @Hidden @RestController @@ -15,18 +15,9 @@ @RequiredArgsConstructor public class HealthCheck { - private final S3Service s3Service; - // 연결 확인 용 @GetMapping("") public ResponseEntity healthCheck() { return ResponseEntity.ok("UMC 7th CodePlay Well Connected!"); } - - // s3 업로드 테스트 용 - @PostMapping("/s3") - public ResponseEntity s3HealthCheck(@RequestPart(value = "file") MultipartFile file) { - final String s3Url = s3Service.uploadFile(file); - return ResponseEntity.ok("S3 FIle is uploaded! : " + s3Url); - } } diff --git a/src/main/java/umc/codeplay/dto/LikeRequestDTO.java b/src/main/java/umc/codeplay/dto/LikeRequestDTO.java index a11802e..4592517 100644 --- a/src/main/java/umc/codeplay/dto/LikeRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/LikeRequestDTO.java @@ -6,7 +6,6 @@ public class LikeRequestDTO { @Getter public static class addLikeRequestDTO { - Long musicId; } diff --git a/src/main/java/umc/codeplay/dto/LikeResponseDTO.java b/src/main/java/umc/codeplay/dto/LikeResponseDTO.java index 138060b..84ade09 100644 --- a/src/main/java/umc/codeplay/dto/LikeResponseDTO.java +++ b/src/main/java/umc/codeplay/dto/LikeResponseDTO.java @@ -14,7 +14,6 @@ public class LikeResponseDTO { @AllArgsConstructor @NoArgsConstructor public static class addLikeResponseDTO { - Long musicId; MusicLike like; } @@ -24,7 +23,6 @@ public static class addLikeResponseDTO { @AllArgsConstructor @NoArgsConstructor public static class removeLikeResponseDTO { - Long musicId; } } diff --git a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java index e2d4214..cb1d051 100644 --- a/src/main/java/umc/codeplay/dto/MemberRequestDTO.java +++ b/src/main/java/umc/codeplay/dto/MemberRequestDTO.java @@ -9,6 +9,7 @@ public class MemberRequestDTO { @Getter public static class JoinDto { + @NotBlank(message = "이름은 필수 입력값입니다.") String name; @@ -22,6 +23,7 @@ public static class JoinDto { @Getter public static class LoginDto { + @NotBlank(message = "이메일은 필수 입력값입니다.") @Email(message = "이메일 형식이 아닙니다.") String email; From 3e511d175406a623490df0da43f9344abd49c715 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:33:25 +0900 Subject: [PATCH 15/19] =?UTF-8?q?[Fix]=20baseEntity=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=20=EB=B0=98=EC=98=81=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/umc/codeplay/CodeplayApplication.java | 2 ++ src/main/java/umc/codeplay/domain/Member.java | 3 ++- src/main/java/umc/codeplay/domain/Music.java | 3 +++ src/main/java/umc/codeplay/domain/common/BaseEntity.java | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/umc/codeplay/CodeplayApplication.java b/src/main/java/umc/codeplay/CodeplayApplication.java index ebe4f4d..b2f1000 100644 --- a/src/main/java/umc/codeplay/CodeplayApplication.java +++ b/src/main/java/umc/codeplay/CodeplayApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class CodeplayApplication { diff --git a/src/main/java/umc/codeplay/domain/Member.java b/src/main/java/umc/codeplay/domain/Member.java index 0972b1c..532e639 100644 --- a/src/main/java/umc/codeplay/domain/Member.java +++ b/src/main/java/umc/codeplay/domain/Member.java @@ -6,6 +6,7 @@ import lombok.*; +import umc.codeplay.domain.common.BaseEntity; import umc.codeplay.domain.enums.Role; import umc.codeplay.domain.enums.SocialStatus; import umc.codeplay.domain.mapping.MusicLike; @@ -16,7 +17,7 @@ @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor -public class Member { +public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/umc/codeplay/domain/Music.java b/src/main/java/umc/codeplay/domain/Music.java index 1985e24..8bd87a8 100644 --- a/src/main/java/umc/codeplay/domain/Music.java +++ b/src/main/java/umc/codeplay/domain/Music.java @@ -26,6 +26,9 @@ public class Music extends BaseEntity { @JoinColumn(name = "member_id") private Member member; + @Column(columnDefinition = "TEXT") + private String musicUrl; + @OneToMany(mappedBy = "music", cascade = CascadeType.ALL) private List likeList = new ArrayList<>(); } diff --git a/src/main/java/umc/codeplay/domain/common/BaseEntity.java b/src/main/java/umc/codeplay/domain/common/BaseEntity.java index 844f213..0dc3cff 100644 --- a/src/main/java/umc/codeplay/domain/common/BaseEntity.java +++ b/src/main/java/umc/codeplay/domain/common/BaseEntity.java @@ -14,6 +14,7 @@ @EntityListeners(AuditingEntityListener.class) @Getter public abstract class BaseEntity { + @CreatedDate private LocalDateTime createdAt; @LastModifiedDate private LocalDateTime updatedAt; From 1532a1f1469e6c47d94915e58f29bb23ba366f79 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:45:31 +0900 Subject: [PATCH 16/19] [Test] Update build-test.yml --- .github/workflows/build-test.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1c3a507..d86f601 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,20 +9,20 @@ jobs: build: runs-on: ubuntu-latest env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} - S3_BUCKET: ${{ secrets.S3_BUCKET }} - JWT_SECRET: ${{ secrets.JWT_SECRET }} - MYSQL_USERNAME: ${{ secrets.MYSQL_USERNAME }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - DB_URL: ${{ secrets.DB_URL }} - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} - GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} - KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} - KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} - KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }} + AWS_ACCESS_KEY_ID: "mock-aws-access-key-id" + AWS_SECRET_ACCESS_KEY: "mock-aws-secret-access-key" + AWS_DEFAULT_REGION: "mock-aws-region" + S3_BUCKET: "mock-s3-bucket-name" + JWT_SECRET: "mock-jwt-secret" + MYSQL_USERNAME: "mock-mysql-username" + MYSQL_PASSWORD: "mock-mysql-password" + DB_URL: "mock-db-url" + GOOGLE_CLIENT_ID: "mock-google-client-id" + GOOGLE_CLIENT_SECRET: "mock-google-client-secret" + GOOGLE_REDIRECT_URI: "mock-google-redirect-uri" + KAKAO_CLIENT_ID: "mock-kakao-client-id" + KAKAO_CLIENT_SECRET: "mock-kakao-client-secret" + KAKAO_REDIRECT_URI: "mock-kakao-redirect-uri" steps: - name: ✔️ 리포지토리 가져오기 From fdff517e5373549e24ccad2ea56214405401e3f7 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 17:48:01 +0900 Subject: [PATCH 17/19] [Test] Update build-test.yml --- .github/workflows/build-test.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d86f601..1c3a507 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -9,20 +9,20 @@ jobs: build: runs-on: ubuntu-latest env: - AWS_ACCESS_KEY_ID: "mock-aws-access-key-id" - AWS_SECRET_ACCESS_KEY: "mock-aws-secret-access-key" - AWS_DEFAULT_REGION: "mock-aws-region" - S3_BUCKET: "mock-s3-bucket-name" - JWT_SECRET: "mock-jwt-secret" - MYSQL_USERNAME: "mock-mysql-username" - MYSQL_PASSWORD: "mock-mysql-password" - DB_URL: "mock-db-url" - GOOGLE_CLIENT_ID: "mock-google-client-id" - GOOGLE_CLIENT_SECRET: "mock-google-client-secret" - GOOGLE_REDIRECT_URI: "mock-google-redirect-uri" - KAKAO_CLIENT_ID: "mock-kakao-client-id" - KAKAO_CLIENT_SECRET: "mock-kakao-client-secret" - KAKAO_REDIRECT_URI: "mock-kakao-redirect-uri" + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + S3_BUCKET: ${{ secrets.S3_BUCKET }} + JWT_SECRET: ${{ secrets.JWT_SECRET }} + MYSQL_USERNAME: ${{ secrets.MYSQL_USERNAME }} + MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} + DB_URL: ${{ secrets.DB_URL }} + GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} + KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} + KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }} steps: - name: ✔️ 리포지토리 가져오기 From 34de645a83edf93ef5e28f20e2e9d5cbe554f2cb Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:27:41 +0900 Subject: [PATCH 18/19] =?UTF-8?q?[Refactor]=20S3=20presignedurl=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Refactor] S3 presignedurl로 변경 * [Test] 필요없는 단계 제거 * [Test] 테스트 코드에 환경변수 주입 * [Fix] S3Client 객체 생성 * [Fix] test * [Fix] 설정 변경 * [Fix] 환경변수 이름 변경 * [Fix] test 설정 제거 * [Fix] 원복 * [Test] 변수 제거 * [Test] 변수 추가 * [Test] 환경변수 추가 * [Test] jwt 환경변수 추가 --- .github/workflows/build-test.yml | 32 +----- build.gradle | 5 +- .../apiPayLoad/code/status/ErrorStatus.java | 4 + .../java/umc/codeplay/config/AWSConfig.java | 42 ++++--- .../codeplay/controller/FileController.java | 49 ++++++++ .../umc/codeplay/dto/FileResponseDTO.java | 20 ++++ .../umc/codeplay/service/FileService.java | 108 ++++++++++++++++++ .../java/umc/codeplay/service/S3Service.java | 87 -------------- src/main/resources/application-prod.yml | 19 +-- src/main/resources/application.yml | 19 +-- 10 files changed, 237 insertions(+), 148 deletions(-) create mode 100644 src/main/java/umc/codeplay/controller/FileController.java create mode 100644 src/main/java/umc/codeplay/dto/FileResponseDTO.java create mode 100644 src/main/java/umc/codeplay/service/FileService.java delete mode 100644 src/main/java/umc/codeplay/service/S3Service.java diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 1c3a507..f7ca5d8 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -14,15 +14,12 @@ jobs: AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} S3_BUCKET: ${{ secrets.S3_BUCKET }} JWT_SECRET: ${{ secrets.JWT_SECRET }} - MYSQL_USERNAME: ${{ secrets.MYSQL_USERNAME }} - MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }} - DB_URL: ${{ secrets.DB_URL }} - GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} - GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} - GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} - KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} - KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} - KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }} + # GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} + # GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }} + # GOOGLE_REDIRECT_URI: ${{ secrets.GOOGLE_REDIRECT_URI }} + # KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} + # KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} + # KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }} steps: - name: ✔️ 리포지토리 가져오기 @@ -37,23 +34,6 @@ jobs: - name: ✔️ Gradle 명령 실행 권한 설정 run: chmod +x gradlew - - name: ✔️ Debugging Environment Variables - run: | - echo "🔍 AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID" - echo "🔍 AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY" - echo "🔍 AWS_DEFAULT_REGION: $AWS_DEFAULT_REGION" - echo "🔍 S3_BUCKET: $S3_BUCKET" - echo "🔍 JWT_SECRET: $JWT_SECRET" - echo "🔍 MYSQL_USERNAME: $MYSQL_USERNAME" - echo "🔍 MYSQL_PASSWORD: $MYSQL_PASSWORD" - echo "🔍 DB_URL: $DB_URL" - echo "🔍 GOOGLE_CLIENT_ID: $GOOGLE_CLIENT_ID" - echo "🔍 GOOGLE_CLIENT_SECRET: $GOOGLE_CLIENT_SECRET" - echo "🔍 GOOGLE_REDIRECT_URI: $GOOGLE_REDIRECT_URI" - echo "🔍 KAKAO_CLIENT_ID: $KAKAO_CLIENT_ID" - echo "🔍 KAKAO_CLIENT_SECRET: $KAKAO_CLIENT_SECRET" - echo "🔍 KAKAO_REDIRECT_URI: $KAKAO_REDIRECT_URI" - - name: ✔️ Gradle build run: ./gradlew build -x installLocalGitHook -x spotlessInternalRegisterDependencies -x spotlessJava -x spotlessJavaApply -x spotlessApply -x spotlessJavaCheck -x spotlessCheck -x test diff --git a/build.gradle b/build.gradle index dc91a43..bba05d2 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ dependencies { testRuntimeOnly 'com.h2database:h2' // 스웨거 - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' // 스프링 시큐리티 implementation 'org.springframework.boot:spring-boot-starter-security' @@ -60,7 +60,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' // s3 - implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.1.1")) + implementation("io.awspring.cloud:spring-cloud-aws-starter-s3") // oauth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' diff --git a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java index bd22056..30ebaa6 100644 --- a/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java +++ b/src/main/java/umc/codeplay/apiPayLoad/code/status/ErrorStatus.java @@ -30,6 +30,10 @@ public enum ErrorStatus implements BaseErrorCode { INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH406", "유효하지 않은 OAuth 제공자입니다."), AWS_SERVICE_UNAVAILABLE(HttpStatus.BAD_REQUEST, "AWS400", "AWS S3에 파일을 업로드할 수 없습니다."), + AWS_METHOD_NOT_ALLOWED( + HttpStatus.METHOD_NOT_ALLOWED, + "AWS405", + "AWS S3 presigned url에서 해당 method는 허용되지 않습니다."), MUSIC_NOT_FOUND(HttpStatus.BAD_REQUEST, "MUSIC400", "음원을 찾을 수 없습니다."), diff --git a/src/main/java/umc/codeplay/config/AWSConfig.java b/src/main/java/umc/codeplay/config/AWSConfig.java index f521913..9c8a8c9 100644 --- a/src/main/java/umc/codeplay/config/AWSConfig.java +++ b/src/main/java/umc/codeplay/config/AWSConfig.java @@ -4,31 +4,43 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; @Configuration public class AWSConfig { - @Value("${cloud.aws.credentials.accessKey}") + @Value("${spring.cloud.aws.credentials.access-key}") private String accessKey; - @Value("${cloud.aws.credentials.secretKey}") + @Value("${spring.cloud.aws.credentials.secret-key}") private String secretKey; - @Value("${cloud.aws.region.static}") + @Value("${spring.cloud.aws.region.static}") private String region; @Bean - public AmazonS3Client amazonS3Client() { - final BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); - - return (AmazonS3Client) - AmazonS3ClientBuilder.standard() - .withRegion(region) - .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) - .build(); + public S3Client s3Client() { + System.out.println("s3Client region: " + region); + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .build(); + } + + @Bean + public S3Presigner s3Presigner() { + System.out.println("s3Presigner region: " + region); + return S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKey, secretKey))) + .build(); } } diff --git a/src/main/java/umc/codeplay/controller/FileController.java b/src/main/java/umc/codeplay/controller/FileController.java new file mode 100644 index 0000000..d22fab0 --- /dev/null +++ b/src/main/java/umc/codeplay/controller/FileController.java @@ -0,0 +1,49 @@ +package umc.codeplay.controller; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import lombok.RequiredArgsConstructor; + +import io.swagger.v3.oas.annotations.Operation; +import software.amazon.awssdk.http.SdkHttpMethod; +import umc.codeplay.apiPayLoad.ApiResponse; +import umc.codeplay.dto.FileResponseDTO; +import umc.codeplay.service.FileService; + +import static umc.codeplay.service.FileService.buildFilename; + +@RestController +@RequestMapping("/files") +@RequiredArgsConstructor +public class FileController { + + private final FileService fileService; + + @Operation( + summary = "Download용 Presigned URL 생성", + description = "다운로드를 위한 Presigned URL 생성 - 유효시간 존재") + @GetMapping("/download") + public ApiResponse getUrl( + @RequestParam(value = "fileName") String fileName) { + String downloadUrl = fileService.generatePreSignedUrl(fileName, SdkHttpMethod.GET); + FileResponseDTO.DownloadFile result = new FileResponseDTO.DownloadFile(downloadUrl); + + return ApiResponse.onSuccess(result); + } + + @Operation( + summary = "Upload용 Presigned URL 생성", + description = "업로드를 위한 Presigned URL 생성 - 유효시간 존재") + @PostMapping("/upload") + public ApiResponse generateUrl( + @RequestParam(value = "fileName") String fileName) { + String username = SecurityContextHolder.getContext().getAuthentication().getName(); + String newFileName = buildFilename(fileName); + Long musicId = fileService.uploadMusic(newFileName, username); + + String uploadUrl = fileService.generatePreSignedUrl(newFileName, SdkHttpMethod.PUT); + FileResponseDTO.UploadFile result = new FileResponseDTO.UploadFile(uploadUrl, musicId); + return ApiResponse.onSuccess(result); + } +} diff --git a/src/main/java/umc/codeplay/dto/FileResponseDTO.java b/src/main/java/umc/codeplay/dto/FileResponseDTO.java new file mode 100644 index 0000000..3fd5f41 --- /dev/null +++ b/src/main/java/umc/codeplay/dto/FileResponseDTO.java @@ -0,0 +1,20 @@ +package umc.codeplay.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public class FileResponseDTO { + + @Getter + @AllArgsConstructor + public static class DownloadFile { + private String downloadS3Url; + } + + @Getter + @AllArgsConstructor + public static class UploadFile { + private String uploadS3Url; + private Long musicId; + } +} diff --git a/src/main/java/umc/codeplay/service/FileService.java b/src/main/java/umc/codeplay/service/FileService.java new file mode 100644 index 0000000..06e6cd5 --- /dev/null +++ b/src/main/java/umc/codeplay/service/FileService.java @@ -0,0 +1,108 @@ +package umc.codeplay.service; + +import java.text.Normalizer; +import java.time.Duration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; + +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; +import umc.codeplay.apiPayLoad.code.status.ErrorStatus; +import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; +import umc.codeplay.domain.Member; +import umc.codeplay.domain.Music; +import umc.codeplay.repository.MemberRepository; +import umc.codeplay.repository.MusicRepository; + +@Service +@RequiredArgsConstructor +public class FileService { + + @Value("${s3.bucket}") + private String bucketName; + + @Value("${spring.cloud.aws.region.static}") + private String region; + + private final S3Presigner s3Presigner; + private final MusicRepository musicRepository; + private final MemberRepository memberRepository; + + // 타임스탬프_파일명 형식으로 파일 이름 저장 + public static String buildFilename(String filename) { + return String.format("%s_%s", System.currentTimeMillis(), sanitizeFileName(filename)); + } + + // 특수 문자나 공백 등을 정리 + private static String sanitizeFileName(String fileName) { + String normalizedFileName = Normalizer.normalize(fileName, Normalizer.Form.NFC); + return normalizedFileName.replaceAll("\\s+", "_").replaceAll("[^a-zA-Z0-9.\\-_]", ""); + } + + // 파일 업로드(HTTP PUT) 또는 다운로드(HTTP GET)를 위한 Presigned URL 생성 + public String generatePreSignedUrl(String fileName, SdkHttpMethod method) { + + return switch (method) { + case GET -> generateGetPresignedUrl(fileName); + case PUT -> generatePutPresignedUrl(fileName); + default -> throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE); + }; + } + + // S3에서 파일을 다운로드할 수 있는 Presigned URL 생성 + private String generateGetPresignedUrl(String fileName) { + GetObjectRequest getObjectRequest = + GetObjectRequest.builder().bucket(bucketName).key(fileName).build(); + + GetObjectPresignRequest presignRequest = + GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(60)) + .getObjectRequest(getObjectRequest) + .build(); + + PresignedGetObjectRequest presignedRequest = s3Presigner.presignGetObject(presignRequest); + return presignedRequest.url().toString(); + } + + // S3에 파일을 업로드할 수 있는 Presigned URL 생성 + private String generatePutPresignedUrl(String fileName) { + PutObjectRequest putObjectRequest = + PutObjectRequest.builder().bucket(bucketName).key(fileName).build(); + + PutObjectPresignRequest presignRequest = + PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(60)) + .putObjectRequest(putObjectRequest) + .build(); + + PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest); + return presignedRequest.url().toString(); + } + + // music 레포지토리에 업로드 + public Long uploadMusic(String newFileName, String userEmail) { + Member member = + memberRepository + .findByEmail(userEmail) + .orElseThrow(() -> new GeneralHandler(ErrorStatus.MEMBER_NOT_FOUND)); + + // 저장하는 url은 유효시간이 없는 public + // TODO: 업로드에만 presigned 사용할지 아님 다운로드시에도 사용할지에 따라 변경해야함. + String s3Url = + String.format("https://%s.s3.%s.amazonaws.com/%s", bucketName, region, newFileName); + Music newMusic = Music.builder().title(newFileName).musicUrl(s3Url).member(member).build(); + + return musicRepository.save(newMusic).getId(); + } + + // TODO: 필요시 직접 업로드 방법 구현 필요 +} diff --git a/src/main/java/umc/codeplay/service/S3Service.java b/src/main/java/umc/codeplay/service/S3Service.java deleted file mode 100644 index 1c58580..0000000 --- a/src/main/java/umc/codeplay/service/S3Service.java +++ /dev/null @@ -1,87 +0,0 @@ -package umc.codeplay.service; - -import java.io.ByteArrayInputStream; -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -import lombok.RequiredArgsConstructor; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.model.ObjectMetadata; -import com.amazonaws.services.s3.model.PutObjectRequest; -import com.amazonaws.util.IOUtils; -import umc.codeplay.apiPayLoad.code.status.ErrorStatus; -import umc.codeplay.apiPayLoad.exception.handler.GeneralHandler; - -@Service -@RequiredArgsConstructor -@Transactional -public class S3Service { - - @Value("${cloud.aws.s3.bucket}") - private String bucket; - - private final AmazonS3Client amazonS3Client; - - /* - s3에 파일 업로드 - */ - public String uploadFile(MultipartFile file) { - if (file.getOriginalFilename() == null || file.getOriginalFilename().isEmpty()) { - throw new GeneralHandler(ErrorStatus._BAD_REQUEST); - } - - final String fileName = newFileName(file.getOriginalFilename()); - - ObjectMetadata objectMetadata = new ObjectMetadata(); - try { - objectMetadata.setContentLength(file.getSize()); - objectMetadata.setContentType(file.getContentType()); - - /* - TODO: 용량 문제가 생긴다면 아래 ByteArrayInputStream 방식을 변경해야함 - 1. 임시파일 방식으로 변경 - 2. 가능한 용량 limit 설정 - */ - byte[] bytes = IOUtils.toByteArray(file.getInputStream()); - ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); - - PutObjectRequest putObjectRequest = - new PutObjectRequest(bucket, fileName, inputStream, objectMetadata); - amazonS3Client.putObject(putObjectRequest); - - inputStream.close(); - - } catch (AmazonClientException | IOException e) { - throw new GeneralHandler(ErrorStatus.AWS_SERVICE_UNAVAILABLE); - } - - return amazonS3Client.getUrl(bucket, fileName).toString(); - } - - /* - s3 업로드시 파일 이름 변경 - */ - private String newFileName(String fileName) { - - final String FILE_EXTENSION_SEPARATOR = "."; - final String now = String.valueOf(System.currentTimeMillis()); - - int fileExtensionIndex = fileName.lastIndexOf(FILE_EXTENSION_SEPARATOR); - if (fileExtensionIndex == -1) { - return fileName - + "_" - + System.currentTimeMillis(); // No extension found, just add timestamp - } - - final String fileExtension = fileName.substring(fileExtensionIndex); - final String originalFileName = fileName.substring(0, fileExtensionIndex); - - return originalFileName + "_" + now + fileExtension; - } -} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 8518a34..064e089 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -28,15 +28,16 @@ spring: use_sql_comments: true default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화) -cloud: - aws: - s3: - bucket: ${S3_BUCKET} - stack.auto: false - region.static: ${AWS_DEFAULT_REGION} - credentials: - accessKey: ${AWS_ACCESS_KEY_ID} - secretKey: ${AWS_SECRET_ACCESS_KEY} + cloud: + aws: + region: + static: ${AWS_DEFAULT_REGION} + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} + +s3: + bucket: ${S3_BUCKET} jwt: secret: ${JWT_SECRET} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7d2b15d..75a5102 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,15 +28,16 @@ spring: use_sql_comments: true default_batch_fetch_size: 1000 # 배치 크기 설정 (성능 최적화) -cloud: - aws: - s3: - bucket: ${S3_BUCKET} - stack.auto: false - region.static: ${AWS_DEFAULT_REGION} - credentials: - accessKey: ${AWS_ACCESS_KEY_ID} - secretKey: ${AWS_SECRET_ACCESS_KEY} + cloud: + aws: + region: + static: ${AWS_DEFAULT_REGION} + credentials: + access-key: ${AWS_ACCESS_KEY_ID} + secret-key: ${AWS_SECRET_ACCESS_KEY} + +s3: + bucket: ${S3_BUCKET} jwt: secret: ${JWT_SECRET} From c695292820210cfe15fa9f98fdb1dd43fd8eed27 Mon Sep 17 00:00:00 2001 From: Kiara <80676180+2020147542@users.noreply.github.com> Date: Sun, 26 Jan 2025 19:41:28 +0900 Subject: [PATCH 19/19] =?UTF-8?q?[Test]=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Test] 필요없는 부분 제거 테스트 * [Chore] 필요없는 코드 제거 --- src/main/java/umc/codeplay/config/AWSConfig.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/main/java/umc/codeplay/config/AWSConfig.java b/src/main/java/umc/codeplay/config/AWSConfig.java index 9c8a8c9..dea3721 100644 --- a/src/main/java/umc/codeplay/config/AWSConfig.java +++ b/src/main/java/umc/codeplay/config/AWSConfig.java @@ -7,7 +7,6 @@ import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.regions.Region; -import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; @Configuration @@ -22,20 +21,8 @@ public class AWSConfig { @Value("${spring.cloud.aws.region.static}") private String region; - @Bean - public S3Client s3Client() { - System.out.println("s3Client region: " + region); - return S3Client.builder() - .region(Region.of(region)) - .credentialsProvider( - StaticCredentialsProvider.create( - AwsBasicCredentials.create(accessKey, secretKey))) - .build(); - } - @Bean public S3Presigner s3Presigner() { - System.out.println("s3Presigner region: " + region); return S3Presigner.builder() .region(Region.of(region)) .credentialsProvider(