From d6d868532e2b5456f3b9e91ab0df3923891d6d07 Mon Sep 17 00:00:00 2001 From: Unan Date: Sat, 22 Jul 2023 11:37:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[deploy]=20=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=8B=9C=EC=97=90=20slack=20=EC=95=8C?= =?UTF-8?q?=EB=9E=8C=EC=98=A4=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [fix #142] 앨범 내 사진 조회 최신순으로 정렬 (#143) * [fix #140] 앨범 공유 수락 API 로직 수정 (#144) * [feat #128] 포토부스 관련 API 구현 (#146) * [feat #128] 포토부스 관련 API 구현 * [fix #128] Photo 공유 수락 API 로직 수정 * [chore #128] swagger 업데이트 * [chore #128] 공백 제거 * [fix #147] 포포리즘 공유 API 수정 (#148) * [feat #128] 포토부스 관련 API 구현 * [fix #128] Photo 공유 수락 API 로직 수정 * [chore #128] swagger 업데이트 * [chore #128] 공백 제거 * [fix #147] 포포리즘 공유 API 수정 * [chore #147] swagger 수정 * [hotfix #149] 포포리즘 공유 API request body 수정 (#150) * [hotfix #149] 포포리즘 공유 request body 수정 * [chore #149] 사용하지 않는 import 제거 * [setting #149] banner 추가 * [chore #149] CI 스크립트 수정 * [chore #149] CI prod 스크립트 수정 * [chore #149] cd 스크립트 수정 * [fix #151] 배포 전 버그 수정 (access token 만료 시간 변경, 포포리즘 공유 API 수정) (#152) * [fix #151] Access Token 만료시간 변경 * [fix #151] 사진 upload url 변경 * [fix #151] 포포리즘 공유시에 이미지 저장 url 변경 (#153) * [fix #151] Access Token 만료시간 변경 * [fix #151] 사진 upload url 변경 * [hotfix #151] 포포리즘 저장 url 변경 * [refactor #156] slack 회원가입시에 알람 추가 (#157) * [refactor #156] slack 회원가입시에 알람 추가 * [fix #156] IOException 처리 * [fix #158] slack 메세지 알림 수정 (#159) * [fix #158] 어노테이션 변경 (#160) * [fix #158] slack 메세지 알림 수정 * [fix #158] annotation 변경 --------- Co-authored-by: Yunseo Kang <65678579+yungu0010@users.noreply.github.com> --- .github/workflows/cd.yml | 97 ------------------- scripts/deploy.sh | 2 +- .../pophoryserver/HealthCheckController.java | 9 +- .../domain/member/MemberService.java | 6 ++ .../member/controller/MemberV1Controller.java | 1 + .../member/controller/MemberV2Controller.java | 1 + .../domain/slack/SlackService.java | 30 ++++++ .../domain/slack/dto/SlackMessageDto.java | 10 ++ 8 files changed, 55 insertions(+), 101 deletions(-) delete mode 100644 .github/workflows/cd.yml create mode 100644 src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java create mode 100644 src/main/java/com/pophory/pophoryserver/domain/slack/dto/SlackMessageDto.java diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml deleted file mode 100644 index fc6186d..0000000 --- a/.github/workflows/cd.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: pophory cd - -on: - push: - branches: [ develop ] # main branch로 push 될 때 실행됩니다. - -env: - S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }} - -jobs: - build: - name: Code deployment - - # 실행 환경 - runs-on: ubuntu-latest - - steps: - - # 1) 워크플로우 실행 전 기본적으로 체크아웃 필요 - - name: checkout - uses: actions/checkout@v3 - - # 2) JDK 11버전 설치, 다른 JDK 버전을 사용하다면 수정 - - name: Set up JDK 11 - uses: actions/setup-java@v3 - with: - java-version: '11' - distribution: 'corretto' - - - name: make application.yml, application-infra.yml - run: | - ## create application.yml - cd ./src/main/resources - - # application.yml 파일 생성 - touch ./application.yml - touch ./application-infra.yml - - # GitHub-Actions 에서 설정한 값을 application.yml 파일에 쓰기 - echo "${{ secrets.APPLICATION }}" >> ./application.yml - echo "${{ secrets.APPLICATION_INFRA }}" >> ./application-infra.yml - - # application.yml 파일 확인 - cat ./application.yml - cat ./application-infra.yml - shell: bash - - # 이 워크플로우는 gradle build - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외) - run: ./gradlew build -x test - - # 디렉토리 생성 - - name: Make Directory - run: mkdir -p deploy - - # Jar 파일 복사 - - name: Copy Jar - run: cp ./build/libs/*.jar ./deploy - - - name: Copy appspec.yml - run: - cp appspec.yml ./deploy - - # script files 복사 - - name: Copy script - run: cp ./scripts/*.sh ./deploy - - - name: Make zip file - run: zip -r ./pophory-server.zip ./deploy - shell: bash - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} - aws-region: ap-northeast-2 - - - name: Upload to S3 - run: aws s3 cp --region ap-northeast-2 ./pophory-server.zip s3://$S3_BUCKET_NAME/ - - # Deploy - - name: Deploy - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY }} - run: - aws deploy create-deployment - --application-name pophory-ec2-deploy - --deployment-config-name CodeDeployDefault.AllAtOnce - --deployment-group-name pophory-deploy-group - --file-exists-behavior OVERWRITE - --s3-location bucket=${{ secrets.S3_BUCKET_NAME }},bundleType=zip,key=pophory-server.zip - --region ap-northeast-2 \ No newline at end of file diff --git a/scripts/deploy.sh b/scripts/deploy.sh index a4124cf..7980a15 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -51,7 +51,7 @@ nohup java -jar -Duser.timezone=Asia/Seoul -Dspring.profiles.active=$IDLE_PROFIL echo "> $IDLE_PROFILE 10초 후 Health check 시작" echo "> curl -s http://localhost:$IDLE_PORT/health" -sleep 30 +sleep 20 for retry_count in {1..10} do diff --git a/src/main/java/com/pophory/pophoryserver/HealthCheckController.java b/src/main/java/com/pophory/pophoryserver/HealthCheckController.java index 353ff90..8ad7993 100644 --- a/src/main/java/com/pophory/pophoryserver/HealthCheckController.java +++ b/src/main/java/com/pophory/pophoryserver/HealthCheckController.java @@ -2,17 +2,20 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import java.io.IOException; + @RestController +@RequiredArgsConstructor @Tag(name = "[Health Check] 서버 상태 확인 API") public class HealthCheckController { + @GetMapping("/health") @Operation(summary = "health check API", description = "항상 OK를 반환합니다.") - public ResponseEntity test() { - return ResponseEntity.ok("OK"); - } + public ResponseEntity test() {return ResponseEntity.ok("OK");} } diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java b/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java index 61bcd6c..a70fbff 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java +++ b/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java @@ -14,6 +14,7 @@ import com.pophory.pophoryserver.domain.member.dto.response.*; import com.pophory.pophoryserver.domain.photo.Photo; import com.pophory.pophoryserver.domain.photo.dto.response.PhotoGetResponseDto; +import com.pophory.pophoryserver.domain.slack.SlackService; import com.pophory.pophoryserver.global.util.RandomUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -21,6 +22,7 @@ import javax.persistence.EntityExistsException; import javax.persistence.EntityNotFoundException; +import java.io.IOException; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -35,17 +37,21 @@ public class MemberService { private final AlbumDesignJpaRepository albumDesignJpaRepository; private final FcmJpaRepository fcmJpaRepository; + private final SlackService slackService; + private static final int INITIAL_PHOTO_LIMIT = 15; @Transactional public void update(MemberCreateRequestDto request, Long memberId) { checkNicknameDuplicate(request.getNickname()); + slackService.sendSignInAlert(request.getNickname()); updateMemberInfo(request, memberId); } @Transactional public MemberCreateResponseDto updateV2(MemberCreateV2RequestDto request, Long memberId) { checkNicknameDuplicate(request.getNickname()); + slackService.sendSignInAlert(request.getNickname()); return MemberCreateResponseDto.of(updateMemberInfoV2(request, memberId)); } diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV1Controller.java b/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV1Controller.java index 9ca9a43..b5ab5b9 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV1Controller.java +++ b/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV1Controller.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import java.io.IOException; import java.security.Principal; import static com.pophory.pophoryserver.global.util.MemberUtil.getMemberId; diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV2Controller.java b/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV2Controller.java index ca61cb8..7d2d436 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV2Controller.java +++ b/src/main/java/com/pophory/pophoryserver/domain/member/controller/MemberV2Controller.java @@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.*; import javax.validation.Valid; +import java.io.IOException; import java.security.Principal; import static com.pophory.pophoryserver.global.util.MemberUtil.getMemberId; diff --git a/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java b/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java new file mode 100644 index 0000000..628fbbf --- /dev/null +++ b/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java @@ -0,0 +1,30 @@ +package com.pophory.pophoryserver.domain.slack; + +import com.pophory.pophoryserver.domain.slack.dto.SlackMessageDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + + +@Service +public class SlackService { + + @Value("${slack.webhook.url}") + private String SLACK_WEBHOOK_URL; + + public void sendSignInAlert(String nickname){ + RestTemplate restTemplate = new RestTemplate(); + restTemplate.postForEntity( + SLACK_WEBHOOK_URL, + createSlackHttpRequest("🎉 " + nickname + "님이 포포리의 회원가입을 완료했습니다. 🎉"), + String.class); + } + + private HttpEntity createSlackHttpRequest(String text) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Accept", "application/json; UTF-8"); + return new HttpEntity<>(SlackMessageDto.of(text), headers); + } +} diff --git a/src/main/java/com/pophory/pophoryserver/domain/slack/dto/SlackMessageDto.java b/src/main/java/com/pophory/pophoryserver/domain/slack/dto/SlackMessageDto.java new file mode 100644 index 0000000..a43e148 --- /dev/null +++ b/src/main/java/com/pophory/pophoryserver/domain/slack/dto/SlackMessageDto.java @@ -0,0 +1,10 @@ +package com.pophory.pophoryserver.domain.slack.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor(staticName = "of") +@Data +public class SlackMessageDto { + private String text; +} From 5cc64da5d217bb72bd17965a14fe56617942cd13 Mon Sep 17 00:00:00 2001 From: Unan Date: Tue, 12 Sep 2023 21:52:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?deploy:=20v1.0.2=20=EB=B0=B0=ED=8F=AC=20(#1?= =?UTF-8?q?73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [fix #142] 앨범 내 사진 조회 최신순으로 정렬 (#143) * [fix #140] 앨범 공유 수락 API 로직 수정 (#144) * [feat #128] 포토부스 관련 API 구현 (#146) * [feat #128] 포토부스 관련 API 구현 * [fix #128] Photo 공유 수락 API 로직 수정 * [chore #128] swagger 업데이트 * [chore #128] 공백 제거 * [fix #147] 포포리즘 공유 API 수정 (#148) * [feat #128] 포토부스 관련 API 구현 * [fix #128] Photo 공유 수락 API 로직 수정 * [chore #128] swagger 업데이트 * [chore #128] 공백 제거 * [fix #147] 포포리즘 공유 API 수정 * [chore #147] swagger 수정 * [hotfix #149] 포포리즘 공유 API request body 수정 (#150) * [hotfix #149] 포포리즘 공유 request body 수정 * [chore #149] 사용하지 않는 import 제거 * [setting #149] banner 추가 * [chore #149] CI 스크립트 수정 * [chore #149] CI prod 스크립트 수정 * [chore #149] cd 스크립트 수정 * [fix #151] 배포 전 버그 수정 (access token 만료 시간 변경, 포포리즘 공유 API 수정) (#152) * [fix #151] Access Token 만료시간 변경 * [fix #151] 사진 upload url 변경 * [fix #151] 포포리즘 공유시에 이미지 저장 url 변경 (#153) * [fix #151] Access Token 만료시간 변경 * [fix #151] 사진 upload url 변경 * [hotfix #151] 포포리즘 저장 url 변경 * [refactor #156] slack 회원가입시에 알람 추가 (#157) * [refactor #156] slack 회원가입시에 알람 추가 * [fix #156] IOException 처리 * [fix #158] slack 메세지 알림 수정 (#159) * [fix #158] 어노테이션 변경 (#160) * [fix #158] slack 메세지 알림 수정 * [fix #158] annotation 변경 * [test #163] add domain test code (#167) * [feat #169] 토큰 재발급, 회원 탈퇴 API V2 구현 (#170) * [feat #169] 토큰 재발급, 회원 탈퇴 API V2 구현 * [feat #169] 애플리케이션 실행 스크립트 추가 * [refactor #165] slack 알림 기능 고도화 (#171) --------- Co-authored-by: Yunseo Kang <65678579+yungu0010@users.noreply.github.com> --- build.gradle | 6 ++ scripts/run.sh | 26 +++++++++ .../pophoryserver/domain/album/Album.java | 14 +++++ .../domain/auth/AuthService.java | 9 +-- .../auth/controller/AuthV2Controller.java | 35 +++++++++++- .../pophoryserver/domain/member/Member.java | 6 +- .../domain/member/MemberJpaRepository.java | 2 + .../domain/member/MemberQueryRepository.java | 22 +++++++ .../domain/member/MemberService.java | 9 ++- .../domain/slack/SlackService.java | 44 +++++++++----- .../pophoryserver/domain/studio/Studio.java | 11 +++- .../advice/ControllerExceptionHandler.java | 10 ++++ .../PophoryserverApplicationTests.java | 1 - .../pophoryserver/config/TestConfig.java | 20 +++++++ .../pophoryserver/domain/album/AlbumTest.java | 32 +++++++++++ .../domain/member/MemberTest.java | 51 +++++++++++++++++ .../domain/studio/StudioTest.java | 57 +++++++++++++++++++ .../fixture/member/MemberFixture.java | 18 ++++++ 18 files changed, 347 insertions(+), 26 deletions(-) create mode 100644 scripts/run.sh create mode 100644 src/main/java/com/pophory/pophoryserver/domain/member/MemberQueryRepository.java create mode 100644 src/test/java/com/pophory/pophoryserver/config/TestConfig.java create mode 100644 src/test/java/com/pophory/pophoryserver/domain/album/AlbumTest.java create mode 100644 src/test/java/com/pophory/pophoryserver/domain/member/MemberTest.java create mode 100644 src/test/java/com/pophory/pophoryserver/domain/studio/StudioTest.java create mode 100644 src/test/java/com/pophory/pophoryserver/fixture/member/MemberFixture.java diff --git a/build.gradle b/build.gradle index f941f3a..4cebf24 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ plugins { id 'org.springframework.boot' version '2.7.12' id 'io.spring.dependency-management' version '1.0.15.RELEASE' id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" + id("application") } group = 'com.pophory' @@ -47,10 +48,15 @@ dependencies { // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' // sentry implementation 'io.sentry:sentry-spring-boot-starter:6.23.0' + // slack + implementation("com.slack.api:slack-api-client:1.31.0") + // Database runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100644 index 0000000..1e6e1fb --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Check if an argument is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +# Set the environment variable +ENV=$1 + +# Run the Gradle command +./gradlew clean build -x test + +# Check if the gradle build command succeeded +if [ $? -ne 0 ]; then + echo "Gradle build failed!" + exit 2 +fi + +# Change directory and run the Java command +cd build/libs +nohup java -Dspring.profiles.active=${ENV} -jar pophoryserver-0.0.1-SNAPSHOT.jar --server.port=8080 & + +echo "Server started with profile: ${ENV}" + diff --git a/src/main/java/com/pophory/pophoryserver/domain/album/Album.java b/src/main/java/com/pophory/pophoryserver/domain/album/Album.java index bcfcb6b..19c1141 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/album/Album.java +++ b/src/main/java/com/pophory/pophoryserver/domain/album/Album.java @@ -5,10 +5,12 @@ import com.pophory.pophoryserver.domain.member.Member; import com.pophory.pophoryserver.domain.photo.Photo; import com.pophory.pophoryserver.global.entity.BaseTimeEntity; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import javax.persistence.*; +import javax.validation.constraints.Min; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -29,6 +31,7 @@ public class Album extends BaseTimeEntity { @OneToOne(fetch = LAZY) private AlbumDesign albumDesign; + @Min(value = 1, message = "사진제한은 1장 이상이어야 합니다.") private int photoLimit; private String imageUrl; @@ -48,6 +51,17 @@ public void softDelete() { this.deletedAt = LocalDateTime.now(); } + @Builder + public Album(String title, AlbumDesign albumDesign, int photoLimit, String imageUrl, Member member) { + this.title = title; + this.albumDesign = albumDesign; + this.photoLimit = photoLimit; + this.imageUrl = imageUrl; + this.member = member; + } + + + public void setAlbumDesign(AlbumDesign albumDesign) { this.albumDesign = albumDesign; } diff --git a/src/main/java/com/pophory/pophoryserver/domain/auth/AuthService.java b/src/main/java/com/pophory/pophoryserver/domain/auth/AuthService.java index 386bac5..9713bd9 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/auth/AuthService.java +++ b/src/main/java/com/pophory/pophoryserver/domain/auth/AuthService.java @@ -4,6 +4,7 @@ import com.pophory.pophoryserver.domain.auth.dto.response.TokenResponseDto; import com.pophory.pophoryserver.domain.member.Member; import com.pophory.pophoryserver.domain.member.MemberJpaRepository; +import com.pophory.pophoryserver.domain.member.MemberQueryRepository; import com.pophory.pophoryserver.global.config.jwt.UserAuthentication; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,6 +16,7 @@ public class AuthService { private final MemberJpaRepository memberJpaRepository; + private final MemberQueryRepository memberQueryRepository; private final SocialService socialService; @Transactional @@ -24,14 +26,9 @@ public void signOut(Long memberId) { @Transactional public TokenResponseDto reIssue(Long memberId) { - Member member = getMemberById(memberId); + Member member = memberQueryRepository.findMemberById(memberId); TokenVO tokenVO = socialService.generateToken(new UserAuthentication(member.getId(), null, null)); member.updateRefreshToken(tokenVO.getRefreshToken()); return TokenResponseDto.of(tokenVO.getAccessToken(), tokenVO.getRefreshToken()); } - - private Member getMemberById(Long memberId) { - return memberJpaRepository.findById(memberId).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 사용자입니다. memberId: " + memberId)); - } - } diff --git a/src/main/java/com/pophory/pophoryserver/domain/auth/controller/AuthV2Controller.java b/src/main/java/com/pophory/pophoryserver/domain/auth/controller/AuthV2Controller.java index ba6252b..16d32e5 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/auth/controller/AuthV2Controller.java +++ b/src/main/java/com/pophory/pophoryserver/domain/auth/controller/AuthV2Controller.java @@ -4,10 +4,15 @@ import com.pophory.pophoryserver.domain.auth.SocialService; import com.pophory.pophoryserver.domain.auth.dto.request.AuthRequestDto; import com.pophory.pophoryserver.domain.auth.dto.response.AuthResponseDto; +import com.pophory.pophoryserver.domain.auth.dto.response.TokenResponseDto; import com.pophory.pophoryserver.global.config.jwt.JwtTokenProvider; import com.pophory.pophoryserver.global.config.jwt.UserAuthentication; +import com.pophory.pophoryserver.global.util.MemberUtil; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @@ -30,10 +35,8 @@ public class AuthV2Controller { private final SocialService socialService; private final AuthService authService; - private final JwtTokenProvider jwtTokenProvider; @PostMapping - @SecurityRequirement(name = "Authorization") @Operation(summary = "소셜로그인 API") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "소셜로그인 성공"), @@ -43,4 +46,32 @@ public class AuthV2Controller { public ResponseEntity socialLogin(@RequestHeader("Authorization") String socialAccessToken, @RequestBody AuthRequestDto request) throws NoSuchAlgorithmException, InvalidKeySpecException { return ResponseEntity.ok(socialService.signIn(socialAccessToken, request)); } + + @PostMapping("/token") + @SecurityRequirement(name = "Authorization") + @Operation(summary = "토큰 재발급 API") + @Parameter(name = "Authorization", description = "Bearer {access_token}", in = ParameterIn.HEADER, schema = @Schema(type = "string")) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공"), + @ApiResponse(responseCode = "400", description = "토큰 재발급 실패", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + public ResponseEntity reissue(Principal principal) { + return ResponseEntity.ok(authService.reIssue(MemberUtil.getMemberId(principal))); + } + + + @DeleteMapping(produces = "application/json") + @SecurityRequirement(name = "Authorization") + @Operation(summary = "회원탈퇴 API") + @Parameter(name = "Authorization", description = "Bearer {access_token}", in = ParameterIn.HEADER, required = true, schema = @Schema(type = "string")) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "회원탈퇴 성공"), + @ApiResponse(responseCode = "400", description = "회원탈퇴 실패", content = @Content), + @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) + }) + public ResponseEntity signOut(Principal principal) { + authService.signOut(MemberUtil.getMemberId(principal)); + return ResponseEntity.noContent().build(); + } } \ No newline at end of file diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/Member.java b/src/main/java/com/pophory/pophoryserver/domain/member/Member.java index 2eff17e..f84f618 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/member/Member.java +++ b/src/main/java/com/pophory/pophoryserver/domain/member/Member.java @@ -10,6 +10,7 @@ import lombok.NoArgsConstructor; import javax.persistence.*; +import javax.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -22,13 +23,15 @@ @NoArgsConstructor public class Member extends BaseTimeEntity { + private static final int MEMBER_DELETE_EXPIRE_TIME = 7; + @Id @GeneratedValue private Long id; @Column(length = 6) private String realName; - @Column(length = 15) + @Column(length = 15, unique = true) private String nickname; private String profileImage; @@ -36,6 +39,7 @@ public class Member extends BaseTimeEntity { @Enumerated(value = STRING) private SocialType socialType; + @NotNull(message = "소셜 아이디는 필수입니다.") private String socialId; private String refreshToken; diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/MemberJpaRepository.java b/src/main/java/com/pophory/pophoryserver/domain/member/MemberJpaRepository.java index 120306e..7aaed6f 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/member/MemberJpaRepository.java +++ b/src/main/java/com/pophory/pophoryserver/domain/member/MemberJpaRepository.java @@ -13,4 +13,6 @@ public interface MemberJpaRepository extends JpaRepository { Optional getMemberBySocialIdAndSocialType(String socialId, SocialType socialType); Optional findByNickname(String nickname); + + Optional findBySocialId(String socialId); } diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/MemberQueryRepository.java b/src/main/java/com/pophory/pophoryserver/domain/member/MemberQueryRepository.java new file mode 100644 index 0000000..a95f5b8 --- /dev/null +++ b/src/main/java/com/pophory/pophoryserver/domain/member/MemberQueryRepository.java @@ -0,0 +1,22 @@ +package com.pophory.pophoryserver.domain.member; + + +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import static com.pophory.pophoryserver.domain.member.QMember.*; + +@Repository +@RequiredArgsConstructor +public class MemberQueryRepository { + + private final JPAQueryFactory queryFactory; + + public Member findMemberById(Long id) { + return queryFactory.select(member) + .from(member) + .where(member.id.eq(id)) + .fetchOne(); + } +} diff --git a/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java b/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java index a70fbff..e891e44 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java +++ b/src/main/java/com/pophory/pophoryserver/domain/member/MemberService.java @@ -17,6 +17,7 @@ import com.pophory.pophoryserver.domain.slack.SlackService; import com.pophory.pophoryserver.global.util.RandomUtil; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +33,9 @@ @RequiredArgsConstructor public class MemberService { + @Value("${slack.channel.signin}") + private String SLACK_CHANNEL_SIGNIN; + private final MemberJpaRepository memberJpaRepository; private final AlbumJpaRepository albumJpaRepository; private final AlbumDesignJpaRepository albumDesignJpaRepository; @@ -40,18 +44,19 @@ public class MemberService { private final SlackService slackService; private static final int INITIAL_PHOTO_LIMIT = 15; + private static final String SLACK_MESSAGE = " \uD83C\uDF89 %s 님이 포포리의 회원가입을 완료했습니다. \uD83C\uDF89"; @Transactional public void update(MemberCreateRequestDto request, Long memberId) { checkNicknameDuplicate(request.getNickname()); - slackService.sendSignInAlert(request.getNickname()); + slackService.sendMessage(SLACK_CHANNEL_SIGNIN, String.format(SLACK_MESSAGE, request.getNickname())); updateMemberInfo(request, memberId); } @Transactional public MemberCreateResponseDto updateV2(MemberCreateV2RequestDto request, Long memberId) { checkNicknameDuplicate(request.getNickname()); - slackService.sendSignInAlert(request.getNickname()); + slackService.sendMessage(SLACK_CHANNEL_SIGNIN, String.format(SLACK_MESSAGE, request.getNickname())); return MemberCreateResponseDto.of(updateMemberInfoV2(request, memberId)); } diff --git a/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java b/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java index 628fbbf..0ddb2af 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java +++ b/src/main/java/com/pophory/pophoryserver/domain/slack/SlackService.java @@ -1,30 +1,48 @@ package com.pophory.pophoryserver.domain.slack; import com.pophory.pophoryserver.domain.slack.dto.SlackMessageDto; +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; +import com.slack.api.Slack; + + +import java.io.IOException; +import java.util.Arrays; @Service +@RequiredArgsConstructor public class SlackService { - @Value("${slack.webhook.url}") - private String SLACK_WEBHOOK_URL; + @Value("${slack.bot.token}") + private String SLACK_TOKEN; - public void sendSignInAlert(String nickname){ - RestTemplate restTemplate = new RestTemplate(); - restTemplate.postForEntity( - SLACK_WEBHOOK_URL, - createSlackHttpRequest("🎉 " + nickname + "님이 포포리의 회원가입을 완료했습니다. 🎉"), - String.class); - } + private final Environment env; - private HttpEntity createSlackHttpRequest(String text) { - HttpHeaders headers = new HttpHeaders(); - headers.add("Accept", "application/json; UTF-8"); - return new HttpEntity<>(SlackMessageDto.of(text), headers); + + public void sendMessage(String channel, String text) { + try { + Slack slack = Slack.getInstance(); + ChatPostMessageResponse response = slack.methods(SLACK_TOKEN).chatPostMessage(req -> req + .channel(channel) + .text("["+getProfiles()+"]"+ text)); + System.out.println(response); + } catch (IOException | SlackApiException e) { + throw new RuntimeException(e); + } + } + private String getProfiles() { + return Arrays.stream(env.getActiveProfiles()) + .findFirst() + .orElse(""); } } diff --git a/src/main/java/com/pophory/pophoryserver/domain/studio/Studio.java b/src/main/java/com/pophory/pophoryserver/domain/studio/Studio.java index b27c298..3fd9eab 100644 --- a/src/main/java/com/pophory/pophoryserver/domain/studio/Studio.java +++ b/src/main/java/com/pophory/pophoryserver/domain/studio/Studio.java @@ -1,5 +1,6 @@ package com.pophory.pophoryserver.domain.studio; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -7,6 +8,7 @@ import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.validation.constraints.NotNull; @Entity @Getter @@ -16,8 +18,15 @@ public class Studio { @Id @GeneratedValue private Long id; - @Column(unique = true, nullable = false) + @NotNull + @Column(unique = true) private String name; private String imageUrl; + + @Builder + public Studio(String name, String imageUrl) { + this.name = name; + this.imageUrl = imageUrl; + } } diff --git a/src/main/java/com/pophory/pophoryserver/global/advice/ControllerExceptionHandler.java b/src/main/java/com/pophory/pophoryserver/global/advice/ControllerExceptionHandler.java index a43d38a..efc8ef9 100644 --- a/src/main/java/com/pophory/pophoryserver/global/advice/ControllerExceptionHandler.java +++ b/src/main/java/com/pophory/pophoryserver/global/advice/ControllerExceptionHandler.java @@ -1,5 +1,6 @@ package com.pophory.pophoryserver.global.advice; +import com.pophory.pophoryserver.domain.slack.SlackService; import com.pophory.pophoryserver.global.exception.AlbumLimitExceedException; import com.pophory.pophoryserver.global.exception.BadRequestException; import com.pophory.pophoryserver.global.exception.S3UploadException; @@ -7,7 +8,9 @@ import com.pophory.pophoryserver.global.response.CodeResponse; import com.pophory.pophoryserver.global.response.ResponseCode; import io.sentry.Sentry; +import lombok.RequiredArgsConstructor; import org.aspectj.apache.bcel.classfile.Code; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -21,8 +24,14 @@ import java.io.IOException; @RestControllerAdvice +@RequiredArgsConstructor public class ControllerExceptionHandler { + private final SlackService slackService; + + @Value("${slack.channel.monitor}") + private String SLACK_CHANNEL_MONITOR; + @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(final BadRequestException e) { Sentry.captureException(e); @@ -86,6 +95,7 @@ public ResponseEntity handleEntityExistsException(final EntityExistsExcept @ExceptionHandler(Exception.class) public ResponseEntity handleException(final Exception e) { Sentry.captureException(e); + slackService.sendMessage(SLACK_CHANNEL_MONITOR, e.getMessage()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } diff --git a/src/test/java/com/pophory/pophoryserver/PophoryserverApplicationTests.java b/src/test/java/com/pophory/pophoryserver/PophoryserverApplicationTests.java index ecb018c..54e83c6 100644 --- a/src/test/java/com/pophory/pophoryserver/PophoryserverApplicationTests.java +++ b/src/test/java/com/pophory/pophoryserver/PophoryserverApplicationTests.java @@ -3,7 +3,6 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -@SpringBootTest class PophoryserverApplicationTests { @Test diff --git a/src/test/java/com/pophory/pophoryserver/config/TestConfig.java b/src/test/java/com/pophory/pophoryserver/config/TestConfig.java new file mode 100644 index 0000000..f81f4a2 --- /dev/null +++ b/src/test/java/com/pophory/pophoryserver/config/TestConfig.java @@ -0,0 +1,20 @@ +package com.pophory.pophoryserver.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +@TestConfiguration +public class TestConfig { + + @PersistenceContext + EntityManager em; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(em); + } +} diff --git a/src/test/java/com/pophory/pophoryserver/domain/album/AlbumTest.java b/src/test/java/com/pophory/pophoryserver/domain/album/AlbumTest.java new file mode 100644 index 0000000..666a1b9 --- /dev/null +++ b/src/test/java/com/pophory/pophoryserver/domain/album/AlbumTest.java @@ -0,0 +1,32 @@ +package com.pophory.pophoryserver.domain.album; + +import com.pophory.pophoryserver.config.TestConfig; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@DataJpaTest +@RequiredArgsConstructor +@Import(TestConfig.class) +public class AlbumTest { + + @Nested + @DisplayName("앨범 생성 테스트") + public class AlbumRegisterTest { + + @Test + @DisplayName("앨범이 성공적으로 만들어진다.") + void successCreateAlbum() { + Album album = Album.builder() + .title("기본 앨범") + .imageUrl("https://localhost:8080/album/album_default.png") + .photoLimit(15) + .build(); + } + } +} diff --git a/src/test/java/com/pophory/pophoryserver/domain/member/MemberTest.java b/src/test/java/com/pophory/pophoryserver/domain/member/MemberTest.java new file mode 100644 index 0000000..1f76608 --- /dev/null +++ b/src/test/java/com/pophory/pophoryserver/domain/member/MemberTest.java @@ -0,0 +1,51 @@ +package com.pophory.pophoryserver.domain.member; + +import com.pophory.pophoryserver.config.TestConfig; +import com.pophory.pophoryserver.domain.auth.SocialType; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@ActiveProfiles("test") +@DataJpaTest +@RequiredArgsConstructor +@Import(TestConfig.class) +public class MemberTest { + + @Autowired + private MemberJpaRepository memberJpaRepository; + + @Nested + @DisplayName("회원 등록 테스트") + public class MemberRegisterTest { + + @Test + @Transactional + @DisplayName("소셜로그인 후에 멤버가 성공적으로 만들어진다.") + void successRegisterMember() { + Member member = Member.builder() + .socialId("123456789") + .socialType(SocialType.KAKAO) + .build(); + memberJpaRepository.save(member); + assertThat(member.getSocialId()).isEqualTo("123456789"); + assertThat(member.getSocialType()).isEqualTo(SocialType.KAKAO); + + memberJpaRepository.findBySocialId("123456789") + .ifPresent(m -> { + assertThat(m.getSocialId()).isEqualTo("123456789"); + assertThat(m.getSocialType()).isEqualTo(SocialType.KAKAO); + }); + } + } + +} diff --git a/src/test/java/com/pophory/pophoryserver/domain/studio/StudioTest.java b/src/test/java/com/pophory/pophoryserver/domain/studio/StudioTest.java new file mode 100644 index 0000000..1bc1723 --- /dev/null +++ b/src/test/java/com/pophory/pophoryserver/domain/studio/StudioTest.java @@ -0,0 +1,57 @@ +package com.pophory.pophoryserver.domain.studio; + +import com.pophory.pophoryserver.config.TestConfig; +import lombok.RequiredArgsConstructor; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@ActiveProfiles("test") +@DataJpaTest +@RequiredArgsConstructor +@Import(TestConfig.class) +public class StudioTest { + + @Autowired + private StudioJpaRepository studioJpaRepository; + + private static final String NAME = "기본 스튜디오"; + private static final String IMAGE_URL = "https://localhost:8080/studio/studio_default.png"; + private static final String IMAGE_URL_2 = "https://localhost:8080/studio/studio_default2.png"; + + @Test + @DisplayName("스튜디오가 성공적으로 만들어진다.") + @Transactional + void successCreateStudio() { + Studio studio = Studio.builder() + .name(NAME) + .imageUrl(IMAGE_URL) + .build(); + Assertions.assertThat(studio.getName()).isEqualTo(NAME); + Assertions.assertThat(studio.getImageUrl()).isEqualTo(IMAGE_URL); + } + + @Test + @DisplayName("스튜디오 이름이 중복될 경우 예외가 발생한다.") + @Transactional + void failCreateStudioWithDuplicatedName() { + Studio studio = Studio.builder() + .name(NAME) + .imageUrl(IMAGE_URL) + .build(); + studioJpaRepository.save(studio); + Studio studio2 = Studio.builder() + .name(NAME) + .imageUrl(IMAGE_URL_2) + .build(); + Assertions.assertThatThrownBy(() -> studioJpaRepository.saveAndFlush(studio2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + +} diff --git a/src/test/java/com/pophory/pophoryserver/fixture/member/MemberFixture.java b/src/test/java/com/pophory/pophoryserver/fixture/member/MemberFixture.java new file mode 100644 index 0000000..a66bdf5 --- /dev/null +++ b/src/test/java/com/pophory/pophoryserver/fixture/member/MemberFixture.java @@ -0,0 +1,18 @@ +package com.pophory.pophoryserver.fixture.member; + +import com.pophory.pophoryserver.domain.auth.SocialType; +import com.pophory.pophoryserver.domain.member.Member; + +public class MemberFixture { + + private static final String SOCIAL_ID = "socialId"; + private static final String NICKNAME = "nickname"; + private static final SocialType SOCIAL_TYPE = SocialType.KAKAO; + + public static Member createMember() { + return Member.builder() + .socialId(SOCIAL_ID) + .socialType(SOCIAL_TYPE) + .build(); + } +}