diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..57997e9d --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,62 @@ +name: CD Workflow +on: + push: + branches: [ "main" ] + +jobs: + docker: + timeout-minutes: 10 + runs-on: ubuntu-latest + + steps: + # JDK setting - github actions에서 사용할 JDK 설정 + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + # 환경별 properties 파일 생성 - API-KEY + - name: make application-API-KEY.properties + run: | + cd ./src/main/resources + touch ./application-API-KEY.properties + echo "${{ secrets.YML }}" > ./application-API-KEY.properties + shell: bash + + # gradle 권한 설정 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # gradle build + - name: Build with Gradle + run: ./gradlew clean build -x test + + # Docker + - name: Login DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Docker Image Build & Push + run: | + docker build -t ${{ secrets.DOCKERHUB_REGISTRY }}/${{ secrets.DOCKERHUB_IMAGE_NAME }} -f Dockerfile . + docker push ${{ secrets.DOCKERHUB_REGISTRY }}/${{ secrets.DOCKERHUB_IMAGE_NAME }} + + - name: EC2 Login + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.HOST_NAME }} + username: ${{secrets.USER_NAME }} + key: ${{ secrets.SERVER_SSH_KEY }} + script: | + docker-compose down + docker image prune -f + docker rm $(docker ps -a -q) + docker pull ${{ secrets.DOCKERHUB_REGISTRY }}/${{ secrets.DOCKERHUB_IMAGE_NAME }} + docker-compose up -d + diff --git a/.gitignore b/.gitignore index c2065bc2..1b3d91c5 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ out/ ### VS Code ### .vscode/ +src/main/resources/application-API-KEY.properties diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..472d4a02 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:17-alpine + +WORKDIR /usr/src/app + +ARG JAR_PATH=./build/libs + +COPY ${JAR_PATH}/leaguehub-backend-0.0.1-SNAPSHOT.jar ${JAR_PATH}/leaguehub-backend-0.0.1-SNAPSHOT.jar + +CMD ["java","-jar","-Dspring.profiles.active=prod","./build/libs/leaguehub-backend-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..82ab64fb --- /dev/null +++ b/README.md @@ -0,0 +1,262 @@ +## 아마추어 대회 개최 플랫폼 + +## LeagueHub + +## 바로가기 및 시연 + +### [LeagueHub 사이트](https://leaguehub.co.kr/) + +### [LeagueHub 노션 바로가기](https://hyeonjun0530.notion.site/League-Hub-850d21e06cb844eea424eae8f7b3bc24?pvs=4) + +### Github + +### 🔗[Github(Frontend)](https://github.com/TheUpperPart/leaguehub-Frontend) + +### 🔗[Github(Backend)](https://github.com/TheUpperPart/leaguehub-backend) + +### 🔗[Github(Organization)](https://github.com/TheUpperPart) + +# ****배경(Problem)**** + +--- + +## LeagueHub + +> **개인이 E-스포츠 대회를 개최, 참가를 편리하게 할 수 있도록 하였어요.** +> + + + +E-스포츠 대회의 세계는 다양한 규모의 대회가 존재합니다. 작은 커뮤니티 대회에서부터 큰 기관이 주최하는 대규모 대회에 이르기까지, 많은 E-스포츠 대회가 있어요. + +하지만 각 대회에서는 대회의 조 배정, 경기 순위 기록 등 대회 관리는 엑셀을 이용해 수작업으로 진행되는 경우가 많아, 대회 주최자와 참가자 모두에게 불편함을 주었어요. + +기록 시에 실수의 가능성, 비직관적인 프로세스 및 UI 등은 E-스포츠 대회를 개최하거나 참가하기 부담스럽게 만들었어요. + +이러한 문제를 해결하기 위해, 우리는 LeagueHub를 개발하였어요. + +# **서비스 소개(Solution)** + +--- +![124124](https://github.com/hennible0612/leaguehub-backend/assets/48763809/aaed7471-874f-43bd-83ee-22553885e19d) + +### 기대효과 + +> 1️⃣ **사용자 증가 및 참여 활성화** +> + +- 아마추어 리그에 대한 적극적인 참여를 독려함으로써 플랫폼에 대한 사용자 수를 증가시킬 수 있어요. +- 다양한 게임 지원과 유연한 리그 설정을 통해 사용자들은 자신이 원하는 게임에서 참가할 수 있으며, 자신만의 리그를 만들 수 있어요. + +> 2️⃣ **커뮤니티 형성과 상호 작용 강화** +> + +- 아마추어 리그 플랫폼을 통해 게임 커뮤니티가 형성될 수 있어요 +- 사용자들은 자신의 게임 능력을 측정하고 비교하며, 다른 플레이어들과 소통하고 경쟁할 수 있는 경험을 즐길 수 있어요 +- 이는 사용자들 간의 상호 작용을 촉진하고, 게임에 대한 더욱 흥미로운 경험을 제공해요요 + +> 3️⃣ **사용자 중심의 서비스 제공** +> +> + +- 사용자들은 자신의 리그를 쉽게 생성하고 관리할 수 있는 사용자 중심의 서비스를 경험할 수 있어요 +- 사용자들이 플랫폼을 편리하게 이용할 수 있어요 +- 사용자 피드백을 반영한 지속적인 서비스 개선을 통해 사용자들의 만족도를 높일 수 있어요 + +# **핵심 기능** + +--- + +## `로그인` + +![1241254](https://github.com/hennible0612/leaguehub-backend/assets/48763809/386ecf46-1a09-485b-babd-3e09466ba2b7) + + +❶ 카카오를 이용한 로그인 + +❷ 추후 다른 소셜 로그인 도입 예정 + + + +### `메인페이지(관리자, 사용자)` + +![124124](https://github.com/hennible0612/leaguehub-backend/assets/48763809/85f5031d-af86-41c9-83e8-071d48921a50) + + +❶ 대회(채널) 참가 + +❷ 이미 들어간 채널을 선택하여 채널 둘러보기 + +❸ 해당 웹 서비스의 공지사항 확인 + +❹ 게임의 패치노트 확인 + +### `관리자 - 대회 관리 페이지` + +| ![123123](https://github.com/hennible0612/leaguehub-backend/assets/48763809/8cc8eb4b-b5ac-48aa-95de-2b4fae047b5a) | ![11110](https://github.com/hennible0612/leaguehub-backend/assets/48763809/338a557c-119c-4bea-b871-9d8e8763a600) | +|---|---| + + +❶ 채널의 홈 화면에서 대회의 정보를 수정 + +❷ 대회 관리에서 대회 설정(대회 시작, 경기 배정, 채널 정보 수정) 및 대회 알림 확인 + +### `리그허브 공지사항` +| ![Untitled](https://github.com/hennible0612/leaguehub-backend/assets/48763809/b1dc83bd-f16f-46e9-a375-723851c126ff) | ![124124](https://github.com/hennible0612/leaguehub-backend/assets/48763809/abd8156b-9143-48d0-a51e-f096bdd200a1) | +|---|---| + + + +❶ 게임의 패치노트 확인 + +### `채널 추가하기` + +![888](https://github.com/hennible0612/leaguehub-backend/assets/48763809/ff0b1f4a-e822-456c-b932-39df737e8140) + + +❶ 대회 개최를 이용하여 자신의 채널 생성 + +❷ 다른 채널의 참여 코드를 이용하여 채널 참여 + + +### `채널 생성하기` + +![777](https://github.com/hennible0612/leaguehub-backend/assets/48763809/3b74e827-9187-4180-9cc8-f3703579320b) + + +❶ 예시 폼을 작성하여 채널을 생성 + +❷ 커스텀 룰, 대회 이미지를 선택하여 설정 + +### `채널 참가하기` + +![666](https://github.com/hennible0612/leaguehub-backend/assets/48763809/9ffb2e96-b2fb-46aa-b714-e0fc49ade500) + + +❶ 참여 코드를 입력하여 채널 참여 + +### 경기 페이지(관리자, 사용자) + +### `관리자 - 경기 페이지` + +![555](https://github.com/hennible0612/leaguehub-backend/assets/48763809/947717d9-5582-48cf-b6fc-40e9372e89ba) + +❶ 실격 처리 버튼을 이용하여 해당 대회 참가자 실격 + +❷ 채팅을 통하여 참가들과 소통 + +❸ 체크 박스(체크인)을 통한 준비 확인 + + +### `사용자 - 경기 페이지` + +![444](https://github.com/hennible0612/leaguehub-backend/assets/48763809/5cfa95b9-4a48-4d42-a1c9-fe476798b8ce) + +❶ 준비 버튼을 통한 준비(체크 박스) 체크 + +❷ 초록색 배경을 통한 자신의 위치 확인 + +❸ 체크 박스(체크인)을 통한 준비 확인 + +❹ 채팅을 통한 관리자 및 참가자와의 소통, Call Admin을 통한 관리자 호출 + +### 채널 공지 페이지(관리자, 사용자) + +### `사용자 - 채널 공지 페이지` + +![333](https://github.com/hennible0612/leaguehub-backend/assets/48763809/e6d18b3e-0fb2-4bee-b1ba-e216de72daec) + +❶ 참여자 규칙 탭을 통하여 해당 대회의 규칙 확인 가능 + +### `관리자 - 채널 공지 페이지` + +![222](https://github.com/hennible0612/leaguehub-backend/assets/48763809/a6006cb4-8cd2-47d3-aa56-fa81eacf142f) + +❶ 공지 삭제, 수정을 통한 관리 + +### `대진표` + +![111](https://github.com/hennible0612/leaguehub-backend/assets/48763809/eb6f25f7-058f-44f6-81da-0f8b2aaedf48) + +❶ 대진표를 통하여 참여자 확인 + +❷ 대진표의 회색 바탕을 통하여 실격자 확인 + +❸ 자세히 버튼 또는 현재 라운드(라운드 2)를 통하여 해당 그룹의 대회 페이지로 이동 가능 + +❹ 빨간 점을 통하여 현재 Round 확인 + +### 시스템 아키텍처 + +![Architecure](https://github.com/hennible0612/leaguehub-backend/assets/48763809/d0621460-3578-4d4a-96c9-863738401c86) + +### Frontend + +> 1️⃣ Nextjs 13을 사용해서 SSR 환경을 구축했어요. +> + +- `page router`를 사용해서 SSR 환경을 구축했어요. +- meta 태그 작성, title 변경등으로 SEO 최적화를 했어요. + +> 2️⃣ 테스트 코드를 작성했어요. +> + +- `Jest`와 `React Testing Library`로 테스트 코드를 작성했어요. + +> 3️⃣ `Docker` 환경을 구축하여 `CI/CD`를 자동화하였어요. +> + +- CI/CD를 작성하여 배포 시간을 단축하였어요. + +> 4️⃣ Stomp 웹 소켓을 사용하여 실시간 서비스를 구현했어요. +> + + +### Backend + +> 1️⃣ Java 기반의 Spring Boot를 이용하여 서버를 구성했어요. +> + +- `SpringBoot 3.1.0`, `Gradle 7.6.1` +- `Jacoco 0.8.7`를 사용하여 테스트 커버리지 70%를 유지했어요. + +> 2️⃣ ORM 기술인 JPA과 MYSQL 8.0을 이용하여 데이터베이스를 구축했어요. +> + +- `Spring Data JPA`, `MYSQL 8.0` +- ERD 구성은 [ErdCloud](https://www.erdcloud.com/d/Ty8N6HdCtzwRYPE4r)를 참고하시길 바랄께요 ! + +> 3️⃣ Spring Security를 이용한 OAuth 2.0 기반의 다양한 소셜 로그인을 구현했어요. +> + +- `Jtw 4.4.0` , `Spring Security 3.1.0` +- Jwt를 활용해 카카오 로그인을 구현했어요. +- Jwt 기반의 `stateless`한 로그인을 구현했어요. + +> 4️⃣ Stomp 웹 소켓을 사용하여 실시간 서비스를 구현했어요. +> + +- `Stomp 3.1.0`, `Redis 6.0.16` +- `Riot API`를 사용하여 경기 결과를 확인 후 실시간 점수 업데이트를 했어요. +- 실시간 채팅 서비스를 구현하기 위해 `Redis`를 이용했어요. +- 유저의 체크인, 알림을 구현했어요. + +> 5️⃣ AWS LightSail, AWS S3를 이용해 배포하였어요. +> + +# **팀 소개** + +--- + +### CONTACT + +| 김현준 | 박정석 | 이상엽 | 이경훈 | 홍성우 | +|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| [![image](https://avatars.githubusercontent.com/u/102659136?v=4)](https://github.com/HyeonJun0530) | [![image](https://avatars.githubusercontent.com/u/100738049?v=4)](https://github.com/navyjeongs) | [![image](https://avatars.githubusercontent.com/u/71641127?v=4)](https://github.com/pp449) | [![image](https://avatars.githubusercontent.com/u/87762815?v=4)](https://github.com/TinyFrogs) | [![image](https://avatars.githubusercontent.com/u/48763809?v=4)](https://github.com/hennible0612) | +| [🔗Github](https://github.com/HyeonJun0530) | [🔗github](https://github.com/navyjeongs) | [🔗github](https://github.com/pp449) | [🔗github](https://github.com/TinyFrogs) | [🔗github](https://github.com/hennible0612) | +| BE | FE | FE | BE | BE | +| nexus2697@pukyong.ac.kr | wjdtjr8649@naver.com | mma7710@naver.com | qns0147@gmail.com | sungwoo166@gmail.com + diff --git a/build.gradle b/build.gradle index ceb80db6..759b9eb4 100644 --- a/build.gradle +++ b/build.gradle @@ -24,14 +24,28 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'com.googlecode.json-simple:json-simple:1.1.1' implementation group: 'org.modelmapper', name: 'modelmapper', version: '2.3.8' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' compileOnly 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-webflux' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'com.auth0:java-jwt:4.4.0' + implementation 'com.squareup.okhttp3:okhttp:4.10.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.10.0' + implementation 'org.springframework.boot:spring-boot-starter-mail' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.jsoup:jsoup:1.16.2' + + } tasks.named('test') { @@ -92,7 +106,27 @@ jacocoTestCoverageVerification { test { finalizedBy jacocoTestReport + useJUnitPlatform() + jacoco { + excludes += ["leaguehub/leaguehubbackend/config/**", + "leaguehub/leaguehubbackend/exception/**", + "leaguehub/leaguehubbackend/repository/**", + "leaguehub/leaguehubbackend/service/kakao/**", + "leaguehub/leaguehubbackend/service/s3/**" + ] + } } jacocoTestReport { dependsOn test + afterEvaluate { + + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, + exclude: ["leaguehub/leaguehubbackend/config/**", + "leaguehub/leaguehubbackend/exception/**", + "leaguehub/leaguehubbackend/repository/**", + "leaguehub/leaguehubbackend/service/kakao/**", + "leaguehub/leaguehubbackend/service/s3/**"]) + })) + } } \ No newline at end of file diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..8f7e8aa1 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/LeaguehubBackendApplication.java b/src/main/java/leaguehub/leaguehubbackend/LeaguehubBackendApplication.java index 8e138bad..a8ec7e89 100644 --- a/src/main/java/leaguehub/leaguehubbackend/LeaguehubBackendApplication.java +++ b/src/main/java/leaguehub/leaguehubbackend/LeaguehubBackendApplication.java @@ -5,9 +5,7 @@ @SpringBootApplication public class LeaguehubBackendApplication { - public static void main(String[] args) { SpringApplication.run(LeaguehubBackendApplication.class, args); } - } diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelBoardController.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelBoardController.java new file mode 100644 index 00000000..6a96fa3d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelBoardController.java @@ -0,0 +1,128 @@ +package leaguehub.leaguehubbackend.domain.channel.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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 jakarta.validation.Valid; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardIndexListDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardInfoDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardLoadDto; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelBoardService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.OK; + +@Slf4j +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ChannelBoardController { + + private final ChannelBoardService channelBoardService; + + @Operation(summary = "채널 보드 만들기") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelBoardLoadDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음, 관리자 권한 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel/{channelLink}/new") + public ResponseEntity createChannelBoard(@PathVariable("channelLink") String channelLink, + @RequestBody @Valid ChannelBoardDto request, + BindingResult bindingResult) { + ChannelBoardLoadDto channelBoardLoadDto = channelBoardService.createChannelBoard(channelLink, request); + + return new ResponseEntity(channelBoardLoadDto, OK); + } + + @Operation(summary = "채널 보드 업데이트") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "boardId", description = "게시판 고유 Id", example = "0, 1, 2") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음, 관리자 권한 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel/{channelLink}/{boardId}") + public ResponseEntity updateChannelBoard(@PathVariable("channelLink") String channelLink, + @PathVariable("boardId") Long boardId, + @RequestBody ChannelBoardDto channelBoardDto) { + channelBoardService.updateChannelBoard(channelLink, boardId, channelBoardDto); + + return new ResponseEntity("Board successfully updated", OK); + } + + @Operation(summary = "채널 보드 삭제") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "boardId", description = "게시판 고유 Id", example = "0, 1, 2") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음, 관리자 권한 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @DeleteMapping("/channel/{channelLink}/{boardId}") + public ResponseEntity deleteChannelBoard(@PathVariable("channelLink") String channelLink, + @PathVariable("boardId") Long boardId) { + + channelBoardService.deleteChannelBoard(channelLink, boardId); + + return new ResponseEntity("Board successfully deleted", OK); + } + + @Operation(summary = "채널 보드 가져오기 - 단일 채널 보드 읽기") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "boardId", description = "게시판 고유 Id", example = "0, 1, 2") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelBoardDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}/{boardId}") + public ResponseEntity getChannelBoard(@PathVariable("channelLink") String channelLink, + @PathVariable("boardId") Long boardId) { + ChannelBoardDto channelBoardDto = channelBoardService.getChannelBoard(channelLink, boardId); + + return new ResponseEntity(channelBoardDto, OK); + } + + @Operation(summary = "채널 보드 인덱스 업데이트 - 채널 보드 인덱스 커스텀") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음, 권한x", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel/{channelLink}/order") + public ResponseEntity updateChannelBoardIndex(@PathVariable("channelLink") String channelLink, + @RequestBody @Valid ChannelBoardIndexListDto channelBoardIndexListDto) { + channelBoardService.updateChannelBoardIndex(channelLink, channelBoardIndexListDto.getChannelBoardLoadDtoList()); + + return new ResponseEntity("BoardIndex successfully updated", OK); + } + + @Operation(summary = "채널 보드 가져오기 - 채널 로드시 현재 라운드와 제목과 보드ID 가져오기 리스트로 반환") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelBoardInfoDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}/boards") + public ResponseEntity loadChannelBoards(@PathVariable("channelLink") String channelLink) { + + ChannelBoardInfoDto channelBoardLoadDtoList = channelBoardService.loadChannelBoards(channelLink); + + return new ResponseEntity(channelBoardLoadDtoList, OK); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelController.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelController.java new file mode 100644 index 00000000..f595136c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelController.java @@ -0,0 +1,141 @@ +package leaguehub.leaguehubbackend.domain.channel.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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 jakarta.validation.Valid; +import leaguehub.leaguehubbackend.domain.channel.dto.*; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelStatusAlreadyException; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelDeleteService; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotGameHostException; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantQueryService; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static org.springframework.http.HttpStatus.OK; + +@Slf4j +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ChannelController { + + private final ChannelService channelService; + private final ParticipantService participantService; + private final ChannelDeleteService channelDeleteService; + private final ParticipantQueryService participantQueryService; + + @Operation(summary = "채널 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ParticipantChannelDto.class))), + @ApiResponse(responseCode = "400", description = "Dto 유효성 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel") + public ResponseEntity createChannel(@Valid @RequestBody CreateChannelDto createChannelDto) { + + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + + return new ResponseEntity<>(participantChannelDto, OK); + } + + @Operation(summary = "채널 가져오기 - 단일 채널(화면 구성)") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseChannelDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}") + public ResponseEntity getChannel(@PathVariable("channelLink") String channelLink) { + + ChannelDto channelInfo = channelService.findChannel(channelLink); + + ResponseChannelDto responseChannelDto = ResponseChannelDto.builder() + .gameCategory(channelInfo.getGameCategory().getNum()) + .hostName(participantQueryService.findChannelHost(channelLink)) + .participateNum(channelInfo.getRealPlayer()) + .maxPlayer(channelInfo.getMaxPlayer()) + .leagueTitle(channelInfo.getTitle()) + .permission(participantQueryService.findParticipantPermission(channelLink)) + .build(); + + return new ResponseEntity(responseChannelDto, OK); + } + + @Operation(summary = "채널 가져오기 - 여러 채널(로그인시 사이드바 화면 구성)") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Dto를 리스트로 반환", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ParticipantChannelDto.class))), + }) + @GetMapping("/channels") + public ResponseEntity loadChannels() { + + List participantChannelList = channelService.findParticipantChannelList(); + + return new ResponseEntity(participantChannelList, OK); + } + + + @Operation(summary = "채널 업데이트 - 채널이름, 최대 참가자 수, 채널 이미지)") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel/{channelLink}") + public ResponseEntity updateChannel(@PathVariable("channelLink") String channelLink, + @RequestBody UpdateChannelDto updateChannelDto) { + + channelService.updateChannel(channelLink, updateChannelDto); + + return new ResponseEntity("Channel successfully updated", OK); + } + + @Operation(summary = "채널 상태 업데이트 - 준비중(PREPARING, 0), 진행중(PROCEEDING, 1), 끝남(FINISH, 2)") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "status", description = "채널 진행 상태 변경 쿼리 파라미터", example = "준비중(0), 진행중(1), 끝남(2)") + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PutMapping("/channel/{channelLink}") + public ResponseEntity updateChannelStatus(@PathVariable("channelLink") String channelLink, + @RequestParam("status") Integer status) { + channelService.updateChannelStatus(channelLink, status); + return new ResponseEntity("Channel Status Successfully updated", OK); + } + + @Operation(summary = "채널 삭제 - 준비 중 상태의 채널만 삭제 가능") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + } + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "404", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelNotFoundException.class))), + @ApiResponse(responseCode = "400", description = "채널 경기가 준비중이 아님", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelStatusAlreadyException.class))), + @ApiResponse(responseCode = "401", description = "해당 채널의 호스트가 아님", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ParticipantNotGameHostException.class))), + @ApiResponse(responseCode = "401", description = "해당 채널의 참가자가 아님", content = @Content(mediaType = "application/json", schema = @Schema(implementation = InvalidParticipantAuthException.class))) + }) + @DeleteMapping("/channel/{channelLink}") + public ResponseEntity deleteChannel(@PathVariable("channelLink") String channelLink) { + + channelDeleteService.deleteChannel(channelLink); + + return new ResponseEntity(OK); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelInfoController.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelInfoController.java new file mode 100644 index 00000000..74bf91cc --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelInfoController.java @@ -0,0 +1,55 @@ +package leaguehub.leaguehubbackend.domain.channel.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 jakarta.validation.Valid; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelInfoDto; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelInfoService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.OK; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ChannelInfoController { + + private final ChannelInfoService channelInfoService; + + @Operation(summary = "채널 상품, 참가조건, 경기시간 정보 수정하기") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel/{channelLink}/main") + public ResponseEntity updateChannelInfo(@PathVariable("channelLink") String channelLink, + @RequestBody @Valid ChannelInfoDto channelInfoDto) { + + channelInfoService.updateChannelInfo(channelLink, channelInfoDto); + + return new ResponseEntity("수정이 완료되었습니다.",OK); + } + + @Operation(summary = "채널 상품, 참가조건, 경기시간 정보 가져오기") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelInfoDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}/main") + public ResponseEntity getChannelInfo(@PathVariable("channelLink") String channelLink) { + + ChannelInfoDto channelInfoDto = channelInfoService.getChannelInfoDto(channelLink); + + return new ResponseEntity(channelInfoDto, OK); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelRuleController.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelRuleController.java new file mode 100644 index 00000000..025e3f22 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/controller/ChannelRuleController.java @@ -0,0 +1,53 @@ +package leaguehub.leaguehubbackend.domain.channel.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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 leaguehub.leaguehubbackend.domain.channel.dto.ChannelRuleDto; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelRuleService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.OK; + +@Slf4j +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ChannelRuleController { + + private final ChannelRuleService channelRuleService; + + @Operation(summary = "채널 룰 가져오기)") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelRuleDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}/rule") + public ResponseEntity getChannelRule(@PathVariable("channelLink") String channelLink) { + ChannelRuleDto channelRule = channelRuleService.getChannelRule(channelLink); + + return new ResponseEntity<>(channelRule, OK); + } + + @Operation(summary = "채널 룰 업데이트 - 티어, 판수)") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChannelRuleDto.class))), + @ApiResponse(responseCode = "400", description = "채널 링크가 올바르지 않음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/channel/{channelLink}/rule") + public ResponseEntity updateChannelRule(@PathVariable("channelLink") String channelLink, + @RequestBody ChannelRuleDto channelRuleDto) { + ChannelRuleDto channelRule = channelRuleService.updateChannelRule(channelLink, channelRuleDto); + + return new ResponseEntity(channelRule, OK); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardDto.java new file mode 100644 index 00000000..199e98e5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardDto.java @@ -0,0 +1,24 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ChannelBoardDto { + + @NotBlank + @Schema(description = "해당 게시판의 제목", example = "제목입니다.") + private String title; + + @NotBlank + @Schema(description = "해당 게시판의 내용", example = "내용입니다.") + private String content; + + public ChannelBoardDto(String title, String content) { + this.title = title; + this.content = content; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardIndexListDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardIndexListDto.java new file mode 100644 index 00000000..62c2d25a --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardIndexListDto.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@NoArgsConstructor +@Data +public class ChannelBoardIndexListDto { + + private List channelBoardLoadDtoList; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardInfoDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardInfoDto.java new file mode 100644 index 00000000..e1eb40fb --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardInfoDto.java @@ -0,0 +1,27 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class ChannelBoardInfoDto { + + @Schema(description = "진행중인 매치 라운드", example = "1, 2, 3 없으면 0") + Integer myMatchRound; + + @Schema(description = "진행중인 매치 PK", example = "1, 2, 3 없으면 0") + Long myMatchId; + + @Schema(description = "게시판 정보들") + List channelBoardLoadDtoList; + + public ChannelBoardInfoDto(Integer myMatchRound, Long myMatchId, List channelBoardLoadDtoList) { + this.myMatchRound = myMatchRound; + this.myMatchId = myMatchId; + this.channelBoardLoadDtoList = channelBoardLoadDtoList; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardLoadDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardLoadDto.java new file mode 100644 index 00000000..142783f4 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelBoardLoadDto.java @@ -0,0 +1,33 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ChannelBoardLoadDto { + + @NotBlank + @JsonProperty("boardId") + @Schema(description = "게시판 고유 Id", example = "0, 1, 2") + private Long boardId; + + @NotBlank + @JsonProperty("boardTitle") + @Schema(description = "게시판 제목", example = "제목입니다.") + private String boardTitle; + + @NotBlank + @JsonProperty("boardIndex") + @Schema(description = "게시판 위치", example = "0, 1, 2") + private int boardIndex; + + public ChannelBoardLoadDto(Long channelBoardId, String title, int index) { + this.boardId = channelBoardId; + this.boardTitle = title; + this.boardIndex = index; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelDto.java new file mode 100644 index 00000000..374303d6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelDto.java @@ -0,0 +1,29 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import leaguehub.leaguehubbackend.domain.channel.entity.GameCategory; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ChannelDto { + + private String title; + + @JsonFormat(shape = JsonFormat.Shape.STRING) + private GameCategory gameCategory; + + private Integer realPlayer; + + private Integer maxPlayer; + + @Builder + public ChannelDto(String title, GameCategory gameCategory, Integer realPlayer, Integer maxPlayer) { + this.title = title; + this.gameCategory = gameCategory; + this.realPlayer = realPlayer; + this.maxPlayer = maxPlayer; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelInfoDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelInfoDto.java new file mode 100644 index 00000000..ce6dbeee --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelInfoDto.java @@ -0,0 +1,40 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ChannelInfoDto { + + @NotBlank + @Schema(description = "해당 채널의 제목", example = "부경대 대회") + private String channelTitleInfo; + + @NotBlank + @Schema(description = "해당 채널의 소제목", example = "안녕하세요 부경대 대회입니다.") + private String channelContentInfo; + + @NotBlank + @Schema(description = "해당 채널의 참가조건", example = "브론즈 이상 마스터 이하") + private String channelRuleInfo; + + @NotBlank + @Schema(description = "해당 채널의 대회 시간", example = "해당 대회는 2023-11-18 오후 9시부터 시작입니다.") + private String channelTimeInfo; + + @NotBlank + @Schema(description = "해당 채널의 상품 ", example = "1등 1,000원 2등 100원 3등 10원 4등 1원 5등 꽝") + private String channelPrizeInfo; + + public ChannelInfoDto(String channelTitleInfo, String channelContentInfo, String channelRuleInfo, String channelTimeInfo, String channelPrizeInfo) { + this.channelTitleInfo = channelTitleInfo; + this.channelContentInfo = channelContentInfo; + this.channelRuleInfo = channelRuleInfo; + this.channelTimeInfo = channelTimeInfo; + this.channelPrizeInfo = channelPrizeInfo; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelRuleDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelRuleDto.java new file mode 100644 index 00000000..4ad5b79a --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ChannelRuleDto.java @@ -0,0 +1,46 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ChannelRuleDto { + + @NotNull + @JsonProperty("tier") + @Schema(description = "티어 제한의 유무", example = "true, false") + private Boolean tier; + + @JsonProperty("tierMax") + @Schema(description = "최대 티어", example = "platinum III일 경우 12000") + private Integer tierMax; + + @JsonProperty("tierMin") + @Schema(description = "최소 티어", example = "bronze II 일경우 600") + private Integer tierMin; + + @NotNull + @JsonProperty("playCount") + @Schema(description = "최소 경기 제한의 유무", example = "true, false") + private Boolean playCount; + + @Min(0) + @JsonProperty("playCountMin") + @Schema(description = "최소 경기 수", example = "30, 40, 50") + private Integer playCountMin; + + @Builder + public ChannelRuleDto(Boolean tier, Integer tierMax, Integer tierMin, Boolean playCount, Integer playCountMin) { + this.tier = tier; + this.tierMax = tierMax; + this.tierMin = tierMin; + this.playCount = playCount; + this.playCountMin = playCountMin; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/CreateChannelDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/CreateChannelDto.java new file mode 100644 index 00000000..a554a2de --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/CreateChannelDto.java @@ -0,0 +1,81 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Data; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Data +@NoArgsConstructor +public class CreateChannelDto { + + @NotNull + @JsonProperty("gameCategory") + @Schema(description = "게임 종목(TFT, LOL, FIFA)의 숫자", example = "0, 1, 2") + private int gameCategory; + + @NotNull + @JsonProperty("matchFormat") + @Schema(description = "토너먼트 종류의 숫자", example = "싱글 엘리미네이션(0), 프리 포 올(1)") + private Integer matchFormat; + + @NotNull + @JsonProperty("title") + @Schema(description = "채널의 제목", example = "채널의 제목입니다.") + private String title; + + @NotNull + @Min(8) + @JsonProperty("maxPlayer") + @Schema(description = "매치 최대 참가자 수", example = "8, 16, 32, 64") + private Integer maxPlayer; + + @NotNull + @JsonProperty("tier") + @Schema(description = "티어 제한의 유무", example = "true, false") + private Boolean tier; + + @JsonProperty("tierMax") + @Schema(description = "최대 티어", example = "1200") + private Integer tierMax; + + @JsonProperty("tierMin") + @Schema(description = "최소 티어", example = "1600") + private Integer tierMin; + + @JsonProperty("channelImageUrl") + @Schema(description = "채널의 이미지 주소", example = "https://s3.[aws-region].amazonaws.com/[bucket name]") + private String channelImageUrl; + + @NotNull + @JsonProperty("playCount") + @Schema(description = "최소 경기 제한의 유무", example = "true, false") + private Boolean playCount; + + @Min(0) + @JsonProperty("playCountMin") + @Schema(description = "최소 경기 수", example = "30, 40, 50") + private Integer playCountMin; + + @Builder + public CreateChannelDto(@NotNull int gameCategory, @NotNull Integer matchFormat, @NotNull String title, + @NotNull Integer maxPlayer, @NotNull Boolean tier, + Integer tierMax, Integer tierMin, String channelImageUrl, + @NotNull Boolean playCount, Integer playCountMin) { + this.gameCategory = gameCategory; + this.matchFormat = matchFormat; + this.title = title; + this.maxPlayer = maxPlayer; + this.tier = tier; + this.tierMax = tierMax; + this.tierMin = tierMin; + this.channelImageUrl = channelImageUrl; + this.playCount = playCount; + this.playCountMin = playCountMin; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ParticipantChannelDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ParticipantChannelDto.java new file mode 100644 index 00000000..f1c9bf5d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ParticipantChannelDto.java @@ -0,0 +1,37 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ParticipantChannelDto { + + @Schema(description = "채널 Id", example = "1") + private Long channelId; + + @Schema(description = "조회하는 매치 링크", example = "42aa1b11ab88") + private String channelLink; + + @Schema(description = "채널의 제목", example = "42aa1b11ab88") + private String title; + + @Schema(description = "게임 종목(TFT, LOL, FIFA)의 숫자", example = "0, 1, 2") + private Integer gameCategory; + + @Schema(description = "채널의 이미지 주소", example = "https://s3.[aws-region].amazonaws.com/[bucket name]") + private String imgSrc; + + @Schema(description = "사이드 바의 채널의 순서 인덱스", example = "0, 1, 2") + private Integer customChannelIndex; + + public ParticipantChannelDto(Long channelId, String channelLink, String title, Integer gameCategory, String imgSrc, Integer customChannelIndex) { + this.channelId = channelId; + this.channelLink = channelLink; + this.title = title; + this.gameCategory = gameCategory; + this.imgSrc = imgSrc; + this.customChannelIndex = customChannelIndex; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ResponseChannelDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ResponseChannelDto.java new file mode 100644 index 00000000..48b4a3e9 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/ResponseChannelDto.java @@ -0,0 +1,35 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ResponseChannelDto { + + private String hostName; + + private String leagueTitle; + + private Integer gameCategory; + + @JsonProperty("currentPlayer") + private Integer participateNum; + + @JsonProperty("maxPlayer") + private Integer maxPlayer; + + private Integer permission; + + @Builder + public ResponseChannelDto(String hostName, String leagueTitle, Integer gameCategory, Integer participateNum, Integer maxPlayer, Integer permission) { + this.hostName = hostName; + this.leagueTitle = leagueTitle; + this.gameCategory = gameCategory; + this.participateNum = participateNum; + this.permission = permission; + this.maxPlayer = maxPlayer; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/UpdateChannelBoardDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/UpdateChannelBoardDto.java new file mode 100644 index 00000000..8cb43e67 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/UpdateChannelBoardDto.java @@ -0,0 +1,16 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UpdateChannelBoardDto { + private Long channelId; + + private Long channelBoardId; + + private String title; + + private String content; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/UpdateChannelDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/UpdateChannelDto.java new file mode 100644 index 00000000..08b4bd8f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/dto/UpdateChannelDto.java @@ -0,0 +1,24 @@ +package leaguehub.leaguehubbackend.domain.channel.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UpdateChannelDto { + + @JsonProperty("title") + @Schema(description = "채널의 제목", example = "채널의 제목입니다.") + private String title; + + @JsonProperty("maxPlayer") + @Schema(description = "매치 최대 참가자 수", example = "8, 16, 32, 64") + private Integer maxPlayer; + + @JsonProperty("channelImageUrl") + @Schema(description = "채널의 이미지 주소", example = "https://s3.[aws-region].amazonaws.com/[bucket name]") + private String channelImageUrl; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/Channel.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/Channel.java new file mode 100644 index 00000000..84daae61 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/Channel.java @@ -0,0 +1,112 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.beans.factory.annotation.Value; + +import static java.util.UUID.randomUUID; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Channel extends BaseTimeEntity { + + @Value("${cloud.aws.s3.bucket.url}") + @Transient + private String defaultUrl; + + @Id + @Column(name = "channel_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, length = 100) + private String title; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private GameCategory gameCategory; + + @Column(nullable = false) + private Integer maxPlayer; + + private Integer realPlayer; + + @Column(unique = true) + private String channelLink; + + private int liveRound; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private MatchFormat matchFormat; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ChannelStatus channelStatus; + + private String channelImageUrl; + + //-- 비즈니스 로직 --// + public static Channel createChannel(String title, int game, int maxPlayer, + int matchFormat, String channelImageUrl) { + Channel channel = new Channel(); + String uuid = randomUUID().toString(); + channel.title = title; + channel.gameCategory = GameCategory.getByNumber(game); + channel.maxPlayer = maxPlayer; + channel.realPlayer = 0; + channel.channelStatus = ChannelStatus.PREPARING; + channel.matchFormat = MatchFormat.getByNumber(matchFormat); + channel.channelLink = channel.createParticipationLink(uuid); + channel.channelImageUrl = channel.validateChannelImageUrl(channelImageUrl); + channel.liveRound = 0; + + return channel; + } + + public String createParticipationLink(String uuid) { + String channelLink = uuid.substring(24, uuid.length()); + + return channelLink; + } + + //채널 이미지 Url에 대한 정보가 없으면 기본 채널 이미지를 반환한다. + private String validateChannelImageUrl(String channelImageUrl) { + if (channelImageUrl == null) { + channelImageUrl = null; //Default 값 + } + + return channelImageUrl; + } + + //실제 참가자 수를 업데이트 한다. + public Channel updateRealPlayer(Integer realPlayer) { + this.realPlayer = realPlayer; + + return this; + } + + public void updateTitle(String title) { + this.title = title; + } + + public void updateMaxPlayer(Integer maxPlayer) { + this.maxPlayer = maxPlayer; + } + + public void updateChannelImageUrl(String channelImageUrl) { + this.channelImageUrl = channelImageUrl; + } + + public void updateChannelStatus(ChannelStatus channelStatus) { + this.channelStatus = channelStatus; + } + + public void updateChannelLiveRound(Integer liveRound){ + this.liveRound = liveRound; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelBoard.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelBoard.java new file mode 100644 index 00000000..1dfc59e1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelBoard.java @@ -0,0 +1,88 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@NoArgsConstructor +@Entity +public class ChannelBoard extends BaseTimeEntity { + + @Id + @Column(name = "channel_board_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + private String content; + + @Column(name = "channel_board_index", nullable = false) + private int index; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "channel_id") + private Channel channel; + + public static List createDefaultBoard(Channel channel) { + List channelBoardList = new ArrayList<>(); + + ChannelBoard announcementBoard = new ChannelBoard(); + announcementBoard.title = "리그 공지사항"; + announcementBoard.content = "공지사항을 작성해주세요."; + announcementBoard.channel = channel; + announcementBoard.index = 1; + + ChannelBoard ruleBoard = new ChannelBoard(); + ruleBoard.title = "참여자 규칙"; + ruleBoard.content = "참여자 규칙을 작성해주세요."; + ruleBoard.channel = channel; + ruleBoard.index = 2; + + ChannelBoard participateBoard = new ChannelBoard(); + participateBoard.title = "참여하기"; + participateBoard.content = "글을 작성해주세요."; + participateBoard.channel = channel; + participateBoard.index = 3; + + channelBoardList.add(announcementBoard); + channelBoardList.add(ruleBoard); + channelBoardList.add(participateBoard); + + return channelBoardList; + } + + + public static ChannelBoard createChannelBoard(Channel channel, + String title, String content, int index) { + ChannelBoard channelBoard = new ChannelBoard(); + channelBoard.channel = channel; + channelBoard.title = title; + channelBoard.content = content; + channelBoard.index = index; + + return channelBoard; + } + + public ChannelBoard updateChannelBoard(String title, String content) { + this.title = title; + this.content = content; + + return this; + } + + public void updateIndex(int updateIndex) { + this.index = updateIndex; + } + + public void deleteChannel() { + this.channel = null; + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelInfo.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelInfo.java new file mode 100644 index 00000000..77146d22 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelInfo.java @@ -0,0 +1,60 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Getter +public class ChannelInfo extends BaseTimeEntity { + + @Id + @Column(name = "channel_info_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String channelContentInfo; + + @Column(nullable = false) + private String channelRuleInfo; + + @Column(nullable = false) + private String channelTimeInfo; + + @Column(nullable = false) + private String channelPrizeInfo; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "channel_id") + private Channel channel; + + public static ChannelInfo createChannelInfo(Channel channel){ + ChannelInfo channelInfo = new ChannelInfo(); + channelInfo.channelContentInfo = "대회의 소제목을 입력해주세요"; + channelInfo.channelTimeInfo = "대회 진행 시간을 입력해주세요"; + channelInfo.channelRuleInfo = "대회 참가 조건을 입력해주세요"; + channelInfo.channelPrizeInfo ="대회 상품 & 상금을 입력해주세요"; + channelInfo.channel = channel; + + return channelInfo; + } + + + public ChannelInfo updateChannelBoard(String channelContentInfo, String channelTimeInfo, String channelRuleInfo, String channelPrizeInfo){ + this.channelContentInfo = channelContentInfo; + this.channelPrizeInfo = channelPrizeInfo; + this.channelTimeInfo = channelTimeInfo; + this.channelRuleInfo = channelRuleInfo; + + return this; + } + + public void deleteChannel() { + this.channel = null; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelRule.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelRule.java new file mode 100644 index 00000000..67217bc1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelRule.java @@ -0,0 +1,98 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Optional; + +@Getter +@NoArgsConstructor +@Entity +public class ChannelRule extends BaseTimeEntity { + + @Id + @Column(name = "channel_rule_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer limitedPlayCount; + + private Integer tierMax; + + private Integer tierMin; + + private Boolean tier; + + private Boolean playCount; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "channel_id") + private Channel channel; + + public static ChannelRule createChannelRule(Channel channel,Boolean tier, Integer tierMax, Integer tierMin, + Boolean playCount, Integer playCountMin) { + ChannelRule channelRule = new ChannelRule(); + + channelRule.channel = channel; + channelRule.playCount = playCount; + channelRule.tier = tier; + + if (tier) { + channelRule.validateTier(tierMax, tierMin); + channelRule.tierMax = Optional.ofNullable(tierMax).orElse(Integer.MIN_VALUE); + channelRule.tierMin = Optional.ofNullable(tierMin).orElse(Integer.MIN_VALUE); + } else { + channelRule.tierMax = Integer.MIN_VALUE; + channelRule.tierMin = Integer.MIN_VALUE; + } + + if (playCount) { + channelRule.validatePlayCount(playCountMin); + channelRule.limitedPlayCount = playCountMin; + } else { + channelRule.limitedPlayCount = Integer.MAX_VALUE; + } + + return channelRule; + } + + public void updateTierRule(boolean tier, Integer tierMax, Integer tierMin) { + validateTier(tierMax, tierMin); + this.tier = tier; + this.tierMax = Optional.ofNullable(tierMax).orElse(Integer.MIN_VALUE); + this.tierMin = Optional.ofNullable(tierMin).orElse(Integer.MIN_VALUE); + } + + public void updatePlayCountMin(boolean playCount, Integer playCountMin) { + validatePlayCount(playCountMin); + this.playCount = playCount; + this.limitedPlayCount = playCountMin; + } + + public void updateTierRule(boolean tier) { + this.tier = tier; + } + + public void updatePlayCountMin(boolean playCount) { + this.playCount = playCount; + } + + private void validateTier(Integer tierMax, Integer tierMin) { + if (tierMax == null && tierMin == null) { + throw new ChannelRequestException(); + } + } + + private void validatePlayCount(Integer playCountMin) { + if (playCountMin == null) { + throw new ChannelRequestException(); + } + } + + public void deleteChannel() { + this.channel = null; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelStatus.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelStatus.java new file mode 100644 index 00000000..56232a26 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/ChannelStatus.java @@ -0,0 +1,23 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + + +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; + +import java.util.Arrays; + +public enum ChannelStatus { + PREPARING(0), PROCEEDING(1), FINISH(2); + + private final Integer status; + + ChannelStatus(Integer status) { + this.status = status; + } + + public static ChannelStatus convertStatus(Integer status) { + return Arrays.stream(ChannelStatus.values()) + .filter(channelStatus -> (channelStatus.status == status)) + .findFirst() + .orElseThrow(ChannelRequestException::new); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/GameCategory.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/GameCategory.java new file mode 100644 index 00000000..b9ebc571 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/GameCategory.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum GameCategory { + TFT(0); + + private final int num; + + GameCategory(int num) { + this.num = num; + } + + + public static GameCategory getByNumber(int game) { + return Arrays.stream(GameCategory.values()) + .filter(gameCategory -> gameCategory.num == game) + .findFirst() + .orElseThrow(ChannelRequestException::new); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/MatchFormat.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/MatchFormat.java new file mode 100644 index 00000000..6fbf0d21 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/entity/MatchFormat.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.domain.channel.entity; + +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum MatchFormat { + SINGLE_ELIMINATION(1), FREE_FOR_ALL(0); + + private final int num; + + MatchFormat(int num) { + this.num = num; + } + + + public static MatchFormat getByNumber(int tournament) { + return Arrays.stream(MatchFormat.values()) + .filter(matchFormat -> matchFormat.num == tournament) + .findFirst() + .orElseThrow(ChannelRequestException::new); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/ChannelExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/ChannelExceptionCode.java new file mode 100644 index 00000000..ed601dbb --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/ChannelExceptionCode.java @@ -0,0 +1,29 @@ +package leaguehub.leaguehubbackend.domain.channel.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@Getter +@RequiredArgsConstructor +public enum ChannelExceptionCode implements ExceptionCode { + + INVALID_PARTICIPATED_REQUEST(BAD_REQUEST, "CH-C-001", "유효하지 않은 대회 참가 요청입니다."), + INELIGIBLE_PARTICIPANT_REQUEST(BAD_REQUEST, "CH-C-002", "정해진 대회 룰에 적합하지 않는 대회 참가 요청입니다."), + INVALID_JOIN_REQUEST(BAD_REQUEST, "CH-C-003", "유효하지 않은 참가 링크입니다."), + INVALID_CHANNEL_IMAGE(BAD_REQUEST, "CH-C-004", "유효하지 않은 이미지입니다."), + INVALID_ACCESS_CODE(BAD_REQUEST, "CH-C-005", "유효하지 않은 대회 참가 코드입니다."), + INVALID_REQUEST_CHANNEL(BAD_REQUEST, "CH-C-006", " 유효하지 않은 요청 값입니다."), + CHANNEL_NOT_FOUND(NOT_FOUND, "CH-C-007", "채널을 찾을 수 없습니다."), + CHANNEL_BOARD_NOT_FOUND(NOT_FOUND, "CH-C-008", "채널 게시판을 찾을 수 없습니다."), + CHANNEL_STATUS_ALREADY_PROCEEDING(BAD_REQUEST, "CH-C-009", "해당 채널은 이미 경기 진행중입니다."); + + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/ChannelExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/ChannelExceptionHandler.java new file mode 100644 index 00000000..a86e08b9 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/ChannelExceptionHandler.java @@ -0,0 +1,72 @@ +package leaguehub.leaguehubbackend.domain.channel.exception; + +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelBoardNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelStatusAlreadyException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class ChannelExceptionHandler { + + @ExceptionHandler(ChannelRequestException.class) + public ResponseEntity channelCreateException( + ChannelRequestException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ChannelBoardNotFoundException.class) + public ResponseEntity channelBoardNotfoundException( + ChannelBoardNotFoundException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ChannelNotFoundException.class) + public ResponseEntity channelNotFoundException( + ChannelNotFoundException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ChannelStatusAlreadyException.class) + public ResponseEntity channelStatusAlreadyException( + ChannelStatusAlreadyException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelBoardNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelBoardNotFoundException.java new file mode 100644 index 00000000..ec7e6d9c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelBoardNotFoundException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.channel.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.exception.ResourceNotFoundException; + +import static leaguehub.leaguehubbackend.domain.channel.exception.ChannelExceptionCode.CHANNEL_BOARD_NOT_FOUND; + +public class ChannelBoardNotFoundException extends ResourceNotFoundException { + + private final ExceptionCode exceptionCode; + + public ChannelBoardNotFoundException() { + super(CHANNEL_BOARD_NOT_FOUND); + this.exceptionCode = CHANNEL_BOARD_NOT_FOUND; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelNotFoundException.java new file mode 100644 index 00000000..b34ca2d2 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelNotFoundException.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.domain.channel.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.exception.ResourceNotFoundException; + +import static leaguehub.leaguehubbackend.domain.channel.exception.ChannelExceptionCode.CHANNEL_NOT_FOUND; + + +public class ChannelNotFoundException extends ResourceNotFoundException { + + private final ExceptionCode exceptionCode; + + public ChannelNotFoundException() { + super(CHANNEL_NOT_FOUND); + this.exceptionCode = CHANNEL_NOT_FOUND; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelRequestException.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelRequestException.java new file mode 100644 index 00000000..e718a108 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelRequestException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.channel.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.channel.exception.ChannelExceptionCode.INVALID_REQUEST_CHANNEL; + +public class ChannelRequestException extends IllegalArgumentException { + + private final ExceptionCode exceptionCode; + + public ChannelRequestException() { + super(INVALID_REQUEST_CHANNEL.getMessage()); + this.exceptionCode = INVALID_REQUEST_CHANNEL; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelStatusAlreadyException.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelStatusAlreadyException.java new file mode 100644 index 00000000..bdaea598 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/exception/exception/ChannelStatusAlreadyException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.channel.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.channel.exception.ChannelExceptionCode.CHANNEL_STATUS_ALREADY_PROCEEDING; + + +public class ChannelStatusAlreadyException extends RuntimeException { + private final ExceptionCode exceptionCode; + + public ChannelStatusAlreadyException() { + super(CHANNEL_STATUS_ALREADY_PROCEEDING.getMessage()); + this.exceptionCode = CHANNEL_STATUS_ALREADY_PROCEEDING; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelBoardRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelBoardRepository.java new file mode 100644 index 00000000..f73d3176 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelBoardRepository.java @@ -0,0 +1,28 @@ +package leaguehub.leaguehubbackend.domain.channel.repository; + +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface ChannelBoardRepository extends JpaRepository { + + List findAllByChannel_ChannelLinkOrderByIndex(String channelLink); + + Optional findChannelBoardsByIdAndChannel_ChannelLink(Long boardId, String channelLink); + + Optional findChannelBoardsByIdAndChannel_Id(Long boardId, Long channelId); + + List findAllByChannel_IdOrderByIndex(Long channelId); + + List findAllByChannelAndIndexGreaterThan(Channel channel, int deleteIndex); + + List findChannelBoardsByChannel_ChannelLink(String channelLink); + + @Query("SELECT MAX(b.index) FROM ChannelBoard b WHERE b.channel = :channel") + Integer findMaxIndexByChannel(@Param("channel") Channel channel); +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelInfoRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelInfoRepository.java new file mode 100644 index 00000000..5a7634ad --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelInfoRepository.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.channel.repository; + +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelInfo; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface ChannelInfoRepository extends JpaRepository { + + @Query("select c from ChannelInfo c join fetch c.channel where c.channel.channelLink = :channelLink") + Optional findChannelInfoByChannel_ChannelLink(@Param("channelLink") String channelLink); +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelRepository.java new file mode 100644 index 00000000..1699099d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelRepository.java @@ -0,0 +1,12 @@ +package leaguehub.leaguehubbackend.domain.channel.repository; + +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ChannelRepository extends JpaRepository { + + Optional findByChannelLink(String channelLink); + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelRuleRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelRuleRepository.java new file mode 100644 index 00000000..ee495805 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/repository/ChannelRuleRepository.java @@ -0,0 +1,11 @@ +package leaguehub.leaguehubbackend.domain.channel.repository; + +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ChannelRuleRepository extends JpaRepository { + + ChannelRule findChannelRuleByChannel_Id(Long channelId); + + ChannelRule findChannelRuleByChannel_ChannelLink(String channelLink); +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelBoardService.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelBoardService.java new file mode 100644 index 00000000..2a2f577d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelBoardService.java @@ -0,0 +1,148 @@ +package leaguehub.leaguehubbackend.domain.channel.service; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardInfoDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardLoadDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelBoardNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.match.dto.MyMatchDto; +import leaguehub.leaguehubbackend.domain.match.service.MatchQueryService; +import leaguehub.leaguehubbackend.domain.match.service.MatchService; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + + +@Service +@RequiredArgsConstructor +public class ChannelBoardService { + + private final ChannelService channelService; + private final ChannelBoardRepository channelBoardRepository; + private final MemberService memberService; + private final MatchService matchService; + private final MatchQueryService matchQueryService; + + @Transactional + public ChannelBoardLoadDto createChannelBoard(String channelLink, ChannelBoardDto request) { + + Member member = memberService.findCurrentMember(); + Participant participant = channelService.getParticipant(member.getId(), channelLink); + Channel channel = participant.getChannel(); + channelService.checkRoleHost(participant.getRole()); + + Integer maxIndexByChannel = channelBoardRepository.findMaxIndexByChannel(channel); + + + ChannelBoard channelBoard = ChannelBoard.createChannelBoard(channel, + request.getTitle(), request.getContent(), maxIndexByChannel + 1); + channelBoardRepository.save(channelBoard); + + return new ChannelBoardLoadDto(channelBoard.getId(), channelBoard.getTitle(), channelBoard.getIndex()); + } + + + /** + * 채널 로딩 시점에서 불러오는 채널 게시판(내용은 반환하지 않음.) + * + * @param channelLink + * @return List + */ + @Transactional + public ChannelBoardInfoDto loadChannelBoards(String channelLink) { + + + channelService.getChannel(channelLink); + + List channelBoards = channelBoardRepository.findAllByChannel_ChannelLinkOrderByIndex(channelLink); + + List channelBoardLoadDtoList = channelBoards.stream() + .map(channelBoard -> new ChannelBoardLoadDto(channelBoard.getId(), channelBoard.getTitle(), channelBoard.getIndex())) + .collect(Collectors.toList()); + + MyMatchDto matchDto = matchQueryService.getMyMatchRound(channelLink); + + return new ChannelBoardInfoDto(matchDto.getMyMatchRound(), matchDto.getMyMatchId(), channelBoardLoadDtoList); + } + + @Transactional + public ChannelBoardDto getChannelBoard(String channelLink, Long boardId) { + ChannelBoard channelBoard = validateChannelBoard(boardId, channelLink); + + return new ChannelBoardDto(channelBoard.getTitle(), channelBoard.getContent()); + } + + @Transactional + public void updateChannelBoard(String channelLink, Long boardId, ChannelBoardDto update) { + + Member member = memberService.findCurrentMember(); + + Participant participant = channelService.getParticipant(member.getId(), channelLink); + + Channel channel = participant.getChannel(); + channelService.checkRoleHost(participant.getRole()); + + ChannelBoard channelBoard = validateChannelBoard(boardId, channel.getId()); + + + channelBoard.updateChannelBoard(update.getTitle(), update.getContent()); + } + + @Transactional + public void deleteChannelBoard(String channelLink, Long boardId) { + Member member = memberService.findCurrentMember(); + + Participant participant = channelService.getParticipant(member.getId(), channelLink); + Channel channel = participant.getChannel(); + + channelService.checkRoleHost(participant.getRole()); + + ChannelBoard channelBoard = validateChannelBoard(boardId, channel.getId()); + + + channelBoardRepository.delete(channelBoard); + List boardsAfterDeleted = channelBoardRepository.findAllByChannelAndIndexGreaterThan(channel, channelBoard.getIndex()); + for (ChannelBoard board : boardsAfterDeleted) { + board.updateIndex(board.getIndex() - 1); + } + } + + @Transactional + public void updateChannelBoardIndex(String channelLink, List channelBoardLoadDtoList) { + Member member = memberService.findCurrentMember(); + + Participant participant = channelService.getParticipant(member.getId(), channelLink); + + Channel channel = participant.getChannel(); + + channelService.checkRoleHost(participant.getRole()); + + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + + channelBoardLoadDtoList.forEach(channelBoardLoadDto -> { + channelBoards.stream() + .filter(channelBoard -> channelBoard.getId().equals(channelBoardLoadDto.getBoardId())) + .findFirst() + .ifPresent(channelBoard -> channelBoard.updateIndex(channelBoardLoadDto.getBoardIndex())); + }); + } + + public ChannelBoard validateChannelBoard(Long channelBoardId, String channelLink) { + return channelBoardRepository.findChannelBoardsByIdAndChannel_ChannelLink(channelBoardId, channelLink) + .orElseThrow(() -> new ChannelBoardNotFoundException()); + } + + public ChannelBoard validateChannelBoard(Long channelBoardId, Long channelId) { + return channelBoardRepository.findChannelBoardsByIdAndChannel_Id(channelBoardId, channelId) + .orElseThrow(() -> new ChannelBoardNotFoundException()); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelDeleteService.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelDeleteService.java new file mode 100644 index 00000000..7bdc2b52 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelDeleteService.java @@ -0,0 +1,148 @@ +package leaguehub.leaguehubbackend.domain.channel.service; + +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelStatusAlreadyException; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelInfoRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import leaguehub.leaguehubbackend.domain.match.entity.MatchSet; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchSetRepository; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotGameHostException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static leaguehub.leaguehubbackend.domain.channel.entity.ChannelStatus.PREPARING; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.HOST; + +@RequiredArgsConstructor +@Transactional +@Service +public class ChannelDeleteService { + + private final ChannelRepository channelRepository; + private final MemberService memberService; + private final ChannelBoardRepository channelBoardRepository; + private final ParticipantRepository participantRepository; + private final MatchPlayerRepository matchPlayerRepository; + private final ChannelRuleRepository channelRuleRepository; + private final MatchSetRepository matchSetRepository; + private final ChannelInfoRepository channelInfoRepository; + private final MatchRepository matchRepository; + + public void deleteChannel(String channelLink) { + Member member = memberService.findCurrentMember(); + Participant participant = getParticipant(member.getId(), channelLink); + Channel channel = getChannel(channelLink); + + if (channel.getChannelStatus() != PREPARING) { + throw new ChannelStatusAlreadyException(); + } + + if (participant.getRole() != HOST) { + throw new ParticipantNotGameHostException(); + } + + deleteParticipant(channelLink); + deleteMatch(channelLink); + + deleteChannelBoards(channelLink); + + deleteChannelInfo(channelLink); + + deleteChannelRule(channelLink); + + channelRepository.delete(channel); + channelRepository.flush(); + } + + private void deleteChannelBoards(String channelLink) { + List channelBoards = channelBoardRepository.findChannelBoardsByChannel_ChannelLink(channelLink); + channelBoards.stream().forEach(channelBoard -> { + channelBoard.deleteChannel(); + }); + + channelBoardRepository.deleteAllInBatch(channelBoards); + channelBoardRepository.flush(); + } + + private void deleteChannelInfo(String channelLink) { + channelInfoRepository.findChannelInfoByChannel_ChannelLink(channelLink).ifPresent( + channelInfo -> { + channelInfo.deleteChannel(); + channelInfoRepository.delete(channelInfo); + } + ); + + channelInfoRepository.flush(); + } + + private void deleteChannelRule(String channelLink) { + ChannelRule channelRule = channelRuleRepository.findChannelRuleByChannel_ChannelLink(channelLink); + channelRule.deleteChannel(); + channelRuleRepository.delete(channelRule); + channelRuleRepository.flush(); + } + + private void deleteMatch(String channelLink) { + List matchList = matchRepository.findAllByChannel_ChannelLink(channelLink); + + matchList.stream().forEach(match -> { + match.deleteChannel(); + List matchSetList = matchSetRepository.findAllByMatch_Channel_ChannelLink(channelLink); + matchSetList.stream().forEach(matchSet -> { + matchSet.getMatchRankList().stream().forEach(matchRank -> { + matchRank.deleteMatchSet(); + }); + matchSet.deleteMatchAndMatchRankList(); + }); + + matchSetRepository.deleteAllInBatch(matchSetList); + }); + + matchRepository.deleteAllInBatch(matchList); + } + + private void deleteParticipant(String channelLink) { + List participants = participantRepository.findAllByChannel_ChannelLink(channelLink); + + participants.stream().forEach(p -> { + p.deleteChannelAndMember(); + List matchPlayers = matchPlayerRepository.findMatchPlayersByParticipantId(p.getId()); + matchPlayers.stream().forEach(mp -> { + mp.deleteParticipantAndMatch(); + }); + matchPlayerRepository.deleteAllInBatch(matchPlayers); + }); + + participantRepository.deleteAllInBatch(participants); + } + + private Channel getChannel(String channelLink) { + Channel channel = channelRepository.findByChannelLink(channelLink) + .orElseThrow(ChannelNotFoundException::new); + return channel; + } + + + private Participant getParticipant(Long memberId, String channelLink) { + return participantRepository + .findParticipantByMemberIdAndChannel_ChannelLink(memberId, channelLink) + .orElseThrow(() -> new InvalidParticipantAuthException()); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelInfoService.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelInfoService.java new file mode 100644 index 00000000..a5991c95 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelInfoService.java @@ -0,0 +1,48 @@ +package leaguehub.leaguehubbackend.domain.channel.service; + + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelInfoDto; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelInfo; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelInfoRepository; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class ChannelInfoService { + + private final ChannelInfoRepository channelInfoRepository; + private final ChannelService channelService; + private final MemberService memberService; + + + public ChannelInfoDto getChannelInfoDto(String channelLink) { + + ChannelInfo channelInfo = validateChannelBoard(channelLink); + String channelTitle = channelInfo.getChannel().getTitle(); + + return new ChannelInfoDto(channelTitle, channelInfo.getChannelContentInfo(), channelInfo.getChannelRuleInfo(), channelInfo.getChannelTimeInfo(), channelInfo.getChannelPrizeInfo()); + } + + public void updateChannelInfo(String channelLink, ChannelInfoDto channelInfoDto) { + Member member = memberService.findCurrentMember(); + Participant participant = channelService.getParticipant(member.getId(), channelLink); + channelService.checkRoleHost(participant.getRole()); + + ChannelInfo findChannelInfo = validateChannelBoard(channelLink); + + findChannelInfo.updateChannelBoard(channelInfoDto.getChannelContentInfo(), channelInfoDto.getChannelTimeInfo(), channelInfoDto.getChannelRuleInfo(), channelInfoDto.getChannelPrizeInfo()); + } + + + public ChannelInfo validateChannelBoard(String channelLink) { + return channelInfoRepository.findChannelInfoByChannel_ChannelLink(channelLink) + .orElseThrow(() -> new ChannelNotFoundException()); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelRuleService.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelRuleService.java new file mode 100644 index 00000000..134d9056 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelRuleService.java @@ -0,0 +1,65 @@ +package leaguehub.leaguehubbackend.domain.channel.service; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelRuleDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class ChannelRuleService { + + private final ChannelRuleRepository channelRuleRepository; + private final ChannelService channelService; + private final MemberService memberService; + + @Transactional + public ChannelRuleDto updateChannelRule(String channelLink, ChannelRuleDto channelRuleDto) { + Member member = memberService.findCurrentMember(); + Participant participant = channelService.getParticipant(member.getId(), channelLink); + Channel channel = participant.getChannel(); + channelService.checkRoleHost(participant.getRole()); + + ChannelRule channelRule = channelRuleRepository.findChannelRuleByChannel_Id(channel.getId()); + + Optional.ofNullable(channelRuleDto.getTier()) + .ifPresent(tier -> { + if (tier) { + channelRule.updateTierRule(true, channelRuleDto.getTierMax(), channelRuleDto.getTierMin()); + } else { + channelRule.updateTierRule(false); + } + }); + + Optional.ofNullable(channelRuleDto.getPlayCount()) + .ifPresent(playCount -> { + if (playCount) { + channelRule.updatePlayCountMin(true, channelRuleDto.getPlayCountMin()); + } else { + channelRule.updatePlayCountMin(false); + } + }); + + return new ChannelRuleDto().builder().tier(channelRule.getTier()).tierMax(channelRule.getTierMax()) + .tierMin(channelRule.getTierMin()).playCount(channelRule.getPlayCount()) + .playCountMin(channelRule.getLimitedPlayCount()).build(); + } + + @Transactional + public ChannelRuleDto getChannelRule(String channelLink) { + ChannelRule channelRule = channelRuleRepository.findChannelRuleByChannel_ChannelLink(channelLink); + + return new ChannelRuleDto().builder().tier(channelRule.getTier()).tierMax(channelRule.getTierMax()) + .tierMin(channelRule.getTierMin()).playCount(channelRule.getPlayCount()) + .playCountMin(channelRule.getLimitedPlayCount()).build(); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelService.java b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelService.java new file mode 100644 index 00000000..e1dd7258 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/channel/service/ChannelService.java @@ -0,0 +1,183 @@ +package leaguehub.leaguehubbackend.domain.channel.service; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.UpdateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.*; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelStatusAlreadyException; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelInfoRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.email.exception.exception.UnauthorizedEmailException; +import leaguehub.leaguehubbackend.domain.match.service.MatchService; +import leaguehub.leaguehubbackend.domain.match.service.chat.MatchChatService; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.global.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static leaguehub.leaguehubbackend.domain.channel.entity.ChannelStatus.PROCEEDING; +import static leaguehub.leaguehubbackend.domain.member.entity.BaseRole.USER; + + +@Service +@RequiredArgsConstructor +public class ChannelService { + + private final ChannelRepository channelRepository; + private final MemberService memberService; + private final ChannelBoardRepository channelBoardRepository; + private final ParticipantRepository participantRepository; + private final MatchService matchService; + private final ChannelRuleRepository channelRuleRepository; + private final MatchChatService matchChatService; + private final ChannelInfoRepository channelInfoRepository; + + @Transactional + public ParticipantChannelDto createChannel(CreateChannelDto createChannelDto) { + + Member member = memberService.findCurrentMember(); + + checkEmail(SecurityUtils.getAuthenticatedUser()); + + + Channel channel = Channel.createChannel(createChannelDto.getTitle(), + createChannelDto.getGameCategory(), createChannelDto.getMaxPlayer(), + createChannelDto.getMatchFormat(), createChannelDto.getChannelImageUrl()); + + channelRepository.save(channel); + + ChannelRule channelRule = ChannelRule.createChannelRule(channel, createChannelDto.getTier(), createChannelDto.getTierMax(), + createChannelDto.getTierMin(), + createChannelDto.getPlayCount(), + createChannelDto.getPlayCountMin()); + + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + channelInfoRepository.save(ChannelInfo.createChannelInfo(channel)); + + Participant participant = Participant.createHostChannel(member, channel); + participant.newCustomChannelIndex(participantRepository.findMaxIndexByParticipant(member.getId())); + + participantRepository.save(participant); + ParticipantChannelDto participantChannelDto = convertParticipantChannelDto(participant); + + matchService.createSubMatches(channel, createChannelDto.getMaxPlayer()); + + return participantChannelDto; + } + + @Transactional + public List findParticipantChannelList() { + + Member member = memberService.findCurrentMember(); + + + List allByParticipantList = participantRepository + .findAllByMemberIdOrderByIndex(member.getId()); + + List participantChannelDtoList = allByParticipantList.stream() + .map(participant -> convertParticipantChannelDto(participant)) + .collect(Collectors.toList()); + + return participantChannelDtoList; + + } + + @Transactional + public ChannelDto findChannel(String channelLink) { + + Channel findChannel = getChannel(channelLink); + + ChannelDto channelDto = ChannelDto.builder().title(findChannel.getTitle()) + .realPlayer(findChannel.getRealPlayer()).gameCategory(findChannel.getGameCategory()) + .maxPlayer(findChannel.getMaxPlayer()).build(); + + return channelDto; + } + + @Transactional + public void updateChannel(String channelLink, UpdateChannelDto updateChannelDto) { + Member member = memberService.findCurrentMember(); + Participant participant = getParticipant(member.getId(), channelLink); + Channel channel = participant.getChannel(); + checkRoleHost(participant.getRole()); + + + Optional.ofNullable(updateChannelDto.getTitle()).ifPresent(channel::updateTitle); + Optional.ofNullable(updateChannelDto.getMaxPlayer()).ifPresent(channel::updateMaxPlayer); + Optional.ofNullable(updateChannelDto.getChannelImageUrl()).ifPresent(channel::updateChannelImageUrl); + } + + + public Channel getChannel(String channelLink) { + Channel channel = channelRepository.findByChannelLink(channelLink) + .orElseThrow(ChannelNotFoundException::new); + return channel; + } + + + public Participant getParticipant(Long memberId, String channelLink) { + return participantRepository + .findParticipantByMemberIdAndChannel_ChannelLink(memberId, channelLink) + .orElseThrow(() -> new InvalidParticipantAuthException()); + } + + public void checkRoleHost(Role role) { + if (role != Role.HOST) { + throw new InvalidParticipantAuthException(); + } + } + + private void checkEmail(UserDetails userDetails) { + if (!userDetails.getAuthorities().toString().equals(USER.convertBaseRole())) + throw new UnauthorizedEmailException(); + } + + private ParticipantChannelDto convertParticipantChannelDto(Participant participant) { + Channel channel = participant.getChannel(); + return new ParticipantChannelDto( + channel.getId(), + channel.getChannelLink(), + channel.getTitle(), + channel.getGameCategory().getNum(), + channel.getChannelImageUrl(), + participant.getIndex() + ); + } + + public void updateChannelStatus(String channelLink, Integer status) { + Member member = memberService.findCurrentMember(); + Participant participant = getParticipant(member.getId(), channelLink); + checkRoleHost(participant.getRole()); + + Channel channel = participant.getChannel(); + + if(channel.getChannelStatus().equals(PROCEEDING)) + throw new ChannelStatusAlreadyException(); + + channel.updateChannelStatus(ChannelStatus.convertStatus(status)); + + if (status == 2) { + matchChatService.deleteChannelMatchChat(channel); + } + + if (status == 1) { + matchService.processMatchSet(channelLink); + } + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/controller/EmailController.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/controller/EmailController.java new file mode 100644 index 00000000..a9308eb7 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/controller/EmailController.java @@ -0,0 +1,50 @@ +package leaguehub.leaguehubbackend.domain.email.controller; + +import io.swagger.v3.oas.annotations.Operation; +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.SecurityRequirements; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import leaguehub.leaguehubbackend.domain.email.dto.EmailDto; +import leaguehub.leaguehubbackend.domain.email.service.EmailService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/api") +@Tag(name = "Email-Controller", description = "Email 인증") +public class EmailController { + + private final EmailService emailService; + + @Operation(summary = "인증 메일 보내기", description = "엑세스 토큰이 유효하면 받은 email 주소로 인증 메일을 보낸다") + @SecurityRequirements + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Email Successfully Sent", content = @Content(mediaType = "string", schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "404", description = "MB-C-001 존재하지 않는 회원입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + @ApiResponse(responseCode = "500", description = "G-S-001 Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + }) + @PostMapping("/member/auth/email") + public ResponseEntity verifyUser(@RequestBody @Valid EmailDto emailDto) { + + String email = emailService.sendEmailWithConfirmation(emailDto.getEmail()); + + return ResponseEntity.ok("Email Successfully Sent to " + email); + } + + @GetMapping("/member/oauth/email") + public String confirmUserEmail(@RequestParam("token") String token) { + if (emailService.confirmUserEmail(token)) { + return "redirect:/mypage"; + } else { + return "redirect:/"; + } + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/dto/EmailDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/dto/EmailDto.java new file mode 100644 index 00000000..8734328f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/dto/EmailDto.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.domain.email.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class EmailDto { + + @NotBlank + @Email + @Schema(description = "이메일 주소", example = "test@naver.com") + private String email; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/entity/EmailAuth.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/entity/EmailAuth.java new file mode 100644 index 00000000..81fa537a --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/entity/EmailAuth.java @@ -0,0 +1,38 @@ +package leaguehub.leaguehubbackend.domain.email.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EmailAuth extends BaseTimeEntity { + + @Id + @Column(name = "email_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true) + private String email; + + private String authToken; + + private LocalDateTime emailExpireDate; + @Builder + public EmailAuth(String email, String authToken) { + this.email = email; + this.authToken = authToken; + this.emailExpireDate = LocalDateTime.now().plusMinutes(10); + } + + public void changeExpireDate(LocalDateTime localDateTime) { + this.emailExpireDate = emailExpireDate; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/EmailExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/EmailExceptionCode.java new file mode 100644 index 00000000..2ff31cb5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/EmailExceptionCode.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.domain.email.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.*; + +@Getter +@RequiredArgsConstructor +public enum EmailExceptionCode implements ExceptionCode { + + INVALID_EMAIL_ADDRESS(BAD_REQUEST, "MB-C-003", "유효하지 않은 이메일 형식입니다."), + DUPLICATE_EMAIL_EXCEPTION(CONFLICT, "MB-C-004", "중복되는 이메일입니다."), + UNAUTHORIZED_EMAIL_EXCEPTION(UNAUTHORIZED, "MB-C-005", "이메일이 인증되지 않은 사용자입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/EmailExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/EmailExceptionHandler.java new file mode 100644 index 00000000..4b88cdeb --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/EmailExceptionHandler.java @@ -0,0 +1,57 @@ +package leaguehub.leaguehubbackend.domain.email.exception; + +import leaguehub.leaguehubbackend.domain.email.exception.exception.DuplicateEmailException; +import leaguehub.leaguehubbackend.domain.email.exception.exception.InvalidEmailAddressException; +import leaguehub.leaguehubbackend.domain.email.exception.exception.UnauthorizedEmailException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class EmailExceptionHandler { + + @ExceptionHandler(InvalidEmailAddressException.class) + public ResponseEntity invalidEmailAddress( + InvalidEmailAddressException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(DuplicateEmailException.class) + public ResponseEntity invalidEmailAddress( + DuplicateEmailException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(UnauthorizedEmailException.class) + public ResponseEntity UnauthorizedEmailException( + UnauthorizedEmailException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/DuplicateEmailException.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/DuplicateEmailException.java new file mode 100644 index 00000000..637b2bde --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/DuplicateEmailException.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.domain.email.exception.exception; + +import leaguehub.leaguehubbackend.domain.email.exception.EmailExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.email.exception.EmailExceptionCode.DUPLICATE_EMAIL_EXCEPTION; + + +public class DuplicateEmailException extends RuntimeException{ + + private final EmailExceptionCode exceptionCode; + + public DuplicateEmailException() { + super(DUPLICATE_EMAIL_EXCEPTION.getMessage()); + this.exceptionCode = DUPLICATE_EMAIL_EXCEPTION; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/InvalidEmailAddressException.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/InvalidEmailAddressException.java new file mode 100644 index 00000000..8a8bf5a7 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/InvalidEmailAddressException.java @@ -0,0 +1,22 @@ +package leaguehub.leaguehubbackend.domain.email.exception.exception; + +import leaguehub.leaguehubbackend.domain.email.exception.EmailExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.email.exception.EmailExceptionCode.INVALID_EMAIL_ADDRESS; + + +public class InvalidEmailAddressException extends IllegalArgumentException { + private final EmailExceptionCode exceptionCode; + + public InvalidEmailAddressException() { + + super(INVALID_EMAIL_ADDRESS.getMessage()); + this.exceptionCode = INVALID_EMAIL_ADDRESS; + + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/UnauthorizedEmailException.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/UnauthorizedEmailException.java new file mode 100644 index 00000000..5a3ee02c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/exception/exception/UnauthorizedEmailException.java @@ -0,0 +1,23 @@ +package leaguehub.leaguehubbackend.domain.email.exception.exception; + +import leaguehub.leaguehubbackend.domain.email.exception.EmailExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +import static leaguehub.leaguehubbackend.domain.email.exception.EmailExceptionCode.UNAUTHORIZED_EMAIL_EXCEPTION; + + +public class UnauthorizedEmailException extends AuthenticationException { + private final EmailExceptionCode exceptionCode; + + public UnauthorizedEmailException() { + + super(UNAUTHORIZED_EMAIL_EXCEPTION.getMessage()); + this.exceptionCode = UNAUTHORIZED_EMAIL_EXCEPTION; + + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/repository/EmailAuthRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/repository/EmailAuthRepository.java new file mode 100644 index 00000000..6f86d141 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/repository/EmailAuthRepository.java @@ -0,0 +1,11 @@ +package leaguehub.leaguehubbackend.domain.email.repository; + +import leaguehub.leaguehubbackend.domain.email.entity.EmailAuth; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EmailAuthRepository extends JpaRepository { + Optional findAuthByEmail(String email); + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/email/service/EmailService.java b/src/main/java/leaguehub/leaguehubbackend/domain/email/service/EmailService.java new file mode 100644 index 00000000..0b99666e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/email/service/EmailService.java @@ -0,0 +1,215 @@ +package leaguehub.leaguehubbackend.domain.email.service; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.email.entity.EmailAuth; +import leaguehub.leaguehubbackend.domain.email.exception.exception.DuplicateEmailException; +import leaguehub.leaguehubbackend.domain.email.exception.exception.InvalidEmailAddressException; +import leaguehub.leaguehubbackend.domain.email.repository.EmailAuthRepository; +import leaguehub.leaguehubbackend.domain.member.entity.BaseRole; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.global.exception.global.exception.GlobalServerErrorException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.util.FileCopyUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.regex.Pattern; + +@Slf4j +@Service +@RequiredArgsConstructor +public class EmailService { + + private final MemberRepository memberRepository; + + private final EmailAuthRepository emailAuthRepository; + + private final MemberService memberService; + + private final ResourceLoader resourceLoader; + + @Value("${EMAIL_SECRET_KEY}") + private String secretKey; + + @Value("${LEAGUE_HUB_ADDRESS}") + private String leagueHubAddress; + + private final JavaMailSender mailSender; + + @Transactional + public String sendEmailWithConfirmation(String email) { + + validateEmail(email); + + Member member = memberService.findCurrentMember(); + + if (member.getEmailAuth() != null) { + removeExistingEmailAuth(member); + } + + String uniqueToken = generateUniqueTokenForUser(email); + + EmailAuth emailAuth = createAndSaveEmailAuth(email, member, uniqueToken); + + sendConfirmationEmail(emailAuth, uniqueToken); + + return email; + } + + public void removeUnverifiedEmail(String email, Member member) { + EmailAuth emailAuth = member.getEmailAuth(); + if (emailAuth != null) { + emailAuthRepository.delete(emailAuth); + member.assignEmailAuth(null); + memberRepository.save(member); + } + } + + private void validateEmail(String email) { + if (!isValidEmailFormat(email)) { + throw new InvalidEmailAddressException(); + } + + Optional memberOptional = memberRepository.findMemberByEmail(email); + + if (memberOptional.isPresent()) { + Member member = memberOptional.get(); + if (!member.isEmailUserVerified()) { + removeUnverifiedEmail(email, member); + } + if (member.isEmailUserVerified()) { + throw new DuplicateEmailException(); + } + } + } + + public void sendConfirmationEmail(EmailAuth emailAuth, String uniqueToken) { + try { + String link = generateConfirmationLink(uniqueToken); + String htmlTemplate = loadEmailTemplate("static/emailTemplate.html"); + String htmlContent = changeTemplate(htmlTemplate, link); + sendEmail(emailAuth.getEmail(), "회원가입 이메일 인증", htmlContent); + } catch (Exception e) { + log.error("Error in sendConfirmationEmail", e); + throw new GlobalServerErrorException(); + } + } + + private String generateConfirmationLink(String uniqueToken) { + return "http://" + leagueHubAddress + "/api/member/oauth/email?token=" + uniqueToken; + } + + private String loadEmailTemplate(String path) throws IOException { + ClassPathResource resource = new ClassPathResource(path); + InputStream inputStream = resource.getInputStream(); + byte[] bdata = FileCopyUtils.copyToByteArray(inputStream); + return new String(bdata, StandardCharsets.UTF_8); + } + + private String changeTemplate(String template, String link) { + return template.replace("{{LINK}}", link); + } + + private void sendEmail(String to, String subject, String content) throws MessagingException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED, + "UTF-8"); + + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content, true); + + mailSender.send(message); + } + + private EmailAuth createAndSaveEmailAuth(String email, Member member, String uniqueToken) { + EmailAuth emailAuth = new EmailAuth(email, uniqueToken); + + member.unverifyEmail(); + member.assignEmailAuth(emailAuth); + + emailAuthRepository.save(emailAuth); + memberRepository.save(member); + + return emailAuth; + } + + private void removeExistingEmailAuth(Member member) { + emailAuthRepository.delete(member.getEmailAuth()); + member.assignEmailAuth(null); + } + + public String generateUniqueTokenForUser(String email) { + return JWT.create() + .withSubject(email) + .sign(Algorithm.HMAC256(secretKey)); + } + + public boolean isValidEmailFormat(String email) { + String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$"; + Pattern pat = Pattern.compile(emailRegex); + return pat.matcher(email).matches(); + } + + public String getEmailFromToken(String token) { + try { + return JWT.require(Algorithm.HMAC256(secretKey)) + .build() + .verify(token) + .getSubject(); + } catch (Exception e) { + return null; + } + } + + @Transactional + public boolean confirmUserEmail(String token) { + try { + + String email = getEmailFromToken(token); + + if (email == null) { + throw new RuntimeException("인증 토큰이 잘못되었습니다."); + } + + EmailAuth emailAuth = emailAuthRepository.findAuthByEmail(email) + .orElseThrow(() -> new RuntimeException("인증 토큰이 잘못되었습니다.")); + + if (emailAuth.getEmailExpireDate().isBefore(LocalDateTime.now())) { + throw new RuntimeException("인증 토큰이 만료되었습니다."); + } + + Member member = memberRepository.findByEmailAuth(emailAuth) + .orElseThrow(() -> new RuntimeException("멤버 정보를 찾을 수 없습니다.")); + + member.verifyEmail(); + + if (member.getBaseRole() == BaseRole.GUEST) { + member.updateRole(BaseRole.USER); + } + + memberRepository.save(member); + + return true; + } catch (Exception e) { + log.error("이메일 링크 확인 중 에러 발생", e); + return false; + } + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchChatController.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchChatController.java new file mode 100644 index 00000000..55c36423 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchChatController.java @@ -0,0 +1,46 @@ +package leaguehub.leaguehubbackend.domain.match.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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 leaguehub.leaguehubbackend.domain.match.dto.MatchMessage; +import leaguehub.leaguehubbackend.domain.match.service.chat.MatchChatService; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class MatchChatController { + + private final MatchChatService matchChatService; + + @MessageMapping("/match/chat") + public void sendMessage(@Payload MatchMessage message) { + matchChatService.processMessage(message); + } + + @Operation(summary = "관리자 페이지에서 매치 채팅 내역 조회") + @Parameters(value = { + @Parameter(name = "channelId", description = "채널 id", example = "1"), + @Parameter(name = "matchId", description = "매치 id", example = "1") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "매치 채팅 내역 조회성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = MatchMessage.class))), + }) + @PostMapping("/api/channelLink/{channelLink}/match/{matchId}/chat/history") + public List getMatchChatHistory(@PathVariable("channelLink") String channelLink, @PathVariable("matchId") Long matchId) { + + return matchChatService.findMatchChatHistory(channelLink, matchId); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchController.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchController.java new file mode 100644 index 00000000..bac769e4 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchController.java @@ -0,0 +1,103 @@ +package leaguehub.leaguehubbackend.domain.match.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.tags.Tag; +import leaguehub.leaguehubbackend.domain.match.dto.MatchCallAdminDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchSetCountDto; +import leaguehub.leaguehubbackend.domain.match.service.MatchService; +import leaguehub.leaguehubbackend.domain.match.service.chat.MatchChatService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.OK; + +@Tag(name = "Match-Controller", description = "대회 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class MatchController { + + private final MatchService matchService; + private final SimpMessagingTemplate simpMessagingTemplate; + private final MatchChatService matchChatService; + + + @Operation(summary = "해당 채널의 라운드 경기 배정") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "matchRound", description = "배정 싶은 매치의 라운드(1, 2 라운드)", example = "1, 2, 3, 4") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가자들이 첫 매치에 배정되었습니다."), + @ApiResponse(responseCode = "403", description = "권한이 관리자가 아님,채널을 찾을 수 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/match/{channelLink}/{matchRound}") + public ResponseEntity assignmentMatches(@PathVariable("channelLink") String channelLink, @PathVariable("matchRound") Integer matchRound) { + + matchService.matchAssignment(channelLink, matchRound); + + return new ResponseEntity<>("참가자들이 첫 매치에 배정되었습니다.", OK); + } + + + @Operation(summary = "해당 채널의 (1, 2, 3)라운드에 대한 경기 횟수 설정") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "roundCountList", description = "설정할려는 횟수 배열", example = "[3, 4, 2, 1]") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "경기 횟수가 배정되었습니다."), + @ApiResponse(responseCode = "403", description = "매치 또는 채널을 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/match/{channelLink}/count") + public ResponseEntity setMatchRoundCount(@PathVariable("channelLink") String channelLink, + @RequestBody MatchSetCountDto matchSetCountDto) { + + matchService.setMatchSetCount(channelLink, matchSetCountDto.getMatchSetCountList()); + + return new ResponseEntity("경기 횟수가 배정되었습니다.", OK); + } + + @MessageMapping("/match/{channelLink}/{participantId}/{matchId}/call-admin") + public void callAdmin(@DestinationVariable("channelLink") String channelLink, + @DestinationVariable("participantId") String participantId, + @DestinationVariable("matchId") String matchId) { + + MatchCallAdminDto matchCallAdminDto = matchService.callAdmin(channelLink, Long.valueOf(matchId), Long.valueOf(participantId)); + + matchChatService.processAdminAlert(channelLink, Long.valueOf(matchId)); + + simpMessagingTemplate.convertAndSend("/match/" + channelLink, matchCallAdminDto); + } + + + @Operation(summary = "해당 매치 알람 끄기") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "matchId", description = "알람을 끄는 match의 PK", example = "1, 2, 3, 4") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "403", description = "권한이 관리자가 아님,채널을 찾을 수 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/match/{channelLink}/{matchId}/call-off") + public ResponseEntity turnOffAlarm(@PathVariable("channelLink") String channelLink, + @PathVariable("matchId") Long matchId) { + + matchService.turnOffAlarm(channelLink, matchId); + + return new ResponseEntity(OK); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchPlayerController.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchPlayerController.java new file mode 100644 index 00000000..bb489457 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchPlayerController.java @@ -0,0 +1,55 @@ +package leaguehub.leaguehubbackend.domain.match.controller; + + +import io.swagger.v3.oas.annotations.tags.Tag; +import leaguehub.leaguehubbackend.domain.match.dto.MatchInfoDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchSetReadyMessage; +import leaguehub.leaguehubbackend.domain.match.service.MatchPlayerService; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantIdResponseDto; +import lombok.RequiredArgsConstructor; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "Match-Player-Controller", description = "대회 경기자 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class MatchPlayerController { + + + private final MatchPlayerService matchPlayerService; + private final SimpMessagingTemplate simpMessagingTemplate; + + + /** + * 참가자 매치 체크인 + * @param matchIdStr + * @param message + */ + @MessageMapping("/match/{matchId}/checkIn") + public void checkIn(@DestinationVariable("matchId") String matchIdStr, @Payload MatchSetReadyMessage message) { + + ParticipantIdResponseDto participantIdResponseDto = matchPlayerService.markPlayerAsReady(message, matchIdStr); + + simpMessagingTemplate.convertAndSend("/match/" + matchIdStr, participantIdResponseDto); + } + + /** + * 참가자 매치 점수 업데이트 + * @param matchIdStr + * @param matchSetStr + */ + @MessageMapping("/match/{matchId}/{matchSet}/score-update") + public void updateMatchPlayerScore(@DestinationVariable("matchId") String matchIdStr, @DestinationVariable("matchSet") String matchSetStr) { + Long matchId = Long.valueOf(matchIdStr); + Integer matchSet = Integer.valueOf(matchSetStr); + long endTime = System.currentTimeMillis() / 1000; + MatchInfoDto matchInfoDto = matchPlayerService.updateMatchPlayerScore(matchId, matchSet, endTime); + + simpMessagingTemplate.convertAndSend("/match/" + matchId + "/" + matchSet, matchInfoDto); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchQueryController.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchQueryController.java new file mode 100644 index 00000000..ac122efc --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/controller/MatchQueryController.java @@ -0,0 +1,123 @@ +package leaguehub.leaguehubbackend.domain.match.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.tags.Tag; +import leaguehub.leaguehubbackend.domain.match.dto.*; +import leaguehub.leaguehubbackend.domain.match.service.MatchQueryService; +import leaguehub.leaguehubbackend.domain.match.service.chat.MatchChatService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static org.springframework.http.HttpStatus.OK; + +@Tag(name = "Match-Query-Controller", description = "대회 조회 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class MatchQueryController { + + + private final MatchChatService matchChatService; + private final MatchQueryService matchQueryService; + + + @Operation(summary = "라운드 수(몇 강) 리스트 반환 - 사용자") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "라운드(몇 강) 리스트 반환", content = @Content(mediaType = "application/json", schema = @Schema(implementation = MatchRoundListDto.class))), + @ApiResponse(responseCode = "403", description = "매치 결과를 찾을 수 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/match/{channelLink}") + public ResponseEntity loadMatchRoundList(@PathVariable("channelLink") String channelLink) { + + MatchRoundListDto roundList = matchQueryService.getRoundList(channelLink); + + return new ResponseEntity<>(roundList, OK); + } + + + @Operation(summary = "해당 채널의 (1, 2, 3)라운드에 대한 매치 조회") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "matchRound", description = "조회하고 싶은 매치의 라운드(1, 2, 3)", example = "1, 2, 3, 4") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "매치가 조회되었습니다. - 배열로 반환", content = @Content(mediaType = "application/json", schema = @Schema(implementation = MatchRoundInfoDto.class))), + @ApiResponse(responseCode = "403", description = "권한이 관리자가 아님,채널을 찾을 수 없음", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/match/{channelLink}/{matchRound}") + public ResponseEntity loadMatchInfo(@PathVariable("channelLink") String channelLink, @PathVariable("matchRound") Integer matchRound) { + + MatchRoundInfoDto matchInfoDtoList = matchQueryService.loadMatchPlayerList(channelLink, matchRound); + + return new ResponseEntity<>(matchInfoDtoList, OK); + + } + + @Operation(summary = "현재 진행중인 매치의 정보 조회.") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "matchId", description = "조회 대상 matchId", example = "1") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "매치가 조회됨", content = @Content(mediaType = "application/json", schema = @Schema(implementation = MatchScoreInfoDto.class))), + @ApiResponse(responseCode = "404", description = "매치를 찾지 못함", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}/match/{matchId}/player/info") + public ResponseEntity loadMatchScore(@PathVariable("channelLink") String channelLink, @PathVariable("matchId") Long matchId) { + + MatchScoreInfoDto matchScoreInfoDto = matchQueryService.getMatchScoreInfo(channelLink, matchId); + + List matchMessages = matchChatService.findMatchChatHistory(channelLink, matchId); + + matchScoreInfoDto.setMatchMessages(matchMessages); + + return new ResponseEntity<>(matchScoreInfoDto, OK); + } + + + @Operation(summary = "해당 채널의 (1, 2, 3)라운드에 대한 설정된 경기 횟수를 반환") + @Parameter(name = "roundCountList", description = "설정할려는 횟수 배열 결승전부터", example = "[3, 4, 2, 1]") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "경기 횟수 반환"), + @ApiResponse(responseCode = "403", description = "매치 또는 채널을 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("match/{channelLink}/count") + public ResponseEntity getMatchRoundCount(@PathVariable("channelLink") String channelLink) { + + MatchSetCountDto matchSetCountDto = matchQueryService.getMatchSetCount(channelLink); + + return new ResponseEntity(matchSetCountDto, OK); + } + + @Operation(summary = "해당 채널 매치의 결과 - 이전 경기 결과를 가져옴 매치 세트 결과를 다 가져온다.") + @Parameters(value = { + @Parameter(name = "matchId", description = "불러오고 싶은 매치의 PK", example = "3"), + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "매치 결과를 리스트로 가져온다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = GameResultDto.class))), + @ApiResponse(responseCode = "404", description = "매치 세트를 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/match/{matchId}/result") + public ResponseEntity getGameResult(@PathVariable Long matchId) { + List gameResultList = matchQueryService.getGameResult(matchId); + + return new ResponseEntity(gameResultList, OK); + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/GameResultDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/GameResultDto.java new file mode 100644 index 00000000..4abfc4ab --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/GameResultDto.java @@ -0,0 +1,26 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@NoArgsConstructor +public class GameResultDto { + + @Schema(description = "몇번째 매치 세트인지 나타낸다. 매치의 첫번째 매치세트 즉 3세트까지 있으면 1, 2, 3이 반환된다.", example = "1, 2, 3") + private Integer matchSetCount; + + @Schema(description = "플레이어 아이디와 등수과 담겨져있다.") + private List matchRankResultDtos = new ArrayList<>(); + + @Builder + public GameResultDto(Integer matchSetCount, List matchRankResultDtos) { + this.matchSetCount = matchSetCount; + this.matchRankResultDtos = matchRankResultDtos; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchCallAdminDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchCallAdminDto.java new file mode 100644 index 00000000..e949fb69 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchCallAdminDto.java @@ -0,0 +1,13 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Data; + +@Data +public class MatchCallAdminDto { + + Integer matchRound; + + String matchName; + + String callName; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchInfoDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchInfoDto.java new file mode 100644 index 00000000..59a9f0f8 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchInfoDto.java @@ -0,0 +1,51 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import leaguehub.leaguehubbackend.domain.match.entity.MatchStatus; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class MatchInfoDto { + + @Schema(description = "매치 이름", example = "Group A") + private String matchName; + + @Schema(description = "해당 매치의 상세보기 or 체크인하기 위한 매치 링크", example = "1") + private Long matchId; + + @Schema(description = "매치 상태", example = "대기 | 경기 중 | 경기 종료") + private MatchStatus matchStatus; + + @Schema(description = "몇 강", example = "64(강), 32(강), 16(강)") + private Integer matchRound; + + @Schema(description = "해당 매치 경기 현재 횟수", example = "1(회)") + private Integer matchCurrentSet; + + @Schema(description = "해당 매치 최대 경기 횟수", example = "3(회)") + private Integer matchSetCount; + + @Schema(description = "매치에 속해있는 플레이어의 정보", example = "배열로 반환") + private List matchPlayerInfoList; + + @Schema(description = "매치의 알람", example = "true, false") + private boolean alarm; + + @Builder + public MatchInfoDto(String matchName, Long matchId, MatchStatus matchStatus, Integer matchRound, Integer matchCurrentSet, + Integer matchSetCount, List matchPlayerInfoList, boolean matchAlarm) { + this.matchName = matchName; + this.matchId = matchId; + this.matchStatus = matchStatus; + this.matchRound = matchRound; + this.matchCurrentSet = matchCurrentSet; + this.matchSetCount = matchSetCount; + this.matchPlayerInfoList = matchPlayerInfoList; + this.alarm = matchAlarm; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchMessage.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchMessage.java new file mode 100644 index 00000000..bdef2931 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchMessage.java @@ -0,0 +1,35 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import leaguehub.leaguehubbackend.domain.match.entity.MessageType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MatchMessage { + + private String channelLink; + + private String content; + + private Long matchId; + + private Long participantId; + + private String adminName; + + private String accessToken; + + private LocalDateTime timestamp; + + private MessageType type; + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchPlayerInfo.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchPlayerInfo.java new file mode 100644 index 00000000..25af97f1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchPlayerInfo.java @@ -0,0 +1,55 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayerResultStatus; +import leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class MatchPlayerInfo { + + @Schema(description = "플레이어의 matchPlayerId", example = "1") + private Long matchPlayerId; + + @Schema(description = "플레이어의 ParticipantId", example = "2") + private Long participantId; + + @Schema(description = "플레이어의 게임 닉네임", example = "돈절래") + private String gameId; + + @Schema(description = "플레이어의 게임 티어", example = "Diamond II") + private String gameTier; + + @Schema(description = "플레이어의 체크인 상태", example = "READY, WAITING") + private PlayerStatus playerStatus; + + @Schema(description = "참가자 점수", example = "8(점), 5(점), ...") + private Integer score; + + @Schema(description = "참가자 순위", example = "1, 2, 3, 3, 5...") + private Integer matchRank; + + @Schema(description = "참가자 프로필 이미지 주소", example = "https://league.s3.ap-northeast-2.amazonaws.com/imgSrc.png") + private String profileSrc; + + @Schema(description = "매치 결과 상태", example = "진행중 | 탈락 | 다음 라운드로 진출 | 실격") + private MatchPlayerResultStatus matchPlayerResultStatus; + + @Builder + public MatchPlayerInfo(Long matchPlayerId, Long participantId, String gameId, String gameTier, PlayerStatus playerStatus, Integer score, MatchPlayerResultStatus matchPlayerResultStatus, String profileSrc, Integer matchRank) { + this.matchPlayerId = matchPlayerId; + this.participantId = participantId; + this.gameId = gameId; + this.gameTier = gameTier; + this.playerStatus = playerStatus; + this.score = score; + this.matchPlayerResultStatus = matchPlayerResultStatus; + this.profileSrc = profileSrc; + this.matchRank = matchRank; + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRankResultDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRankResultDto.java new file mode 100644 index 00000000..51a7bf1f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRankResultDto.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class MatchRankResultDto { + + private String gameId; + + private Integer placement; + + public MatchRankResultDto(String gameId, Integer placement) { + this.gameId = gameId; + this.placement = placement; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRoundInfoDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRoundInfoDto.java new file mode 100644 index 00000000..ccf974e5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRoundInfoDto.java @@ -0,0 +1,13 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class MatchRoundInfoDto { + + private String myGameId; + + private List matchInfoDtoList; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRoundListDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRoundListDto.java new file mode 100644 index 00000000..03da44d9 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchRoundListDto.java @@ -0,0 +1,13 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class MatchRoundListDto { + + private Integer liveRound; + + private List roundList; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchScoreInfoDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchScoreInfoDto.java new file mode 100644 index 00000000..7414b206 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchScoreInfoDto.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class MatchScoreInfoDto { + private Long requestMatchPlayerId; + + private Integer matchRound; + + private Integer matchCurrentSet; + + private Integer matchSetCount; + + + private List matchPlayerInfos; + + private List matchMessages; + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetCountDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetCountDto.java new file mode 100644 index 00000000..5a798b92 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetCountDto.java @@ -0,0 +1,13 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@NoArgsConstructor +public class MatchSetCountDto { + + private List matchSetCountList; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetReadyMessage.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetReadyMessage.java new file mode 100644 index 00000000..adbd0669 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetReadyMessage.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@AllArgsConstructor +@Data +@NoArgsConstructor +@ToString +public class MatchSetReadyMessage { + private Long matchPlayerId; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetStatusMessage.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetStatusMessage.java new file mode 100644 index 00000000..f259adc6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MatchSetStatusMessage.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.ToString; + +@AllArgsConstructor +@Data +@ToString +public class MatchSetStatusMessage { + private Long playerId; + private PlayerStatus status; +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MyMatchDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MyMatchDto.java new file mode 100644 index 00000000..4882835b --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/MyMatchDto.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class MyMatchDto { + + @Schema(description = "진행중인 매치 라운드", example = "1, 2, 3 없으면 0") + Integer myMatchRound; + + @Schema(description = "진행중인 매치 PK", example = "1, 2, 3 없으면 0") + Long myMatchId; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/RiotAPIDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/RiotAPIDto.java new file mode 100644 index 00000000..ca1fd7b4 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/dto/RiotAPIDto.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.match.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class RiotAPIDto { + + private String matchUuid; + + private List matchRankResultDtoList; + + public RiotAPIDto(String matchUuid, List matchRankResultDtoList) { + this.matchUuid = matchUuid; + this.matchRankResultDtoList = matchRankResultDtoList; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/Match.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/Match.java new file mode 100644 index 00000000..634a5535 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/Match.java @@ -0,0 +1,81 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import leaguehub.leaguehubbackend.global.audit.GlobalConstant; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; + +@Getter +@NoArgsConstructor +@Entity +public class Match extends BaseTimeEntity { + + @Id + @Column(name = "match_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private MatchStatus matchStatus; + + private Integer matchRound; + + private String matchName; + + private String matchPasswd; + + private Integer matchSetCount; + + private Integer matchCurrentSet; + + private boolean alarm; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "channel_id") + private Channel channel; + + @Builder + public Match(MatchStatus matchStatus, Integer matchRound, String matchName, String matchPasswd) { + this.matchStatus = matchStatus; + this.matchRound = matchRound; + this.matchName = matchName; + this.matchPasswd = matchPasswd; + } + + public static Match createMatch(Integer matchRound, Channel channel, String matchName) { + Match match = new Match(); + match.matchStatus = MatchStatus.READY; + match.matchRound = matchRound; + match.matchName = matchName; + match.matchPasswd = GlobalConstant.NO_DATA.getData(); + match.matchCurrentSet = 1; + match.matchSetCount = 3; + match.channel = channel; + match.alarm = false; + + return match; + } + + public void updateMatchStatus(MatchStatus matchStatus) { + this.matchStatus = matchStatus; + } + + public void updateMatchSetCount(Integer matchSetCount) { this.matchSetCount = matchSetCount; } + + public void updateCurrentMatchSet(Integer matchCurrentSet) { + this.matchCurrentSet = matchCurrentSet; + } + + public void updateCallAlarm(){ this.alarm = true; } + + public void updateOffAlarm(){ this.alarm = false; } + + public void deleteChannel() { + this.channel = null; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchPlayer.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchPlayer.java new file mode 100644 index 00000000..fe63992b --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchPlayer.java @@ -0,0 +1,68 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import static jakarta.persistence.FetchType.LAZY; + +@Getter +@NoArgsConstructor +@Entity +public class MatchPlayer extends BaseTimeEntity { + + @Id + @Column(name = "match_player_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer playerScore; + + @Enumerated(EnumType.STRING) + private PlayerStatus playerStatus; + + @Enumerated(EnumType.STRING) + private MatchPlayerResultStatus matchPlayerResultStatus; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "participant_id") + private Participant participant; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "match_id") + private Match match; + + public static MatchPlayer createMatchPlayer(Participant participant, Match match){ + MatchPlayer matchPlayer = new MatchPlayer(); + matchPlayer.playerStatus = PlayerStatus.WAITING; + matchPlayer.matchPlayerResultStatus = MatchPlayerResultStatus.PROGRESS; + matchPlayer.participant = participant; + matchPlayer.playerScore = 0; + matchPlayer.match = match; + + return matchPlayer; + } + + public void updateMatchPlayerScore(Integer placement) { + this.playerScore += 9 - placement; + } + + public void updatePlayerCheckInStatus(PlayerStatus playerStatus) { + this.playerStatus = playerStatus; + } + + public void updateMatchPlayerScoreDisqualified(){ + this.playerScore = -1; + } + + public void updateMatchPlayerResultStatus(MatchPlayerResultStatus matchPlayerResultStatus) { + this.matchPlayerResultStatus = matchPlayerResultStatus; + } + + public void deleteParticipantAndMatch() { + this.participant = null; + this.match = null; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchPlayerResultStatus.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchPlayerResultStatus.java new file mode 100644 index 00000000..73263295 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchPlayerResultStatus.java @@ -0,0 +1,5 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +public enum MatchPlayerResultStatus { + ADVANCE, DROPOUT, DISQUALIFICATION, PROGRESS +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchRank.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchRank.java new file mode 100644 index 00000000..b1b40aad --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchRank.java @@ -0,0 +1,39 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MatchRank extends BaseTimeEntity { + + @Id + @Column(name = "match_rank_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "match_set_id") + private MatchSet matchSet; + + private String gameId; + + private Integer placement; + + public static MatchRank createMatchRank(MatchSet matchSet,String gameId, Integer placement) { + MatchRank matchRank = new MatchRank(); + matchRank.matchSet = matchSet; + matchRank.gameId = gameId; + matchRank.placement = placement; + + return matchRank; + } + + public void deleteMatchSet() { + this.matchSet = null; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchSet.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchSet.java new file mode 100644 index 00000000..f95341e1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchSet.java @@ -0,0 +1,60 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Getter +@Entity +@NoArgsConstructor +public class MatchSet extends BaseTimeEntity { + + @Id + @Column(name = "match_set_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "match_id") + private Match match; + + @Column(unique = true, name = "riot_match_uuid") + private String riotMatchUuid; + + private Boolean updateScore; + + private Integer setCount; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "matchSet", cascade = CascadeType.REMOVE, orphanRemoval = true) + List matchRankList = new ArrayList<>(); + + public void updateRiotMatchUuid(String riotMatchUuid) { + this.riotMatchUuid = riotMatchUuid; + } + + public void updateScore(boolean updateScore) { + this.updateScore = updateScore; + } + + public static MatchSet createMatchSet(Match match, Integer setCount){ + MatchSet matchSet = new MatchSet(); + matchSet.match = match; + matchSet.updateScore = false; + matchSet.setCount = setCount; + + return matchSet; + } + + public void addMatchRankList(List matchRankList) { + this.matchRankList = matchRankList; + } + + public void deleteMatchAndMatchRankList() { + this.match = null; + this.matchRankList.clear(); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchStatus.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchStatus.java new file mode 100644 index 00000000..0e95626f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MatchStatus.java @@ -0,0 +1,5 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +public enum MatchStatus { + READY, PROGRESS, END +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MessageType.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MessageType.java new file mode 100644 index 00000000..655fcad1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/MessageType.java @@ -0,0 +1,8 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +public enum MessageType { + USER, + ALERT, + ADMIN + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/PlayerStatus.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/PlayerStatus.java new file mode 100644 index 00000000..417ec462 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/entity/PlayerStatus.java @@ -0,0 +1,11 @@ +package leaguehub.leaguehubbackend.domain.match.entity; + +public enum PlayerStatus { + READY(1), WAITING(0), DISQUALIFICATION(2); + + private final int status; + + PlayerStatus(int status) { this.status = status; } + + public int getStatus() { return status; } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/MatchExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/MatchExceptionCode.java new file mode 100644 index 00000000..2d346f38 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/MatchExceptionCode.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.domain.match.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@Getter +@RequiredArgsConstructor +public enum MatchExceptionCode implements ExceptionCode { + + MATCH_NOT_FOUND(NOT_FOUND, "MA-C-001", "유효하지 않은 경기입니다."), + MATCH_RESULT_NOT_FOUNT(NOT_FOUND, "MA-C-002", "매치 결과를 찾을 수 없습니다."), + MATCH_NOT_ENOUGH_PLAYER(BAD_REQUEST, "MA-C-003", "매치 인원수가 충분하지 않습니다."), + MATCH_ALREADY_UPDATE(BAD_REQUEST, "MA-C-004", "이미 매치의 점수가 업데이트 되었습니다."), + MATCH_PLAYER_NOT_FOUND(NOT_FOUND, "MA-C-005", "해당 매치 플레이어가 없습니다."), + MATCH_NOT_END(BAD_REQUEST, "MA-C-006", "이전 경기가 끝나지 않았습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/MatchExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/MatchExceptionHandler.java new file mode 100644 index 00000000..2d6c8c4e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/MatchExceptionHandler.java @@ -0,0 +1,81 @@ +package leaguehub.leaguehubbackend.domain.match.exception; + +import leaguehub.leaguehubbackend.domain.match.exception.exception.*; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class MatchExceptionHandler { + + @ExceptionHandler(MatchNotFoundException.class) + public ResponseEntity matchNotFoundException( + MatchNotFoundException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(MatchResultIdNotFoundException.class) + public ResponseEntity matchResultIdNotFoundException( + MatchResultIdNotFoundException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(MatchNotEnoughPlayerException.class) + public ResponseEntity matchNotEnoughPlayerException( + MatchNotEnoughPlayerException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(MatchAlreadyUpdateException.class) + public ResponseEntity matchAlreadyUpdateException( + MatchAlreadyUpdateException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(MatchNotEndException.class) + public ResponseEntity MatchNotEndException( + MatchNotEndException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/ChatExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/ChatExceptionCode.java new file mode 100644 index 00000000..88eb4f97 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/ChatExceptionCode.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.match.exception.chat; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +@Getter +@RequiredArgsConstructor +public enum ChatExceptionCode implements ExceptionCode { + + MATCH_CHAT_CONVERSION_EXCEPTION(INTERNAL_SERVER_ERROR, "CH-C-001", "메시지 변환 중 실패했습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/ChatExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/ChatExceptionHandler.java new file mode 100644 index 00000000..9d948b86 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/ChatExceptionHandler.java @@ -0,0 +1,30 @@ +package leaguehub.leaguehubbackend.domain.match.exception.chat; + +import leaguehub.leaguehubbackend.domain.match.exception.chat.exception.MatchChatMessageConversionException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class ChatExceptionHandler { + + @ExceptionHandler(MatchChatMessageConversionException.class) + public ResponseEntity invalidEmailAddress( + MatchChatMessageConversionException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/exception/MatchChatMessageConversionException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/exception/MatchChatMessageConversionException.java new file mode 100644 index 00000000..d542a01e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/chat/exception/MatchChatMessageConversionException.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.domain.match.exception.chat.exception; + +import leaguehub.leaguehubbackend.domain.match.exception.chat.ChatExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.chat.ChatExceptionCode.MATCH_CHAT_CONVERSION_EXCEPTION; + +public class MatchChatMessageConversionException extends RuntimeException{ + + private final ChatExceptionCode exceptionCode; + + public MatchChatMessageConversionException() { + super(MATCH_CHAT_CONVERSION_EXCEPTION.getMessage()); + this.exceptionCode = MATCH_CHAT_CONVERSION_EXCEPTION; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} + diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchAlreadyUpdateException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchAlreadyUpdateException.java new file mode 100644 index 00000000..14dff7e7 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchAlreadyUpdateException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.match.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_ALREADY_UPDATE; + +public class MatchAlreadyUpdateException extends RuntimeException { + + private final ExceptionCode exceptionCode; + + public MatchAlreadyUpdateException() { + super(MATCH_ALREADY_UPDATE.getMessage()); + this.exceptionCode = MATCH_ALREADY_UPDATE; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotEndException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotEndException.java new file mode 100644 index 00000000..10913188 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotEndException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.match.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_NOT_END; + + +public class MatchNotEndException extends RuntimeException{ + private final ExceptionCode exceptionCode; + + public MatchNotEndException(){ + super(MATCH_NOT_END.getMessage()); + this.exceptionCode = MATCH_NOT_END; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotEnoughPlayerException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotEnoughPlayerException.java new file mode 100644 index 00000000..25adf364 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotEnoughPlayerException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.match.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_NOT_ENOUGH_PLAYER; + +public class MatchNotEnoughPlayerException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public MatchNotEnoughPlayerException(){ + super(MATCH_NOT_ENOUGH_PLAYER.getMessage()); + this.exceptionCode = MATCH_NOT_ENOUGH_PLAYER; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotFoundException.java new file mode 100644 index 00000000..6c51bb0b --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchNotFoundException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.match.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_NOT_FOUND; + +public class MatchNotFoundException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public MatchNotFoundException(){ + super(MATCH_NOT_FOUND.getMessage()); + this.exceptionCode = MATCH_NOT_FOUND; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchPlayerNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchPlayerNotFoundException.java new file mode 100644 index 00000000..404a799e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchPlayerNotFoundException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.match.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_PLAYER_NOT_FOUND; + + +public class MatchPlayerNotFoundException extends RuntimeException { + + private final ExceptionCode exceptionCode; + + public MatchPlayerNotFoundException(){ + super(MATCH_PLAYER_NOT_FOUND.getMessage()); + this.exceptionCode = MATCH_PLAYER_NOT_FOUND; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchResultIdNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchResultIdNotFoundException.java new file mode 100644 index 00000000..2cd0df70 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/exception/exception/MatchResultIdNotFoundException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.match.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_NOT_FOUND; +import static leaguehub.leaguehubbackend.domain.match.exception.MatchExceptionCode.MATCH_RESULT_NOT_FOUNT; + +public class MatchResultIdNotFoundException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public MatchResultIdNotFoundException(){ + super(MATCH_NOT_FOUND.getMessage()); + this.exceptionCode = MATCH_RESULT_NOT_FOUNT; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchPlayerRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchPlayerRepository.java new file mode 100644 index 00000000..8c6beb4d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchPlayerRepository.java @@ -0,0 +1,38 @@ +package leaguehub.leaguehubbackend.domain.match.repository; + +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface MatchPlayerRepository extends JpaRepository { + + List findAllByMatch_Id(Long matchId); + + List findAllByMatch_MatchNameAndMatch_MatchRound(String matchName, Integer matchRound); + + @Query("select mp from MatchPlayer mp join fetch mp.participant where mp.match.id = :matchId") + List findAllByMatch_IdOrderByPlayerScoreDesc(@Param("matchId") Long matchId); + + @Query("select mp from MatchPlayer mp join fetch mp.participant join fetch mp.match where mp.match.id = :matchId " + + "order by mp.playerScore desc, mp.participant.gameId") + List findMatchPlayersAndMatchAndParticipantByMatchId(@Param("matchId") Long matchId); + + @Query("select mp from MatchPlayer mp join fetch mp.participant join fetch mp.match where mp.match.id = :matchId " + + "and mp.matchPlayerResultStatus <> leaguehub.leaguehubbackend.domain.match.entity.MatchPlayerResultStatus.DISQUALIFICATION " + + "and mp.match.matchStatus <> leaguehub.leaguehubbackend.domain.match.entity.MatchStatus.END " + + "order by mp.playerScore desc, mp.participant.gameId") + List findMatchPlayersWithoutDisqualification(@Param("matchId") Long matchId); + + Optional findByParticipantIdAndMatchId(Long participantId, Long matchId); + + + List findMatchPlayersByParticipantId(Long participantId); + + Optional findMatchPlayerByIdAndMatch_Id(@Param("matchPlayerId") Long matchPlayerId, @Param("matchId") Long matchId); + + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchRankRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchRankRepository.java new file mode 100644 index 00000000..74147407 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchRankRepository.java @@ -0,0 +1,7 @@ +package leaguehub.leaguehubbackend.domain.match.repository; + +import leaguehub.leaguehubbackend.domain.match.entity.MatchRank; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MatchRankRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchRepository.java new file mode 100644 index 00000000..d5a661de --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchRepository.java @@ -0,0 +1,17 @@ +package leaguehub.leaguehubbackend.domain.match.repository; + +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface MatchRepository extends JpaRepository { + List findAllByChannel_ChannelLinkAndMatchRoundOrderByMatchName(String channelLink, Integer matchRound); + + List findAllByChannel_ChannelLink(String channelLink); + + List findAllByChannel_ChannelLinkOrderByMatchRoundDesc(String channelLink); + + Optional findById(Long matchId); +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchSetRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchSetRepository.java new file mode 100644 index 00000000..da52b628 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/repository/MatchSetRepository.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.match.repository; + +import leaguehub.leaguehubbackend.domain.match.entity.MatchSet; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface MatchSetRepository extends JpaRepository { + Optional findMatchSetByMatchIdAndAndSetCount(Long matchId, Integer setCount); + + List findAllByMatch_Channel_ChannelLink(String channelLink); + + @Query("select distinct ms from MatchSet ms join fetch ms.matchRankList where ms.match.id = :matchId") + List findMatchSetsByMatch_Id(@Param("matchId") Long matchId); + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchPlayerService.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchPlayerService.java new file mode 100644 index 00000000..46bb149d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchPlayerService.java @@ -0,0 +1,432 @@ +package leaguehub.leaguehubbackend.domain.match.service; + +import leaguehub.leaguehubbackend.domain.match.dto.*; +import leaguehub.leaguehubbackend.domain.match.entity.*; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchAlreadyUpdateException; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchNotFoundException; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchPlayerNotFoundException; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchResultIdNotFoundException; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRankRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchSetRepository; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantIdResponseDto; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static leaguehub.leaguehubbackend.domain.match.entity.MatchPlayerResultStatus.ADVANCE; +import static leaguehub.leaguehubbackend.domain.match.entity.MatchPlayerResultStatus.DROPOUT; +import static leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus.READY; +import static leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus.WAITING; + +@Service +@RequiredArgsConstructor +@Transactional +public class MatchPlayerService { + + private final JSONParser jsonParser; + private final MatchPlayerRepository matchPlayerRepository; + private final MatchSetRepository matchSetRepository; + private final MatchService matchService; + private final MatchRankRepository matchRankRepository; + + private final MatchWebClientService matchWebClientService; + private final MatchQueryService matchQueryService; + + + @SneakyThrows + public List setPlacement(JSONArray participantList, List findMatchPlayerList) { + List dtoList = new ArrayList<>(); + + + for (int jsonIndex = 0; jsonIndex < 8; jsonIndex++) { + + JSONObject participants = (JSONObject) jsonParser.parse(participantList.get(jsonIndex).toString()); + + Integer placement = Integer.parseInt(participants.get("placement").toString()); + + String parti1puuid = participants.get("puuid").toString(); + + MatchRankResultDto matchRankResultDto = new MatchRankResultDto(); + findMatchPlayerList.stream() + .map(MatchPlayer::getParticipant) + .filter(participant -> participant.getPuuid().equalsIgnoreCase(parti1puuid)) + .forEach(participant -> { + matchRankResultDto.setGameId(participant.getGameId()); + matchRankResultDto.setPlacement(placement); + dtoList.add(matchRankResultDto); + }); + } + return dtoList; + } + + + /** + * 라이엇 API로 경기 결과 호출 + * + * @param gameId + * @return matchRankResultDto + */ + @SneakyThrows + public RiotAPIDto getMatchDetailFromRiot(String gameId, List findMatchPlayerList, Long endTime) { + String puuid = matchWebClientService.getSummonerPuuid(gameId); + String riotMatchUuid = matchWebClientService.getMatch(puuid, endTime); + + JSONObject matchDetailJSON = matchWebClientService.responseMatchDetail(riotMatchUuid); + + + JSONObject info = (JSONObject) jsonParser.parse(matchDetailJSON.get("info").toString()); + JSONArray participantList = (JSONArray) jsonParser.parse(info.get("participants").toString()); + + List matchRankResultDtoList = setPlacement(participantList, findMatchPlayerList); + + + return new RiotAPIDto(riotMatchUuid, matchRankResultDtoList); + } + + public MatchInfoDto updateMatchPlayerScore(Long matchId, Integer setCount, Long endTime) { + List findMatchPlayerList = matchPlayerRepository.findMatchPlayersWithoutDisqualification(matchId); + + if(findMatchPlayerList.size() == 0) throw new MatchNotFoundException(); + + RiotAPIDto matchDetailFromRiot = + getMatchDetailFromRiot(findMatchPlayerList.get(0).getParticipant().getGameId(), + findMatchPlayerList, endTime); + + MatchSet matchSet = getMatchSet(matchId, setCount); + + if (matchSet.getRiotMatchUuid() == null) matchSet.updateRiotMatchUuid(matchDetailFromRiot.getMatchUuid()); + + List matchRankResultDtoList = matchDetailFromRiot.getMatchRankResultDtoList(); + validMatchResult(findMatchPlayerList, matchRankResultDtoList); + Collections.sort(matchRankResultDtoList, Comparator.comparing(MatchRankResultDto::getPlacement)); + + replaceMatchResult(findMatchPlayerList.stream() + .map(matchPlayer -> matchPlayer.getParticipant().getGameId()).collect(Collectors.toList()), matchRankResultDtoList); + + matchRankResultDtoList + .forEach(matchRankResultDto -> + findMatchPlayerList.stream() + .filter(matchPlayer -> matchPlayer.getParticipant().getGameId().equals(matchRankResultDto.getGameId())) + .forEach(matchPlayer -> matchPlayer.updateMatchPlayerScore(matchRankResultDto.getPlacement())) + ); + + + matchSet.updateScore(true); + + List matchRanks = matchRankResultDtoList.stream() + .map(dto -> MatchRank.createMatchRank(matchSet, dto.getGameId(), dto.getPlacement())) + .collect(Collectors.toList()); + matchSet.addMatchRankList(matchRanks); + matchRankRepository.saveAll(matchRanks); + + findMatchPlayerList.stream() + .forEach(matchPlayer -> matchPlayer.updatePlayerCheckInStatus(WAITING)); + + Match match = findMatchPlayerList.get(0).getMatch(); + checkMatchEnd(matchSet, match); + + List allByMatchId = matchPlayerRepository.findAllByMatch_Id(matchId); + MatchInfoDto matchInfoDto = matchService.convertMatchInfoDto(match, allByMatchId); + + return matchInfoDto; + } + + private void replaceMatchResult(List findMatchPlayerGameIdList, List matchRankResultDtoList) { + matchRankResultDtoList.removeIf(matchRankResultDto -> + !findMatchPlayerGameIdList.contains(matchRankResultDto.getGameId())); + + matchRankResultDtoList.stream().sorted(Comparator.comparing(MatchRankResultDto::getPlacement)); + + IntStream.range(0, matchRankResultDtoList.size()) + .forEach(i -> matchRankResultDtoList.get(i).setPlacement(i + 1)); + } + + + + /** + * MatchResult에 실격한 멤버를 제외한 모든 멤버가 있는지 체크 + * + * @param findMatchPlayerList + * @param matchRankResultDtoList + */ + private void validMatchResult(List findMatchPlayerList, List matchRankResultDtoList) { + long count = matchRankResultDtoList.stream() + .flatMap(dto -> findMatchPlayerList.stream() + .filter(player -> dto.getGameId().equals(player.getParticipant().getGameId()))) + .count(); + + if (count != findMatchPlayerList.size()) { + throw new MatchResultIdNotFoundException(); + } + } + + /** + * 매치가 끝난지 체크하는 로직 + * 매치가 끝났다면 매치 상태를 업데이트하고 updateEndMatchResult로 진출자, 탈락자를 결정한다. + * + * @param matchSet + * @param match + */ + private void checkMatchEnd(MatchSet matchSet, Match match) { + if (match.getMatchSetCount().equals(matchSet.getSetCount())) { + match.updateMatchStatus(MatchStatus.END); + updateEndMatchResult(match); + } else { + match.updateCurrentMatchSet(matchSet.getSetCount() + 1); + } + } + + private List getMatchPlayers(Long matchId) { + List findMatchPlayerList = matchPlayerRepository.findMatchPlayersAndMatchAndParticipantByMatchId(matchId); + + if (findMatchPlayerList.size() == 0) { + throw new MatchNotFoundException(); + } + return findMatchPlayerList; + } + + + private MatchSet getMatchSet(Long matchId, Integer setCount) { + MatchSet matchSet = matchSetRepository.findMatchSetByMatchIdAndAndSetCount(matchId, setCount) + .orElseThrow(() -> new MatchNotFoundException()); + + if (matchSet.getUpdateScore()) { + throw new MatchAlreadyUpdateException(); + } + + return matchSet; + } + + private MatchPlayer findMatchPlayer(Long matchPlayerId, Long matchId) { + return matchPlayerRepository.findMatchPlayerByIdAndMatch_Id(matchPlayerId, matchId) + .orElseThrow(MatchPlayerNotFoundException::new); + } + + @Transactional + public ParticipantIdResponseDto markPlayerAsReady(MatchSetReadyMessage message, String matchIdStr) { + + Long matchId = Long.valueOf(matchIdStr); + Long matchPlayerId = message.getMatchPlayerId(); + + MatchPlayer matchPlayer = findMatchPlayer(matchPlayerId, matchId); + + if(matchPlayer.getMatchPlayerResultStatus() != MatchPlayerResultStatus.PROGRESS) { + throw new MatchAlreadyUpdateException(); + } + + if(matchPlayer.getMatchPlayerResultStatus() == MatchPlayerResultStatus.DISQUALIFICATION){ + throw new InvalidParticipantAuthException(); + } + + matchPlayer.updatePlayerCheckInStatus(READY); + + return new ParticipantIdResponseDto(message.getMatchPlayerId(), READY.getStatus()); + } + + public List getAllPlayerStatusForMatch(Long matchId) { + List matchPlayers = matchPlayerRepository.findAllByMatch_Id(matchId); + + if (matchPlayers.isEmpty()) { + throw new MatchNotFoundException(); + } + + return matchPlayers.stream() + .map(mp -> new MatchSetStatusMessage(mp.getId(), mp.getPlayerStatus())) + .collect(Collectors.toList()); + } + + /** + * 매치 종료 후 진출자, 탈락자를 결정한다. + * 실격을 제외한 매치 플레이어들을 점수대로 정렬해 불러와서 + * 4번째 위치한 선수를 기준으로 동점자, 진출자, 탈락자를 결정한다. + * + * @param match + */ + public void updateEndMatchResult(Match match) { + List matchPlayersWithoutDisqualification = matchPlayerRepository.findMatchPlayersWithoutDisqualification(match.getId()); + Integer advanceScore = matchPlayersWithoutDisqualification.get(3).getPlayerScore(); + + long winCount = advanceMatchPlayer(matchPlayersWithoutDisqualification, advanceScore); + + List tieMatchPlayerList = getTiePlayerList(matchPlayersWithoutDisqualification, advanceScore); + + dropoutMatchPlayerWithScore(matchPlayersWithoutDisqualification, advanceScore); + + if (winCount == 3 && tieMatchPlayerList.size() == 1) { + MatchPlayer matchPlayer = tieMatchPlayerList.get(0); + matchPlayer.updateMatchPlayerResultStatus(ADVANCE); + } else if (winCount < 4 && tieMatchPlayerList.size() > 1) { + tieBreaker(tieMatchPlayerList, match.getId(), 4 - Long.valueOf(winCount).intValue()); + } + } + + private void dropoutMatchPlayerWithScore(List matchPlayersWithoutDisqualification, Integer advanceScore) { + matchPlayersWithoutDisqualification.stream() + .filter(mp -> mp.getPlayerScore() < advanceScore) + .forEach(mp -> { + dropoutMatchPlayerAndParticipantStatus(mp); + }); + } + + private void dropoutMatchPlayerAndParticipantStatus(MatchPlayer mp) { + mp.updateMatchPlayerResultStatus(DROPOUT); + mp.getParticipant().dropoutParticipantStatus(); + } + + @NotNull + private List getTiePlayerList(List matchPlayersWithoutDisqualification, Integer advanceScore) { + List tieMatchPlayerList = matchPlayersWithoutDisqualification.stream() + .filter(mp -> mp.getPlayerScore().equals(advanceScore)) + .collect(Collectors.toList()); + return tieMatchPlayerList; + } + + private long advanceMatchPlayer(List matchPlayersWithoutDisqualification, Integer advanceScore) { + long winCount = matchPlayersWithoutDisqualification.stream() + .filter(mp -> mp.getPlayerScore() > advanceScore) + .peek(mp -> mp.updateMatchPlayerResultStatus(ADVANCE)) + .count(); + return winCount; + } + + + /** + * 동점자 처리 로직 + * i. 1등을 많이 한 플레이어 + * ii. 가장 최근 게임 등수에서 가장 높은 순위를 가진 플레이어 + * + * @param tieMatchPlayerList + * @param matchId + */ + public void tieBreaker(List tieMatchPlayerList, Long matchId, Integer advanceCount) { + List matchSetResult = matchQueryService.getGameResult(matchId); + + //게임 Id만 뽑는 로직 + List tiePlayerGameIdList = tieMatchPlayerList.stream() + .map(matchPlayer -> matchPlayer.getParticipant().getGameId()) + .collect(Collectors.toList()); + + int tiePlayerCount = tieMatchPlayerList.size(); + + //가장 많이 1등 한 사람들을 뽑는 로직 + List firstPlayer = mostFirstPlayer(matchSetResult, tiePlayerGameIdList); + + //진출 숫자가 1등 플레이어보다 많으면 1등 플레이어 모두 진출 + if (advanceCount - firstPlayer.size() >= 0) { + //1등 플레이어 제외 모두 drop으로 바뀜 + updateMatchPlayerStatus(tieMatchPlayerList, firstPlayer); + tieMatchPlayerList.removeIf(matchPlayer -> firstPlayer.contains(matchPlayer.getParticipant().getGameId())); + tiePlayerGameIdList.removeAll(firstPlayer); + advanceCount -= firstPlayer.size(); + tiePlayerCount -= firstPlayer.size(); + } else { + //그게 아니면 1등 플레이어들만 남기고 전부 탈락 + updateMatchPlayerStatus(tieMatchPlayerList, firstPlayer); + tieMatchPlayerList.removeIf(matchPlayer -> !firstPlayer.contains(matchPlayer.getParticipant().getGameId())); + } + + //advanceCount가 0보다 크면 들어감, 만약 advanceCount가 0이면, 위에서 전부 Drop 했기 때문에 그냥 넘어감 + if (advanceCount > 0) { + //1등 플레이어 로직으로 걸러진 동점자들이 남은 advanceCount보다 크면 최근 경기 등수 대로, 만약 작거나 같으면 전부 진출 + if (tiePlayerCount > advanceCount) { + List tiePlayerGameIdOfAdvanceList = lastGamePlacement(tiePlayerGameIdList, matchSetResult, advanceCount); + updateMatchPlayerStatus(tieMatchPlayerList, tiePlayerGameIdOfAdvanceList); + } else { + updateMatchPlayerStatus(tieMatchPlayerList, tiePlayerGameIdList); + } + } + } + + private void updateMatchPlayerStatus(List tieMatchPlayerList, List tiePlayerGameIdList) { + for (MatchPlayer matchPlayer : tieMatchPlayerList) { + if (tiePlayerGameIdList.contains(matchPlayer.getParticipant().getGameId())) { + matchPlayer.updateMatchPlayerResultStatus(ADVANCE); + } else { + dropoutMatchPlayerAndParticipantStatus(matchPlayer); + } + } + } + + + /** + * 가장 1등을 많이 한 플레이어(들)을 가져오는 로직 + * 없으면 동점자들을 들어온 그대로 다시 반환한다. + * 있는데 여러명이라면 여러명을 다시 반환해 tiePlayerGameIdList로 만들어버린다. + * + * @param matchSetResult + * @param tiePlayerGameIdList + * @return + */ + private List mostFirstPlayer(List matchSetResult, List tiePlayerGameIdList) { + Map countFirstPlayerMap = new ConcurrentHashMap<>(); + + int maxCount = 0; + + for (GameResultDto gameResult : matchSetResult) { + for (MatchRankResultDto matchRankResultDto : gameResult.getMatchRankResultDtos()) { + if (matchRankResultDto.getPlacement() == 1) { + String gameId = matchRankResultDto.getGameId(); + int count = countFirstPlayerMap.getOrDefault(gameId, 0) + 1; + countFirstPlayerMap.put(gameId, count); + maxCount = Math.max(maxCount, count); + } + } + } + + List mostFirstPlayerInTieList = new ArrayList<>(); + for (String gameId : countFirstPlayerMap.keySet()) { + if (maxCount == countFirstPlayerMap.get(gameId) && tiePlayerGameIdList.contains(gameId)) { + mostFirstPlayerInTieList.add(gameId); + } + } + + if (!mostFirstPlayerInTieList.isEmpty()) { + return mostFirstPlayerInTieList; + } + + return tiePlayerGameIdList; + } + + /** + * 가장 최근 게임에서 가장 높은 등수를 가진 참가자를 뽑는 로직 + * 가장 최근 게임은 가장 최근에 수정된 gameResult 로 판단함 + * + * @param tiePlayerGameIdList + * @param matchSetResult + * @return + */ + private List lastGamePlacement(List tiePlayerGameIdList, List matchSetResult, Integer advanceCount) { + + List matchRankResultDtos = matchSetResult.stream().filter(gameResultDto -> gameResultDto.getMatchSetCount() == 3) + .findFirst().orElseThrow(() -> new MatchNotFoundException()).getMatchRankResultDtos(); + + + matchRankResultDtos.sort(Comparator.comparing(MatchRankResultDto::getPlacement)); + + List tiePlayerGameIdOfAdvanceList = new ArrayList<>(); + + + for (MatchRankResultDto matchRankResultDto : matchRankResultDtos) { + String resultGameId = matchRankResultDto.getGameId(); + if (tiePlayerGameIdList.contains(resultGameId) && advanceCount > 0) { + tiePlayerGameIdOfAdvanceList.add(resultGameId); + advanceCount--; + } + } + + return tiePlayerGameIdOfAdvanceList; + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchQueryService.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchQueryService.java new file mode 100644 index 00000000..920f76c3 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchQueryService.java @@ -0,0 +1,330 @@ +package leaguehub.leaguehubbackend.domain.match.service; + +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.match.dto.*; +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import leaguehub.leaguehubbackend.domain.match.entity.MatchSet; +import leaguehub.leaguehubbackend.domain.match.entity.MatchStatus; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchNotFoundException; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchResultIdNotFoundException; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchSetRepository; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberAuthService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static leaguehub.leaguehubbackend.domain.channel.entity.ChannelStatus.PROCEEDING; +import static leaguehub.leaguehubbackend.domain.match.entity.MatchStatus.END; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.PLAYER; +import static leaguehub.leaguehubbackend.global.audit.GlobalConstant.NO_DATA; + +@Service +@RequiredArgsConstructor +public class MatchQueryService { + + private final MatchRepository matchRepository; + private final MatchPlayerRepository matchPlayerRepository; + private final MemberService memberService; + private final MemberAuthService memberAuthService; + private final MatchSetRepository matchSetRepository; + private final MatchService matchService; + + + /** + * 해당 채널의 매치 라운드를 보여줌(64, 32, 16, 8) + * + * @param channelLink + * @return 2 4 8 16 32 64 + */ + @Transactional(readOnly = true) + public MatchRoundListDto getRoundList(String channelLink) { + Member member = memberService.findCurrentMember(); + Participant participant = matchService.getParticipant(member.getId(), channelLink); + Channel findChannel = participant.getChannel(); + + int maxPlayers = findChannel.getMaxPlayer(); + List roundList = calculateRoundList(maxPlayers); + + MatchRoundListDto roundListDto = new MatchRoundListDto(); + roundListDto.setLiveRound(0); + roundListDto.setRoundList(roundList); + + findLiveRound(channelLink, roundList, roundListDto); + + if (participant.getRole().equals(Role.HOST)) + roundListDto.setLiveRound(findChannel.getLiveRound()); + + return roundListDto; + } + + + /** + * 해당 채널의 참가한 플레이어 리스트를 반환 + * + * @param channelLink + * @param matchRound + * @return + */ + @Transactional(readOnly = true) + public MatchRoundInfoDto loadMatchPlayerList(String channelLink, Integer matchRound) { + Member member = memberService.findCurrentMember(); + Participant participant = matchService.getParticipant(member.getId(), channelLink); + + List matchList = matchService.findMatchList(channelLink, matchRound); + + List matchInfoDtoList = matchList.stream() + .map(this::createMatchInfoDto) + .collect(Collectors.toList()); + + MatchRoundInfoDto matchRoundInfoDto = new MatchRoundInfoDto(); + + findMyRoundName(participant, matchList, matchRoundInfoDto); + + matchRoundInfoDto.setMatchInfoDtoList(matchInfoDtoList); + return matchRoundInfoDto; + } + + /** + * 현재 진행중인 라운드 표시(참가자 x -> 라운드 x) + * + * @param channelLink + * @return + */ + @Transactional(readOnly = true) + public MyMatchDto getMyMatchRound(String channelLink) { + Member member = memberService.findCurrentMember(); + Participant participant = matchService.getParticipant(member.getId(), channelLink); + + MyMatchDto myMatchDto = new MyMatchDto(); + + myMatchDto.setMyMatchRound(0); + myMatchDto.setMyMatchId(0L); + + findMyMatch(channelLink, participant, myMatchDto); + + return myMatchDto; + } + + + /** + * 해당 매치의 경기 횟수 반환 + * + * @param channelLink + * @return + */ + @Transactional(readOnly = true) + public MatchSetCountDto getMatchSetCount(String channelLink) { + + List matchList = matchRepository.findAllByChannel_ChannelLinkOrderByMatchRoundDesc(channelLink); + List matchSetCountList = getMatchSetCountList(matchList); + + MatchSetCountDto matchSetCountDto = new MatchSetCountDto(); + matchSetCountDto.setMatchSetCountList(matchSetCountList); + + return matchSetCountDto; + } + + /** + * 해당 매치의 점수 정보 반환 + * + * @param channelLink + * @param matchId + * @return + */ + @Transactional(readOnly = true) + public MatchScoreInfoDto getMatchScoreInfo(String channelLink, Long matchId) { + List matchPlayers = Optional.ofNullable( + matchPlayerRepository.findMatchPlayersAndMatchAndParticipantByMatchId(matchId)) + .filter(list -> !list.isEmpty()) + .orElseThrow(MatchNotFoundException::new); + + Match match = matchRepository.findById(matchId) + .orElseThrow(MatchNotFoundException::new); + + List matchPlayerInfoList = matchService.convertMatchPlayerInfoList(matchPlayers); + + Long requestMatchPlayerId = getRequestMatchPlayerId(channelLink, matchPlayers); + + return MatchScoreInfoDto.builder() + .matchPlayerInfos(matchPlayerInfoList) + .matchRound(match.getMatchRound()) + .matchCurrentSet(match.getMatchCurrentSet()) + .matchSetCount(match.getMatchSetCount()) + .requestMatchPlayerId(requestMatchPlayerId) + .build(); + } + + /** + * 이전 경기의 결과를 보여줌 + * @param matchId + * @return + */ + @Transactional(readOnly = true) + public List getGameResult(Long matchId) { + List matchSets = matchSetRepository.findMatchSetsByMatch_Id(matchId); + if (matchSets.isEmpty()) throw new MatchResultIdNotFoundException(); + List gameResultDtoList = matchSets.stream().map(matchSet -> GameResultDto.builder() + .matchSetCount(matchSet.getSetCount()).matchRankResultDtos( + matchSet.getMatchRankList().stream().map(matchRank -> new MatchRankResultDto(matchRank.getGameId(), matchRank.getPlacement())) + .collect(Collectors.toList()) + ).build()).collect(Collectors.toList()); + + gameResultDtoList.sort(Comparator.comparing(GameResultDto::getMatchSetCount)); + + return gameResultDtoList; + } + + + private List calculateRoundList(int maxPlayers) { + List defaultroundList = Arrays.asList(0, 8, 16, 32, 64, 128, 256); + + int roundIndex = defaultroundList.indexOf(maxPlayers); + + if (roundIndex == -1) { + throw new ChannelNotFoundException();// 에러 처리 시 빈 리스트 반환 + } + + return IntStream.rangeClosed(1, roundIndex) + .boxed() + .collect(Collectors.toList()); + } + + private void findLiveRound(String channelLink, List roundList, MatchRoundListDto roundListDto) { + roundList.forEach(round -> { + List matchList = matchService.findMatchList(channelLink, round); + matchList.stream() + .filter(match -> match.getMatchStatus().equals(MatchStatus.PROGRESS)) + .findFirst() + .ifPresent(match -> roundListDto.setLiveRound(match.getMatchRound())); + } + ); + } + + private MatchInfoDto createMatchInfoDto(Match match) { + MatchInfoDto matchInfoDto = new MatchInfoDto(); + matchInfoDto.setMatchName(match.getMatchName()); + matchInfoDto.setMatchId(match.getId()); + matchInfoDto.setMatchStatus(match.getMatchStatus()); + matchInfoDto.setMatchRound(match.getMatchRound()); + matchInfoDto.setMatchCurrentSet(match.getMatchCurrentSet()); + matchInfoDto.setMatchSetCount(match.getMatchSetCount()); + matchInfoDto.setAlarm(match.isAlarm()); + + List playerList = matchPlayerRepository.findAllByMatch_IdOrderByPlayerScoreDesc(match.getId()); + List matchPlayerInfoList = createMatchPlayerInfoList(playerList); + matchInfoDto.setMatchPlayerInfoList(matchPlayerInfoList); + + return matchInfoDto; + } + + + private List createMatchPlayerInfoList(List playerList) { + List matchPlayerInfoList = playerList.stream() + .map(matchPlayer -> { + MatchPlayerInfo matchPlayerInfo = new MatchPlayerInfo(); + matchPlayerInfo.setMatchPlayerId(matchPlayer.getId()); + matchPlayerInfo.setParticipantId(matchPlayer.getParticipant().getId()); + matchPlayerInfo.setGameId(matchPlayer.getParticipant().getGameId()); + matchPlayerInfo.setGameTier(matchPlayer.getParticipant().getGameTier()); + matchPlayerInfo.setPlayerStatus(matchPlayer.getPlayerStatus()); + matchPlayerInfo.setScore(matchPlayer.getPlayerScore()); + matchPlayerInfo.setProfileSrc(matchPlayer.getParticipant().getProfileImageUrl()); + return matchPlayerInfo; + }) + .collect(Collectors.toList()); + + return matchPlayerInfoList; + + } + + + private void findMyRoundName(Participant participant, List matchList, MatchRoundInfoDto matchRoundInfoDto) { + matchRoundInfoDto.setMyGameId(NO_DATA.getData()); + + if (!participant.getGameId().equalsIgnoreCase(NO_DATA.getData())) { + matchList.forEach(match -> { + List playerList = matchPlayerRepository.findAllByMatch_IdOrderByPlayerScoreDesc(match.getId()); + playerList.stream() + .filter(player -> participant.getGameId().equalsIgnoreCase(player.getParticipant().getGameId())) + .findFirst() + .ifPresent(player -> matchRoundInfoDto.setMyGameId(participant.getGameId())); + }); + } + } + + private void findMyMatch(String channelLink, Participant participant, MyMatchDto myMatchDto) { + if (participant.getRole().equals(PLAYER) + && participant.getChannel().getChannelStatus().equals(PROCEEDING)) { + matchRepository.findAllByChannel_ChannelLink(channelLink).stream() + .filter(match -> !match.getMatchStatus().equals(END)) + .flatMap(match -> getMatchPlayerList(match).stream()) + .filter(matchPlayer -> isSameParticipant(matchPlayer, participant)) + .findFirst() + .ifPresent(matchPlayer -> setMyMatchInfo(myMatchDto, matchPlayer.getMatch())); + } + } + + private List getMatchPlayerList(Match match) { + return matchPlayerRepository.findAllByMatch_IdOrderByPlayerScoreDesc(match.getId()); + } + + private boolean isSameParticipant(MatchPlayer matchPlayer, Participant participant) { + return matchPlayer.getParticipant().getId().equals(participant.getId()); + } + + private void setMyMatchInfo(MyMatchDto mymatchDTO, Match match) { + mymatchDTO.setMyMatchId(match.getId()); + mymatchDTO.setMyMatchRound(match.getMatchRound()); + } + + private static List getMatchSetCountList(List matchList) { + List matchSetCountList = new ArrayList<>(); + int matchRound = 0; + for (Match match : matchList) { + if (matchRound == match.getMatchRound()) + continue; + else { + matchSetCountList.add(match.getMatchSetCount()); + matchRound = match.getMatchRound(); + } + } + return matchSetCountList; + } + + + private Long getRequestMatchPlayerId(String channelLink, List matchPlayers) { + if (memberAuthService.checkIfMemberIsAnonymous()) { + return 0L; + } + Member member = memberService.findCurrentMember(); + Participant participant = matchService.getParticipant(member.getId(), channelLink); + + if (participant.getRole() == Role.HOST) { + return -1L; + } + + return findRequestMatchPlayerId(member, matchPlayers); + } + + private Long findRequestMatchPlayerId(Member member, List matchPlayers) { + for (MatchPlayer mp : matchPlayers) { + if (mp.getParticipant().getMember().getId().equals(member.getId())) { + return mp.getId(); + } + } + return 0L; + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchService.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchService.java new file mode 100644 index 00000000..4cebfccc --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchService.java @@ -0,0 +1,338 @@ +package leaguehub.leaguehubbackend.domain.match.service; + +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelStatusAlreadyException; +import leaguehub.leaguehubbackend.domain.match.dto.MatchCallAdminDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchInfoDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchPlayerInfo; +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import leaguehub.leaguehubbackend.domain.match.entity.MatchSet; +import leaguehub.leaguehubbackend.domain.match.entity.MatchStatus; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchNotEnoughPlayerException; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchNotFoundException; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchSetRepository; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotFoundException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantRejectedRequestedException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static leaguehub.leaguehubbackend.domain.channel.entity.ChannelStatus.PROCEEDING; +import static leaguehub.leaguehubbackend.domain.match.entity.MatchStatus.END; +import static leaguehub.leaguehubbackend.domain.participant.entity.ParticipantStatus.DISQUALIFICATION; +import static leaguehub.leaguehubbackend.domain.participant.entity.ParticipantStatus.PROGRESS; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.PLAYER; + +@Service +@Transactional +@RequiredArgsConstructor +public class MatchService { + + private static final int MIN_PLAYERS_FOR_SUB_MATCH = 8; + private final MatchRepository matchRepository; + private final MatchPlayerRepository matchPlayerRepository; + private final ParticipantRepository participantRepository; + private final MatchSetRepository matchSetRepository; + private final MemberService memberService; + private static final int INITIAL_RANK = 1; + + + /** + * 채널을 만들 때 빈 값인 매치를 만듦 + * + * @param channel + * @param maxPlayers + */ + public void createSubMatches(Channel channel, int maxPlayers) { + int currentPlayers = maxPlayers; + int matchRoundIndex = 1; + + while (currentPlayers >= MIN_PLAYERS_FOR_SUB_MATCH) { + currentPlayers = createSubMatchesForRound(channel, currentPlayers, matchRoundIndex); + matchRoundIndex++; + } + } + + + /** + * 경기 배정 + * + * @param channelLink + * @param matchRound + */ + public void matchAssignment(String channelLink, Integer matchRound) { + Participant participant = checkHost(channelLink); + + if (!participant.getChannel().getChannelStatus().equals(PROCEEDING)) { + throw new ChannelRequestException(); + } + + List matchList = findMatchList(channelLink, matchRound); + + + if (matchRound != 1) + checkUpdateScore(matchList); + + checkPreviousMatchEnd(channelLink, matchRound); + + List playerList = getParticipantList(channelLink, matchRound); + + assignSubMatches(matchList, playerList); + participant.getChannel().updateChannelLiveRound(matchRound); + } + + + public void setMatchSetCount(String channelLink, List roundCount) { + Participant participant = checkHost(channelLink); + + checkChannelProceeding(participant); + + List findMatchList = matchRepository.findAllByChannel_ChannelLink(channelLink); + + if (findMatchList.isEmpty()) + throw new MatchNotFoundException(); + + updateMatchSetCount(roundCount, findMatchList); + } + + public void processMatchSet(String channelLink) { + List matchList = matchRepository.findAllByChannel_ChannelLink(channelLink); + + createMatchSet(matchList); + } + + public MatchCallAdminDto callAdmin(String channelLink, Long matchId, Long participantId) { + Participant participant = participantRepository.findParticipantByIdAndChannel_ChannelLink(participantId, channelLink) + .orElseThrow(() -> new ParticipantNotFoundException()); + + if (!participant.getParticipantStatus().equals(PROGRESS)) { + throw new ParticipantRejectedRequestedException(); + } + + Match match = matchRepository.findById(matchId) + .orElseThrow(() -> new MatchNotFoundException()); + + match.updateCallAlarm(); + + MatchCallAdminDto matchCallAdminDto = new MatchCallAdminDto(); + matchCallAdminDto.setCallName(participant.getNickname()); + matchCallAdminDto.setMatchRound(match.getMatchRound()); + matchCallAdminDto.setMatchName(match.getMatchName()); + + return matchCallAdminDto; + } + + public void turnOffAlarm(String channelLink, Long matchId) { + Participant participant = checkHost(channelLink); + + Match match = matchRepository.findById(matchId) + .orElseThrow(() -> new MatchNotFoundException()); + + match.updateOffAlarm(); + } + + + private int createSubMatchesForRound(Channel channel, int maxPlayers, int matchRoundIndex) { + int currentPlayers = maxPlayers; + int tableCount = currentPlayers / MIN_PLAYERS_FOR_SUB_MATCH; + + for (int tableIndex = 1; tableIndex <= tableCount; tableIndex++) { + String groupName = "Group " + (char) (64 + tableIndex); + Match match = Match.createMatch(matchRoundIndex, channel, groupName); + matchRepository.save(match); + } + + return currentPlayers / 2; + } + + public Participant getParticipant(Long memberId, String channelLink) { + Participant participant = participantRepository.findParticipantByMemberIdAndChannel_ChannelLink(memberId, channelLink) + .orElseThrow(() -> new InvalidParticipantAuthException()); + return participant; + } + + private void checkRoleHost(Role role) { + if (role != Role.HOST) { + throw new InvalidParticipantAuthException(); + } + } + + public List findMatchList(String channelLink, Integer matchRound) { + List matchList = matchRepository.findAllByChannel_ChannelLinkAndMatchRoundOrderByMatchName(channelLink, matchRound); + return matchList; + } + + private List getParticipantList(String channelLink, Integer matchRound) { + List playerList = participantRepository + .findAllByChannel_ChannelLinkAndRoleAndParticipantStatus(channelLink, PLAYER, PROGRESS); + + if (playerList.size() < matchRound * 0.75) throw new MatchNotEnoughPlayerException(); + return playerList; + } + + private void assignSubMatches(List matchList, List playerList) { + Collections.shuffle(playerList); + + int totalPlayers = playerList.size(); + int matchCount = matchList.size(); + int playersPerMatch = totalPlayers / matchCount; + int remainingPlayers = totalPlayers % matchCount; + int playerIndex = 0; + + for (Match match : matchList) { + int currentPlayerCount = playersPerMatch + (remainingPlayers > 0 ? 1 : 0); + + for (int i = 0; i < currentPlayerCount; i++) { + Participant player = playerList.get(playerIndex); + MatchPlayer matchPlayer = MatchPlayer.createMatchPlayer(player, match); + matchPlayerRepository.save(matchPlayer); + + playerIndex++; + remainingPlayers--; + } + + match.updateMatchStatus(MatchStatus.PROGRESS); + } + } + + + public MatchInfoDto convertMatchInfoDto(Match match, List matchPlayers) { + return MatchInfoDto.builder().matchId(match.getId()) + .matchName(match.getMatchName()) + .matchStatus(match.getMatchStatus()) + .matchRound(match.getMatchRound()) + .matchSetCount(match.getMatchSetCount()) + .matchCurrentSet(match.getMatchCurrentSet()) + .matchPlayerInfoList(convertMatchPlayerInfoList(matchPlayers)) + .matchAlarm(match.isAlarm()) + .build(); + } + + + public List convertMatchPlayerInfoList(List matchPlayers) { + List matchPlayerInfoList = matchPlayers.stream() + .map(matchPlayer -> new MatchPlayerInfo( + matchPlayer.getId(), + matchPlayer.getParticipant().getId(), + matchPlayer.getParticipant().getGameId(), + matchPlayer.getParticipant().getGameTier(), + matchPlayer.getPlayerStatus(), + matchPlayer.getPlayerScore(), + matchPlayer.getMatchPlayerResultStatus(), + matchPlayer.getParticipant().getProfileImageUrl(), + matchPlayer.getPlayerScore() + )) + .sorted(Comparator.comparingInt(MatchPlayerInfo::getScore).reversed() + .thenComparing(MatchPlayerInfo::getGameId)) + .collect(Collectors.toList()); + + assignRankToMatchPlayerInfoList(matchPlayerInfoList); + + return matchPlayerInfoList; + } + + private Participant checkHost(String channelLink) { + Member member = memberService.findCurrentMember(); + Participant participant = getParticipant(member.getId(), channelLink); + checkRoleHost(participant.getRole()); + + return participant; + } + + private void checkUpdateScore(List matchList) { + for (Match currentMatch : matchList) { + List matchplayerList = matchPlayerRepository.findAllByMatch_IdOrderByPlayerScoreDesc(currentMatch.getId()); + + int progressCount = 0; + + for (MatchPlayer matchPlayer : matchplayerList) { + if (progressCount >= 5) { + if (!matchPlayer.getParticipant().getParticipantStatus().equals(DISQUALIFICATION)) { + matchPlayer.getParticipant().dropoutParticipantStatus(); + } + continue; + } + + if (matchPlayer.getParticipant().getParticipantStatus().equals(PROGRESS)) { + progressCount++; + } else { + matchPlayer.getParticipant().dropoutParticipantStatus(); + } + } + } + } + + private void checkPreviousMatchEnd(String channelLink, Integer matchRound) { + if (matchRound != 1) { + List previousMatch = findMatchList(channelLink, matchRound - 1); + previousMatch.stream() + .filter(match -> !match.getMatchStatus().equals(END)) + .findAny() + .ifPresent(match -> { + throw new MatchNotFoundException(); + }); + + List presentMatch = findMatchList(channelLink, matchRound); + presentMatch.stream() + .filter(match -> match.getMatchStatus().equals(MatchStatus.PROGRESS)) + .findAny() + .ifPresent(match -> { + throw new MatchNotFoundException(); + }); + } + } + + + private void assignRankToMatchPlayerInfoList(List matchPlayerInfoList) { + int rank = INITIAL_RANK; + for (int i = 0; i < matchPlayerInfoList.size(); i++) { + MatchPlayerInfo info = matchPlayerInfoList.get(i); + if (i > 0 && !info.getScore().equals(matchPlayerInfoList.get(i - 1).getScore())) { + rank = i + 1; + } + info.setMatchRank(rank); + } + } + + + private static void updateMatchSetCount(List roundCount, List findMatchList) { + int responseIndex = 0; + for (int i = roundCount.size(); i >= 1; i--) { + for (Match match : findMatchList) { + if (match.getMatchRound().equals(i)) + match.updateMatchSetCount(roundCount.get(responseIndex)); + } + responseIndex++; + } + + } + + private static void checkChannelProceeding(Participant participant) { + if (participant.getChannel().getChannelStatus().equals(PROCEEDING)) + throw new ChannelStatusAlreadyException(); + } + + private void createMatchSet(List matchList) { + matchList.stream() + .flatMap(match -> IntStream.rangeClosed(1, match.getMatchSetCount()) + .mapToObj(setCount -> MatchSet.createMatchSet(match, setCount))) + .forEach(matchSetRepository::save); + } + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchWebClientService.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchWebClientService.java new file mode 100644 index 00000000..38307353 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/MatchWebClientService.java @@ -0,0 +1,104 @@ +package leaguehub.leaguehubbackend.domain.match.service; + +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchResultIdNotFoundException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantGameIdNotFoundException; +import leaguehub.leaguehubbackend.global.exception.global.exception.GlobalServerErrorException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + + +@Service +@RequiredArgsConstructor +@Transactional +public class MatchWebClientService { + + + private final WebClient webClient; + private final JSONParser jsonParser; + @Value("${riot-api-key-1}") + private String riot_api_key_1; + @Value("${riot-api-key-2}") + private String riot_api_key_2; + + + /** + * 소환사의 라이엇 puuid를 얻는 메서드 + * + * @param name 게임 닉네임 + * @return puuid + */ + public String getSummonerPuuid(String name) { + String gameId = name.split("#")[0]; + String gameTag = name.split("#")[1]; + + String summonerPuuidUrl = "https://asia.api.riotgames.com/riot/account/v1/accounts/by-riot-id/"; + + JSONObject userAccount = webClient.get() + .uri(summonerPuuidUrl + gameId + "/" + gameTag + riot_api_key_1) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new ParticipantGameIdNotFoundException())) + .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(JSONObject.class) + .block(); + + + String puuid = userAccount.get("puuid").toString(); + + return puuid; + + } + + + /** + * 게임 Id로 얻은 puuid로 라이엇 서버에 고유 매치 Id 검색 + * + * @param puuid + * @return + */ + public String getMatch(String puuid, long endTime) { +// long endTime = System.currentTimeMillis() / 1000; + long statTime = 0; + + String matchUrl = "https://asia.api.riotgames.com/tft/match/v1/matches/by-puuid/"; + String Option = "/ids?start=0&endTime=" + endTime + "&startTime=" + statTime + "&count=1"; + + + JSONArray matchArray = webClient.get() + .uri(matchUrl + puuid + Option + riot_api_key_2) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new MatchResultIdNotFoundException())) + .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(JSONArray.class) + .block(); + + + return matchArray.get(0).toString(); + } + + + + @SneakyThrows + public JSONObject responseMatchDetail(String matchId) { + String matchDetailUrl = "https://asia.api.riotgames.com/tft/match/v1/matches/"; + + return (JSONObject) jsonParser.parse + (webClient + .get() + .uri(matchDetailUrl + matchId + riot_api_key_1) + .retrieve() + .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(JSONObject.class) + .block().toJSONString()); + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/service/chat/MatchChatService.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/chat/MatchChatService.java new file mode 100644 index 00000000..2dfb5a81 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/chat/MatchChatService.java @@ -0,0 +1,132 @@ +package leaguehub.leaguehubbackend.domain.match.service.chat; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.match.dto.MatchMessage; +import leaguehub.leaguehubbackend.domain.match.entity.MessageType; +import leaguehub.leaguehubbackend.domain.match.exception.chat.exception.MatchChatMessageConversionException; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class MatchChatService { + + private static final String REDIS_KEY_FORMAT = "channelLink:%s:matchId:%d:messages"; + private static final String PUBLISH_KEY_FORMAT = "matchId:%d:messages"; + private static final String DELETE_CHANNEL_CHAT_FORMAT = "channelLink:%s:matchId:*:messages"; + + private final StringRedisTemplate stringRedisTemplate; + + private final JwtService jwtService; + + private final MemberRepository memberRepository; + + private final ObjectMapper objectMapper; + + public void processMessage(MatchMessage message) { + Long matchId = message.getMatchId(); + String channelLink = message.getChannelLink(); + + message.setTimestamp(LocalDateTime.now()); + + setAdminNameIfAdmin(message); + + String messageJson = convertMessageToJson(message); + String redisKey = String.format(REDIS_KEY_FORMAT, channelLink, matchId); + String publishKey = String.format(PUBLISH_KEY_FORMAT, matchId); + + saveMessageToRedis(redisKey, messageJson); + publishMessage(publishKey, messageJson); + } + + private String convertMessageToJson(MatchMessage message) { + try { + return objectMapper.writeValueAsString(message); + } catch (JsonProcessingException e) { + log.error("message로 변경 실패: {}", e.getMessage()); + throw new MatchChatMessageConversionException(); + } + } + + private void saveMessageToRedis(String key, String messageJson) { + stringRedisTemplate.opsForList().leftPush(key, messageJson); + } + + private void publishMessage(String key, String messageJson) { + stringRedisTemplate.convertAndSend(key, messageJson); + } + + private void setAdminNameIfAdmin(MatchMessage message) { + MessageType messageType = message.getType(); + if (messageType == MessageType.ADMIN) { + String personalId = String.valueOf(jwtService.extractPersonalId(message.getAccessToken())); + Optional memberOpt = memberRepository.findMemberByPersonalId(personalId); + if (memberOpt.isPresent()) { + Member member = memberOpt.get(); + message.setAdminName(member.getNickname() + "(관리자)"); + } else { + message.setAdminName("알 수 없는 관리자"); + } + } + message.setAccessToken(null); + } + + public List findMatchChatHistory(String channelLink, Long matchId) { + + String targetMatch = String.format(REDIS_KEY_FORMAT, channelLink, matchId); + + List messageList = stringRedisTemplate.opsForList().range(targetMatch, 0, -1); + + return messageList.stream() + .map(this::convertJsonToMatchMessage) + .collect(Collectors.toList()); + } + + private MatchMessage convertJsonToMatchMessage(String json) { + try { + return objectMapper.readValue(json, MatchMessage.class); + } catch (JsonProcessingException e) { + throw new MatchChatMessageConversionException(); + } + } + + public void deleteChannelMatchChat(Channel channel) { + String targetChannel = String.format(DELETE_CHANNEL_CHAT_FORMAT, channel.getChannelLink()); + Set keys = stringRedisTemplate.keys(targetChannel); + + if (keys != null) { + for (String key : keys) { + stringRedisTemplate.delete(key); + } + } + } + + public void processAdminAlert(String channelLink, Long matchId) { + MatchMessage message = MatchMessage.builder() + .channelLink(channelLink) + .content("관리자 호출") + .matchId(matchId) + .participantId(-1L) + .timestamp(LocalDateTime.now()) + .type(MessageType.ALERT) + .build(); + + processMessage(message); + + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/match/service/chat/MatchChatSubscriber.java b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/chat/MatchChatSubscriber.java new file mode 100644 index 00000000..00d80818 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/match/service/chat/MatchChatSubscriber.java @@ -0,0 +1,51 @@ +package leaguehub.leaguehubbackend.domain.match.service.chat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import leaguehub.leaguehubbackend.domain.match.dto.MatchMessage; +import leaguehub.leaguehubbackend.domain.match.exception.chat.exception.MatchChatMessageConversionException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MatchChatSubscriber implements MessageListener { + + private final ObjectMapper objectMapper; + + private final SimpMessagingTemplate messagingTemplate; + + private static final String MATCH_CHAT_DESTINATION_FORMAT = "/match/%d/chat"; + + + @Override + public void onMessage(Message message, byte[] pattern) { + MatchMessage receivedMessageObj = convertToMatchMessage(message); + broadcastMessage(receivedMessageObj); + } + + private MatchMessage convertToMatchMessage(Message message) { + try { + return objectMapper.readValue(message.getBody(), MatchMessage.class); + } catch (IOException e) { + log.error("message로 변경 실패: {}", e.getMessage()); + throw new MatchChatMessageConversionException(); + } + } + + private String getDestination(Long matchId) { + return String.format(MATCH_CHAT_DESTINATION_FORMAT, matchId); + } + + private void broadcastMessage(MatchMessage message) { + Long matchId = message.getMatchId(); + String destination = getDestination(matchId); + messagingTemplate.convertAndSend(destination, message); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/controller/MemberAuthController.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/controller/MemberAuthController.java new file mode 100644 index 00000000..3dfa4b87 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/controller/MemberAuthController.java @@ -0,0 +1,103 @@ +package leaguehub.leaguehubbackend.domain.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +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.SecurityRequirements; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoTokenResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.member.exception.kakao.exception.KakaoInvalidCodeException; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import leaguehub.leaguehubbackend.domain.member.service.MemberAuthService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Optional; +import java.util.function.Predicate; + +import static org.springframework.http.HttpStatus.OK; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class MemberAuthController { + + private final JwtService jwtService; + private final MemberAuthService kakaoService; + private final MemberService memberService; + private final MemberAuthService memberAuthService; + + @Operation(summary = "카카오 로그인/회원가입", description = "카카오 AccessCode를 사용하여 로그인/회원가입을 한다") + @SecurityRequirements + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인/회원가입 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = LoginMemberResponse.class))), + @ApiResponse(responseCode = "400", description = "KA-C-001 유효하지 않은 카카오 코드입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + @ApiResponse(responseCode = "500", description = "G-S-001 Internal Server Error", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + }) + @PostMapping("/member/oauth/kakao") + public ResponseEntity handleKakaoLogin(@RequestHeader HttpHeaders headers, HttpServletResponse response) { + String kakaoCode = headers.getFirst("Kakao-Code"); + + Optional.ofNullable(kakaoCode) + .filter(Predicate.not(String::isEmpty)) + .orElseThrow(KakaoInvalidCodeException::new); + + KakaoTokenResponseDto KakaoToken = kakaoService.getKakaoToken(kakaoCode); + KakaoUserDto userDto = kakaoService.getKakaoUser(KakaoToken); + LoginMemberResponse loginMemberResponse = memberAuthService.findOrSaveMember(userDto); + + Cookie refreshTokenCookie = new Cookie("refreshToken", loginMemberResponse.getRefreshToken()); + refreshTokenCookie.setHttpOnly(true); + refreshTokenCookie.setPath("/"); + refreshTokenCookie.setSecure(true); + response.addCookie(refreshTokenCookie); + response.setHeader("Authorization", "Bearer " + loginMemberResponse.getAccessToken()); + + return new ResponseEntity("Login Successful", OK); + } + + @Operation(summary = "앱 로그아웃", description = "앱에서 사용자를 로그아웃") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = String.class))), + @ApiResponse(responseCode = "404", description = "MB-C-001 존재하지 않는 회원입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + }) + @PostMapping("/member/logout") + public ResponseEntity handleKakaoLogout(HttpServletRequest request, HttpServletResponse response) { + + memberAuthService.logoutMember(request, response); + + return ResponseEntity.ok("Logout Success!"); + } + + + @Operation(summary = "토큰 재발급", description = "refreshToken을 사용해서 accessToken 과 refreshToken 재발급") + @SecurityRequirements + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "토큰 재발급 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = LoginMemberResponse.class))), + @ApiResponse(responseCode = "400_1", description = "AT-C-004 요청에 토큰이 존재하지 않습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + @ApiResponse(responseCode = "400_2", description = "AT-C-005 해당 리프레쉬 토큰을 가지는 멤버가 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + @ApiResponse(responseCode = "401", description = "AT-C-001 유효하지 않은 토큰입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + }) + @PostMapping("/member/token") + public ResponseEntity refreshAccessToken(HttpServletRequest request) { + + LoginMemberResponse loginMemberResponse = jwtService.refreshAccessToken(request); + + return ResponseEntity.ok(loginMemberResponse); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/controller/MemberInfoController.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/controller/MemberInfoController.java new file mode 100644 index 00000000..02a9f050 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/controller/MemberInfoController.java @@ -0,0 +1,63 @@ +package leaguehub.leaguehubbackend.domain.member.controller; + +import io.swagger.v3.oas.annotations.Operation; +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.tags.Tag; +import jakarta.validation.Valid; +import leaguehub.leaguehubbackend.domain.member.dto.member.MypageResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.NicknameRequestDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.ProfileDto; +import leaguehub.leaguehubbackend.domain.member.service.MemberProfileService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +@Tag(name = "Member-Controller", description = "사용자 API") +public class MemberInfoController { + + + private final MemberProfileService memberProfileService; + + @Operation(summary = "사용자 프로필 조회", description = "사용자의 이미지 URL과 닉네임을 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "사용자 프로필 조회 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProfileDto.class))), + @ApiResponse(responseCode = "404", description = "MB-C-001 존재하지 않는 회원입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + }) + @GetMapping("/member/profile") + public ProfileDto getProfile() { + return memberProfileService.getProfile(); + } + + @Operation(summary = "사용자 마이페이지 조회", description = "사용자의 이미지 URL, 닉네임, 이메일, 이메일 인증 상태를 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "사용자 마이페이지 조회 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = MypageResponseDto.class))), + @ApiResponse(responseCode = "404", description = "MB-C-001 존재하지 않는 회원입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))), + }) + @GetMapping("/member/mypage") + public MypageResponseDto getMypage() { + return memberProfileService.getMypageProfile(); + } + + + @Operation(summary = "사용자 닉네임 변경", description = "사용자 닉네임 변경") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "닉네임 변경 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ProfileDto.class))), + @ApiResponse(responseCode = "404", description = "MB-C-001 PA-C-015 멤버 또는 참가자를 찾을 수 없음", content = @Content(mediaType = "application/json")), + }) + @PostMapping("/member/profile/nickname") + public ProfileDto changeNickName(@RequestBody @Valid NicknameRequestDto nicknameRequestDto) { + + return memberProfileService.changeMemberParticipantNickname(nicknameRequestDto); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoTokenRequestDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoTokenRequestDto.java new file mode 100644 index 00000000..28dc6296 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoTokenRequestDto.java @@ -0,0 +1,23 @@ +package leaguehub.leaguehubbackend.domain.member.dto.kakao; + +import lombok.AllArgsConstructor; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@AllArgsConstructor +public class KakaoTokenRequestDto { + + private String grantType; + private String clientId; + private String redirectUri; + private String code; + public MultiValueMap toMultiValueMap() { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", grantType); + params.add("client_id", clientId); + params.add("redirect_uri", redirectUri); + params.add("code", code); + + return params; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoTokenResponseDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoTokenResponseDto.java new file mode 100644 index 00000000..21402e76 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoTokenResponseDto.java @@ -0,0 +1,26 @@ +package leaguehub.leaguehubbackend.domain.member.dto.kakao; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class KakaoTokenResponseDto { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("expires_in") + private int expiresIn; + + private String scope; + + @JsonProperty("refresh_token_expires_in") + private int refreshTokenExpiresIn; + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoUserDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoUserDto.java new file mode 100644 index 00000000..fcb0a5bd --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/kakao/KakaoUserDto.java @@ -0,0 +1,58 @@ +package leaguehub.leaguehubbackend.domain.member.dto.kakao; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; +@Data +public class KakaoUserDto implements Serializable { + + private Long id; + + @JsonProperty("connected_at") + private String connectedAt; + + private Properties properties; + + @JsonProperty("kakao_account") + private KakaoAccount kakaoAccount; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Properties { + + private String nickname; + + @JsonProperty("profile_image") + private String profileImage; + + @JsonProperty("thumbnail_image") + private String thumbnailImage; + + } + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class KakaoAccount { + + @JsonProperty("profile_nickname_needs_agreement") + private Boolean profileNicknameNeedsAgreement; + + @JsonProperty("profile_image_needs_agreement") + private Boolean profileImageNeedsAgreement; + + private Profile profile; + + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Profile { + private String nickname; + + @JsonProperty("thumbnail_image_url") + private String thumbnailImageUrl; + + } + + } + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/LoginMemberResponse.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/LoginMemberResponse.java new file mode 100644 index 00000000..20957637 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/LoginMemberResponse.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.member.dto.member; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class LoginMemberResponse { + + private String accessToken; + private String refreshToken; + private boolean verifiedUser; +} + diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/MypageResponseDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/MypageResponseDto.java new file mode 100644 index 00000000..5fcfe59e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/MypageResponseDto.java @@ -0,0 +1,17 @@ +package leaguehub.leaguehubbackend.domain.member.dto.member; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class MypageResponseDto { + + private String profileImageUrl; + + private String nickName; + + private String email; + + private boolean userEmailVerified; +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/NicknameRequestDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/NicknameRequestDto.java new file mode 100644 index 00000000..2be47d96 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/NicknameRequestDto.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.member.dto.member; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class NicknameRequestDto { + + @NotBlank + @Size(max = 20, message = "닉네임은 20자 이하여야 합니다") + private String nickName; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/ProfileDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/ProfileDto.java new file mode 100644 index 00000000..56442f20 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/ProfileDto.java @@ -0,0 +1,13 @@ +package leaguehub.leaguehubbackend.domain.member.dto.member; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ProfileDto { + + private String profileImageUrl; + + private String nickName; +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/ProfileResponseDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/ProfileResponseDto.java new file mode 100644 index 00000000..79ccca67 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/dto/member/ProfileResponseDto.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.member.dto.member; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ProfileResponseDto { + + private String profileId; + + private String profileImageUrl; + + private String nickName; + + private String email; + + private Boolean userEmailVerified; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/BaseRole.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/BaseRole.java new file mode 100644 index 00000000..e40164e7 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/BaseRole.java @@ -0,0 +1,17 @@ +package leaguehub.leaguehubbackend.domain.member.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BaseRole { + ADMIN("ROLE_ADMIN"), + USER("ROLE_USER"), + GUEST("ROLE_GUEST"); + private final String key; + + public String convertBaseRole() { + return "[" + this.key + "]"; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/LoginProvider.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/LoginProvider.java new file mode 100644 index 00000000..b5d49a91 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/LoginProvider.java @@ -0,0 +1,5 @@ +package leaguehub.leaguehubbackend.domain.member.entity; + +public enum LoginProvider { + KAKAO, +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/Member.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/Member.java new file mode 100644 index 00000000..270f4ac3 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/entity/Member.java @@ -0,0 +1,67 @@ +package leaguehub.leaguehubbackend.domain.member.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.domain.email.entity.EmailAuth; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.*; +import org.hibernate.annotations.DynamicUpdate; + + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@AllArgsConstructor +@Entity +@DynamicUpdate +public class Member extends BaseTimeEntity { + + @Id + @Column(name = "member_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String personalId; + + @Column(length = 20) + private String nickname; + + private String profileImageUrl; + + @OneToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "email_auth_id", referencedColumnName = "email_id") + private EmailAuth emailAuth; + + private boolean emailUserVerified; + + @Enumerated(EnumType.STRING) + private LoginProvider loginProvider; + + @Enumerated(EnumType.STRING) + private BaseRole baseRole; + + public void updateRole(BaseRole role) { this.baseRole = role; } + + public static Member kakaoUserToMember(KakaoUserDto kakaoUserDto) { + return Member.builder() + .personalId(String.valueOf(kakaoUserDto.getId())) + .nickname(kakaoUserDto.getProperties().getNickname()) + .profileImageUrl(kakaoUserDto.getProperties().getProfileImage()) + .emailUserVerified(false) + .baseRole(BaseRole.GUEST) + .loginProvider(LoginProvider.KAKAO) + .build(); + + } + public void assignEmailAuth(EmailAuth emailAuth) { + this.emailAuth = emailAuth; + } + public void verifyEmail() { + this.emailUserVerified = true; + } + public void unverifyEmail() { + this.emailUserVerified = false; + } + public void updateNickname(String newNickname) { this.nickname = newNickname; } + public void updateProfileImageUrl(String profileImageUrl) { this.profileImageUrl = profileImageUrl; } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/AuthExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/AuthExceptionCode.java new file mode 100644 index 00000000..f3a136dd --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/AuthExceptionCode.java @@ -0,0 +1,52 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.*; + +@Getter +@RequiredArgsConstructor +public enum AuthExceptionCode implements ExceptionCode { + + /** + * JWT + * 001 ~ 099 + */ + INVALID_TOKEN(UNAUTHORIZED, "AT-C-001", "유효하지 않은 토큰입니다."), + EXPIRED_TOKEN(UNAUTHORIZED, "AT-C-002", "만료된 토큰입니다."), + NOT_EXPIRED_TOKEN(BAD_REQUEST, "AT-C-003", "만료되지 않은 토큰입니다."), + REQUEST_TOKEN_NOT_FOUND(BAD_REQUEST, "AT-C-004", "요청에 토큰이 존재하지 않습니다."), + INVALID_REFRESH_TOKEN(BAD_REQUEST, "AT-C-005", "해당 리프레쉬 토큰을 가지는 멤버가 없습니다."), + UNTRUSTED_CREDENTIAL(UNAUTHORIZED, "AT-C-006", "신뢰할 수 없는 자격증명 입니다."), + LOGGED_OUT_TOKEN(UNAUTHORIZED, "AT-C-007", "로그아웃된 토큰입니다."), + + /** + * MEMBER + * 100 ~ 199 + */ + LOGIN_PROVIDER_MISMATCH(BAD_REQUEST, "AT-C-100", "잘못된 OAuth2 인증입니다."), + INVALID_LOGIN_PROVIDER(BAD_REQUEST, "AT-C-101", "유효하지 않은 로그인 제공자입니다."), + INVALID_MEMBER_ROLE(FORBIDDEN, "AT-C-102", "유효하지 않은 사용자 권한입니다."), + NOT_AUTHORIZATION_USER(NOT_FOUND, "AT-C-103", "인가된 사용자가 아닙니다."), + INVALID_REDIRECT_URI(UNAUTHORIZED, "AT-C-104", "허용되지 않은 리다이렉션 URI 입니다."), + AUTH_MEMBER_NOT_FOUND(NOT_FOUND, "AT-C-105", "존재하지 않는 회원입니다."), + + /** + * Common Exception + * 200 ~ + */ + AUTHENTICATION_ERROR(UNAUTHORIZED, "AT-C-200", "Authentication exception."), + + /** + * Exception + * 400 ~ + */ + BAD_REQUEST_EXCEPTION(BAD_REQUEST, "AT-S-400", "Bad Request"); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/AuthExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/AuthExceptionHandler.java new file mode 100644 index 00000000..e1d1191d --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/AuthExceptionHandler.java @@ -0,0 +1,72 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth; + +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthExpiredTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidRefreshToken; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthTokenNotFoundException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class AuthExceptionHandler { + + @ExceptionHandler(AuthInvalidTokenException.class) + public ResponseEntity authInvalidTokenException( + AuthInvalidTokenException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(AuthExpiredTokenException.class) + public ResponseEntity authExpiredTokenException( + AuthExpiredTokenException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(AuthInvalidRefreshToken.class) + public ResponseEntity authInvalidRefreshToken( + AuthInvalidRefreshToken e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(AuthTokenNotFoundException.class) + public ResponseEntity authNoRefreshToken( + AuthTokenNotFoundException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthExpiredTokenException.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthExpiredTokenException.java new file mode 100644 index 00000000..6e51e3a5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthExpiredTokenException.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.EXPIRED_TOKEN; + +public class AuthExpiredTokenException extends AuthenticationException { + private final ExceptionCode exceptionCode; + public AuthExpiredTokenException() { + super(EXPIRED_TOKEN.getCode()); + this.exceptionCode = EXPIRED_TOKEN; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthInvalidRefreshToken.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthInvalidRefreshToken.java new file mode 100644 index 00000000..6baa2f8e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthInvalidRefreshToken.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.INVALID_REFRESH_TOKEN; + + +public class AuthInvalidRefreshToken extends AuthenticationException { + private final ExceptionCode exceptionCode; + public AuthInvalidRefreshToken() { + super(INVALID_REFRESH_TOKEN.getCode()); + this.exceptionCode = INVALID_REFRESH_TOKEN; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthInvalidTokenException.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthInvalidTokenException.java new file mode 100644 index 00000000..e6591004 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthInvalidTokenException.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth.exception; + +import leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +public class AuthInvalidTokenException extends AuthenticationException { + private final ExceptionCode exceptionCode; + public AuthInvalidTokenException() { + super(AuthExceptionCode.INVALID_TOKEN.getCode()); + this.exceptionCode = AuthExceptionCode.INVALID_TOKEN; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthMemberNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthMemberNotFoundException.java new file mode 100644 index 00000000..425ea2c2 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthMemberNotFoundException.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.AUTH_MEMBER_NOT_FOUND; + +public class AuthMemberNotFoundException extends AuthenticationException { + private final ExceptionCode exceptionCode; + public AuthMemberNotFoundException() { + super(AUTH_MEMBER_NOT_FOUND.getCode()); + this.exceptionCode = AUTH_MEMBER_NOT_FOUND; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthTokenNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthTokenNotFoundException.java new file mode 100644 index 00000000..1cff68c6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/auth/exception/AuthTokenNotFoundException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.member.exception.auth.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.REQUEST_TOKEN_NOT_FOUND; + +public class AuthTokenNotFoundException extends AuthenticationException { + private final ExceptionCode exceptionCode; + public AuthTokenNotFoundException() { + super(REQUEST_TOKEN_NOT_FOUND.getCode()); + this.exceptionCode = REQUEST_TOKEN_NOT_FOUND; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } + +} + diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/KakaoExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/KakaoExceptionCode.java new file mode 100644 index 00000000..4c2d458b --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/KakaoExceptionCode.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.member.exception.kakao; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@Getter +@RequiredArgsConstructor +public enum KakaoExceptionCode implements ExceptionCode { + INVALID_KAKAO_CODE(BAD_REQUEST, "KA-C-001", "유효하지 않은 카카오 코드입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/KakaoExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/KakaoExceptionHandler.java new file mode 100644 index 00000000..8b69421e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/KakaoExceptionHandler.java @@ -0,0 +1,29 @@ +package leaguehub.leaguehubbackend.domain.member.exception.kakao; + +import leaguehub.leaguehubbackend.domain.member.exception.kakao.exception.KakaoInvalidCodeException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +@ControllerAdvice +@RequiredArgsConstructor +public class KakaoExceptionHandler { + + @ExceptionHandler(KakaoInvalidCodeException.class) + public ResponseEntity kakaoInvalidCodeException( + KakaoInvalidCodeException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/exception/KakaoInvalidCodeException.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/exception/KakaoInvalidCodeException.java new file mode 100644 index 00000000..1f0a521e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/kakao/exception/KakaoInvalidCodeException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.member.exception.kakao.exception; + +import leaguehub.leaguehubbackend.domain.member.exception.kakao.KakaoExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.member.exception.kakao.KakaoExceptionCode.INVALID_KAKAO_CODE; + +public class KakaoInvalidCodeException extends RuntimeException{ + private final ExceptionCode exceptionCode; + + public KakaoInvalidCodeException() { + super(INVALID_KAKAO_CODE.getMessage()); + this.exceptionCode = KakaoExceptionCode.INVALID_KAKAO_CODE; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/MemberExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/MemberExceptionCode.java new file mode 100644 index 00000000..5483f937 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/MemberExceptionCode.java @@ -0,0 +1,22 @@ +package leaguehub.leaguehubbackend.domain.member.exception.member; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@Getter +@RequiredArgsConstructor +public enum MemberExceptionCode implements ExceptionCode { + + MEMBER_NOT_FOUND(NOT_FOUND, "MB-C-001", "존재하지 않는 회원입니다."), + INVALID_MEMBER_IMAGE(BAD_REQUEST, "MB-C-002", "유효하지 않은 이미지입니다."); + + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/MemberExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/MemberExceptionHandler.java new file mode 100644 index 00000000..e7966f2c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/MemberExceptionHandler.java @@ -0,0 +1,31 @@ +package leaguehub.leaguehubbackend.domain.member.exception.member; + +import leaguehub.leaguehubbackend.domain.member.exception.member.exception.MemberNotFoundException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class MemberExceptionHandler { + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity memberNotFoundException( + MemberNotFoundException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/exception/MemberNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/exception/MemberNotFoundException.java new file mode 100644 index 00000000..ca1d5cc4 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/exception/member/exception/MemberNotFoundException.java @@ -0,0 +1,23 @@ +package leaguehub.leaguehubbackend.domain.member.exception.member.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.exception.ResourceNotFoundException; + +import static leaguehub.leaguehubbackend.domain.member.exception.member.MemberExceptionCode.MEMBER_NOT_FOUND; + +public class MemberNotFoundException extends ResourceNotFoundException { + + + private final ExceptionCode exceptionCode; + + public MemberNotFoundException() { + + super(MEMBER_NOT_FOUND); + this.exceptionCode = MEMBER_NOT_FOUND; + + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/repository/MemberRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/repository/MemberRepository.java new file mode 100644 index 00000000..cf27e8d0 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/repository/MemberRepository.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.domain.member.repository; + +import leaguehub.leaguehubbackend.domain.email.entity.EmailAuth; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + + Optional findMemberByPersonalId(String personalId); + Optional findByNickname(String nickname); + @Query("SELECT m FROM Member m JOIN m.emailAuth e WHERE e.email = :email") + Optional findMemberByEmail(@Param("email") String email); + @Query("SELECT m FROM Member m WHERE m.emailAuth = :emailAuth") + Optional findByEmailAuth(@Param("emailAuth") EmailAuth emailAuth); + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/service/JwtService.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/JwtService.java new file mode 100644 index 00000000..02a03c6a --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/JwtService.java @@ -0,0 +1,193 @@ +package leaguehub.leaguehubbackend.domain.member.service; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import jakarta.servlet.http.HttpServletRequest; +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidRefreshToken; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthTokenNotFoundException; +import leaguehub.leaguehubbackend.domain.member.exception.member.exception.MemberNotFoundException; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.global.redis.service.RedisService; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +@Getter +@Slf4j +public class JwtService { + @Value("${JWT_SECRET_KEY}") + private String secretKey; + + @Value("${JWT_ACCESS_TOKEN_TIME}") + private Long accessTokenExpirationPeriod; + + @Value("${JWT_REFRESH_TOKEN_TIME}") + private Long refreshTokenExpirationPeriod; + + private static final String BEARER = "Bearer "; + + private final MemberRepository memberRepository; + + private final RedisService redisService; + + /** + * AccessToken 생성 메소드 + */ + public String createAccessToken(String personalId) { + Date now = new Date(); + return JWT.create() + .withSubject("AccessToken") + .withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod)) + .withClaim("personalId", personalId) + .sign(Algorithm.HMAC512(secretKey)); + } + /** + * Refresh 토큰 생성 메소드 + */ + public String createRefreshToken(String personalId) { + Date now = new Date(); + return JWT.create() + .withSubject("RefreshToken") + .withClaim("uuid", UUID.randomUUID().toString()) + .withClaim("personalId", personalId) + .withExpiresAt(new Date(now.getTime() + refreshTokenExpirationPeriod)) + .sign(Algorithm.HMAC512(secretKey)); + } + + /** + * 헤더에서 RefreshToken 추출 + */ + public Optional extractRefreshToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader("Authorization-refresh")) + .filter(refreshToken -> refreshToken.startsWith(BEARER)) + .map(refreshToken -> refreshToken.replace(BEARER, "")); + } + + /** + * 헤더에서 AccessToken 추출 + */ + public Optional extractAccessToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader("Authorization")) + .filter(accessToken -> accessToken.startsWith(BEARER)) + .map(refreshToken -> refreshToken.replace(BEARER, "")); + } + /** + * STOMP 헤더에서 AccessToken 추출 + */ + public Optional extractAccessToken(StompHeaderAccessor accessor) { + return Optional.ofNullable(accessor.getFirstNativeHeader("Authorization")) + .filter(accessToken -> accessToken.startsWith(BEARER)) + .map(accessToken -> accessToken.replace(BEARER, "")); + } + /** + * AccessToken에서 PersonalId 추출 + */ + public Optional extractPersonalId(String accessToken) { + try { + String personalId = JWT.require(Algorithm.HMAC512(secretKey)) + .build() + .verify(accessToken) + .getClaim("personalId") + .asString(); + return Optional.ofNullable(personalId); + } catch (Exception e) { + log.error("액세스 토큰이 유효하지 않습니다."); + return Optional.empty(); + } + } + /** + * AccessToken과 RefreshToken 생성 + */ + public LoginMemberResponse createTokens(String personalId) { + String accessToken = createAccessToken(personalId); + String refreshToken = createRefreshToken(personalId); + + LoginMemberResponse tokenDto = LoginMemberResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + return tokenDto; + } + /** + * 매번 검증을 위한 매서드 + */ + public boolean isTokenValid(String token) { + try { + // 토큰 유효성 검증 + JWT.require(Algorithm.HMAC512(secretKey)).build().verify(token); + log.info("토큰 유효함"); + return true; + } catch (Exception e) { + log.error("유효하지 않은 토큰입니다. {}", e.getMessage()); + return false; + } + } + /** + * RefreshToken DB 저장(업데이트) + */ + public void updateRefreshToken(String personalId, String refreshToken) { + memberRepository.findMemberByPersonalId(personalId) + .ifPresentOrElse( + member -> redisService.saveRefreshToken(personalId, refreshToken), + () -> { + throw new MemberNotFoundException();} + ); + } + /** + * Token 기간만료 확인 + */ + public boolean isTokenExpired(String token) { + try { + Date expirationDate = JWT.decode(token).getExpiresAt(); + Date now = new Date(); + if(expirationDate.before(now)) { + log.info("토큰이 만료되었습니다."); + return true; + } else { + log.info("토큰이 아직 유효합니다."); + return false; + } + } catch (Exception e) { + log.error("토큰의 만료일을 판단하는 중 오류가 발생했습니다. {}", e.getMessage()); + return false; + } + } + + public LoginMemberResponse refreshAccessToken(HttpServletRequest request) { + String refreshToken = extractRefreshToken(request) + .orElseThrow(() -> { + log.info("요청에 리프레쉬토큰이 없습니다."); + return new AuthTokenNotFoundException(); + }); + + String personalId = extractPersonalId(refreshToken) + .orElseThrow(() -> { + log.info("개인 ID를 찾을 수 없습니다."); + return new MemberNotFoundException(); + }); + + String redisRefreshToken = redisService.getRefreshToken(personalId); + + return refreshTokens(refreshToken, redisRefreshToken, personalId); + } + + public LoginMemberResponse refreshTokens(String clientRefreshToken, String redisRefreshToken, String personalId) { + if (!clientRefreshToken.equals(redisRefreshToken)) { + throw new AuthInvalidRefreshToken(); + } + LoginMemberResponse loginMemberResponse = createTokens(personalId); + updateRefreshToken(personalId, loginMemberResponse.getRefreshToken()); + return loginMemberResponse; + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberAuthService.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberAuthService.java new file mode 100644 index 00000000..5099fd50 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberAuthService.java @@ -0,0 +1,146 @@ +package leaguehub.leaguehubbackend.domain.member.service; + + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoTokenRequestDto; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoTokenResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.member.entity.BaseRole; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.exception.kakao.exception.KakaoInvalidCodeException; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.global.exception.global.exception.GlobalServerErrorException; +import leaguehub.leaguehubbackend.global.redis.service.RedisService; +import leaguehub.leaguehubbackend.global.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Service; +import org.springframework.util.MultiValueMap; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Collection; + +@Service +@RequiredArgsConstructor +public class MemberAuthService { + + @Value("${KAKAO_CLIENT_ID}") + private String kakaoClientId; + + @Value("${KAKAO_REDIRECT_URI}") + private String kakaoRedirectUri; + + @Value("${KAKAO_TOKEN_REQUEST_URI}") + private String kakaoToeknRequestUri; + + @Value("${KAKAO_USERINFO_REQUEST_URI}") + private String kakaoUserInfoRequestUri; + + private final WebClient webClient; + private final MemberRepository memberRepository; + private final MemberService memberService; + private final RedisService redisService; + private final JwtService jwtService; + + /** + * 카카오로 부터 토큰을 받는 함수 + */ + public KakaoTokenResponseDto getKakaoToken(String kakaoCode) { + + KakaoTokenRequestDto kakaoTokenRequestDto = new KakaoTokenRequestDto("authorization_code", kakaoClientId, kakaoRedirectUri, kakaoCode); + MultiValueMap params = kakaoTokenRequestDto.toMultiValueMap(); + + return webClient.post() + .uri(kakaoToeknRequestUri) + .body(BodyInserters.fromFormData(params)) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> Mono.error(new KakaoInvalidCodeException())) + .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(KakaoTokenResponseDto.class) + .block(); + + } + + /** + * 카카오로 부터 유저 정보를 받는 함수 + */ + public KakaoUserDto getKakaoUser(KakaoTokenResponseDto token) { + + return webClient.get() + .uri(kakaoUserInfoRequestUri) + .header("Content-type", "application/x-www-form-urlencoded;charset=utf-8") + .header("Authorization", "Bearer " + token.getAccessToken()) + .retrieve() + .onStatus(HttpStatusCode::is5xxServerError, clientResponse -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(KakaoUserDto.class) + .block(); + + } + + + //Member Logout 메서드 + public void logoutMember(HttpServletRequest request, HttpServletResponse response) { + + Member member = memberService.findCurrentMember(); + + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + + if (auth != null) { + new SecurityContextLogoutHandler().logout(request, response, auth); + redisService.deleteRefreshToken(member.getPersonalId()); + SecurityContextHolder.clearContext(); + memberRepository.save(member); + } + } + + //멤버를 찾거나 저장 + @Transactional + public LoginMemberResponse findOrSaveMember(KakaoUserDto kakaoUserDto) { + Member member = memberRepository.findMemberByPersonalId(String.valueOf(kakaoUserDto.getId())) + .map(existingMember -> updateProfileUrl(existingMember, kakaoUserDto)) + .orElseGet(() -> memberService.saveMember(kakaoUserDto).orElseThrow(GlobalServerErrorException::new)); + return createLoginResponse(member); + } + + + //로그인 반응 생성 + public LoginMemberResponse createLoginResponse(Member member) { + LoginMemberResponse loginMemberResponse = jwtService.createTokens(String.valueOf(member.getPersonalId())); + + jwtService.updateRefreshToken(member.getPersonalId(), loginMemberResponse.getRefreshToken()); + + loginMemberResponse.setVerifiedUser(member.getBaseRole() != BaseRole.GUEST); + return loginMemberResponse; + } + + //익명의 로그인 반응 생성 + public Boolean checkIfMemberIsAnonymous() { + UserDetails userDetails = SecurityUtils.getAuthenticatedUser(); + Collection authorities = userDetails.getAuthorities(); + + for (GrantedAuthority authority : authorities) { + if ("ROLE_ANONYMOUS".equals(authority.getAuthority())) { + return true; + } + } + return false; + } + + //프로필 이미지 변경 + private Member updateProfileUrl(Member member, KakaoUserDto kakaoUserDto) { + member.updateProfileImageUrl(kakaoUserDto.getKakaoAccount().getProfile().getThumbnailImageUrl()); + return member; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberProfileService.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberProfileService.java new file mode 100644 index 00000000..7fdda561 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberProfileService.java @@ -0,0 +1,68 @@ +package leaguehub.leaguehubbackend.domain.member.service; + +import leaguehub.leaguehubbackend.domain.member.dto.member.MypageResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.NicknameRequestDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.ProfileDto; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotFoundException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class MemberProfileService { + + + private final MemberService memberService; + private final ParticipantRepository participantRepository; + + + //Member profile 조회 + @Transactional(readOnly = true) + public ProfileDto getProfile() { + + Member member = memberService.findCurrentMember(); + + return ProfileDto.builder() + .profileImageUrl(member.getProfileImageUrl()) + .nickName(member.getNickname()) + .build(); + } + + //자기 자신의 profile 조회 + @Transactional(readOnly = true) + public MypageResponseDto getMypageProfile() { + + Member member = memberService.findCurrentMember(); + + return MypageResponseDto.builder() + .profileImageUrl(member.getProfileImageUrl()) + .nickName(member.getNickname()) + .email(memberService.getVerifiedEmail(member)) + .userEmailVerified(member.isEmailUserVerified()) + .build(); + } + + //Member 닉네임 변경 + @Transactional + public ProfileDto changeMemberParticipantNickname(NicknameRequestDto nicknameRequestDto) { + + Member member = memberService.findCurrentMember(); + + member.updateNickname(nicknameRequestDto.getNickName()); + + List participants = participantRepository.findAllByMemberId(member.getId()); + if (participants.isEmpty()) { + throw new ParticipantNotFoundException(); + } + + participants.forEach(participant -> participant.updateNickname(member.getNickname())); + + return getProfile(); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberService.java b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberService.java new file mode 100644 index 00000000..cd08fb3f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/member/service/MemberService.java @@ -0,0 +1,60 @@ +package leaguehub.leaguehubbackend.domain.member.service; + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.exception.member.exception.MemberNotFoundException; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.global.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.util.Optional; + + +@Service +@RequiredArgsConstructor +public class MemberService { + + private final MemberRepository memberRepository; + + + //PersonalId를 이용하여 Member 추출 + public Optional findMemberByPersonalId(String personalId) { + return memberRepository.findMemberByPersonalId(personalId); + } + + //Member Repository에 엔티티 저장(회원가입) + @Transactional + public Optional saveMember(KakaoUserDto kakaoUserDto) { + Member newUser = Member.kakaoUserToMember(kakaoUserDto); + memberRepository.save(newUser); + return Optional.of(newUser); + } + + //존재하는 Member인지 확인 + public Member validateMember(String personalId) { + Member member = memberRepository.findMemberByPersonalId(personalId) + .orElseThrow(MemberNotFoundException::new); + return member; + } + + //Email이 인증되었는지 확인 + public String getVerifiedEmail(Member member) { + if (member.getEmailAuth() != null && member.isEmailUserVerified()) { + return member.getEmailAuth().getEmail(); + } + return "N/A"; + } + + + //자신의 Member 추출 + public Member findCurrentMember() { + UserDetails userDetails = SecurityUtils.getAuthenticatedUser(); + + return validateMember(userDetails.getUsername()); + } + +} + diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/controller/NoticeController.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/controller/NoticeController.java new file mode 100644 index 00000000..d9de1091 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/controller/NoticeController.java @@ -0,0 +1,49 @@ +package leaguehub.leaguehubbackend.domain.notice.controller; + +import io.swagger.v3.oas.annotations.Operation; +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.tags.Tag; +import leaguehub.leaguehubbackend.domain.notice.dto.NoticeDto; +import leaguehub.leaguehubbackend.domain.notice.entity.GameType; +import leaguehub.leaguehubbackend.domain.notice.service.NoticeService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.OK; + +@Tag(name = "Notice-Controller", description = "공지사항 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class NoticeController { + + private final NoticeService noticeService; + + @Operation(summary = "DB에 저장된 공지사항 추출", description = "DB에 저장된 공지사항들을 가져온다") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요청하는 게임의 공지사항 가져오기", content = @Content(mediaType = "application/json", schema = @Schema(implementation = NoticeDto.class))), + }) + @GetMapping("/notice/{target}") + public ResponseEntity findTargetNotice(@PathVariable("target") GameType target) { + return new ResponseEntity<>(noticeService.getNotices(target), OK); + } + + + @Operation(summary = "게임사 공지사항 가져오기 - 수동", description = "원하는 게임사의 공지사항을 업데이트 -수동") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK"), + }) + @PostMapping("/notice/new/{target}") + public ResponseEntity updateNewNotice(@PathVariable("target") GameType gameType) { + + noticeService.updateNotices(gameType); + + return new ResponseEntity<>(gameType + "update Success", OK); + } + + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/dto/NoticeDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/dto/NoticeDto.java new file mode 100644 index 00000000..6a7bc5b1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/dto/NoticeDto.java @@ -0,0 +1,17 @@ +package leaguehub.leaguehubbackend.domain.notice.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class NoticeDto { + + private String noticeLink; + + private String noticeTitle; + + private String noticeInfo; +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/entity/GameType.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/entity/GameType.java new file mode 100644 index 00000000..e98bed2e --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/entity/GameType.java @@ -0,0 +1,35 @@ +package leaguehub.leaguehubbackend.domain.notice.entity; + +import lombok.Getter; + +@Getter +public enum GameType { + + + TFT("https://www.leagueoflegends.com/ko-kr/news/game-updates/", + "#gatsby-focus-wrapper > div > div.style__Wrapper-sc-1ynvx8h-0.style__ResponsiveWrapper-sc-1ynvx8h-6.bNRNtU.dzWqHp > div > div.style__Wrapper-sc-106zuld-0.style__ResponsiveWrapper-sc-106zuld-4.enQqER.jYHLfd.style__List-sc-1ynvx8h-3.qfKFn > div > ol > li", + "a > article > div.style__Info-sc-1h41bzo-6.eBtwVi > div > h2"), + LOL("https://www.leagueoflegends.com/ko-kr/news/notices/", + "#gatsby-focus-wrapper > div > div.style__Wrapper-sc-1ynvx8h-0.style__ResponsiveWrapper-sc-1ynvx8h-6.bNRNtU.dzWqHp > div > div.style__Wrapper-sc-106zuld-0.style__ResponsiveWrapper-sc-106zuld-4.enQqER.jYHLfd.style__List-sc-1ynvx8h-3.qfKFn > div > ol > li", + "a > article > div.style__Info-sc-1h41bzo-6.eBtwVi > div > h2"), + FC("https://fconline.nexon.com/news/notice/list", + "#divListPart > div.board_list > div.content > div.list_wrap > div.tbody > div:nth-child(%d) > a", + "a > span.td.subject"), + HOS("https://news.blizzard.com/ko-kr/hearthstone", + "#recent-articles > li:nth-child(%d) > article > a", + null), + MAIN(null, null, null); + + + private final String url; + + private final String selector; + + private final String titleSelector; + + GameType(String url, String selector, String titleSelector) { + this.url = url; + this.selector = selector; + this.titleSelector = titleSelector; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/entity/Notice.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/entity/Notice.java new file mode 100644 index 00000000..67828cb7 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/entity/Notice.java @@ -0,0 +1,49 @@ +package leaguehub.leaguehubbackend.domain.notice.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@DynamicUpdate +@Getter +public class Notice extends BaseTimeEntity { + + @Id + @Column(name = "notice_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + private GameType gameType; + + private String gameLink; + + private String gameTitle; + + private String gameInfo; + + + public static Notice createNotice(GameType gameType, String gameLink, String gameTitle, String gameInfo) { + + Notice notice = new Notice(); + notice.gameType = gameType; + notice.gameLink = gameLink; + notice.gameTitle = gameTitle; + notice.gameInfo = gameInfo; + + return notice; + } + + public void updateNotice(Notice updateNotice) { + this.gameLink = updateNotice.gameLink; + this.gameTitle = updateNotice.gameTitle; + this.gameInfo = updateNotice.gameInfo; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/NoticeExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/NoticeExceptionCode.java new file mode 100644 index 00000000..4b7c258c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/NoticeExceptionCode.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.notice.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@Getter +@RequiredArgsConstructor +public enum NoticeExceptionCode implements ExceptionCode { + + NOTICE_UNSUPPORTED(BAD_REQUEST, "NT-C-002", "지원되지 않는 공지사항 기능입니다."), + WEB_SCRAPING_ERROR(BAD_REQUEST, "WS-C-001", "웹 페이지에서 정보를 추출하는 과정에서 오류가 발생했습니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/NoticeExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/NoticeExceptionHandler.java new file mode 100644 index 00000000..f191700b --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/NoticeExceptionHandler.java @@ -0,0 +1,45 @@ +package leaguehub.leaguehubbackend.domain.notice.exception; + +import leaguehub.leaguehubbackend.domain.notice.exception.exception.NoticeUnsupportedException; +import leaguehub.leaguehubbackend.domain.notice.exception.exception.WebScrapingException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class NoticeExceptionHandler { + + @ExceptionHandler(NoticeUnsupportedException.class) + public ResponseEntity noticeUnsupportedException( + NoticeUnsupportedException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(WebScrapingException.class) + public ResponseEntity webScrapingException( + WebScrapingException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/exception/NoticeUnsupportedException.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/exception/NoticeUnsupportedException.java new file mode 100644 index 00000000..57f5c6c7 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/exception/NoticeUnsupportedException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.notice.exception.exception; + +import leaguehub.leaguehubbackend.domain.notice.exception.NoticeExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.exception.ResourceNotFoundException; + +public class NoticeUnsupportedException extends ResourceNotFoundException { + + private final ExceptionCode exceptionCode; + + public NoticeUnsupportedException() { + super(NoticeExceptionCode.NOTICE_UNSUPPORTED); + this.exceptionCode = NoticeExceptionCode.NOTICE_UNSUPPORTED; + } + + @Override + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/exception/WebScrapingException.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/exception/WebScrapingException.java new file mode 100644 index 00000000..bf101275 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/exception/exception/WebScrapingException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.notice.exception.exception; + +import leaguehub.leaguehubbackend.domain.notice.exception.NoticeExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.exception.ResourceNotFoundException; + +public class WebScrapingException extends ResourceNotFoundException { + + private final ExceptionCode exceptionCode; + + public WebScrapingException() { + super(NoticeExceptionCode.WEB_SCRAPING_ERROR); + this.exceptionCode = NoticeExceptionCode.WEB_SCRAPING_ERROR; + } + + @Override + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/repository/NoticeRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/repository/NoticeRepository.java new file mode 100644 index 00000000..a2602815 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/repository/NoticeRepository.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.domain.notice.repository; + +import leaguehub.leaguehubbackend.domain.notice.entity.GameType; +import leaguehub.leaguehubbackend.domain.notice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface NoticeRepository extends JpaRepository { + + + List findAllByGameTypeOrderByIdAsc(@Param("gameType") GameType gameType); +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/notice/service/NoticeService.java b/src/main/java/leaguehub/leaguehubbackend/domain/notice/service/NoticeService.java new file mode 100644 index 00000000..2a12c45c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/notice/service/NoticeService.java @@ -0,0 +1,196 @@ +package leaguehub.leaguehubbackend.domain.notice.service; + +import leaguehub.leaguehubbackend.domain.notice.dto.NoticeDto; +import leaguehub.leaguehubbackend.domain.notice.entity.GameType; +import leaguehub.leaguehubbackend.domain.notice.entity.Notice; +import leaguehub.leaguehubbackend.domain.notice.exception.exception.NoticeUnsupportedException; +import leaguehub.leaguehubbackend.domain.notice.exception.exception.WebScrapingException; +import leaguehub.leaguehubbackend.domain.notice.repository.NoticeRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.stream.IntStream; + +import static leaguehub.leaguehubbackend.domain.notice.entity.GameType.*; + + +@Service +@RequiredArgsConstructor +@Slf4j +public class NoticeService { + + + private final NoticeRepository noticeRepository; + + private final int MAX_NOTICE_COUNT = 6; + + /** + * 원하는 게임의 공지사항 반환 + * + * @param gameType + * @return + */ + public List getNotices(GameType gameType) { + List noticeList = noticeRepository.findAllByGameTypeOrderByIdAsc(gameType); + + List noticeDtos = new ArrayList<>(); + + for (Notice notice : noticeList) { + NoticeDto noticedto = NoticeDto.builder() + .noticeLink(notice.getGameLink()) + .noticeTitle(notice.getGameTitle()) + .noticeInfo(notice.getGameInfo()) + .build(); + noticeDtos.add(noticedto); + } + + return noticeDtos; + } + + + /** + * 게임 공지사항 업데이트(수동) + * + * @param gameType + * @return + */ + @Transactional + public List updateNotices(GameType gameType) { + return switch (gameType) { + case LOL, TFT -> + scrapeRiotNotice(gameType, gameType.getUrl(), gameType.getSelector(), gameType.getTitleSelector()); + case FC -> + scrapeAnotherNotice(gameType, gameType.getUrl(), gameType.getSelector(), gameType.getTitleSelector(), 4, 9); + case HOS -> + scrapeAnotherNotice(gameType, gameType.getUrl(), gameType.getSelector(), gameType.getTitleSelector(), 1, 6); + case MAIN -> throw new NoticeUnsupportedException(); + default -> throw new NoticeUnsupportedException(); + }; + } + + + /** + * 일정 주기마다 공지사항 업데이트(자동) + */ + @Transactional + public void updateNoticeSchedule() { + scrapeRiotNotice(LOL, LOL.getUrl(), LOL.getSelector(), LOL.getTitleSelector()); + log.info("LOL 공지사항 업데이트 완료"); + + scrapeRiotNotice(TFT, TFT.getUrl(), TFT.getSelector(), TFT.getTitleSelector()); + log.info("TFT 공지사항 업데이트 완료"); + + scrapeAnotherNotice(FC, FC.getUrl(), FC.getSelector(), FC.getTitleSelector(), 4, 9); + log.info("FC 온라인 공지사항 업데이트 완료"); + + scrapeAnotherNotice(HOS, HOS.getUrl(), HOS.getSelector(), HOS.getTitleSelector(), 1, 6); + log.info("하스스톤 공지사항 업데이트 완료"); + + } + + + private List scrapeRiotNotice(GameType gameType, String url, String itemSelector, String titleSelector) { + try { + List recentNotices = noticeRepository.findAllByGameTypeOrderByIdAsc(gameType); + List updateNotices = findUpdateRiotNotice(gameType, url, itemSelector, titleSelector); + + if (recentNotices.size() < 6) + createSaveNotice(recentNotices, updateNotices); + + if (recentNotices.size() == 6) + updateNotices(recentNotices, updateNotices); + + } catch (Exception e) { + log.error(gameType + "스크래핑 오류"); + throw new WebScrapingException(); + } + + return noticeRepository.findAllByGameTypeOrderByIdAsc(gameType); + } + + private List findUpdateRiotNotice(GameType gameType, String url, String itemSelector, String titleSelector) throws IOException { + List notices = new ArrayList<>(); + Document doc = Jsoup.connect(url).get(); + + Elements newsItems = doc.select( + "#gatsby-focus-wrapper > div > div.style__Wrapper-sc-1ynvx8h-0.style__ResponsiveWrapper-sc-1ynvx8h-6.bNRNtU.dzWqHp > div > div.style__Wrapper-sc-106zuld-0.style__ResponsiveWrapper-sc-106zuld-4.enQqER.jYHLfd.style__List-sc-1ynvx8h-3.qfKFn > div > ol > li"); + + for (Element item : newsItems) { + String newsLink = item.select("a").attr("abs:href"); + String title = item.select(itemSelector).text(); + String metaData = item.select( + titleSelector) + .text(); + + Notice notice = Notice.createNotice(gameType, newsLink, title, metaData); + + notices.add(notice); + } + + return notices; + } + + private List scrapeAnotherNotice(GameType gameType, String url, String itemSelector, String titleSelector, Integer start, Integer end) { + try { + List updateNotices = findUpdateAnotherNotice(gameType, url, itemSelector, titleSelector, start, end); + List recentNotices = noticeRepository.findAllByGameTypeOrderByIdAsc(gameType); + if (recentNotices.size() < 6) + createSaveNotice(recentNotices, updateNotices); + + if (recentNotices.size() == 6) + updateNotices(recentNotices, updateNotices); + + } catch (Exception e) { + log.error(gameType + "스크래핑 오류"); + throw new WebScrapingException(); + } + + return noticeRepository.findAllByGameTypeOrderByIdAsc(gameType); + } + + private List findUpdateAnotherNotice(GameType gameType, String url, String itemSelector, String titleSelector, Integer start, Integer end) throws IOException { + Document doc = Jsoup.connect(url).get(); + + int startIndex = start != null ? start : 1; + int endIndex = end != null ? end : doc.select(itemSelector).size(); + + return IntStream.rangeClosed(startIndex, endIndex) + .mapToObj(i -> doc.select(String.format(itemSelector, i))) + .filter(elements -> !elements.isEmpty()) + .map(Elements::first).filter(Objects::nonNull) + .map(newsItem -> + createNoticeFromElement(gameType, newsItem, titleSelector)) + .toList(); + } + + + private void createSaveNotice(List recentNotices, List updateNotices) { + int createNoticeCount = MAX_NOTICE_COUNT - recentNotices.size(); + for (int i = 0; i < createNoticeCount; i++) { + noticeRepository.save(updateNotices.get(i)); + } + } + + private void updateNotices(List recentNotices, List updateNotices) { + for (int i = 0; i < recentNotices.size(); i++) { + recentNotices.get(i).updateNotice(updateNotices.get(i)); + } + } + + private Notice createNoticeFromElement(GameType gameType, Element element, String titleSelector) { + String newsLink = element.select("a").attr("abs:href"); + String title = titleSelector != null ? element.select(titleSelector).text() : element.text(); + + return Notice.createNotice(gameType, newsLink, title, " "); + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantManagementController.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantManagementController.java new file mode 100644 index 00000000..01841506 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantManagementController.java @@ -0,0 +1,105 @@ +package leaguehub.leaguehubbackend.domain.participant.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.tags.Tag; +import jakarta.validation.Valid; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantIdDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantIdResponseDto; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantManagementService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static org.springframework.http.HttpStatus.OK; + +@Tag(name = "Participants-Management-Controller", description = "참가자 관리 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class ParticipantManagementController { + + + private final SimpMessagingTemplate simpMessagingTemplate; + private final ParticipantManagementService participantManagementService; + + + @Operation(summary = "경기에 참가요청(TFT 만)", description = "관전자가 게임에 참가 요청") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가 요청 확인"), + @ApiResponse(responseCode = "400", description = "해당 게임에 참여할 수 없는 상태입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/{channelLink}/participant") + public ResponseEntity participateMatch(@PathVariable("channelLink") String channelLink, @RequestBody @Valid ParticipantDto responseDto){ + + participantManagementService.participateMatch(responseDto, channelLink); + + return new ResponseEntity<>("Update Participant ROLE", OK); + } + + @Operation(summary = "채널 참가", description = "채널 링크를 통하여 해당 채널 참가") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "그 채널의 정보 반환", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ParticipantChannelDto.class))), + @ApiResponse(responseCode = "403", description = "해당 채널을 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/{channelLink}/participant/observer") + public ResponseEntity enterChannel(@PathVariable("channelLink") String channelLink){ + + ParticipantChannelDto participantChannelDto = participantManagementService.participateChannel(channelLink); + + return new ResponseEntity<>(participantChannelDto, OK); + } + + @Operation(summary = "채널 나가기", description = "채널 링크를 통하여 해당 채널 나가기") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "채널을 나갔습니다."), + @ApiResponse(responseCode = "403", description = "해당 채널을 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @DeleteMapping("/{channelLink}") + public ResponseEntity leaveChannel(@PathVariable("channelLink") String channelLink){ + + participantManagementService.leaveChannel(channelLink); + + return new ResponseEntity<>("Leave this Channel", OK); + } + + @Operation(summary = "채널 순서를 커스텀하게 구성 - 로그인시 사이드바 화면 구성을 커스텀할 수 있음", + description = "입력과 반환 Dto가 동일하게 되어서 API요청 필요 없이 바로 회면 구성 가능") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Dto를 리스트로 반환",content = @Content(mediaType = "application/json", schema = @Schema(implementation = ParticipantChannelDto.class))), + }) + @PostMapping("/channels/order") + public ResponseEntity updateCustomChannelIndex(@RequestBody List participantChannelDtoList){ + + List updateCustomChannelIndexList = participantManagementService.updateCustomChannelIndex(participantChannelDtoList); + + return new ResponseEntity<>(updateCustomChannelIndexList, OK); + } + + //실격, 기권에 대한 웹소켓 + @MessageMapping("/{channelLink}/{matchIdStr}/disqualification") + public void disqualifiedParticipant(@DestinationVariable("channelLink") String channelLink, + @DestinationVariable("matchIdStr") String matchIdStr, + @Payload ParticipantIdDto message) { + ParticipantIdResponseDto participantIdResponseDto = participantManagementService.disqualifiedParticipant(channelLink, message); + + simpMessagingTemplate.convertAndSend("/match/" + matchIdStr, participantIdResponseDto); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantQueryController.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantQueryController.java new file mode 100644 index 00000000..c1d7bb5b --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantQueryController.java @@ -0,0 +1,98 @@ +package leaguehub.leaguehubbackend.domain.participant.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.tags.Tag; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseStatusPlayerDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseUserGameInfoDto; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantQueryService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static org.springframework.http.HttpStatus.OK; + +@Tag(name = "Participants-Query-Controller", description = "참가자 조회 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class ParticipantQueryController { + + private final ParticipantQueryService participantQueryService; + + + @Operation(summary = "티어 검색 (참가 x)", description = "검색 버튼을 누르면 해당 카테고리와 게임 닉네임을 가지고 티어 검색 (참가 x)") + @Parameters(value = { + @Parameter(name = "gameid", description = "해당 게임 닉네임", example = "칸영기"), + @Parameter(name = "gamecategory", description = "게임 종류 (tft, lol, ...)", example = "0, 1, 2") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "검색 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseUserGameInfoDto.class))), + @ApiResponse(responseCode = "404", description = "게임 ID를 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/participant/stat/{gameId}/{gameTag}") + public ResponseEntity getTFTRanked(@PathVariable("gameId") String gameId, @PathVariable("gameTag") String gameTag){ + + ResponseUserGameInfoDto userDetailDto = participantQueryService.selectGameCategory(gameId + "#" + gameTag, 0); + + return new ResponseEntity<>(userDetailDto, OK); + } + + + @Operation(summary = "게임 참가자(Player) 조회 ", description = "채널 내 게임 참가자(Player) 모두 조회") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "참가자 검색 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseStatusPlayerDto.class))), + @ApiResponse(responseCode = "404", description = "해당 채널을 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/{channelLink}/players") + public ResponseEntity getPlayers(@PathVariable("channelLink") String channelLink){ + + List players = participantQueryService.loadPlayers(channelLink); + + return new ResponseEntity<>(players, OK); + } + + @Operation(summary = "게임 참가요청자(Request) 조회 ", description = "채널 내 게임 참가요청자(Request) 모두 조회") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "요청자 검색 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseStatusPlayerDto.class))), + @ApiResponse(responseCode = "400", description = "해당 채널의 관리자가 아닙니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/{channelLink}/player/requests") + public ResponseEntity getRequestPlayers(@PathVariable("channelLink") String channelLink){ + + List responsePlayers = participantQueryService.loadRequestStatusPlayerList(channelLink); + + return new ResponseEntity<>(responsePlayers, OK); + } + + @Operation(summary = "채널 관전자(Observer) 조회 ", description = "채널 내 게임 관전자(Observer) 모두 조회") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "관전자 검색 성공", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ResponseStatusPlayerDto.class))), + @ApiResponse(responseCode = "400", description = "해당 채널의 관리자가 아닙니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/{channelLink}/observers") + public ResponseEntity getObserverPlayer(@PathVariable("channelLink") String channelLink){ + + List responsePlayers = participantQueryService.loadObserverPlayerList(channelLink); + + return new ResponseEntity<>(responsePlayers, OK); + } + + + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantRoleAndPermissionController.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantRoleAndPermissionController.java new file mode 100644 index 00000000..d46d59c9 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/controller/ParticipantRoleAndPermissionController.java @@ -0,0 +1,97 @@ +package leaguehub.leaguehubbackend.domain.participant.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +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.tags.Tag; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantRoleAndPermissionService; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import static org.springframework.http.HttpStatus.OK; + +@Tag(name = "Participants-RoleAndPermission-Controller", description = "참가자 역할 및 권한 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class ParticipantRoleAndPermissionController { + + + private final ParticipantRoleAndPermissionService participantRoleAndPermissionService; + + + @Operation(summary = "참가요청 승인", description = "관리자가 해당 게임 참가요청자(request)를 승인") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "participantId", description = "해당 채널 참가자의 고유 Id", example = "1") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "승인 성공"), + @ApiResponse(responseCode = "404", description = "채널 참가자를 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/{channelLink}/{participantId}/player") + public ResponseEntity approveParticipantRequest(@PathVariable("channelLink") String channelLink, + @PathVariable("participantId") Long participantId){ + + participantRoleAndPermissionService.approveParticipantRequest(channelLink, participantId); + + return new ResponseEntity<>("approve participant", OK); + } + + + @Operation(summary = "참가요청 거절", description = "관리자가 해당 게임 참가요청자(request)를 거절") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "participantId", description = "해당 채널 참가자의 고유 Id", example = "1") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "거절 성공"), + @ApiResponse(responseCode = "404", description = "채널 참가자를 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/{channelLink}/{participantId}/observer") + public ResponseEntity rejectParticipantRequest(@PathVariable("channelLink") String channelLink, + @PathVariable("participantId") Long participantId){ + + participantRoleAndPermissionService.rejectedParticipantRequest(channelLink, participantId); + + return new ResponseEntity<>("reject participant", OK); + } + + @Operation(summary = "관리자 권한 부여", description = "관리자가 관전자에게 권한을 부여") + @Parameters(value = { + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88"), + @Parameter(name = "participantId", description = "해당 채널 참가자의 고유 Id", example = "1") + }) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "관리자 권한 부여 성공"), + @ApiResponse(responseCode = "404", description = "채널 참가자를 찾을 수 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/{channelLink}/{participantId}/host") + public ResponseEntity updateHostParticipant(@PathVariable("channelLink") String channelLink, + @PathVariable("participantId") Long participantId){ + + participantRoleAndPermissionService.updateHostRole(channelLink, participantId); + + return new ResponseEntity<>("update HOST", OK); + } + + @Operation(summary = "관리자 권한 확인", description = "관리자인지 확인하는 기능") + @Parameter(name = "channelLink", description = "해당 채널의 링크", example = "42aa1b11ab88") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Admin Check"), + @ApiResponse(responseCode = "401", description = "해당 권한이 없습니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @GetMapping("/channel/{channelLink}/permission") + public ResponseEntity checkHost(@PathVariable("channelLink") String channelLink){ + + participantRoleAndPermissionService.checkAdminHost(channelLink); + + return new ResponseEntity<>("Admin Check", OK); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantDto.java new file mode 100644 index 00000000..aae43ff9 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantDto.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.dto; + + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Data +public class ParticipantDto { + + + @NotBlank + @Schema(description = "참가하려는 게임 닉네임과 태그", example = "칸영기#KR1") + private String gameId; + + @NotBlank + @Schema(description = "채널 닉네임", example = "채널개인닉네임") + private String nickname; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantIdDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantIdDto.java new file mode 100644 index 00000000..99323188 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantIdDto.java @@ -0,0 +1,17 @@ +package leaguehub.leaguehubbackend.domain.participant.dto; + + +import lombok.Data; + +@Data +public class ParticipantIdDto { + + private String accessToken; + + private Long participantId; + + private Long matchPlayerId; + + //관리자: 0, 플레이어: 1, 관전자: 2 + private int role; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantIdResponseDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantIdResponseDto.java new file mode 100644 index 00000000..dc8f60f6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantIdResponseDto.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.participant.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +public class ParticipantIdResponseDto { + + private Long matchPlayerId; + + //체크인: 1, 실격: 2 + private int matchPlayerStatus; + + @Builder + public ParticipantIdResponseDto(Long matchPlayerId, int matchPlayerStatus){ + this.matchPlayerId = matchPlayerId; + this.matchPlayerStatus = matchPlayerStatus; + + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantSummonerDetail.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantSummonerDetail.java new file mode 100644 index 00000000..6c165b6f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ParticipantSummonerDetail.java @@ -0,0 +1,11 @@ +package leaguehub.leaguehubbackend.domain.participant.dto; + +import lombok.Data; + +@Data +public class ParticipantSummonerDetail { + + String puuid; + + String userGameInfo; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ResponseStatusPlayerDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ResponseStatusPlayerDto.java new file mode 100644 index 00000000..33eb4c56 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ResponseStatusPlayerDto.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.participant.dto; + +import lombok.Data; + +@Data +public class ResponseStatusPlayerDto { + + private Long pk; + + private String nickname; + + private String imgSrc; + + private String gameId; + + private String tier; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ResponseUserGameInfoDto.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ResponseUserGameInfoDto.java new file mode 100644 index 00000000..31dd2f89 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/dto/ResponseUserGameInfoDto.java @@ -0,0 +1,11 @@ +package leaguehub.leaguehubbackend.domain.participant.dto; + +import lombok.Data; + +@Data +public class ResponseUserGameInfoDto { + + private String tier; + + private Integer playCount; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/GameTier.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/GameTier.java new file mode 100644 index 00000000..c8f960fa --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/GameTier.java @@ -0,0 +1,66 @@ +package leaguehub.leaguehubbackend.domain.participant.entity; + +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantGameIdNotFoundException; +import lombok.Getter; + +import java.text.MessageFormat; +import java.util.Arrays; + +@Getter +public enum GameTier { + + UNRANKED(-1), + IRON_IV(0), IRON_III(100), IRON_II(200), IRON_I(300), + BRONZE_IV(400), BRONZE_III(500), BRONZE_II(600), BRONZE_I(700), + SILVER_IV(800), SILVER_III(900), SILVER_II(1000), SILVER_I(1100), + GOLD_IV(1200), GOLD_III(1300), GOLD_II(1400), GOLD_I(1500), + PLATINUM_IV(1600), PLATINUM_III(1700), PLATINUM_II(1800), PLATINUM_I(1900), + EMERALD_IV(2000), EMERALD_III(2100),EMERALD_II(2200),EMERALD_I(2300), + DIAMOND_IV(2400), DIAMOND_III(2500), DIAMOND_II(2600), DIAMOND_I(2700), + MASTER_I(2800), + GRANDMASTER_I(3200), + CHALLENGER_I(3600); + + + private final int score; + + GameTier(int score){ + this.score = score; + } + + + /** + * rank와 grade를 받아 맞는 티어를 반환 + * @param rank + * @param grade + * @return + */ + public static GameTier findGameTier(String rank, String grade){ + + String tier = MessageFormat.format("{0}_{1}", rank, grade); + + for(GameTier gameTier : GameTier.values()) + if(gameTier.name().equalsIgnoreCase(tier)) + return gameTier; + + throw new ParticipantGameIdNotFoundException(); + } + + /** + * 언랭일 경우 반환 + * @return gameTierDto + */ + public static GameTier getUnranked(){ + + return GameTier.UNRANKED; + } + + + public static GameTier getByNumber(int tier) { + return Arrays.stream(GameTier.values()) + .filter(gameTier -> gameTier.score == tier) + .findFirst() + .orElseThrow(() -> new RuntimeException()); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/Participant.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/Participant.java new file mode 100644 index 00000000..62484570 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/Participant.java @@ -0,0 +1,151 @@ +package leaguehub.leaguehubbackend.domain.participant.entity; + +import jakarta.persistence.*; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.global.audit.BaseTimeEntity; +import leaguehub.leaguehubbackend.global.audit.GlobalConstant; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Optional; + +import static jakarta.persistence.FetchType.LAZY; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Participant extends BaseTimeEntity { + + @Id + @Column(name = "participant_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String nickname; + + private String gameId; + + private String gameTier; + + private String profileImageUrl; + + @Enumerated(EnumType.STRING) + private Role role; + + @Enumerated(EnumType.STRING) + private ParticipantStatus participantStatus; + + @Enumerated(EnumType.STRING) + private RequestStatus requestStatus; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "channel_id") + private Channel channel; + + @Column(name = "custom_channel_index") + private int index; + + private String puuid; + + public static Participant createHostChannel(Member member, Channel channel) { + Participant participant = new Participant(); + participant.nickname = member.getNickname(); + participant.profileImageUrl = member.getProfileImageUrl(); + participant.role = Role.HOST; + participant.member = member; + participant.channel = channel; + + + participant.requestStatus = RequestStatus.NO_REQUEST; + + participant.gameId = GlobalConstant.NO_DATA.getData(); + participant.gameTier = GlobalConstant.NO_DATA.getData(); + + return participant; + } + + public static Participant participateChannel(Member member, Channel channel) { + Participant participant = new Participant(); + participant.nickname = member.getNickname(); + participant.profileImageUrl = member.getProfileImageUrl(); + participant.role = Role.OBSERVER; + participant.member = member; + participant.channel = channel; + + participant.requestStatus = RequestStatus.NO_REQUEST; + + participant.gameId = GlobalConstant.NO_DATA.getData(); + participant.gameTier = GlobalConstant.NO_DATA.getData(); + + return participant; + } + + + public Participant approveParticipantMatch() { + this.requestStatus = RequestStatus.DONE; + this.role = Role.PLAYER; + this.participantStatus = ParticipantStatus.PROGRESS; + + return this; + } + + + public Participant rejectParticipantRequest() { + this.requestStatus = RequestStatus.REJECT; + this.role = Role.OBSERVER; + + return this; + } + + public Participant disqualificationParticipant(){ + this.participantStatus = ParticipantStatus.DISQUALIFICATION; + + return this; + } + + + public Participant updateParticipantStatus(String gameId, String gameTier, String nickname, String puuid) { + this.gameId = gameId; + this.gameTier = gameTier; + this.nickname = nickname; + this.puuid = puuid; + + this.requestStatus = RequestStatus.REQUEST; + + return this; + } + + public Participant updateHostRole() { + this.requestStatus = RequestStatus.NO_REQUEST; + this.role = Role.HOST; + + return this; + } + + public Participant dropoutParticipantStatus(){ + this.participantStatus = ParticipantStatus.DROPOUT; + + return this; + } + + public void newCustomChannelIndex(Optional index) { + this.index = index.map(i -> i + 1).orElseGet(() -> 0); + } + + public void updateCustomChannelIndex(Integer index) { + this.index = index; + } + + public void updateNickname(String newNickname) { this.nickname = newNickname; } + + public void deleteChannelAndMember() { + this.channel = null; + this.member = null; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/ParticipantStatus.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/ParticipantStatus.java new file mode 100644 index 00000000..4bf0c2c0 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/ParticipantStatus.java @@ -0,0 +1,5 @@ +package leaguehub.leaguehubbackend.domain.participant.entity; + +public enum ParticipantStatus { + PROGRESS, DROPOUT, DISQUALIFICATION +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/RequestStatus.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/RequestStatus.java new file mode 100644 index 00000000..127827a1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/RequestStatus.java @@ -0,0 +1,16 @@ +package leaguehub.leaguehubbackend.domain.participant.entity; + +public enum RequestStatus { + + NO_REQUEST(0), REQUEST(1), DONE(2), REJECT(3); + + private final int num; + + RequestStatus(int num) { + this.num = num; + } + + public int getNum() { + return num; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/Role.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/Role.java new file mode 100644 index 00000000..a63857d0 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/entity/Role.java @@ -0,0 +1,15 @@ +package leaguehub.leaguehubbackend.domain.participant.entity; + +public enum Role { + HOST(0), PLAYER(1), OBSERVER(2); + + private final int num; + + Role(int num) { + this.num = num; + } + + public int getNum() { + return num; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/ParticipantExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/ParticipantExceptionCode.java new file mode 100644 index 00000000..711434eb --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/ParticipantExceptionCode.java @@ -0,0 +1,32 @@ +package leaguehub.leaguehubbackend.domain.participant.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.*; + +@Getter +@RequiredArgsConstructor +public enum ParticipantExceptionCode implements ExceptionCode { + + INVALID_PARTICIPANT_IMAGE(BAD_REQUEST, "PA-C-003", "유효하지 않은 이미지입니다."), + PARTICIPANT_GAME_ID_NOT_FOUND(NOT_FOUND, "PA-C-004", "게임 ID를 찾을 수 없습니다."), + INVALID_PARTICIPANT_LOGIN_REQUEST(BAD_REQUEST, "PA-C-005", "로그인이 필요합니다."), + INVALID_PARTICIPANT_ROLE_REQUEST(BAD_REQUEST, "PA-C-006", "이미 참가하였거나 경기 관리자입니다."), + INVALID_PARTICIPANT_TIER_REQUEST(BAD_REQUEST, "PA-C-007", "유저 티어가 설정된 티어보다 높습니다."), + INVALID_PARTICIPANT_PLAY_COUNT_REQUEST(BAD_REQUEST, "PA-C-008", "경기 횟수가 설정된 횟수보다 낮습니다."), + INVALID_PARTICIPANT_AUTH(UNAUTHORIZED, "PA-C-009", "해당 채널의 권한이 유효하지 않습니다"), + PARTICIPANT_ALREADY_REQUESTED(BAD_REQUEST, "PA-C-010", "이미 참가요청 되었습니다."), + PARTICIPANT_REJECTED_REQUESTED(BAD_REQUEST, "PA-C-011", "거절된 사용자입니다."), + PARTICIPANT_DUPLICATED_GAME_ID(BAD_REQUEST, "PA-C-012", "해당 게임아이디는 이미 존재합니다."), + PARTICIPANT_NOT_GAME_HOST(UNAUTHORIZED, "PA-C-013", "해당 채널 관리자가 아닙니다."), + PARTICIPANT_REAL_PLAYER_IS_MAX(BAD_REQUEST, "PA-C-014", "플레이어의 수가 최대입니다."), + PARTICIPANT_NOT_FOUNT(NOT_FOUND, "PA-C-015", "참가자를 찾을 수 없습니다."); + + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/ParticipantExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/ParticipantExceptionHandler.java new file mode 100644 index 00000000..2fdaf879 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/ParticipantExceptionHandler.java @@ -0,0 +1,171 @@ +package leaguehub.leaguehubbackend.domain.participant.exception; + +import leaguehub.leaguehubbackend.domain.participant.exception.exception.*; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class ParticipantExceptionHandler { + + @ExceptionHandler(ParticipantGameIdNotFoundException.class) + public ResponseEntity participantNotFoundException( + ParticipantGameIdNotFoundException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(InvalidParticipantAuthException.class) + public ResponseEntity InvalidParticipantAuthException( + InvalidParticipantAuthException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantInvalidLoginException.class) + public ResponseEntity ParticipantInvalidLoginException( + ParticipantInvalidLoginException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantInvalidPlayCountException.class) + public ResponseEntity ParticipantInvalidPlayCountException( + ParticipantInvalidPlayCountException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantInvalidRankException.class) + public ResponseEntity ParticipantInvalidRankException( + ParticipantInvalidRankException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantInvalidRoleException.class) + public ResponseEntity ParticipantInvalidRoleException( + ParticipantInvalidRoleException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantRejectedRequestedException.class) + public ResponseEntity ParticipantRejectedRequestedException( + ParticipantRejectedRequestedException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + @ExceptionHandler(ParticipantAlreadyRequestedException.class) + public ResponseEntity ParticipantAlreadyRequestedException( + ParticipantAlreadyRequestedException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantDuplicatedGameIdException.class) + public ResponseEntity ParticipantDuplicatedGameIdException( + ParticipantDuplicatedGameIdException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantNotGameHostException.class) + public ResponseEntity ParticipantNotGameHostException( + ParticipantNotGameHostException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantRealPlayerIsMaxException.class) + public ResponseEntity ParticipantRealPlayerIsMaxException( + ParticipantRealPlayerIsMaxException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(ParticipantNotFoundException.class) + public ResponseEntity ParticipantNotFoundException( + ParticipantNotFoundException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/InvalidParticipantAuthException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/InvalidParticipantAuthException.java new file mode 100644 index 00000000..f77fc6f1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/InvalidParticipantAuthException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import org.springframework.security.core.AuthenticationException; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.INVALID_PARTICIPANT_AUTH; + +public class InvalidParticipantAuthException extends AuthenticationException { + + private final ExceptionCode exceptionCode; + + public InvalidParticipantAuthException() { + super(INVALID_PARTICIPANT_AUTH.getCode()); + this.exceptionCode = INVALID_PARTICIPANT_AUTH; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantAlreadyRequestedException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantAlreadyRequestedException.java new file mode 100644 index 00000000..aa1d5cbb --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantAlreadyRequestedException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_ALREADY_REQUESTED; + +public class ParticipantAlreadyRequestedException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantAlreadyRequestedException(){ + super(PARTICIPANT_ALREADY_REQUESTED.getMessage()); + this.exceptionCode = PARTICIPANT_ALREADY_REQUESTED; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantDuplicatedGameIdException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantDuplicatedGameIdException.java new file mode 100644 index 00000000..c75839dd --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantDuplicatedGameIdException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_DUPLICATED_GAME_ID; + + +public class ParticipantDuplicatedGameIdException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantDuplicatedGameIdException(){ + super(PARTICIPANT_DUPLICATED_GAME_ID.getMessage()); + this.exceptionCode = PARTICIPANT_DUPLICATED_GAME_ID; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantGameIdNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantGameIdNotFoundException.java new file mode 100644 index 00000000..145b98b1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantGameIdNotFoundException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_GAME_ID_NOT_FOUND; + + +public class ParticipantGameIdNotFoundException extends RuntimeException { + + private final ExceptionCode exceptionCode; + + public ParticipantGameIdNotFoundException(){ + super(PARTICIPANT_GAME_ID_NOT_FOUND.getMessage()); + this.exceptionCode = PARTICIPANT_GAME_ID_NOT_FOUND; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidLoginException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidLoginException.java new file mode 100644 index 00000000..cbd49c29 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidLoginException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.INVALID_PARTICIPANT_LOGIN_REQUEST; + +public class ParticipantInvalidLoginException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantInvalidLoginException(){ + super(INVALID_PARTICIPANT_LOGIN_REQUEST.getMessage()); + this.exceptionCode = INVALID_PARTICIPANT_LOGIN_REQUEST; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidPlayCountException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidPlayCountException.java new file mode 100644 index 00000000..0f570bba --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidPlayCountException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.INVALID_PARTICIPANT_PLAY_COUNT_REQUEST; + +public class ParticipantInvalidPlayCountException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantInvalidPlayCountException(){ + super(INVALID_PARTICIPANT_PLAY_COUNT_REQUEST.getMessage()); + this.exceptionCode = INVALID_PARTICIPANT_PLAY_COUNT_REQUEST; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidRankException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidRankException.java new file mode 100644 index 00000000..2a942ff6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidRankException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.INVALID_PARTICIPANT_TIER_REQUEST; + +public class ParticipantInvalidRankException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantInvalidRankException(){ + super(INVALID_PARTICIPANT_TIER_REQUEST.getMessage()); + this.exceptionCode = INVALID_PARTICIPANT_TIER_REQUEST; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidRoleException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidRoleException.java new file mode 100644 index 00000000..23439e47 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantInvalidRoleException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.INVALID_PARTICIPANT_ROLE_REQUEST; + +public class ParticipantInvalidRoleException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantInvalidRoleException(){ + super(INVALID_PARTICIPANT_ROLE_REQUEST.getMessage()); + this.exceptionCode = INVALID_PARTICIPANT_ROLE_REQUEST; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantNotFoundException.java new file mode 100644 index 00000000..eca7dc3a --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantNotFoundException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_NOT_FOUNT; + +public class ParticipantNotFoundException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantNotFoundException(){ + super(PARTICIPANT_NOT_FOUNT.getMessage()); + this.exceptionCode = PARTICIPANT_NOT_FOUNT; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantNotGameHostException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantNotGameHostException.java new file mode 100644 index 00000000..e2eb9ff3 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantNotGameHostException.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_NOT_GAME_HOST; + +public class ParticipantNotGameHostException extends RuntimeException{ + private final ExceptionCode exceptionCode; + + public ParticipantNotGameHostException(){ + super(PARTICIPANT_NOT_GAME_HOST.getMessage()); + this.exceptionCode = PARTICIPANT_NOT_GAME_HOST; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantRealPlayerIsMaxException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantRealPlayerIsMaxException.java new file mode 100644 index 00000000..25743538 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantRealPlayerIsMaxException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_REAL_PLAYER_IS_MAX; + +public class ParticipantRealPlayerIsMaxException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public ParticipantRealPlayerIsMaxException(){ + super(PARTICIPANT_REAL_PLAYER_IS_MAX.getMessage()); + this.exceptionCode = PARTICIPANT_REAL_PLAYER_IS_MAX; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantRejectedRequestedException.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantRejectedRequestedException.java new file mode 100644 index 00000000..3a2697d1 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/exception/exception/ParticipantRejectedRequestedException.java @@ -0,0 +1,19 @@ +package leaguehub.leaguehubbackend.domain.participant.exception.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.domain.participant.exception.ParticipantExceptionCode.PARTICIPANT_REJECTED_REQUESTED; + +public class ParticipantRejectedRequestedException extends RuntimeException { + + private final ExceptionCode exceptionCode; + + public ParticipantRejectedRequestedException(){ + super(PARTICIPANT_REJECTED_REQUESTED.getMessage()); + this.exceptionCode = PARTICIPANT_REJECTED_REQUESTED; + } + + public ExceptionCode getExceptionCode(){ + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/repository/ParticipantRepository.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/repository/ParticipantRepository.java new file mode 100644 index 00000000..5382ddee --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/repository/ParticipantRepository.java @@ -0,0 +1,41 @@ +package leaguehub.leaguehubbackend.domain.participant.repository; + +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.ParticipantStatus; +import leaguehub.leaguehubbackend.domain.participant.entity.RequestStatus; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface ParticipantRepository extends JpaRepository { + + List findAllByMemberId(Long memberId); + + List findAllByMemberIdOrderByIndex(Long memberId); + + @Query("select p from Participant p join fetch p.channel where p.channel.channelLink = :channelLink and p.member.id = :memberId") + Optional findParticipantByMemberIdAndChannel_ChannelLink(@Param("memberId") Long memberId, @Param("channelLink") String channelLink); + + @Query("select p from Participant p join fetch p.channel where p.channel.channelLink = :channelLink and p.id = :participantId") + Optional findParticipantByIdAndChannel_ChannelLink(@Param("participantId") Long participantId, @Param("channelLink") String channelLink); + + List findAllByChannel_ChannelLinkAndRoleAndRequestStatusOrderByNicknameAsc(String channelLink, Role role, RequestStatus requestStatus); + + List findAllByChannel_ChannelLink(String channelLink); + + List findParticipantByRoleAndChannel_ChannelLinkOrderById(Role role, String channelLink); + + Optional findParticipantByMemberIdAndChannel_Id(Long memberId, Long channelId); + + @Query("SELECT MAX(p.index) from Participant p WHERE p.member.id = :memberId") + Optional findMaxIndexByParticipant(@Param("memberId") Long memberId); + + List findAllByMemberIdAndAndIndexGreaterThan(Long memberId, int deleteIndex); + + List findAllByChannel_ChannelLinkAndRoleAndParticipantStatus(String channelLink, Role role,ParticipantStatus participantStatus); + +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantManagementService.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantManagementService.java new file mode 100644 index 00000000..19bf3cea --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantManagementService.java @@ -0,0 +1,321 @@ +package leaguehub.leaguehubbackend.domain.participant.service; + +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayerResultStatus; +import leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidTokenException; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantIdDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantIdResponseDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantSummonerDetail; +import leaguehub.leaguehubbackend.domain.participant.entity.GameTier; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.*; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus.DISQUALIFICATION; +import static leaguehub.leaguehubbackend.domain.participant.entity.RequestStatus.*; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.*; + +@Service +@Transactional +@RequiredArgsConstructor +public class ParticipantManagementService { + + private final MemberService memberService; + private final ChannelService channelService; + private final ParticipantRepository participantRepository; + private final MemberRepository memberRepository; + private final MatchPlayerRepository matchPlayerRepository; + private final ChannelRuleRepository channelRuleRepository; + private final ParticipantService participantService; + private final JwtService jwtService; + private final ParticipantWebClientService participantWebClientService; + + /** + * 사용자가 지정한 Channel을 참가 + * + * @param channelLink + * @return Participant participant + */ + public ParticipantChannelDto participateChannel(String channelLink) { + + Member member = memberService.findCurrentMember(); + + Channel channel = channelService.getChannel(channelLink); + + duplicateParticipant(member, channelLink); + + Participant participant = Participant.participateChannel(member, channel); + participant.newCustomChannelIndex(participantRepository.findMaxIndexByParticipant(member.getId())); + participantRepository.save(participant); + + return new ParticipantChannelDto( + channel.getId(), + channel.getChannelLink(), + channel.getTitle(), + channel.getGameCategory().getNum(), + channel.getChannelImageUrl(), + participant.getIndex() + ); + } + + /** + * 해당 채널 나가기 + * + * @param channelLink + */ + public void leaveChannel(String channelLink) { + Member member = memberService.findCurrentMember(); + + Participant participant = getParticipant(channelLink, member); + + participantRepository.deleteById(participant.getId()); + + List participantAfterDelete = participantRepository.findAllByMemberIdAndAndIndexGreaterThan( + member.getId(), participant.getIndex()); + + for (Participant allParticipantByMember : participantAfterDelete) { + allParticipantByMember.updateCustomChannelIndex(allParticipantByMember.getIndex() - 1); + } + } + + + /** + * 대회 참가자 실격 & 기권 서비스 + * + * @param channelLink + * @param message + * @return + */ + public ParticipantIdResponseDto disqualifiedParticipant(String channelLink, ParticipantIdDto message) { + if (message.getRole() == HOST.getNum()) { + return disqualifiedToHost(channelLink, message); + } + + if (message.getRole() == PLAYER.getNum()) { + return selfDisqualified(channelLink, message); + } + + throw new InvalidParticipantAuthException(); + } + + + /** + * 관전자인 사용자가 해당 채널의 경기에 참가 + * + * @param responseDto + */ + public void participateMatch(ParticipantDto responseDto, String channelLink) { + Participant participant = participantService.getParticipant(channelLink); + + checkParticipateMatch(participant); + + ChannelRule channelRule = channelRuleRepository + .findChannelRuleByChannel_ChannelLink(channelLink); + + checkDuplicateNickname(responseDto.getGameId(), channelLink); + + ParticipantSummonerDetail participantSummonerDetail = participantWebClientService.requestUserGameInfo(responseDto.getGameId()); + String userGameInfo = participantSummonerDetail.getUserGameInfo(); + String puuid = participantSummonerDetail.getPuuid(); + + GameTier tier = participantWebClientService.searchTier(userGameInfo); + + checkRule(channelRule, userGameInfo, tier); + + participant.updateParticipantStatus(responseDto.getGameId(), tier.toString(), responseDto.getNickname(), puuid); + } + + /** + * 참여 채널의 순서를 커스텀 + * @param participantChannelDtoList + * @return + */ + public List updateCustomChannelIndex(List participantChannelDtoList) { + Member member = memberService.findCurrentMember(); + + List allByMemberId = participantRepository.findAllByMemberId(member.getId()); + + participantChannelDtoList.forEach(participantChannelDto -> { + allByMemberId.stream() + .filter(participant -> participant.getChannel().getChannelLink().equals(participantChannelDto.getChannelLink())) + .forEach(participant -> participant.updateCustomChannelIndex(participantChannelDto.getCustomChannelIndex())); + }); + + return channelService.findParticipantChannelList(); + } + + + /** + * 참가자 중복검사 + * + * @param member + * @param channelLink + */ + private void duplicateParticipant(Member member, String channelLink) { + Optional existingParticipant = participantRepository.findParticipantByMemberIdAndChannel_ChannelLink(member.getId(), channelLink); + + if (existingParticipant.isPresent()) { + throw new ParticipantDuplicatedGameIdException(); + } + } + + + /** + * 관리자가 직접 참가자 실격시키는 메서드 + * + * @param channelLink + * @param message + * @return + */ + private ParticipantIdResponseDto disqualifiedToHost(String channelLink, ParticipantIdDto message) { + Participant myParticipant = findParticipantAccessToken(channelLink, message.getAccessToken()); + participantService.checkRole(myParticipant.getRole(), HOST); + + Participant findParticipant = participantService.getFindParticipant(channelLink, message.getParticipantId()); + + disqualificationParticipant(findParticipant); + + return new ParticipantIdResponseDto(message.getMatchPlayerId(), DISQUALIFICATION.getStatus()); + } + + + /** + * 참가자가 직접 기권하는 메서드 + * + * @param channelLink + * @param message + * @return + */ + private ParticipantIdResponseDto selfDisqualified(String channelLink, ParticipantIdDto message) { + Participant myParticipant = findParticipantAccessToken(channelLink, message.getAccessToken()); + participantService.checkRole(myParticipant.getRole(), PLAYER); + + disqualificationParticipant(myParticipant); + + return new ParticipantIdResponseDto(message.getMatchPlayerId(), DISQUALIFICATION.getStatus()); + } + + + /** + * 상태를 실격으로 변경시키는 메서드 + * + * @param findParticipant + */ + private void disqualificationParticipant(Participant findParticipant) { + findParticipant.disqualificationParticipant(); + matchPlayerRepository.findMatchPlayersByParticipantId(findParticipant.getId()) + .forEach(matchPlayer -> { + matchPlayer.updatePlayerCheckInStatus(PlayerStatus.DISQUALIFICATION); + matchPlayer.updateMatchPlayerResultStatus(MatchPlayerResultStatus.DISQUALIFICATION); + matchPlayer.updateMatchPlayerScoreDisqualified(); + } + ); + } + + /** + * AccessToken을 찾는 메서드 + * + * @param channelLink + * @param accessToken + * @return + */ + private Participant findParticipantAccessToken(String channelLink, String accessToken) { + String personalId = jwtService.extractPersonalId(accessToken) + .orElseThrow(() -> new AuthInvalidTokenException()); + Member member = memberRepository.findMemberByPersonalId(personalId).get(); + Participant myParticipant = getParticipant(channelLink, member); + return myParticipant; + } + + /** + * 채널에 이미 참가되어 있는지 확인하는 메서드 + * + * @param participant + */ + private void checkParticipateMatch(Participant participant) { + + if (participant.getRole() != OBSERVER + || participant.getRequestStatus() == DONE) throw new ParticipantInvalidRoleException(); + + if (participant.getRequestStatus() == REQUEST) throw new ParticipantAlreadyRequestedException(); + + if (participant.getRequestStatus() == REJECT) throw new ParticipantRejectedRequestedException(); + + } + + private void checkDuplicateNickname(String gameId, String channelLink) { + List participantList = participantRepository.findAllByChannel_ChannelLink(channelLink); + + boolean checkDuplicate = participantList.stream() + .anyMatch(participant -> participant.getGameId().equals(gameId)); + + if (checkDuplicate) { + throw new ParticipantDuplicatedGameIdException(); + } + } + + /** + * 해당 채널의 룰을 확인 + * + * @param channelRule + * @param userGameInfo + * @param tier + */ + private void checkRule(ChannelRule channelRule, String userGameInfo, GameTier tier) { + + rankRuleCheck(channelRule, tier); + playCountRuleCheck(channelRule, userGameInfo); + } + + private static void rankRuleCheck(ChannelRule channelRule, GameTier tier) { + + if (channelRule.getTier()) { + int tierMax = channelRule.getTierMax(); + int tierMin = channelRule.getTierMin(); + + int userRankScore = tier.getScore(); + if (userRankScore > tierMax || userRankScore < tierMin) throw new ParticipantInvalidRankException(); + } + } + + private void playCountRuleCheck(ChannelRule channelRule, String userGameInfo) { + + if (channelRule.getPlayCount()) { + int limitedPlayCount = channelRule.getLimitedPlayCount(); + int userPlayCount = participantWebClientService.getPlayCount(userGameInfo); + if (userPlayCount < limitedPlayCount) + throw new ParticipantInvalidPlayCountException(); + } + } + + /** + * channelLink와 member로 해당 채널에서의 참가자 찾기 - + * @param channelLink + * @param member + * @return + */ + private Participant getParticipant(String channelLink, Member member) { + Participant participant = participantRepository.findParticipantByMemberIdAndChannel_ChannelLink(member.getId(), channelLink) + .orElseThrow(ParticipantNotFoundException::new); + return participant; + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantQueryService.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantQueryService.java new file mode 100644 index 00000000..ded2ef5c --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantQueryService.java @@ -0,0 +1,155 @@ +package leaguehub.leaguehubbackend.domain.participant.service; + +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseStatusPlayerDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseUserGameInfoDto; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.global.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static leaguehub.leaguehubbackend.domain.member.entity.BaseRole.USER; +import static leaguehub.leaguehubbackend.domain.participant.entity.RequestStatus.*; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.*; + +@Service +@RequiredArgsConstructor +@Transactional +public class ParticipantQueryService { + + + private final MemberService memberService; + private final ParticipantRepository participantRepository; + private final ParticipantService participantService; + private final ParticipantWebClientService participantWebClientService; + + + /** + * 참여자의 권한 확인 메소드 + * + * @param channelLink + * @return + */ + @Transactional(readOnly = true) + public int findParticipantPermission(String channelLink) { + UserDetails userDetails = SecurityUtils.getAuthenticatedUser(); + if (userDetails == null) return OBSERVER.getNum(); + Member member = memberService.validateMember(userDetails.getUsername()); + + + Optional findParticipant = participantRepository.findParticipantByMemberIdAndChannel_ChannelLink(member.getId(), channelLink); + + return findParticipant.map(participant -> participant.getRole().getNum()) + .orElse(OBSERVER.getNum()); + } + + + /** + * 해당 채널의 첫 번째 관리자 반환 메소드 + * + * @param channelLink + * @return + */ + @Transactional(readOnly = true) + public String findChannelHost(String channelLink) { + return participantRepository.findParticipantByRoleAndChannel_ChannelLinkOrderById(HOST, channelLink).get(0).getNickname(); + } + + + /** + * 해당 채널의 관전자인 유저들을 조회 + * + * @param channelLink + * @return + */ + @Transactional(readOnly = true) + public List loadObserverPlayerList(String channelLink) { + Participant findParticipant = participantService.getParticipant(channelLink); + + participantService.checkRole(findParticipant.getRole(), HOST); + + List findParticipants = participantRepository.findAllByChannel_ChannelLinkAndRoleAndRequestStatusOrderByNicknameAsc(channelLink, OBSERVER, NO_REQUEST); + + return findParticipants.stream() + .filter(participant -> participant.getMember().getBaseRole() == USER) + .map(participant -> mapToResponseStatusPlayerDto(participant)) + .collect(Collectors.toList()); + } + + /** + * 요청을 보낸 사람들을 조회 + * + * @param channelLink + * @return + */ + @Transactional(readOnly = true) + public List loadRequestStatusPlayerList(String channelLink) { + Participant findParticipant = participantService.getParticipant(channelLink); + participantService.checkRole(findParticipant.getRole(), HOST); + + List findParticipants = + participantRepository.findAllByChannel_ChannelLinkAndRoleAndRequestStatusOrderByNicknameAsc + (channelLink, OBSERVER, REQUEST); + + return findParticipants.stream() + .map(participant -> mapToResponseStatusPlayerDto(participant)) + .collect(Collectors.toList()); + } + + /** + * 해당 채널의 PLAYER 역할인 유저들을 반환 + * + * @param channelLink + * @return RequestPlayerDtoList + */ + @Transactional(readOnly = true) + public List loadPlayers(String channelLink) { + + return participantRepository.findAllByChannel_ChannelLinkAndRoleAndRequestStatusOrderByNicknameAsc + (channelLink, PLAYER, DONE) + .stream() + .map(participant -> mapToResponseStatusPlayerDto(participant)) + .collect(Collectors.toList()); + } + + /** + * 게임 카테고리에 따라 요청 분할 + * + * @param gameId + * @param category + * @return + */ + public ResponseUserGameInfoDto selectGameCategory(String gameId, Integer category) { + ResponseUserGameInfoDto userGameInfoDto = new ResponseUserGameInfoDto(); + + if (category.equals(0)) { + userGameInfoDto = participantWebClientService.getTierAndPlayCount(gameId); + } + + return userGameInfoDto; + } + + /** + * 쿼리 컨트롤러에 반환할 DTO 정제하는 서비스 + * + * @param participant + * @return + */ + private ResponseStatusPlayerDto mapToResponseStatusPlayerDto(Participant participant) { + ResponseStatusPlayerDto responsePlayerDto = new ResponseStatusPlayerDto(); + responsePlayerDto.setPk(participant.getId()); + responsePlayerDto.setNickname(participant.getNickname()); + responsePlayerDto.setImgSrc(participant.getProfileImageUrl()); + responsePlayerDto.setGameId(participant.getGameId()); + responsePlayerDto.setTier(participant.getGameTier()); + return responsePlayerDto; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantRoleAndPermissionService.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantRoleAndPermissionService.java new file mode 100644 index 00000000..38284582 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantRoleAndPermissionService.java @@ -0,0 +1,129 @@ +package leaguehub.leaguehubbackend.domain.participant.service; + + +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantRealPlayerIsMaxException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static leaguehub.leaguehubbackend.domain.member.entity.BaseRole.GUEST; +import static leaguehub.leaguehubbackend.domain.participant.entity.RequestStatus.DONE; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.HOST; +import static leaguehub.leaguehubbackend.domain.participant.entity.Role.PLAYER; + +@Service +@Transactional +@RequiredArgsConstructor +public class ParticipantRoleAndPermissionService { + + + private final ParticipantService participantService; + private final ParticipantRepository participantRepository; + + /** + * 해당 채널의 요청한 참가자를 승인해줌 + * + * @param channelLink + * @param participantId + */ + public void approveParticipantRequest(String channelLink, Long participantId) { + Participant participant = participantService.getParticipant(channelLink); + participantService.checkRole(participant.getRole(), HOST); + + checkRealPlayerCount(participant.getChannel()); + + Participant findParticipant = participantService.getFindParticipant(channelLink, participantId); + + findParticipant.approveParticipantMatch(); + + updateRealPlayerCount(channelLink, participant.getChannel()); + } + + + /** + * 해당 채널의 요청한 참가자를 거절함 + * + * @param channelLink + * @param participantId + */ + public void rejectedParticipantRequest(String channelLink, Long participantId) { + Participant participant = participantService.getParticipant(channelLink); + participantService.checkRole(participant.getRole(), HOST); + + + Participant findParticipant = participantService.getFindParticipant(channelLink, participantId); + + findParticipant.rejectParticipantRequest(); + + updateRealPlayerCount(channelLink, participant.getChannel()); + } + + + /** + * 사용자를 관리자로 권한을 변경한다. + * + * @param channelLink + * @param participantId + */ + public void updateHostRole(String channelLink, Long participantId) { + Participant findParticipant = checkHostAndGetParticipant(channelLink, participantId); + if (findParticipant.getMember().getBaseRole() == GUEST) + throw new InvalidParticipantAuthException(); + + findParticipant.updateHostRole(); + } + + + /** + * 해당 사용자가 호스트인지 확인 + * @param channelLink + */ + public void checkAdminHost(String channelLink) { + Participant participant = participantService.getParticipant(channelLink); + participantService.checkRole(participant.getRole(), HOST); + } + + + + /** + * 해당 채널의 경기 참여자 횟수 체크 + * @param channel + */ + private void checkRealPlayerCount(Channel channel) { + if (channel.getRealPlayer() >= channel.getMaxPlayer()) + throw new ParticipantRealPlayerIsMaxException(); + } + + + /** + * 참여된 참가자 수 업데이트 + * @param channelLink + * @param channel + */ + private void updateRealPlayerCount(String channelLink, Channel channel) { + List playerLists = participantRepository + .findAllByChannel_ChannelLinkAndRoleAndRequestStatusOrderByNicknameAsc(channelLink, PLAYER, DONE); + + channel.updateRealPlayer(playerLists.size()); + } + + /** + * 호스트 확인 및 그 사용자를 반환하는 메서드 + * @param channelLink + * @param participantId + * @return + */ + private Participant checkHostAndGetParticipant(String channelLink, Long participantId) { + Participant participant = participantService.getParticipant(channelLink); + participantService.checkRole(participant.getRole(), HOST); + + return participantService.getFindParticipant(channelLink, participantId); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantService.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantService.java new file mode 100644 index 00000000..64a822da --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantService.java @@ -0,0 +1,79 @@ +package leaguehub.leaguehubbackend.domain.participant.service; + +import leaguehub.leaguehubbackend.domain.email.exception.exception.UnauthorizedEmailException; +import leaguehub.leaguehubbackend.domain.member.entity.BaseRole; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotFoundException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static leaguehub.leaguehubbackend.domain.member.entity.BaseRole.USER; + + +@Service +@Transactional +@RequiredArgsConstructor +public class ParticipantService { + + private final ParticipantRepository participantRepository; + private final MemberService memberService; + + + /** + * 제 3자가 channelLink와 participantId로 해당 채널에서의 참가자 찾기 + * @param channelLink + * @param participantId + * @return + */ + public Participant getFindParticipant(String channelLink, Long participantId) { + Participant findParticipant = participantRepository.findParticipantByIdAndChannel_ChannelLink(participantId, channelLink) + .orElseThrow(ParticipantNotFoundException::new); + return findParticipant; + } + + + /** + * 자기 자신이 participant를 찾을 때 + * @param channelLink + * @return + */ + public Participant getParticipant(String channelLink) { + + Member member = memberService.findCurrentMember(); + checkEmail(member.getBaseRole()); + + Participant participant = participantRepository.findParticipantByMemberIdAndChannel_ChannelLink(member.getId(), channelLink) + .orElseThrow(() -> new InvalidParticipantAuthException()); + + return participant; + } + + + /** + * 해당 채널의 역할이 맞는지 확인 + * @param myRole + * @param checkRole + */ + public void checkRole(Role myRole, Role checkRole) { + if (myRole != checkRole) { + throw new InvalidParticipantAuthException(); + } + } + + /** + * 이메일이 인증되었는지 확인 + * + * @param baseRole + */ + private void checkEmail(BaseRole baseRole) { + if (baseRole != USER) throw new UnauthorizedEmailException(); + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantWebClientService.java b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantWebClientService.java new file mode 100644 index 00000000..f3a64fc5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/domain/participant/service/ParticipantWebClientService.java @@ -0,0 +1,183 @@ +package leaguehub.leaguehubbackend.domain.participant.service; + +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantSummonerDetail; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseUserGameInfoDto; +import leaguehub.leaguehubbackend.domain.participant.entity.GameTier; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantGameIdNotFoundException; +import leaguehub.leaguehubbackend.global.exception.global.exception.GlobalServerErrorException; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Service +@Transactional +@RequiredArgsConstructor +public class ParticipantWebClientService { + + + private final WebClient webClient; + private final JSONParser jsonParser; + + @Value("${riot-api-key-1}") + private String riot_api_key; + + + /** + * 닉네임 + 태크로 고유 puuid 추출 + * 받은 nickname으로 split 나누기 + */ + public String getSummonerPUuid(String nickname) { + String pUuidURL = "https://asia.api.riotgames.com/riot/account/v1/accounts/by-riot-id/"; + + String gameId = nickname.split("#")[0]; + String gameTag = nickname.split("#")[1]; + + + JSONObject userAccount = webClient.get() + .uri(pUuidURL + gameId + "/" + gameTag + riot_api_key) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new ParticipantGameIdNotFoundException())) + .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(JSONObject.class) + .block(); + + return userAccount.get("puuid").toString(); + } + + /** + * 고유 puuid로 유저의 정보 추출 + * + * @param nickname + * @return id + */ + public JSONObject getSummonerId(String nickname) { + String puuid = getSummonerPUuid(nickname); + + String summonerUrl = "https://kr.api.riotgames.com/tft/summoner/v1/summoners/by-puuid/"; + + return webClient.get() + .uri(summonerUrl + puuid + riot_api_key) + .retrieve() + .onStatus(HttpStatusCode::is4xxClientError, response -> Mono.error(new ParticipantGameIdNotFoundException())) + .onStatus(HttpStatusCode::is5xxServerError, response -> Mono.error(new GlobalServerErrorException())) + .bodyToMono(JSONObject.class) + .block(); + } + + /** + * 외부 api호출로 유저 상세정보 출력 + * + * @param nickname + * @return + */ + public ParticipantSummonerDetail requestUserGameInfo(String nickname) { + + JSONObject summonerDetail = getSummonerId(nickname); + + String gameId = summonerDetail.get("id").toString(); + String puuid = summonerDetail.get("puuid").toString(); + + String tierUrl = "https://kr.api.riotgames.com/tft/league/v1/entries/by-summoner/"; + + + JSONArray summonerDetails = webClient.get() + .uri(tierUrl + gameId + riot_api_key) + .retrieve() + .bodyToMono(JSONArray.class) + .block(); + + String arraytoString = summonerDetails.toJSONString(); + + ParticipantSummonerDetail participantSummonerDetail = new ParticipantSummonerDetail(); + participantSummonerDetail.setPuuid(puuid); + participantSummonerDetail.setUserGameInfo(arraytoString); + + return participantSummonerDetail; + + } + + /** + * 고유 id로 티어추출 + * + * @param userGameInfo + * @return Tier + */ + @SneakyThrows + public GameTier searchTier(String userGameInfo) { + + String jsonToString = userGameInfo.replaceAll("[\\[\\[\\]]", ""); + + if (jsonToString.isEmpty()) { + return GameTier.getUnranked(); + } + + JSONObject summonerDetail = (JSONObject) jsonParser.parse(jsonToString); + String tier = summonerDetail.get("tier").toString(); + String rank = summonerDetail.get("rank").toString(); + + + return GameTier.findGameTier(tier, rank); + + } + + /** + * 플레이 횟수 검색 + * + * @param userGameInfo + * @return + */ + public Integer getPlayCount(String userGameInfo) { + + String jsonToString = userGameInfo.replaceAll("[\\[\\[\\]]", ""); + + if (jsonToString.isEmpty()) + return 0; + + return stringToIntegerPlayCount(jsonToString); + + } + + /** + * 플레이 횟수 문자열을 정수형으로 변환 + * + * @param userGameInfoJSON + * @return + */ + @SneakyThrows + public Integer stringToIntegerPlayCount(String userGameInfoJSON) { + JSONObject summonerDetail = (JSONObject) jsonParser.parse(userGameInfoJSON); + + return Integer.parseInt(summonerDetail.get("wins").toString()) + Integer.parseInt(summonerDetail.get("losses").toString()); + } + + /** + * 티어와 플레이 횟수를 받아 반환 + * + * @param nickname + * @return + */ + public ResponseUserGameInfoDto getTierAndPlayCount(String nickname) { + + ParticipantSummonerDetail participantSummonerDetail = requestUserGameInfo(nickname); + String userGameInfo = participantSummonerDetail.getUserGameInfo(); + + GameTier tier = searchTier(userGameInfo); + + Integer playCount = getPlayCount(userGameInfo); + + ResponseUserGameInfoDto userGameInfoDto = new ResponseUserGameInfoDto(); + userGameInfoDto.setTier(tier.toString()); + userGameInfoDto.setPlayCount(playCount); + + return userGameInfoDto; + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/audit/AuditingConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/audit/AuditingConfig.java new file mode 100644 index 00000000..24c9d802 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/audit/AuditingConfig.java @@ -0,0 +1,9 @@ +package leaguehub.leaguehubbackend.global.audit; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class AuditingConfig { +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/audit/BaseTimeEntity.java b/src/main/java/leaguehub/leaguehubbackend/global/audit/BaseTimeEntity.java new file mode 100644 index 00000000..376394e3 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/audit/BaseTimeEntity.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.global.audit; + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + @Column(updatable = false) + private LocalDateTime createdDate; + + @LastModifiedDate + private LocalDateTime modifiedDate; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/audit/GlobalConstant.java b/src/main/java/leaguehub/leaguehubbackend/global/audit/GlobalConstant.java new file mode 100644 index 00000000..7847669f --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/audit/GlobalConstant.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.global.audit; + +import lombok.Getter; + +@Getter +public enum GlobalConstant { + NO_DATA("NO_DATA"); + + private final String data; + + GlobalConstant(String data) { + this.data = data; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/ImageUploadRequest.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/ImageUploadRequest.java new file mode 100644 index 00000000..6e6aa996 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/ImageUploadRequest.java @@ -0,0 +1,9 @@ +package leaguehub.leaguehubbackend.global.aws.s3; + +import org.springframework.web.multipart.MultipartFile; + +public record ImageUploadRequest( + + MultipartFile uploadImage +) { +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/S3Config.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/S3Config.java new file mode 100644 index 00000000..a1af98ea --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/S3Config.java @@ -0,0 +1,29 @@ +package leaguehub.leaguehubbackend.global.aws.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @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(){ + BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .build(); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/S3FileUploadService.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/S3FileUploadService.java new file mode 100644 index 00000000..d1583745 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/S3FileUploadService.java @@ -0,0 +1,68 @@ +package leaguehub.leaguehubbackend.global.aws.s3; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.transfer.TransferManager; +import com.amazonaws.services.s3.transfer.Upload; +import leaguehub.leaguehubbackend.global.aws.s3.dto.S3ResponseDto; +import leaguehub.leaguehubbackend.global.aws.s3.exception.exception.S3InvalidImageException; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class S3FileUploadService { + + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.s3.bucket.url}") + private String defaultUrl; + + private final AmazonS3Client amazonS3Client; + + public S3ResponseDto uploadFile(MultipartFile uploadFile) { + + String origName = uploadFile.getOriginalFilename(); + + String ext = origName.substring(origName.lastIndexOf('.')); + + String saveFileName = UUID.randomUUID().toString().replaceAll("-","") + ext; + + File file = new File(System.getProperty("user.dir") + saveFileName); + + try { + uploadFile.transferTo(file); + } catch (IOException e) { + throw new S3InvalidImageException(); + } + + TransferManager transferManager = new TransferManager(this.amazonS3Client); + + PutObjectRequest request = new PutObjectRequest(bucket, saveFileName, file); + + Upload upload = transferManager.upload(request); + + try { + upload.waitForCompletion(); + } catch (InterruptedException e) { + throw new S3InvalidImageException(); + } + + String imageUrl = defaultUrl + saveFileName; + + file.delete(); + + S3ResponseDto s3ResponseDto = new S3ResponseDto(); + s3ResponseDto.setImgUrl(imageUrl); + return s3ResponseDto; + + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/controller/S3Controller.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/controller/S3Controller.java new file mode 100644 index 00000000..948cb8b5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/controller/S3Controller.java @@ -0,0 +1,44 @@ +package leaguehub.leaguehubbackend.global.aws.s3.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.tags.Tag; +import leaguehub.leaguehubbackend.global.aws.s3.ImageUploadRequest; +import leaguehub.leaguehubbackend.global.aws.s3.S3FileUploadService; +import leaguehub.leaguehubbackend.global.aws.s3.dto.S3ResponseDto; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.http.HttpStatus.OK; + +@Slf4j +@Tag(name = "S3-Controller", description = "사진 업로드 관련 API") +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class S3Controller { + + final S3FileUploadService s3FileUploadService; + + @Operation(summary = "사진 업로드 API") + @Parameter(name = "multipartFile", description = "사진 파일", example = "image.jpg") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "사진 저장 주소 반환"), + @ApiResponse(responseCode = "400", description = "유효하지 않은 이미지입니다.", content = @Content(mediaType = "application/json", schema = @Schema(implementation = ExceptionResponse.class))) + }) + @PostMapping("/image") + public ResponseEntity saveProfile(ImageUploadRequest imageUploadRequest) { + S3ResponseDto s3ResponseDto = s3FileUploadService.uploadFile(imageUploadRequest.uploadImage()); + + return new ResponseEntity<>(s3ResponseDto, OK); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/dto/S3ResponseDto.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/dto/S3ResponseDto.java new file mode 100644 index 00000000..f0893819 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/dto/S3ResponseDto.java @@ -0,0 +1,9 @@ +package leaguehub.leaguehubbackend.global.aws.s3.dto; + +import lombok.Data; + +@Data +public class S3ResponseDto { + + private String imgUrl; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/S3ErrorCode.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/S3ErrorCode.java new file mode 100644 index 00000000..f5a541be --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/S3ErrorCode.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.global.aws.s3.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +@Getter +@RequiredArgsConstructor +public enum S3ErrorCode implements ExceptionCode { + INVALID_S3_IMAGE(BAD_REQUEST, "S3-C-001", "유효하지 않은 이미지입니다."); + + private final HttpStatus httpStatus; + private final String code; + private final String message; +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/S3ExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/S3ExceptionHandler.java new file mode 100644 index 00000000..5ff412f9 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/S3ExceptionHandler.java @@ -0,0 +1,29 @@ +package leaguehub.leaguehubbackend.global.aws.s3.exception; + +import leaguehub.leaguehubbackend.global.aws.s3.exception.exception.S3InvalidImageException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +@RequiredArgsConstructor +public class S3ExceptionHandler { + + @ExceptionHandler(S3InvalidImageException.class) + public ResponseEntity s3InvalidImageException( + S3InvalidImageException e + ){ + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/exception/S3InvalidImageException.java b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/exception/S3InvalidImageException.java new file mode 100644 index 00000000..553ef062 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/aws/s3/exception/exception/S3InvalidImageException.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.global.aws.s3.exception.exception; + +import leaguehub.leaguehubbackend.global.aws.s3.exception.S3ErrorCode; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +public class S3InvalidImageException extends RuntimeException{ + + private final ExceptionCode exceptionCode; + + public S3InvalidImageException(){ + super(S3ErrorCode.INVALID_S3_IMAGE.getMessage()); + this.exceptionCode = S3ErrorCode.INVALID_S3_IMAGE; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/CorsConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/CorsConfig.java new file mode 100644 index 00000000..f55f49b6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/CorsConfig.java @@ -0,0 +1,28 @@ +package leaguehub.leaguehubbackend.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + + config.setAllowCredentials(true); //내서버가 응답을 할때 json을 자바스크립트에서 처리할 수 있게 할지 + config.addAllowedOriginPattern("*"); //모든 아이피를 응답허용 + config.addAllowedHeader("*"); //모든 header 응답허용 + //config.addExposedHeader("*"); + config.addAllowedMethod("*"); //모든 post,get,put 허용 + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } + + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/EmailConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/EmailConfig.java new file mode 100644 index 00000000..781749f2 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/EmailConfig.java @@ -0,0 +1,42 @@ +package leaguehub.leaguehubbackend.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class EmailConfig { + + @Value("${LEAGUE_HUB_EMAIL}") + private String emailAddress; + + @Value("${LEAGUE_HUB_EMAIL_PW}") + private String emailPassword; + + @Bean + public JavaMailSender mailSender() { + + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost("smtp.gmail.com"); + mailSender.setPort(587); + mailSender.setUsername(emailAddress); + mailSender.setPassword(emailPassword); + + Properties javaMailProperties = new Properties(); + javaMailProperties.put("mail.transport.protocol", "smtp"); + javaMailProperties.put("mail.smtp.auth", "true"); + javaMailProperties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + javaMailProperties.put("mail.smtp.starttls.enable", "true"); + javaMailProperties.put("mail.debug", "true"); + javaMailProperties.put("mail.smtp.ssl.trust", "smtp.gmail.com"); + javaMailProperties.put("mail.smtp.ssl.protocols", "TLSv1.2"); + + mailSender.setJavaMailProperties(javaMailProperties); + + return mailSender; + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/JsonSimpleConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/JsonSimpleConfig.java new file mode 100644 index 00000000..01486d44 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/JsonSimpleConfig.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.global.config; + +import org.json.simple.parser.JSONParser; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JsonSimpleConfig { + + @Bean + public JSONParser jsonParser(){ + return new JSONParser(); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/SchedulingConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/SchedulingConfig.java new file mode 100644 index 00000000..f5564609 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/SchedulingConfig.java @@ -0,0 +1,28 @@ +package leaguehub.leaguehubbackend.global.config; + + +import leaguehub.leaguehubbackend.domain.notice.service.NoticeService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.Scheduled; + +import java.time.LocalTime; + +@Configuration +@Slf4j +@RequiredArgsConstructor +public class SchedulingConfig { + + private final NoticeService noticeService; + + + @Scheduled(cron = "${my.custom.cron}") + @Bean + public void noticeUpdateRun(){ + + noticeService.updateNoticeSchedule(); + log.info("업데이트 한 시간 = " + LocalTime.now()); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/SecurityConfiguration.java b/src/main/java/leaguehub/leaguehubbackend/global/config/SecurityConfiguration.java new file mode 100644 index 00000000..202bbf04 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/SecurityConfiguration.java @@ -0,0 +1,57 @@ +package leaguehub.leaguehubbackend.global.config; + +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import leaguehub.leaguehubbackend.global.config.jwt.JwtAuthenticationEntryPoint; +import leaguehub.leaguehubbackend.global.config.jwt.JwtAuthenticationProcessingFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfiguration { + + private final MemberRepository memberRepository; + private final JwtService jwtService; + private final CorsConfig config; + + JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint = new JwtAuthenticationEntryPoint(); + + @Bean + WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring() + .requestMatchers(new AntPathRequestMatcher("/h2-console/**")); + } + + @Bean + public JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter() { + return new JwtAuthenticationProcessingFilter(jwtService, memberRepository); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .addFilterAfter(jwtAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(handle -> handle + .authenticationEntryPoint(jwtAuthenticationEntryPoint) + ) + .addFilter(config.corsFilter()) + .build(); + + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/SwaggerConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/SwaggerConfig.java new file mode 100644 index 00000000..ae7d41c6 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/SwaggerConfig.java @@ -0,0 +1,35 @@ +package leaguehub.leaguehubbackend.global.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + final String securitySchemeName = "bearerAuth"; + return new OpenAPI() + .components(new Components() + .addSecuritySchemes(securitySchemeName, + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("Bearer") + .bearerFormat("JWT") + )) + .info(apiInfo()) + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)); + } + + private Info apiInfo() { + return new Info() + .title("LeagueHub API 목록") + .description("리그허브 API 목록") + .version("0.0.1"); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/WebClientConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/WebClientConfig.java new file mode 100644 index 00000000..165efd92 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/WebClientConfig.java @@ -0,0 +1,15 @@ +package leaguehub.leaguehubbackend.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient() { + + return WebClient.create(); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/WebSocketConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/config/WebSocketConfig.java new file mode 100644 index 00000000..de4724bc --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/WebSocketConfig.java @@ -0,0 +1,44 @@ +package leaguehub.leaguehubbackend.global.config; + +import leaguehub.leaguehubbackend.global.config.stomp.StompErrorHandler; +import leaguehub.leaguehubbackend.global.config.stomp.StompHandler; +import lombok.AllArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.ChannelRegistration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@AllArgsConstructor +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private final StompHandler stompHandler; + + private final StompErrorHandler stompExceptionHandler; + + // 임시 prefix, broker + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/match"); + registry.setUserDestinationPrefix("/user"); + } + + // Stomp 임시 엔드포인트 등록 + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry + .setErrorHandler(stompExceptionHandler) + .addEndpoint("/ws") + .addInterceptors() + .setAllowedOriginPatterns("*").withSockJS(); + } + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(stompHandler); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/jwt/JwtAuthenticationEntryPoint.java b/src/main/java/leaguehub/leaguehubbackend/global/config/jwt/JwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..3b707e94 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/jwt/JwtAuthenticationEntryPoint.java @@ -0,0 +1,84 @@ +package leaguehub.leaguehubbackend.global.config.jwt; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.*; + + +@Component +@Slf4j +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { + String exception = (String) request.getAttribute("exception"); + + /** + * 잘못된 요청 + */ + if (exception == null) { + log.info("잘못된 요청"); + setResponse(response, BAD_REQUEST_EXCEPTION); + return; + } + + /** + * 토큰 없는 경우 + */ + if (exception.equals(REQUEST_TOKEN_NOT_FOUND.getCode())) { + log.info("AccessToken이 없음"); + setResponse(response, REQUEST_TOKEN_NOT_FOUND); + return; + } + + /** + * 해당 멤버가 데이터베이스에 없을 경우 + */ + if (exception.equals(AUTH_MEMBER_NOT_FOUND.getCode())) { + setResponse(response, AUTH_MEMBER_NOT_FOUND); + return; + } + + /** + * 토큰 만료된 경우 + */ + if (exception.equals(EXPIRED_TOKEN.getCode())) { + setResponse(response, EXPIRED_TOKEN); + return; + } + + /** + * 유효하지 않은 토큰일 경우 + */ + if (exception.equals(INVALID_TOKEN.getCode())) { + setResponse(response, INVALID_TOKEN); + } + + } + + private void setResponse(HttpServletResponse response, AuthExceptionCode errorCode) throws IOException { + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(errorCode.getHttpStatus().value()); + response.getWriter().println( + "{ " + + "\"statusCode\" : \"" + errorCode.getHttpStatus() + + "\", \"code\" : \"" + errorCode.getCode() + + "\", \"message\" : \"" + errorCode.getMessage() + + "\", \"timestamp\" : \"" + timestamp + "\"" + + "}"); + } + +} + diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/jwt/JwtAuthenticationProcessingFilter.java b/src/main/java/leaguehub/leaguehubbackend/global/config/jwt/JwtAuthenticationProcessingFilter.java new file mode 100644 index 00000000..4b939641 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/jwt/JwtAuthenticationProcessingFilter.java @@ -0,0 +1,156 @@ +package leaguehub.leaguehubbackend.global.config.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthExpiredTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthMemberNotFoundException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthTokenNotFoundException; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +@RequiredArgsConstructor +@Slf4j +public class JwtAuthenticationProcessingFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + + private final MemberRepository memberRepository; + private static final List NO_CHECK_URLS = Arrays.asList( + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/h2-console/**", + "/api/member/oauth/kakao", + "/api/member/token", + "/api/member/oauth/**", + "/verifiedPage.html", + "/invalidPage.html", + "/ws/**", + "/app/**", + "/api/notice/**" + ); + private static final List NO_AUTH_URLS = Arrays.asList( + "/api/match/*/player/info" + ); + + private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + private final AntPathMatcher pathMatcher = new AntPathMatcher(); + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String path = request.getRequestURI(); + // 체크 할필요 없는 url들을 다음 필터로 이동 + if (NO_CHECK_URLS.stream().anyMatch(pattern -> pathMatcher.match(pattern, path))) { + filterChain.doFilter(request, response); + return; + } + + if (NO_AUTH_URLS.stream().anyMatch(pattern -> pathMatcher.match(pattern, path))) { + try { + checkAccessTokenAndAuthentication(request, response, filterChain); + } catch (AuthTokenNotFoundException e) { + saveAnonymousUserAuthentication(); + filterChain.doFilter(request, response); + } catch (Exception e) { + request.setAttribute("exception", e.getMessage()); + throw e; + } + } else { + try { + checkAccessTokenAndAuthentication(request, response, filterChain); + } catch (AuthInvalidTokenException | AuthExpiredTokenException | AuthMemberNotFoundException e) { + request.setAttribute("exception", e.getMessage()); + throw e; + } + } + } + + public void saveAnonymousUserAuthentication() { + UserDetails anonymousUser = org.springframework.security.core.userdetails.User.builder() + .username("anonymous") + .password("anonymous") + .roles("ANONYMOUS") + .build(); + + Authentication authentication = new UsernamePasswordAuthenticationToken( + anonymousUser, null, authoritiesMapper.mapAuthorities(anonymousUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + public void checkAccessTokenAndAuthentication(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + Optional optionalToken = jwtService.extractAccessToken(request); + + if (optionalToken.isEmpty()) { + log.info("AccessToken이 없음"); + throw new AuthTokenNotFoundException(); + } + + String accessToken = optionalToken.get(); + + if (jwtService.isTokenExpired(accessToken)) { + log.info("토큰 기간 만료됨"); + throw new AuthExpiredTokenException(); + } + + if (!jwtService.isTokenValid(accessToken)) { + log.info("유효하지 않은 토큰: " + accessToken); + throw new AuthInvalidTokenException(); + } + + Optional optionalPersonalId = jwtService.extractPersonalId(accessToken); + + if (optionalPersonalId.isEmpty()) { + log.info("해당 토큰에 personalId가 없음: " + accessToken); + throw new AuthInvalidTokenException(); + } + + String personalId = optionalPersonalId.get(); + + memberRepository.findMemberByPersonalId(personalId) + .ifPresentOrElse(this::saveAuthentication, () -> { + log.info("해당 personalId를 가진 member없음: " + personalId); + throw new AuthMemberNotFoundException(); + }); + + filterChain.doFilter(request, response); + + } + + public void saveAuthentication(Member member) { + + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username(member.getPersonalId()) + .password(member.getPersonalId()) + .roles(member.getBaseRole().name()) + .build(); + + Authentication authentication = + new UsernamePasswordAuthenticationToken(userDetailsUser, null, + authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/stomp/StompErrorHandler.java b/src/main/java/leaguehub/leaguehubbackend/global/config/stomp/StompErrorHandler.java new file mode 100644 index 00000000..1e7e22bf --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/stomp/StompErrorHandler.java @@ -0,0 +1,42 @@ +package leaguehub.leaguehubbackend.global.config.stomp; + +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthExpiredTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthTokenNotFoundException; +import lombok.AllArgsConstructor; +import org.springframework.messaging.Message; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.StompSubProtocolErrorHandler; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.*; +import static leaguehub.leaguehubbackend.global.exception.global.GlobalErrorCode.SERVER_ERROR; + +@Component +@AllArgsConstructor +public class StompErrorHandler extends StompSubProtocolErrorHandler { + @Override + public Message handleClientMessageProcessingError(Message clientMessage, Throwable ex) { + StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(clientMessage); + + String errorMessage; + + if (ex.getCause() instanceof AuthExpiredTokenException) { + errorMessage = EXPIRED_TOKEN.getMessage(); + } else if (ex.getCause() instanceof AuthInvalidTokenException) { + errorMessage = INVALID_TOKEN.getMessage(); + } else if (ex.getCause() instanceof AuthTokenNotFoundException) { + errorMessage = REQUEST_TOKEN_NOT_FOUND.getMessage(); + } else { + errorMessage = SERVER_ERROR.getMessage(); + } + + byte[] errorPayload = errorMessage.getBytes(); + + headerAccessor.setLeaveMutable(true); + headerAccessor.setMessage(errorMessage); + + return MessageBuilder.createMessage(errorPayload, headerAccessor.getMessageHeaders()); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/config/stomp/StompHandler.java b/src/main/java/leaguehub/leaguehubbackend/global/config/stomp/StompHandler.java new file mode 100644 index 00000000..9e3526b2 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/config/stomp/StompHandler.java @@ -0,0 +1,49 @@ +package leaguehub.leaguehubbackend.global.config.stomp; + +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthExpiredTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthTokenNotFoundException; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.simp.stomp.StompCommand; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.messaging.support.ChannelInterceptor; +import org.springframework.stereotype.Component; + +import java.util.Optional; +@Slf4j +@Component +@RequiredArgsConstructor +@Order(Ordered.HIGHEST_PRECEDENCE + 99) +public class StompHandler implements ChannelInterceptor { + + private final JwtService jwtService; + @Override + public Message preSend(Message message, MessageChannel channel) { + + final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + + if (StompCommand.CONNECT == accessor.getCommand()) { + Optional accessToken = jwtService.extractAccessToken(accessor); + + if (accessToken.isEmpty()) { + log.info("Stomp : 토큰이 헤더에 없음"); + throw new AuthTokenNotFoundException(); + } + if (jwtService.isTokenExpired(accessToken.get())) { + log.info("Stomp : 토큰 기간 만료됨"); + throw new AuthExpiredTokenException(); + } + if (!jwtService.isTokenValid(accessToken.get())) { + log.info("Stomp : 유효하지 않은 토큰: " + accessToken.get()); + throw new AuthInvalidTokenException(); + } + } + return message; + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/global/exception/global/ExceptionCode.java b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/ExceptionCode.java new file mode 100644 index 00000000..c2360154 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/ExceptionCode.java @@ -0,0 +1,11 @@ +package leaguehub.leaguehubbackend.global.exception.global; + +import org.springframework.http.HttpStatus; + +public interface ExceptionCode { + HttpStatus getHttpStatus(); + + String getCode(); + + String getMessage(); +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/exception/global/ExceptionResponse.java b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/ExceptionResponse.java new file mode 100644 index 00000000..2fe77376 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/ExceptionResponse.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.global.exception.global; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ExceptionResponse { + + private Integer statusCode; + private String code; + private String message; + private LocalDateTime timestamp; + + public ExceptionResponse(final ExceptionCode exceptionCode) { + this.statusCode = exceptionCode.getHttpStatus().value(); + this.code = exceptionCode.getCode(); + this.message = exceptionCode.getMessage(); + this.timestamp = LocalDateTime.now(); + } + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/exception/global/GlobalErrorCode.java b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/GlobalErrorCode.java new file mode 100644 index 00000000..2aa83853 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/GlobalErrorCode.java @@ -0,0 +1,25 @@ +package leaguehub.leaguehubbackend.global.exception.global; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +import static org.springframework.http.HttpStatus.*; + + +@Getter +@RequiredArgsConstructor +public enum GlobalErrorCode implements ExceptionCode { + + SERVER_ERROR(INTERNAL_SERVER_ERROR, "G-S-001", "Internal Server Error"), + INVALID_REQUEST_METHOD(METHOD_NOT_ALLOWED, "G-C-001", "유효하지 않는 http 요청입니다."), + INVALID_REQUEST_PARAMETER(BAD_REQUEST, "G-C-002", "유효하지 않는 파라미터 요청입니다."), + INVALID_RESOURCE_OWNER(FORBIDDEN, "G-C-003", "해당 리소스를 처리할 권한이 없습니다."); + + + + private final HttpStatus httpStatus; + private final String code; + private final String message; + +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/exception/global/GlobalExceptionHandler.java b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/GlobalExceptionHandler.java new file mode 100644 index 00000000..c443f374 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package leaguehub.leaguehubbackend.global.exception.global; + +import leaguehub.leaguehubbackend.domain.member.exception.kakao.exception.KakaoInvalidCodeException; +import leaguehub.leaguehubbackend.global.exception.global.exception.GlobalServerErrorException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.ObjectError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.List; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; + +@Slf4j +@ControllerAdvice +@RequiredArgsConstructor +public class GlobalExceptionHandler { + + @ExceptionHandler(GlobalServerErrorException.class) + public ResponseEntity internalServerErrorException( + KakaoInvalidCodeException e + ) { + ExceptionCode exceptionCode = e.getExceptionCode(); + log.error("{}", exceptionCode.getMessage()); + + return new ResponseEntity<>( + new ExceptionResponse(exceptionCode), + exceptionCode.getHttpStatus() + ); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity processValidationError(MethodArgumentNotValidException exception) { + BindingResult bindingResult = exception.getBindingResult(); + + List errors = bindingResult.getAllErrors(); + for (ObjectError error : errors) { + log.error(error.getObjectName()); + } + + return new ResponseEntity<>(errors, BAD_REQUEST); + } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity alreadyExistsValueInDataBase( + DataIntegrityViolationException exception + ) { + log.error("{}", exception.getMessage()); + + return new ResponseEntity<>( + exception.getMessage(), + BAD_REQUEST + ); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity globalException(Exception e) { + log.error("{}", e); + + return new ResponseEntity(e.getMessage(), INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/exception/global/exception/GlobalServerErrorException.java b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/exception/GlobalServerErrorException.java new file mode 100644 index 00000000..33eb6ce0 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/exception/GlobalServerErrorException.java @@ -0,0 +1,21 @@ +package leaguehub.leaguehubbackend.global.exception.global.exception; + +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +import static leaguehub.leaguehubbackend.global.exception.global.GlobalErrorCode.SERVER_ERROR; + +public class GlobalServerErrorException extends RuntimeException { + + private final ExceptionCode exceptionCode; + + public GlobalServerErrorException() { + + super(SERVER_ERROR.getMessage()); + this.exceptionCode = SERVER_ERROR; + } + + public ExceptionCode getExceptionCode() { + + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/exception/global/exception/ResourceNotFoundException.java b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/exception/ResourceNotFoundException.java new file mode 100644 index 00000000..8ff4630a --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/exception/global/exception/ResourceNotFoundException.java @@ -0,0 +1,20 @@ +package leaguehub.leaguehubbackend.global.exception.global.exception; + +import jakarta.persistence.EntityNotFoundException; +import leaguehub.leaguehubbackend.global.exception.global.ExceptionCode; + +public class ResourceNotFoundException extends EntityNotFoundException { + + private final ExceptionCode exceptionCode; + + public ResourceNotFoundException( + ExceptionCode exceptionCode + ) { + super(exceptionCode.getMessage()); + this.exceptionCode = exceptionCode; + } + + public ExceptionCode getExceptionCode() { + return exceptionCode; + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/redis/RedisConfig.java b/src/main/java/leaguehub/leaguehubbackend/global/redis/RedisConfig.java new file mode 100644 index 00000000..22381f76 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/redis/RedisConfig.java @@ -0,0 +1,57 @@ +package leaguehub.leaguehubbackend.global.redis; + +import leaguehub.leaguehubbackend.domain.match.dto.MatchMessage; +import leaguehub.leaguehubbackend.domain.match.service.chat.MatchChatSubscriber; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; + +@Configuration +public class RedisConfig { + + @Value("${REDIS_ADDRESS}") + private String host; + + @Value("${REDIS_PORT}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisMessage(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + return template; + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(factory); + return template; + } + + @Bean + public MessageListenerAdapter messageListener(MatchChatSubscriber subscriber) { + return new MessageListenerAdapter(subscriber); + } + + @Bean + public RedisMessageListenerContainer redisContainer(RedisConnectionFactory connectionFactory, + MessageListenerAdapter listenerAdapter) { + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(listenerAdapter, new PatternTopic("matchId:*:messages")); + return container; + } +} \ No newline at end of file diff --git a/src/main/java/leaguehub/leaguehubbackend/global/redis/service/RedisService.java b/src/main/java/leaguehub/leaguehubbackend/global/redis/service/RedisService.java new file mode 100644 index 00000000..ec7296c5 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/redis/service/RedisService.java @@ -0,0 +1,29 @@ +package leaguehub.leaguehubbackend.global.redis.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class RedisService { + + private final StringRedisTemplate redisTemplate; + + public void saveRefreshToken(String personalId, String refreshToken) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(personalId, refreshToken, 7, TimeUnit.DAYS); + } + + public String getRefreshToken(String personalId) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(personalId); + } + + public void deleteRefreshToken(String personalId) { + redisTemplate.delete(personalId); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/util/SecurityUtils.java b/src/main/java/leaguehub/leaguehubbackend/global/util/SecurityUtils.java new file mode 100644 index 00000000..3cf30719 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/util/SecurityUtils.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.global.util; + +import leaguehub.leaguehubbackend.domain.member.exception.member.exception.MemberNotFoundException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public class SecurityUtils { + public static UserDetails getAuthenticatedUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + throw new MemberNotFoundException(); + } + + return (UserDetails) authentication.getPrincipal(); + } +} diff --git a/src/main/java/leaguehub/leaguehubbackend/global/util/UserUtil.java b/src/main/java/leaguehub/leaguehubbackend/global/util/UserUtil.java new file mode 100644 index 00000000..83878e18 --- /dev/null +++ b/src/main/java/leaguehub/leaguehubbackend/global/util/UserUtil.java @@ -0,0 +1,76 @@ +package leaguehub.leaguehubbackend.global.util; + +import leaguehub.leaguehubbackend.domain.member.entity.BaseRole; +import leaguehub.leaguehubbackend.domain.member.entity.LoginProvider; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class UserUtil { + + private final MemberRepository memberRepository; + private final JwtService jwtService; + + public String getUserPersonalId() { + return SecurityContextHolder + .getContext() + .getAuthentication() + .getName(); + } + + public void addDefaultUsers() { + Member member1 = Member.builder() + .personalId("1234") + .nickname("member1이메일인증안됨") + .profileImageUrl("https://robohash.org/1234?set=set2&size=180x180") + .baseRole(BaseRole.GUEST) + .loginProvider(LoginProvider.KAKAO) + .build(); + + Member member2 = Member.builder() + .personalId("4321") + .nickname("member2이메일인증됨") + .profileImageUrl("https://robohash.org/4321?set=set2&size=180x180") + .baseRole(BaseRole.USER) + .emailUserVerified(true) + .loginProvider(LoginProvider.KAKAO) + .build(); + + Member manager = Member.builder() + .personalId("1") + .nickname("관리자") + .profileImageUrl("https://robohash.org/1?set=set2&size=180x180") + .baseRole(BaseRole.ADMIN) + .loginProvider(LoginProvider.KAKAO) + .build(); + + List members = Arrays.asList( + member1, + member2, + manager + ); + + memberRepository.saveAll(members); + + createTokens(members); + } + public void createTokens(List members) { + for (Member member : members) { + String accessToken = jwtService.createAccessToken(member.getPersonalId()); + System.out.println("--------------------------"); + System.out.printf("해당 멤버 : '%s':%n", member.getNickname()); + System.out.printf("Access Token : Bearer %s%n", accessToken); + System.out.println("--------------------------"); + System.out.println(); + + } + } +} diff --git a/src/main/resources/application-local.properties b/src/main/resources/application-local.properties new file mode 100644 index 00000000..84471fad --- /dev/null +++ b/src/main/resources/application-local.properties @@ -0,0 +1,17 @@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/connectdb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul&characterEncoding=UTF-8 +spring.datasource.username=root +spring.datasource.password=root + +spring.jpa.properties.hibernate.show_sql=true +spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.type.descriptor.sql=trace +spring.jpa.hibernate.ddl-auto=none +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.devtools.livereload.enabled=true +spring.jpa.properties.hibernate.default_batch_fetch_size=100 +spring.jpa.properties.hibernate.auto_quote_keyword=true + +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB + diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties new file mode 100644 index 00000000..34252e0f --- /dev/null +++ b/src/main/resources/application-prod.properties @@ -0,0 +1,16 @@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=${MYSQL_LIGHTSAIL_URL} +spring.datasource.username=${MYSQL_LIGHTSAIL_USERNAME} +spring.datasource.password=${MYSQL_LIGHTSAIL_PASSWORD} +#spring.jpa.properties.hibernate.show_sql=true +#spring.jpa.properties.hibernate.format_sql=true +logging.level.org.hibernate.type.descriptor.sql=trace +spring.jpa.hibernate.ddl-auto=none +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.devtools.livereload.enabled=true +spring.jpa.properties.hibernate.default_batch_fetch_size=100 +spring.jpa.properties.hibernate.auto_quote_keyword=true +# cron +my.custom.cron=0 0 0/6 * * ? +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB \ No newline at end of file diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index dd9288fd..52e08c4b 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -1,9 +1,20 @@ -spring.datasource.driver-class-name=org.h2.Driver -spring.datasource.url=jdbc:h2:mem:test -spring.datasource.username=sa -spring.datasource.password= -spring.jpa.database-platform=org.hibernate.dialect.H2Dialect -spring.jpa.hibernate.ddl-auto=create +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=${MYSQL_LIGHTSAIL_URL} +spring.data.redis.host=${DATABASE.ADDRESS} +spring.data.redis.port=6379 + +spring.datasource.username=${MYSQL_LIGHTSAIL_USERNAME} +spring.datasource.password=${MYSQL_LIGHTSAIL_PASSWORD} + spring.jpa.properties.hibernate.show_sql=true spring.jpa.properties.hibernate.format_sql=true -logging.level.org.hibernate.type.descriptor.sql=trace \ No newline at end of file +logging.level.org.hibernate.type.descriptor.sql=trace + +spring.jpa.hibernate.ddl-auto=none +spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect +spring.devtools.livereload.enabled=true +spring.jpa.properties.hibernate.default_batch_fetch_size=100 +spring.jpa.properties.hibernate.auto_quote_keyword=true + +spring.servlet.multipart.max-file-size=5MB +spring.servlet.multipart.max-request-size=5MB diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index cd5ead87..ec0bef60 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,11 +1,2 @@ -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.datasource.url= -spring.datasource.username= -spring.datasource.password= -spring.jpa.properties.hibernate.show_sql=true -spring.jpa.properties.hibernate.format_sql=true -logging.level.org.hibernate.type.descriptor.sql=trace -spring.jpa.hibernate.ddl-auto=create -spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect -spring.devtools.livereload.enabled=true -spring.jpa.properties.hibernate.default_batch_fetch_size=1000 \ No newline at end of file +spring.profiles.active=prod +spring.profiles.include=API-KEY diff --git a/src/main/resources/static/emailTemplate.html b/src/main/resources/static/emailTemplate.html new file mode 100644 index 00000000..d572b266 --- /dev/null +++ b/src/main/resources/static/emailTemplate.html @@ -0,0 +1,154 @@ + + + + + + Email Confirmation + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ + Logo + +
+ +
+ + + + + +
+

이메일 인증하기

+
+ +
+ + + + + + + + + + + + + + + + + + +
+

아래의 버튼을 클릭하여 인증을 완료하세요!

+
+ + + + +
+ + + + +
+ + 인증 완료하기 +
+
+
+


LeagueHub

+
+
+ + \ No newline at end of file diff --git a/src/main/resources/static/invalidPage.html b/src/main/resources/static/invalidPage.html new file mode 100644 index 00000000..36e4b206 --- /dev/null +++ b/src/main/resources/static/invalidPage.html @@ -0,0 +1,15 @@ + + + + Invalid Email + + + + + + diff --git a/src/main/resources/static/verifiedPage.html b/src/main/resources/static/verifiedPage.html new file mode 100644 index 00000000..9ece052a --- /dev/null +++ b/src/main/resources/static/verifiedPage.html @@ -0,0 +1,15 @@ + + + + User Verified + + + + + + diff --git a/src/test/java/leaguehub/leaguehubbackend/LeaguehubBackendApplicationTests.java b/src/test/java/leaguehub/leaguehubbackend/LeaguehubBackendApplicationTests.java index 7d590dff..2578d793 100644 --- a/src/test/java/leaguehub/leaguehubbackend/LeaguehubBackendApplicationTests.java +++ b/src/test/java/leaguehub/leaguehubbackend/LeaguehubBackendApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; @SpringBootTest +@TestPropertySource(locations = "classpath:application-test.properties") class LeaguehubBackendApplicationTests { @Test diff --git a/src/test/java/leaguehub/leaguehubbackend/config/JwtAuthenticationEntryPointTest.java b/src/test/java/leaguehub/leaguehubbackend/config/JwtAuthenticationEntryPointTest.java new file mode 100644 index 00000000..d33c30ba --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/config/JwtAuthenticationEntryPointTest.java @@ -0,0 +1,92 @@ +package leaguehub.leaguehubbackend.config; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leaguehub.leaguehubbackend.global.config.jwt.JwtAuthenticationEntryPoint; +import leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.test.context.TestPropertySource; + +import java.io.IOException; + +import static leaguehub.leaguehubbackend.domain.member.exception.auth.AuthExceptionCode.REQUEST_TOKEN_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@TestPropertySource(locations = "classpath:application-test.properties") +public class JwtAuthenticationEntryPointTest { + @InjectMocks + private JwtAuthenticationEntryPoint entryPoint; + @Mock + private HttpServletRequest request; + private MockHttpServletResponse response; + private AuthenticationException authException; + + @BeforeEach + public void setUp() { + response = new MockHttpServletResponse(); + authException = new AuthenticationException("Test Exception") {}; + } + + @Test + @DisplayName("잘못된 요청인 경우 BAD_REQUEST_EXCEPTION") + public void testCommence_badRequest() throws IOException { + when(request.getAttribute("exception")).thenReturn(null); + entryPoint.commence(request, response, authException); + + assertTrue(response.getContentAsString().contains(AuthExceptionCode.BAD_REQUEST_EXCEPTION.getMessage())); + } + + @Test + @DisplayName("토큰이 없는경우 REQUEST_TOKEN_NOT_FOUND") + public void testCommence_noToken() throws IOException { + when(request.getAttribute("exception")).thenReturn(REQUEST_TOKEN_NOT_FOUND.getCode()); + entryPoint.commence(request, response, authException); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + assertTrue(response.getContentAsString().contains(AuthExceptionCode.REQUEST_TOKEN_NOT_FOUND.getMessage())); + } + + @Test + @DisplayName("토큰이 만료된 경우 EXPIRED_TOKEN") + public void testCommence_expiredToken() throws IOException { + when(request.getAttribute("exception")).thenReturn(AuthExceptionCode.EXPIRED_TOKEN.getCode()); + entryPoint.commence(request, response, authException); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertTrue(response.getContentAsString().contains(AuthExceptionCode.EXPIRED_TOKEN.getMessage())); + } + + @Test + @DisplayName("토큰이 유효하지 않을 경우 INVALID_TOKEN") + public void testCommence_invalidToken() throws IOException { + when(request.getAttribute("exception")).thenReturn(AuthExceptionCode.INVALID_TOKEN.getCode()); + entryPoint.commence(request, response, authException); + + assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getStatus()); + assertTrue(response.getContentAsString().contains(AuthExceptionCode.INVALID_TOKEN.getMessage())); + } + + @Test + @DisplayName("멤버가 없을 경우 AUTH_MEMBER_NOT_FOUND") + public void testCommence_memberNotFound() throws IOException { + when(request.getAttribute("exception")).thenReturn(AuthExceptionCode.AUTH_MEMBER_NOT_FOUND.getCode()); + entryPoint.commence(request, response, authException); + + assertEquals(HttpServletResponse.SC_NOT_FOUND, response.getStatus()); + assertTrue(response.getContentAsString().contains(AuthExceptionCode.AUTH_MEMBER_NOT_FOUND.getMessage())); + } + + + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/config/JwtAuthenticationProcessingFilterTest.java b/src/test/java/leaguehub/leaguehubbackend/config/JwtAuthenticationProcessingFilterTest.java new file mode 100644 index 00000000..b0dc4aae --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/config/JwtAuthenticationProcessingFilterTest.java @@ -0,0 +1,137 @@ +package leaguehub.leaguehubbackend.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leaguehub.leaguehubbackend.global.config.jwt.JwtAuthenticationProcessingFilter; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthExpiredTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthInvalidTokenException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthMemberNotFoundException; +import leaguehub.leaguehubbackend.domain.member.exception.auth.exception.AuthTokenNotFoundException; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.TestPropertySource; + +import java.io.IOException; +import java.util.Optional; + +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +@TestPropertySource(locations = "classpath:application-test.properties") +public class JwtAuthenticationProcessingFilterTest { + @Mock + private JwtService jwtService; + @Mock + private MemberRepository memberRepository; + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private FilterChain filterChain; + @InjectMocks + private JwtAuthenticationProcessingFilter jwtAuthenticationProcessingFilter; + + @BeforeEach + public void setup() { + SecurityContextHolder.clearContext(); + jwtAuthenticationProcessingFilter = new JwtAuthenticationProcessingFilter(jwtService, memberRepository); + } + + @Test + @DisplayName("토큰이 유효하고 멤버가 존재할시 doFilterInternal은 정상작동을 한다") + public void whenTokenIsValid_thenAuthenticationIsSet() throws ServletException, IOException { + when(jwtService.extractAccessToken(request)).thenReturn(Optional.of("validToken")); + when(jwtService.isTokenExpired(anyString())).thenReturn(false); + when(jwtService.isTokenValid(anyString())).thenReturn(true); + when(jwtService.extractPersonalId(anyString())).thenReturn(Optional.of("personalId")); + when(memberRepository.findMemberByPersonalId(anyString())).thenReturn(Optional.of(UserFixture.createMember())); + + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + + @Test + @DisplayName("토큰이 없을시 AuthTokenNotFoundException을 던진다") + public void whenNoToken_thenThrowsAuthTokenNotFoundException() throws ServletException, IOException { + when(jwtService.extractAccessToken(request)).thenReturn(Optional.empty()); + + assertThrows(AuthTokenNotFoundException.class, () -> { + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + }); + } + + @Test + @DisplayName("토큰이 만료되었을시 AuthExpiredTokenException을 던진다") + public void whenTokenIsExpired_thenThrowsAuthExpiredTokenException() throws ServletException, IOException { + + when(jwtService.extractAccessToken(request)).thenReturn(Optional.of("expiredToken")); + when(jwtService.isTokenExpired("expiredToken")).thenReturn(true); + assertThrows(AuthExpiredTokenException.class, () -> { + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + }); + } + + @Test + @DisplayName("토큰이 유효하지 않을시 AuthInvalidTokenException을 던진다") + public void whenTokenIsInvalid_thenThrowsAuthInvalidTokenException() throws ServletException, IOException { + when(jwtService.extractAccessToken(request)).thenReturn(Optional.of("invalidToken")); + when(jwtService.isTokenExpired("invalidToken")).thenReturn(false); + when(jwtService.isTokenValid("invalidToken")).thenReturn(false); + + assertThrows(AuthInvalidTokenException.class, () -> { + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + }); + } + + @Test + @DisplayName("토큰에 있는 유저가 데이터베이스에 없을시 AuthMemberNotFoundException을 던진다") + public void whenNoUser_thenThrowsAuthMemberNotFoundException() throws ServletException, IOException { + when(jwtService.extractAccessToken(request)).thenReturn(Optional.of("validTokenNoUser")); + when(jwtService.isTokenExpired(anyString())).thenReturn(false); + when(jwtService.isTokenValid(anyString())).thenReturn(true); + when(jwtService.extractPersonalId(anyString())).thenReturn(Optional.of("noPersonalId")); + when(memberRepository.findMemberByPersonalId(anyString())).thenReturn(Optional.empty()); + + assertThrows(AuthMemberNotFoundException.class, () -> { + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + }); + } + + @Test + @DisplayName("filter에 등록된 url로 요청시 filter는 작동하지 않는다") + public void whenUrlDoesNotNeedCheck_thenChainDoFilterCalled() throws ServletException, IOException { + when(request.getRequestURI()).thenReturn("/api/member/oauth/kakao"); + + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + + verify(filterChain).doFilter(request, response); + } + + @Test + @DisplayName("토큰에 personalId가 없을시 AuthInvalidTokenException을 던진다") + public void whenNoPersonalIdInToken_thenThrowsAuthInvalidTokenException() throws ServletException, IOException { + when(jwtService.extractAccessToken(request)).thenReturn(Optional.of("validTokenNoPersonalId")); + when(jwtService.isTokenExpired(anyString())).thenReturn(false); + when(jwtService.isTokenValid(anyString())).thenReturn(true); + when(jwtService.extractPersonalId(anyString())).thenReturn(Optional.empty()); + + assertThrows(AuthInvalidTokenException.class, () -> { + jwtAuthenticationProcessingFilter.doFilterInternal(request, response, filterChain); + }); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/EmailControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/EmailControllerTest.java new file mode 100644 index 00000000..fab8ca48 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/EmailControllerTest.java @@ -0,0 +1,99 @@ +package leaguehub.leaguehubbackend.controller; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import leaguehub.leaguehubbackend.domain.email.dto.EmailDto; +import leaguehub.leaguehubbackend.domain.email.service.EmailService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +public class EmailControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private EmailService emailService; + + @BeforeEach + void setUp() throws IOException { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("12345") + .password("12345") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + } + + @Test + public void testSendEmailWithConfirmation() throws Exception { + String email = "test@email.com"; + EmailDto emailDto = new EmailDto(); + emailDto.setEmail(email); + + when(emailService.sendEmailWithConfirmation(emailDto.getEmail())) + .thenReturn(email); + + mockMvc.perform(post("/api/member/auth/email") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(emailDto))) + .andExpect(status().isOk()) + .andExpect(content().string("Email Successfully Sent to " + email)); + + verify(emailService, times(1)).sendEmailWithConfirmation(emailDto.getEmail()); + } + + @Test + public void testConfirmUserEmail_validToken() throws Exception { + String validToken = "validToken"; + when(emailService.confirmUserEmail(validToken)).thenReturn(true); + + mockMvc.perform(get("/api/member/oauth/email?token=" + validToken)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/mypage")); + } + + @Test + public void testConfirmUserEmail_invalidToken() throws Exception { + String invalidToken = "invalidToken"; + when(emailService.confirmUserEmail(invalidToken)).thenReturn(false); + + mockMvc.perform(get("/api/member/oauth/email?token=" + invalidToken)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/JwtControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/JwtControllerTest.java new file mode 100644 index 00000000..ea84ac5e --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/JwtControllerTest.java @@ -0,0 +1,63 @@ +package leaguehub.leaguehubbackend.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Optional; +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +public class JwtControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MemberService memberService; + + @MockBean + private JwtService jwtService; + private Member member; + private LoginMemberResponse loginMemberResponse; + + @BeforeEach + public void setUp() { + member = UserFixture.createMember(); + loginMemberResponse = UserFixture.createLoginResponse(); + Mockito.when(jwtService.createTokens(member.getPersonalId())).thenReturn(loginMemberResponse); + } + + @Test + @DisplayName("유효한 토큰을 받았을 때, 상태코드는 OK가 반환되어야 함") + public void whenValidToken_thenReturnsOk() throws Exception { + Mockito.when(jwtService.extractRefreshToken(Mockito.any())).thenReturn(Optional.of("validToken")); + Mockito.when(jwtService.isTokenValid("validToken")).thenReturn(true); + Mockito.when(jwtService.createTokens(member.getPersonalId())).thenReturn(loginMemberResponse); + + mockMvc.perform(post("/api/member/token") + .header("Authorization-refresh", "Bearer validToken") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/KakaoControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/KakaoControllerTest.java new file mode 100644 index 00000000..45594d08 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/KakaoControllerTest.java @@ -0,0 +1,111 @@ +package leaguehub.leaguehubbackend.controller; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import leaguehub.leaguehubbackend.domain.member.controller.MemberAuthController; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoTokenResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import leaguehub.leaguehubbackend.domain.member.service.MemberAuthService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +public class KakaoControllerTest { + + @Autowired + private MockMvc mockMvc; + @InjectMocks + private MemberAuthController memberAuthController; + + @MockBean + private MemberService memberService; + + @MockBean + private MemberAuthService memberAuthService; + + @MockBean + private JwtService jwtService; + + @Test + @DisplayName("유효하지 않은 카카오 코드시 KakaoInvalidCodeException") + public void whenKakaoCodeIsMissingOrEmpty_thenThrowKakaoInvalidCodeException() throws Exception { + mockMvc.perform(post("/api/member/oauth/kakao") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + + mockMvc.perform(post("/api/member/oauth/kakao") + .header("Kakao-Code", "") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("유효한 카카오 코드시 200 Ok 응답") + public void whenValidCode_thenReturnOk() throws Exception { + KakaoTokenResponseDto kakaoTokenResponseDto = new KakaoTokenResponseDto(); + kakaoTokenResponseDto.setAccessToken("validToken"); + KakaoUserDto kakaoUserDto = new KakaoUserDto(); + LoginMemberResponse loginMemberResponse = UserFixture.createLoginResponse(); + + when(memberAuthService.getKakaoToken(anyString())).thenReturn(kakaoTokenResponseDto); + when(memberAuthService.getKakaoUser(kakaoTokenResponseDto)).thenReturn(kakaoUserDto); + when(memberAuthService.findOrSaveMember(kakaoUserDto)).thenReturn(loginMemberResponse); + + mockMvc.perform(post("/api/member/oauth/kakao") + .header("Kakao-Code", "validCode") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("유효한 카카오 코드로 로그인 시 200 OK와 헤더 반환") + void whenValidKakaoCode_thenReturns200AndHeaders() throws Exception { + String kakaoCode = "validKakaoCode"; + KakaoTokenResponseDto kakaoTokenResponseDto = new KakaoTokenResponseDto(); + kakaoTokenResponseDto.setAccessToken("validAccessToken"); + kakaoTokenResponseDto.setRefreshToken("validRefreshToken"); + kakaoTokenResponseDto.setExpiresIn(3600); + kakaoTokenResponseDto.setRefreshTokenExpiresIn(14 * 24 * 3600); + + KakaoUserDto kakaoUserDto = new KakaoUserDto(); + LoginMemberResponse loginMemberResponse = LoginMemberResponse.builder() + .accessToken("validAccessToken") + .refreshToken("validRefreshToken") + .verifiedUser(true) + .build(); + + given(memberAuthService.getKakaoToken(kakaoCode)).willReturn(kakaoTokenResponseDto); + given(memberAuthService.getKakaoUser(kakaoTokenResponseDto)).willReturn(kakaoUserDto); + given(memberAuthService.findOrSaveMember(kakaoUserDto)).willReturn(loginMemberResponse); + + mockMvc.perform(post("/api/member/oauth/kakao") + .header("Kakao-Code", kakaoCode) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(header().string("Authorization", "Bearer validAccessToken")); + } + + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/MatchControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/MatchControllerTest.java new file mode 100644 index 00000000..3c80a260 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/MatchControllerTest.java @@ -0,0 +1,53 @@ +package leaguehub.leaguehubbackend.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.match.service.MatchPlayerService; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +public class MatchControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ChannelRepository channelRepository; + + @Autowired + MatchRepository matchRepository; + + @Autowired + MatchPlayerService matchPlayerService; + + @Autowired + ObjectMapper mapper; + + + @Test + @DisplayName("경기 결과 생성 테스트 - 성공") + public void createMatchRankSuccessTest() throws Exception { + + + } + + @Test + @DisplayName("경기 결과 생성 테스트 - 실패") + public void createMatchRankFailTest() throws Exception { + + } + +} diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/MemberControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/MemberControllerTest.java new file mode 100644 index 00000000..7fc57eaf --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/MemberControllerTest.java @@ -0,0 +1,148 @@ +package leaguehub.leaguehubbackend.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import leaguehub.leaguehubbackend.domain.member.dto.member.MypageResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.NicknameRequestDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.ProfileDto; +import leaguehub.leaguehubbackend.domain.member.service.MemberAuthService; +import leaguehub.leaguehubbackend.domain.member.service.MemberProfileService; +import leaguehub.leaguehubbackend.fixture.MypageResponseFixture; +import leaguehub.leaguehubbackend.fixture.ProfileFixture; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Spy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private MemberService memberService; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private MemberProfileService memberProfileService; + + @MockBean + private MemberAuthService memberAuthService; + + @BeforeEach + void setUp() throws IOException { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("12345") + .password("12345") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + } + + @Test + @DisplayName("로그인한 상태에서 /profile 요청시 사용자 프로필 정보 반환") + public void whenAuthenticated_thenReturnProfile() throws Exception { + + ProfileDto mockProfileResponse = ProfileFixture.createProfile(); + + when(memberProfileService.getProfile()) + .thenReturn(mockProfileResponse); + + mockMvc.perform(get("/api/member/profile")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.profileImageUrl").value("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz0wDuJn1Y")) + .andExpect(jsonPath("$.nickName").value("성우")); + } + + @Test + @DisplayName("로그인한 상태에서 /mypage 요청시 사용자 마이페이지 정보 반환") + public void whenAuthenticated_thenReturnMypage() throws Exception { + + MypageResponseDto mockMypageResponse = MypageResponseFixture.createMypageResponse(); + + when(memberProfileService.getMypageProfile()) + .thenReturn(mockMypageResponse); + + mockMvc.perform(get("/api/member/mypage")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.profileImageUrl").value(mockMypageResponse.getProfileImageUrl())) + .andExpect(jsonPath("$.nickName").value(mockMypageResponse.getNickName())) + .andExpect(jsonPath("$.email").value(mockMypageResponse.getEmail())) + .andExpect(jsonPath("$.userEmailVerified").value(mockMypageResponse.isUserEmailVerified())); + } + + @Test + @DisplayName("로그아웃 요청시 성공 메시지 return") + void whenLogout_thenReturnSuccessMessage() throws Exception { + + doNothing().when(memberAuthService).logoutMember( + any(HttpServletRequest.class), + any(HttpServletResponse.class) + ); + + mockMvc.perform(post("/api/member/logout")) + .andExpect(status().isOk()) + .andExpect(content().string("Logout Success!")); + + verify(memberAuthService).logoutMember( + any(HttpServletRequest.class), + any(HttpServletResponse.class) + ); + } + @Test + @DisplayName("닉네임 변경 요청시 프로필 정보 반환") + public void whenChangeNickname_thenReturnProfile() throws Exception { + + NicknameRequestDto nicknameRequest = new NicknameRequestDto(); + nicknameRequest.setNickName("NewNickname"); + + ProfileDto mockProfileResponse = ProfileFixture.createProfile(); + mockProfileResponse.setNickName("NewNickname"); + + when(memberProfileService.changeMemberParticipantNickname(nicknameRequest)) + .thenReturn(mockProfileResponse); + + mockMvc.perform(post("/api/member/profile/nickname") + .contentType("application/json") + .content(new ObjectMapper().writeValueAsString(nicknameRequest))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.nickName").value("NewNickname")); + } + +} diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/NoticeControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/NoticeControllerTest.java new file mode 100644 index 00000000..52008b3e --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/NoticeControllerTest.java @@ -0,0 +1,56 @@ +package leaguehub.leaguehubbackend.controller; + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.notice.entity.GameType; +import leaguehub.leaguehubbackend.domain.notice.repository.NoticeRepository; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static leaguehub.leaguehubbackend.domain.notice.entity.GameType.MAIN; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +public class NoticeControllerTest { + + + @Autowired + MockMvc mockMvc; + + @Autowired + NoticeRepository noticeRepository; + + @Test + @DisplayName("저장된 공지사항 추출 테스트") + void findTargetNoticeControllerTest() throws Exception { + + mockMvc.perform(get("/api/notice/" + MAIN)) + .andExpect(status().isOk()) + .andExpect(jsonPath("[0].noticeTitle").value("리그허브 서비스 오픈")) + .andExpect(jsonPath("[1].noticeTitle").value("리그허브 서비스 안정화")) + .andExpect(jsonPath("[2].noticeTitle").value("리그허브 이벤트 안내")); + } + + @Test + @DisplayName("공지사항 업데이트 테스트") + void noticeUpdateControllerTest() throws Exception { + //given + GameType selectType = GameType.LOL; + + mockMvc.perform(post("/api/notice/new/" + selectType)) + .andExpect(status().isOk()); + + } +} diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/ParticipantControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/ParticipantControllerTest.java new file mode 100644 index 00000000..20896be6 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/ParticipantControllerTest.java @@ -0,0 +1,634 @@ +package leaguehub.leaguehubbackend.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantManagementService; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantRoleAndPermissionService; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.ParticipantFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.domain.participant.service.ParticipantService; +import org.jetbrains.annotations.NotNull; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ParticipantControllerTest { + + @Autowired + MockMvc mockMvc; + + + @Autowired + ParticipantService participantService; + + @Autowired + MemberRepository memberRepository; + + @Autowired + ChannelRepository channelRepository; + + @Autowired + ChannelBoardRepository channelBoardRepository; + + @Autowired + ParticipantRepository participantRepository; + @Autowired + ChannelRuleRepository channelRuleRepository; + + @Autowired + ParticipantManagementService participantManagementService; + @Autowired + ParticipantRoleAndPermissionService participantRoleAndPermissionService; + + @Autowired + ObjectMapper mapper; + + Channel createCustomChannel(Boolean tier, Boolean playCount, Integer tierMax, Integer tierMin, int playCountMin){ + //Member member = memberRepository.save(UserFixture.createMember()); + Member HostMember = memberRepository.save(UserFixture.createCustomeMember("호스트")); + Member ironMember = memberRepository.save(UserFixture.createCustomeMember("썹맹구")); + Member unrankedMember = memberRepository.save(UserFixture.createCustomeMember("서초임")); + Member platinumMember = memberRepository.save(UserFixture.createCustomeMember("손성한")); + Member masterMember = memberRepository.save(UserFixture.createCustomeMember("채수채수밭")); + Member alreadyMember = memberRepository.save(UserFixture.createCustomeMember("요청한사람")); + Member rejectedMember = memberRepository.save(UserFixture.createCustomeMember("거절된사람")); + Member doneMember1 = memberRepository.save(UserFixture.createCustomeMember("참가된사람1")); + Member doneMember2 = memberRepository.save(UserFixture.createCustomeMember("참가된사람2")); + Member observer1 = memberRepository.save(UserFixture.createCustomeMember("관전자1")); + Member observer2 = memberRepository.save(UserFixture.createCustomeMember("관전자2")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(tier, playCount, tierMax, tierMin, playCountMin); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(HostMember, channel)); + participantRepository.save(Participant.participateChannel(unrankedMember, channel)); + participantRepository.save(Participant.participateChannel(ironMember, channel)); + participantRepository.save(Participant.participateChannel(platinumMember, channel)); + participantRepository.save(Participant.participateChannel(masterMember, channel)); + participantRepository.save(Participant.participateChannel(observer1, channel)); + participantRepository.save(Participant.participateChannel(observer2, channel)); + + Participant alreadyParticipant = participantRepository.save(Participant.participateChannel(alreadyMember, channel)); + Participant rejectedParticipant = participantRepository.save(Participant.participateChannel(rejectedMember, channel)); + Participant doneParticipant1 = participantRepository.save(Participant.participateChannel(doneMember1, channel)); + Participant doneParticipant2 = participantRepository.save(Participant.participateChannel(doneMember2, channel)); + + alreadyParticipant.updateParticipantStatus("participantGameId1", "bronze ii", "participantNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + rejectedParticipant.rejectParticipantRequest(); + doneParticipant1.updateParticipantStatus("participantGameId2", "platinum ii", "participantNickname2", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant2.updateParticipantStatus("participantGameId3", "iron ii", "participantNickname3", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant1.approveParticipantMatch(); + doneParticipant2.approveParticipantMatch(); + + return channel; + } + + @NotNull + private Participant getParticipant(String participantDummyName, Channel channel, String participantDummyGameId, String participantDummyNickname) { + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember(participantDummyName)); + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + dummy1.updateParticipantStatus(participantDummyGameId, "platinum", participantDummyNickname, "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + return dummy1; + } + + @Test + @DisplayName("티어 조회 테스트 (참여 x) - 성공") + void searchTierSuccessTest() throws Exception { + + mockMvc.perform(get("/api/participant/stat/서초임/kr1")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.tier").value("UNRANKED")) + .andExpect(jsonPath("$.playCount").value(0)); + + } + + @Test + @DisplayName("티어 조회 테스트 (참여 x) - 실패") + void searchTierFailTest() throws Exception { + + mockMvc.perform(get("/api/participant/stat?gameid=saovkovsk&gamecategory=0")) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + + } + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어, 판수 제한 x) - 성공") + void participateDefaultMatchSuccessTest() throws Exception { + + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("서초임"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("서초임#kr1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isOk()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (판수 제한 o) - 성공") + void participateLimitedPlayCountMatchSuccessTest() throws Exception { + + Channel channel = createCustomChannel(false, true, 2400, null, 1); + UserFixture.setUpCustomAuth("손성한"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("손성한#kr1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isOk()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (마스터 10000점 이하, 판수 제한 o) - 성공") + void participateLimitedMasterMatchSuccessTest() throws Exception { + + Channel channel = createCustomChannel(true, true, 2400, null, 1); + UserFixture.setUpCustomAuth("손성한"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("손성한#kr1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isOk()); + + } + + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (중복) - 실패") + public void participateDuplicatedMatchFailTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("서초임"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("participantGameId3"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (참가된사람) - 실패") + public void participantDoneMatchFailTest() throws Exception { + //given + Channel channel = createCustomChannel(true, false, 800, null, 100); + UserFixture.setUpCustomAuth("참가된사람1"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("참가된사람1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (이미참가요청한사람) - 실패") + public void participantAlreadyMatchFailTest() throws Exception { + //given + Channel channel = createCustomChannel(true, false, 800, null, 100); + UserFixture.setUpCustomAuth("요청한사람"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("요청한사람"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (거절된사람) - 실패") + public void participantRejectedMatchFailTest() throws Exception { + //given + Channel channel = createCustomChannel(true, false, 800, null, 100); + UserFixture.setUpCustomAuth("거절된사람"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("거절된사람"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어 제한) - 실패") + void participateLimitedTierMatchFailTest() throws Exception { + + Channel channel = createCustomChannel(true, false, 400, null, 1); + UserFixture.setUpCustomAuth("손성한"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("손성한#kr1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (횟수 제한) - 실패") + void participateLimitedPlayCountMatchFailTest() throws Exception { + + Channel channel = createCustomChannel(true, true, 800, null, 100); + UserFixture.setUpCustomAuth("서초임"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("서초임#kr1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (골드 4 이하, 횟수 제한) - 실패") + void participateLimitedMasterMatchFailTest() throws Exception { + + Channel channel = createCustomChannel(true, true, 1200, null, 20); + UserFixture.setUpCustomAuth("손성한"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("손성한#kr1"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (에메랄드, 횟수 제한) - 실패") + void participateLimitedMinMasterMatchFailTest() throws Exception { + + Channel channel = createCustomChannel(true, true, null, + 2300, 20); + UserFixture.setUpCustomAuth("채수채수밭"); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("칸치로#275"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/"+ channel.getChannelLink()+"/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("채널 경기 참여자 조회 테스트") + void loadPlayerTest() throws Exception { + + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("id"); + + mockMvc.perform(get("/api/" + channel.getChannelLink() + "/players")) + .andExpect(status().isOk()) + .andExpect(jsonPath("[0].nickname").value("participantNickname2")) + .andExpect(jsonPath("[0].gameId").value("participantGameId2")) + .andExpect(jsonPath("[1].nickname").value("participantNickname3")) + .andExpect(jsonPath("[1].gameId").value("participantGameId3")) + .andDo(print()); + + } + + + @Test + @DisplayName("요청된사람 조회 테스트 (관리자 x) - 실패") + public void loadRequestStatusPlayerListFailTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("참가된사람1"); + + mockMvc.perform(get("/api/" + channel.getChannelLink() + "/player/requests")) + .andExpect(status().isUnauthorized()); + + } + + @Test + @DisplayName("요청된사람 조회 테스트 (관리자 o) - 성공") + public void loadRequestStatusPlayerListSuccessTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("호스트"); + + mockMvc.perform(get("/api/" + channel.getChannelLink() + "/player/requests")) + .andExpect(status().isOk()) + .andExpect(jsonPath("[0].nickname").value("participantNickname1")) + .andExpect(jsonPath("[0].gameId").value("participantGameId1")); + + } + + + @Test + @DisplayName("요청한사람 승인 테스트 (관리자 o) - 성공") + public void approveParticipantSuccessTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("호스트"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/player")) + .andExpect(status().isOk()); + + } + + @Test + @DisplayName("요청한사람 승인 테스트 (관리자 x) - 실패") + public void approveParticipantFailTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("참가된사람1"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/player")) + .andExpect(status().isUnauthorized()); + + } + + @Test + @DisplayName("요청한사람 거절 테스트 (관리자 o) - 성공") + public void rejectedParticipantSuccessTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("호스트"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/observer")) + .andExpect(status().isOk()); + + } + + @Test + @DisplayName("요청한사람 거절 테스트 (관리자 x) - 실패") + public void rejectedParticipantFailTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("참가된사람1"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/observer")) + .andExpect(status().isUnauthorized()); + + } + + @Test + @DisplayName("참가한사람 거절 테스트 (관리자 o) - 성공") + public void rejectedPlayerSuccessTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("호스트"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + dummy1.approveParticipantMatch(); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/observer")) + .andExpect(status().isOk()); + + } + + @Test + @DisplayName("참가한사람 거절 테스트 (관리자 x) - 실패") + public void rejectedPlayerFailTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("참가된사람1"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + dummy1.approveParticipantMatch(); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/observer")) + .andExpect(status().isUnauthorized()); + + } + + @Test + @DisplayName("관리자 부여 테스트 (관리자 o) - 성공") + public void updateHostSuccessTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("호스트"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/host")) + .andExpect(status().isOk()); + + } + + @Test + @DisplayName("관리자 부여 테스트 (관리자 x) - 실패") + public void updateHostFailTest() throws Exception { + //given + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("참가된사람1"); + + Participant dummy1 = getParticipant("DummyName", channel, "DummyGameId", "DummyNickname"); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/host")) + .andExpect(status().isUnauthorized()); + + } + + + @Test + @DisplayName("채널 관전자 조회 테스트 (관전자 o) - 성공") + void loadObserverSuccessTest() throws Exception { + + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("호스트"); + + mockMvc.perform(get("/api/" + channel.getChannelLink() +"/observers")) + .andExpect(status().isOk()) + .andExpect(jsonPath("[0].nickname").value("관전자1")) + .andExpect(jsonPath("[1].nickname").value("관전자2")) + .andDo(print()); + + } + + @Test + @DisplayName("채널 관전자 조회 테스트 (관전자 x) - 실패") + void loadObserverFailTest() throws Exception { + + Channel channel = createCustomChannel(false, false, 2400, null, 20); + UserFixture.setUpCustomAuth("참가된사람1"); + + mockMvc.perform(get("/api/" + channel.getChannelLink() +"/observers")) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("요청된사람 승인 테스트 (최대 인원수 초과) - 실패") + public void approveParticipantCountFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(false, false, 2400, null, 20); + String[] nickName = new String[13]; + + for (int i = 0; i < nickName.length; i++) { + nickName[i] = "더미" + i; + Participant dummyParticipant = getParticipant(nickName[i], channel, nickName[i], nickName[i]); + dummyParticipant.approveParticipantMatch(); + } + Participant dummy = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + participantRoleAndPermissionService.approveParticipantRequest(channel.getChannelLink(), dummy.getId()); + + Participant dummy1 = getParticipant("DummyName2", channel, "DummyGameId2", "DummyNickname2"); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/" + dummy1.getId() +"/player")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("채널 참여 - 성공") + void participantChannelSuccess() throws Exception { + //given + memberRepository.save(UserFixture.createCustomeMember("참가할사람")); + UserFixture.setUpCustomAuth("참가할사람"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/participant/observer")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("채널 중복 참여 - 실패") + void participantChannelDuplicateFail() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + + //then + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/participant/observer")) + .andExpect(status().isBadRequest()); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (이메일 미인증) - 실패") + void participateUnAuthMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Member guestMember = memberRepository.save(UserFixture.createGuestMember()); + UserFixture.setUpCustomGuest("Guest"); + + Channel channel = createCustomChannel(false, false, 800, null, 100); + + ParticipantDto participantResponseDto = ParticipantFixture.createParticipantResponseDto("연습용아이디가됨"); + String dtoToJson = mapper.writeValueAsString(participantResponseDto); + + mockMvc.perform(post("/api/" + channel.getChannelLink() + "/participant") + .contentType(MediaType.APPLICATION_JSON) + .content(dtoToJson)) + .andExpect(status().isUnauthorized()); + + + } + + @Test + @DisplayName("채널 커스텀 인덱스 API 테스트") + void channelCustomIndexAPI() throws Exception { + Member findMember = memberRepository.save(UserFixture.createCustomeMember("test")); + UserFixture.setUpCustomAuth("test"); + + List channels = IntStream.range(0, 3) + .mapToObj(i -> createCustomChannel(false, false, 800, null, 100)) + .peek(channel -> { + channelRepository.save(channel); + participantManagementService.participateChannel(channel.getChannelLink()); + }) + .collect(Collectors.toList()); + + List participantChannelDtos = IntStream.range(0, 3) + .mapToObj(i -> { + ParticipantChannelDto dto = new ParticipantChannelDto(); + Channel channel = channels.get(i); + dto.setChannelLink(channel.getChannelLink()); + dto.setImgSrc(channel.getChannelImageUrl()); + dto.setTitle(channel.getTitle()); + dto.setGameCategory(channel.getGameCategory().getNum()); + dto.setCustomChannelIndex(2 - i); // Reversed index + return dto; + }) + .collect(Collectors.toList()); + String json = mapper.writeValueAsString(participantChannelDtos); + + mockMvc.perform(post("/api/channels/order") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(status().isOk()) + .andDo(print()); + } + + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelBoardControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelBoardControllerTest.java new file mode 100644 index 00000000..c0b73b66 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelBoardControllerTest.java @@ -0,0 +1,363 @@ +package leaguehub.leaguehubbackend.controller.channel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import leaguehub.leaguehubbackend.domain.channel.controller.ChannelBoardController; +import leaguehub.leaguehubbackend.domain.channel.dto.*; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelBoardService; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelBoardControllerTest { + + @Autowired + MemberRepository memberRepository; + @Autowired + ChannelBoardController channelBoardController; + @Autowired + ChannelService channelService; + @Autowired + ObjectMapper objectMapper; + @Autowired + ChannelRepository channelRepository; + @Autowired + ChannelBoardRepository channelBoardRepository; + @Autowired + ChannelBoardService channelBoardService; + @Autowired + ParticipantRepository participantRepository; + @Autowired + MockMvc mockMvc; + @Autowired + private ChannelRuleRepository channelRuleRepository; + Member member; + + @BeforeEach + void setUp() { + member = memberRepository.save(UserFixture.createMember()); + UserFixture.setUpAuth(); + } + + Channel createCustomChannel(Boolean tier, Boolean playCount, Integer tierMax, Integer tierMin, int playCountMin) throws Exception { + Member ironMember = memberRepository.save(UserFixture.createCustomeMember("썹맹구")); + Member unrankedMember = memberRepository.save(UserFixture.createCustomeMember("서초임")); + Member platinumMember = memberRepository.save(UserFixture.createCustomeMember("손성한")); + Member masterMember = memberRepository.save(UserFixture.createCustomeMember("채수채수밭")); + Member alreadyMember = memberRepository.save(UserFixture.createCustomeMember("요청한사람")); + Member rejectedMember = memberRepository.save(UserFixture.createCustomeMember("거절된사람")); + Member doneMember1 = memberRepository.save(UserFixture.createCustomeMember("참가된사람1")); + Member doneMember2 = memberRepository.save(UserFixture.createCustomeMember("참가된사람2")); + Member observer1 = memberRepository.save(UserFixture.createCustomeMember("관전자1")); + Member observer2 = memberRepository.save(UserFixture.createCustomeMember("관전자2")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(tier, playCount, tierMax, tierMin, playCountMin); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(member, channel)); + participantRepository.save(Participant.participateChannel(unrankedMember, channel)); + participantRepository.save(Participant.participateChannel(ironMember, channel)); + participantRepository.save(Participant.participateChannel(platinumMember, channel)); + participantRepository.save(Participant.participateChannel(masterMember, channel)); + participantRepository.save(Participant.participateChannel(observer1, channel)); + participantRepository.save(Participant.participateChannel(observer2, channel)); + + Participant alreadyParticipant = participantRepository.save(Participant.participateChannel(alreadyMember, channel)); + Participant rejectedParticipant = participantRepository.save(Participant.participateChannel(rejectedMember, channel)); + Participant doneParticipant1 = participantRepository.save(Participant.participateChannel(doneMember1, channel)); + Participant doneParticipant2 = participantRepository.save(Participant.participateChannel(doneMember2, channel)); + + alreadyParticipant.updateParticipantStatus("participantGameId1", "bronze ii", "participantNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + rejectedParticipant.rejectParticipantRequest(); + doneParticipant1.updateParticipantStatus("participantGameId2", "platinum ii", "participantNickname2", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant2.updateParticipantStatus("participantGameId3", "iron ii", "participantNickname3", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant1.approveParticipantMatch(); + doneParticipant2.approveParticipantMatch(); + + return channel; + } + + @Test + @DisplayName("게시판 만들기 - 성공") + void createChannelBoard() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + ChannelBoardDto channelBoardDto = ChannelFixture.createChannelBoardDto(); + String json = objectMapper.writeValueAsString(channelBoardDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + channel.get().getChannelLink() + "/new") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.boardIndex").value(4)) + .andDo(print()); + } + + @Test + @DisplayName("게시판 만들기 - 실패(권한없음)") + void 트FailCreateChannelBoard() throws Exception { + Channel customChannel = createCustomChannel(false, false, 1100, null, 100); + Channel findChannel = channelRepository.save(customChannel); + + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + + participantRepository.save(Participant.participateChannel(test, findChannel)); + + ChannelBoardDto channelBoardDto = ChannelFixture.createChannelBoardDto(); + String json = objectMapper.writeValueAsString(channelBoardDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + findChannel.getChannelLink() + "/new") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + } + + @Test + @DisplayName("게시판 불러오기(제목 + 내용) - 성공") + void loadChannelBoard() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + + channel.get().getChannelLink() + "/" + channelBoards.get(0).getId()) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.content").value("공지사항을 작성해주세요.")); + + } + + @Test + @DisplayName("게시판 불러오기(제목 + 내용) - 실패(채널에 존재하지 않는 게시판 ID)") + void FailLoadChannelBoard() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + Long badId = 1023901L; + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + + channel.get().getChannelLink() + "/" + badId) + ) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } + + @Test + @DisplayName("게시판 업데이트 - 성공") + void updateChannelBoard() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + ChannelBoardDto channelBoardDto = ChannelFixture.updateChannelBoardDto(); + String json = objectMapper.writeValueAsString(channelBoardDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + + channel.get().getChannelLink() + "/" + channelBoards.get(0).getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json) + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + + channel.get().getChannelLink() + "/" + channelBoards.get(0).getId()) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.content").value("test1")); + } + + @Test + @DisplayName("게시판 업데이트 - 실패(권한없음)") + void failUpdateChannelBoard_NoAuth() throws Exception { + Channel customChannel = createCustomChannel(false, false, 1100, null, 100); + Channel findChannel = channelRepository.save(customChannel); + + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + participantRepository.save(Participant.participateChannel(test, findChannel)); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(findChannel.getId()); + ChannelBoardDto channelBoardDto = ChannelFixture.createChannelBoardDto(); + String json = objectMapper.writeValueAsString(channelBoardDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + findChannel.getChannelLink() + + "/" + channelBoards.get(0).getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + } + + @Test + @DisplayName("게시판 업데이트 - 실패(채널에 존재하지 않는 게시판 ID)") + void failUpdateChannelBoard_Invalid_board_id() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + ChannelBoardDto channelBoardDto = ChannelFixture.updateChannelBoardDto(); + String json = objectMapper.writeValueAsString(channelBoardDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + + channel.get().getChannelLink() + "/123141515") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + ) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } + + @Test + @DisplayName("게시판 삭제 - 성공") + void deleteChannelBoard() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + + mockMvc.perform(MockMvcRequestBuilders.delete("/api/channel/" + + channel.get().getChannelLink() + "/" + channelBoards.get(0).getId()) + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @DisplayName("게시판 삭제 - 실패(권한없음)") + void failDeleteChannelBoard_NoAuth() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + participantRepository.save(Participant.participateChannel(test, channel.get())); + + mockMvc.perform(MockMvcRequestBuilders.delete("/api/channel/" + + channel.get().getChannelLink() + "/" + channelBoards.get(0).getId()) + ) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + } + + @Test + @DisplayName("게시판 삭제 - 실패(채널에 존재하지 않는 게시판 ID)") + void failDeleteChannelBoard_Invalid_board_id() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + + Long badId = 1023901L; + + mockMvc.perform(MockMvcRequestBuilders.delete("/api/channel/" + + channel.get().getChannelLink() + "/" + badId) + ) + .andExpect(MockMvcResultMatchers.status().isNotFound()) + .andDo(print()); + } + + @Test + @DisplayName("게시판 인덱스 업데이트 - 성공") + void updateIndex() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoardLoadDtoList = channelBoardService.loadChannelBoards(channel.get().getChannelLink()).getChannelBoardLoadDtoList(); + channelBoardLoadDtoList.get(0).setBoardIndex(3); + channelBoardLoadDtoList.get(2).setBoardIndex(1); + ChannelBoardIndexListDto channelBoardIndexListDto = new ChannelBoardIndexListDto(); + channelBoardIndexListDto.setChannelBoardLoadDtoList(channelBoardLoadDtoList); + + String json = objectMapper.writeValueAsString(channelBoardIndexListDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + + channel.get().getChannelLink() + "/order") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + ) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @DisplayName("게시판 인덱스 업데이트 - 실패(권한 없음)") + void failUpdateIndex() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + List channelBoardLoadDtoList = channelBoardService.loadChannelBoards(channel.get().getChannelLink()).getChannelBoardLoadDtoList(); + channelBoardLoadDtoList.get(0).setBoardIndex(3); + channelBoardLoadDtoList.get(2).setBoardIndex(1); + + ChannelBoardIndexListDto channelBoardIndexListDto = new ChannelBoardIndexListDto(); + channelBoardIndexListDto.setChannelBoardLoadDtoList(channelBoardLoadDtoList); + + String json = objectMapper.writeValueAsString(channelBoardIndexListDto); + + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + participantRepository.save(Participant.participateChannel(test, channel.get())); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + + channel.get().getChannelLink() + "/order") + .contentType(MediaType.APPLICATION_JSON) + .content(json) + ) + .andExpect(MockMvcResultMatchers.status().isUnauthorized()); + + } + + @Test + @DisplayName("채널 보드 불러오기 테스트 - 화면 구성") + void loadChannelBoards() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + + channel.get().getChannelLink() + "/boards") + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(print()); + } + + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelControllerTest.java new file mode 100644 index 00000000..d9e6baea --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelControllerTest.java @@ -0,0 +1,151 @@ +package leaguehub.leaguehubbackend.controller.channel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import leaguehub.leaguehubbackend.domain.channel.controller.ChannelController; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.UpdateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ChannelController channelController; + + @Autowired + ChannelService channelService; + + @Autowired + MemberService memberService; + @Autowired + ObjectMapper objectMapper; + + @Autowired + MemberRepository memberRepository; + @Autowired + private ChannelRepository channelRepository; + + @BeforeEach + void setUp() { + memberRepository.save(UserFixture.createMember()); + UserFixture.setUpAuth(); + } + + @Test + @DisplayName("채널 생성 테스트") + public void testCreateChannel() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + String json = objectMapper.writeValueAsString(createChannelDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @DisplayName("채널 생성 테스트 - 실패") + public void testFailCreateChannel() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.invalidatedTournamentData(); + String json = objectMapper.writeValueAsString(createChannelDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + + + @Test + @DisplayName("채널 바인딩 에러 테스트 - 컨트롤러") + void createChannelControllerError() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.bindingResultCheck(); + String json = objectMapper.writeValueAsString(createChannelDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + + @Test + @DisplayName("채널 정보 가져오기 테스트") + void getChannelTest() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + channel.get().getChannelLink())) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(jsonPath("$.leagueTitle").value("test")) + .andExpect(jsonPath("$.gameCategory").value("0")) + .andExpect(jsonPath("$.hostName").value("id")); + } + + @Test + @DisplayName("채널 정보 가져오기 실패 테스트 - 유효하지 않은 채널 링크") + void getChannelFailTest() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + "NoValid")) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } + + @Test + @DisplayName("채널 리스트 가져오기") + void loadChannelsList() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channels")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(print()); + } + + @Test + @DisplayName("채널 업데이트") + void updateChannel() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + UpdateChannelDto updateChannelDto = ChannelFixture.updateChannelDto(); + String json = objectMapper.writeValueAsString(updateChannelDto); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + channel.get().getChannelLink()) + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(print()); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelRuleControllerTest.java b/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelRuleControllerTest.java new file mode 100644 index 00000000..c8989598 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/controller/channel/ChannelRuleControllerTest.java @@ -0,0 +1,112 @@ +package leaguehub.leaguehubbackend.controller.channel; + +import com.fasterxml.jackson.databind.ObjectMapper; +import leaguehub.leaguehubbackend.domain.channel.controller.ChannelController; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelRuleDto; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelRuleControllerTest { + + @Autowired + MockMvc mockMvc; + + @Autowired + ChannelController channelController; + + @Autowired + ChannelService channelService; + + @Autowired + MemberService memberService; + @Autowired + ObjectMapper objectMapper; + + @Autowired + MemberRepository memberRepository; + + @Autowired + private ChannelRepository channelRepository; + + @BeforeEach + void setUp() { + memberRepository.save(UserFixture.createMember()); + UserFixture.setUpAuth(); + } + + @Test + @DisplayName("체널 룰 업데이트 컨트롤러 테스트") + void updateChannelRule() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + ChannelRuleDto updateChannelRuleDto = new ChannelRuleDto(); + updateChannelRuleDto.setTier(true); + updateChannelRuleDto.setTierMax(800); + + String json = objectMapper.writeValueAsString(updateChannelRuleDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + channel.get().getChannelLink() + "/rule") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @Test + @DisplayName("체널 룰 업데이트 컨트롤러 테스트 - 실패(티어 유효성)") + void updateChannelRule_tierValid() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + ChannelRuleDto updateChannelRuleDto = new ChannelRuleDto(); + updateChannelRuleDto.setTier(true); + + String json = objectMapper.writeValueAsString(updateChannelRuleDto); + mockMvc.perform(MockMvcRequestBuilders.post("/api/channel/" + channel.get().getChannelLink() + "/rule") + .contentType(MediaType.APPLICATION_JSON) + .content(json)) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + + @Test + @DisplayName("채널 룰 가져오기 테스트") + void getChannelRule() throws Exception { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/channel/" + channel.get().getChannelLink() + "/rule")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(print()); + } + + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelBoardTest.java b/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelBoardTest.java new file mode 100644 index 00000000..5a9d2e6e --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelBoardTest.java @@ -0,0 +1,98 @@ +package leaguehub.leaguehubbackend.entity.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardDto; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static leaguehub.leaguehubbackend.fixture.ChannelFixture.*; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelBoardTest { + + @Autowired + ChannelRepository channelRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + ChannelBoardRepository channelBoardRepository; + @Autowired + ParticipantRepository participantRepository; + @Autowired + private ChannelRuleRepository channelRuleRepository; + + public Channel createChannel() { + Member member = memberRepository.save(UserFixture.createMember()); + CreateChannelDto channelDto = createAllPropertiesCustomChannelDto(true, true, 800, 0, 100); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(member, channel)); + + return channel; + } + + + @Test + public void 채널_보드_기본생성_테스트() throws Exception { + Channel channel = createChannel(); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + + assertThat(channelBoards.size()).isEqualTo(3); + } + + @Test + public void 채널보드_생성_테스트() throws Exception { + Channel channel = createChannel(); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + channelBoardRepository.save(ChannelBoard.createChannelBoard(channel, createChannelBoardDto().getTitle(), + createChannelBoardDto().getContent(), channelBoards.size())); + channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + + assertThat(channelBoards.size()).isEqualTo(4); + } + + + @Test + public void 채널보드_업데이트_테스트() throws Exception { + Channel channel = createChannel(); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + ChannelBoard saved = channelBoardRepository.save(ChannelBoard.createChannelBoard(channel, createChannelBoardDto().getTitle(), + createChannelBoardDto().getContent(), channelBoards.size())); + channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + ChannelBoardDto updateChannelBoardDto = updateChannelBoardDto(); + + + channelBoardRepository.save(saved.updateChannelBoard(updateChannelBoardDto.getTitle(), updateChannelBoardDto.getContent())); + channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.getId()); + + assertThat(channelBoards.size()).isEqualTo(4); + assertThat(channelBoards.get(channelBoards.size() - 1).getTitle()).isEqualTo("test1"); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelRuleTest.java b/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelRuleTest.java new file mode 100644 index 00000000..7e1d2646 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelRuleTest.java @@ -0,0 +1,72 @@ +package leaguehub.leaguehubbackend.entity.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelRuleTest { + + @Autowired + ChannelRuleRepository channelRuleRepository; + + @Test + @DisplayName("채널 룰 생성 테스트") + public void createChannelRule() throws Exception { + CreateChannelDto channelDto = ChannelFixture.createChannelDto(); + Channel channel = ChannelFixture.createDummyChannel(false, false, null, null, 100); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), + channelDto.getTierMin(), channelDto.getPlayCount(), channelDto.getPlayCountMin()); + + ChannelRule save = channelRuleRepository.save(channelRule); + + assertThat(channelRule.getLimitedPlayCount()).isEqualTo(save.getLimitedPlayCount()); + assertThat(channelRule.getTier()).isEqualTo(save.getTier()).isEqualTo(channelDto.getTier()); + } + + @Test + @DisplayName("채널 룰 업데이트 테스트 - 티어") + public void updateChannelRule_Tier() { + CreateChannelDto channelDto = ChannelFixture.createChannelDto(); + Channel channel = ChannelFixture.createDummyChannel(false, false, null, null, 100); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), + channelDto.getTierMin(), channelDto.getPlayCount(), channelDto.getPlayCountMin()); + channelRuleRepository.save(channelRule); + channelRule.updateTierRule(true, 800, null); + + assertThat(channelRule.getTierMax()).isEqualTo(800); + assertThat(channelRule.getTierMin()).isEqualTo(Integer.MIN_VALUE); + + channelRule.updateTierRule(false); + + assertThat(channelRule.getTier()).isEqualTo(false); + } + + + @Test + @DisplayName("채널 룰 업데이트 테스트 - 판수") + public void updateChannelRule_PlayCount() { + CreateChannelDto channelDto = ChannelFixture.createChannelDto(); + Channel channel = ChannelFixture.createDummyChannel(false, false, null, null, 100); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), + channelDto.getTierMin(), channelDto.getPlayCount(), channelDto.getPlayCountMin()); + channelRuleRepository.save(channelRule); + channelRule.updatePlayCountMin(true, 100); + assertThat(channelRule.getLimitedPlayCount()).isEqualTo(100); + + channelRule.updatePlayCountMin(false); + assertThat(channelRule.getPlayCount()).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelTest.java b/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelTest.java new file mode 100644 index 00000000..9f01c519 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/entity/channel/ChannelTest.java @@ -0,0 +1,140 @@ +package leaguehub.leaguehubbackend.entity.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.*; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelTest { + + @Autowired + ChannelService channelService; + @Autowired + private ChannelRepository channelRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + ChannelBoardRepository channelBoardRepository; + @Autowired + ParticipantRepository participantRepository; + + Member member; + @Autowired + private ChannelRuleRepository channelRuleRepository; + + @BeforeEach + void setUp() { + member = memberRepository.save(UserFixture.createMember()); + } + + @Test + @DisplayName("채널 생성 테스트 - 티어 제한, 판수제한 X") + public void 채널_생성_테스트() throws Exception { + //given + CreateChannelDto channelDto = ChannelFixture.createChannelDto(); + Channel channel = Channel.createChannel(channelDto.getTitle(), channelDto.getGameCategory(), channelDto.getMaxPlayer(), channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), channelDto.getPlayCount(), channelDto.getPlayCountMin()); + channelRepository.save(channel); + ChannelRule saveChannelRule = channelRuleRepository.save(channelRule); + List channelBoardList = channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + Participant participant = participantRepository.save(Participant.createHostChannel(member, channel)); + + //when + Optional findChannel = channelRepository.findById(channel.getId()); + + //then + assertThat(participant.getMember()).isEqualTo(member); + assertThat(participant.getChannel()).isEqualTo(channel); + assertThat(findChannel.get().getChannelStatus()).isEqualTo(ChannelStatus.PREPARING); + assertThat(findChannel.get().getRealPlayer()).isEqualTo(0); + assertThat(findChannel.get().getGameCategory()).isEqualTo(GameCategory.getByNumber(channelDto.getGameCategory())); + assertThat(saveChannelRule.getTierMax()).isEqualTo(Integer.MIN_VALUE); + assertThat(channelBoardList.size()).isEqualTo(3); + assertThat(channelBoardList.get(0).getChannel()).isEqualTo(channel); + } + + @Test + @DisplayName("채널 생성 테스트 - 티어, 판수 제한 O") + public void 채널_생성_테스트_제한() throws Exception { + //given + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(true, true, 800, 400, 100); + Channel channel = Channel.createChannel(channelDto.getTitle(), channelDto.getGameCategory(), channelDto.getMaxPlayer(), channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + channelRepository.save(channel); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), channelDto.getPlayCount(), channelDto.getPlayCountMin()); + ChannelRule saveChannelRule = channelRuleRepository.save(channelRule); + List channelBoardList = channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + Participant participant = participantRepository.save(Participant.createHostChannel(member, channel)); + + //when + Optional findChannel = channelRepository.findById(channel.getId()); + + //then + assertThat(participant.getMember()).isEqualTo(member); + assertThat(participant.getChannel()).isEqualTo(channel); + assertThat(findChannel.get().getChannelStatus()).isEqualTo(ChannelStatus.PREPARING); + assertThat(findChannel.get().getRealPlayer()).isEqualTo(0); + assertThat(findChannel.get().getGameCategory()).isEqualTo(GameCategory.getByNumber(channelDto.getGameCategory())); + assertThat(saveChannelRule.getLimitedPlayCount()).isEqualTo(channelDto.getPlayCountMin()); + assertThat(channelBoardList.size()).isEqualTo(3); + assertThat(channelBoardList.get(0).getChannel()).isEqualTo(channel); + assertThat(findChannel.get().getChannelLink()).isEqualTo(channel.getChannelLink()); + assertThat(saveChannelRule.getTierMax()).isEqualTo(800); + } + + @Test + public void 유효하지않는_채널_테스트_게임카테고리() throws Exception { + CreateChannelDto channelDto = ChannelFixture.invalidatedCategoryData(); + assertThatThrownBy(() -> Channel.createChannel(channelDto.getTitle(), channelDto.getGameCategory(), channelDto.getMaxPlayer(), channelDto.getMatchFormat(), channelDto.getChannelImageUrl())).isInstanceOf(ChannelRequestException.class); + } + + @Test + public void 유효하지않는_채널_테스트_토너먼트형식() throws Exception { + CreateChannelDto channelDto = ChannelFixture.invalidatedTournamentData(); + assertThatThrownBy(() -> Channel.createChannel(channelDto.getTitle(), channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl())).isInstanceOf(ChannelRequestException.class); + } + + @Test + public void update_test() { + CreateChannelDto channelDto = ChannelFixture.createChannelDto(); + Channel channel = Channel.createChannel(channelDto.getTitle(), channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + channelRepository.save(channel); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), channelDto.getPlayCount(), channelDto.getPlayCountMin()); + ChannelRule saveChannelRule = channelRuleRepository.save(channelRule); + channel.updateMaxPlayer(64); + channel.updateChannelImageUrl("test"); + channel.updateTitle("test123"); + Optional findChannel = channelRepository.findByChannelLink(channel.getChannelLink()); + + assertThat(findChannel.get().getMaxPlayer()).isEqualTo(64); + assertThat(findChannel.get().getTitle()).isEqualTo("test123"); + assertThat(findChannel.get().getChannelImageUrl()).isEqualTo("test"); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/entity/participant/ParticipantTest.java b/src/test/java/leaguehub/leaguehubbackend/entity/participant/ParticipantTest.java new file mode 100644 index 00000000..08d305cf --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/entity/participant/ParticipantTest.java @@ -0,0 +1,73 @@ +package leaguehub.leaguehubbackend.entity.participant; + +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.RequestStatus; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.global.audit.GlobalConstant; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ParticipantTest { + + @Autowired + ChannelService channelService; + @Autowired + ChannelRepository channelRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + ParticipantRepository participantRepository; + @Autowired + private ChannelRuleRepository channelRuleRepository; + + + @Test + @DisplayName("채널 생성시 유저는 호스트 권한을 가진 참가자가 된다.") + public void createHostTest() throws Exception { + + Member member = memberRepository.save(UserFixture.createMember()); + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(true, true, 800, null, 100); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + + ChannelRule channelRule = ChannelRule.createChannelRule(channel, + channelDto.getTier(), channelDto.getTierMax(), + channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + Participant participant = participantRepository.save(Participant.createHostChannel(member, channel)); + + + assertThat(participantRepository.findAllByChannel_ChannelLink(channel.getChannelLink()).size()).isEqualTo(1); + assertThat(participant.getChannel()).isEqualTo(channel); + assertThat(participant.getMember()).isEqualTo(member); + assertThat(participant.getRole()).isEqualTo(Role.HOST); + + assertThat(participant.getRequestStatus()).isEqualTo(RequestStatus.NO_REQUEST); + assertThat(participant.getGameId()).isEqualTo(GlobalConstant.NO_DATA.getData()); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/ChannelFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/ChannelFixture.java new file mode 100644 index 00000000..f6fd91ea --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/ChannelFixture.java @@ -0,0 +1,124 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelRuleDto; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.UpdateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; + +public class ChannelFixture { + + public static CreateChannelDto createChannelDto() { + CreateChannelDto createChannelDto = CreateChannelDto.builder() + .gameCategory(0) + .title("test") + .matchFormat(0) + .maxPlayer(16) + .tier(false) + .playCount(false) + .build(); + + + return createChannelDto; + } + + + public static CreateChannelDto createAllPropertiesCustomChannelDto(Boolean tier, Boolean playCount, + Integer tierMax, Integer tierMin, int playCountMin) { + CreateChannelDto createChannelDto = CreateChannelDto.builder() + .gameCategory(0) + .title("test") + .matchFormat(0) + .maxPlayer(16) + .tier(tier) + .tierMax(tierMax) + .tierMin(tierMin) + .playCount(playCount) + .playCountMin(playCountMin) + .build(); + + + return createChannelDto; + } + + public static CreateChannelDto bindingResultCheck() { + CreateChannelDto createChannelDto = CreateChannelDto.builder() + .gameCategory(0) + .matchFormat(0) + .maxPlayer(16) + .tier(false) + .build(); + + + return createChannelDto; + } + + public static CreateChannelDto invalidatedCategoryData() { + CreateChannelDto createChannelDto = CreateChannelDto.builder() + .gameCategory(12) + .title("test") + .matchFormat(0) + .maxPlayer(16) + .tier(false) + .playCount(false) + .build(); + + + return createChannelDto; + } + + public static CreateChannelDto invalidatedTournamentData() { + CreateChannelDto createChannelDto = CreateChannelDto.builder() + .gameCategory(0) + .title("test") + .matchFormat(12) + .maxPlayer(16) + .tier(false) + .playCount(false) + .build(); + + + return createChannelDto; + } + + public static ChannelBoardDto createChannelBoardDto() { + ChannelBoardDto channelBoardDto = new ChannelBoardDto("test", "test"); + + return channelBoardDto; + } + + public static ChannelBoardDto updateChannelBoardDto() { + ChannelBoardDto channelBoardDto = new ChannelBoardDto("test1", "test1"); + + return channelBoardDto; + } + + public static Channel createDummyChannel(Boolean tier, Boolean playCount, Integer tierMax, Integer tierMin,int playCountMin){ + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(tier, playCount, tierMax, tierMin,playCountMin); + + return Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + } + + public static UpdateChannelDto updateChannelDto() { + UpdateChannelDto updateChannelDto = new UpdateChannelDto(); + updateChannelDto.setTitle("test123"); + updateChannelDto.setChannelImageUrl("test"); + updateChannelDto.setMaxPlayer(64); + + return updateChannelDto; + } + + public static ChannelRuleDto updateChannelRule() { + ChannelRuleDto channelRuleDto = new ChannelRuleDto(); + channelRuleDto.setTier(true); + channelRuleDto.setTierMax(2400); + channelRuleDto.setTierMin(800); + + channelRuleDto.setPlayCount(true); + channelRuleDto.setPlayCountMin(100); + + return channelRuleDto; + } +} diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/KakaoTokenResponseDtoFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/KakaoTokenResponseDtoFixture.java new file mode 100644 index 00000000..79f780fa --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/KakaoTokenResponseDtoFixture.java @@ -0,0 +1,18 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoTokenResponseDto; + +public class KakaoTokenResponseDtoFixture { + public static KakaoTokenResponseDto createKakaoTokenResponseDto() { + KakaoTokenResponseDto responseDto = new KakaoTokenResponseDto(); + + responseDto.setAccessToken("WjqFifAeMXPI2z-OXYFLthU6ag"); + responseDto.setTokenType("bearer"); + responseDto.setRefreshToken("eQOIu2LcFLCFX-MwSg44zmwIM50MzxDhaVV"); + responseDto.setExpiresIn(21599); + responseDto.setScope("profile_image profile_nickname"); + responseDto.setRefreshTokenExpiresIn(5183999); + + return responseDto; + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/KakaoUserDtoFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/KakaoUserDtoFixture.java new file mode 100644 index 00000000..7dcc4289 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/KakaoUserDtoFixture.java @@ -0,0 +1,31 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; + +public class KakaoUserDtoFixture { + public static KakaoUserDto createKakaoUserDto() { + KakaoUserDto responseDto = new KakaoUserDto(); + + responseDto.setId(2808743059L); + responseDto.setConnectedAt("2023-05-27T15:53:05Z"); + + KakaoUserDto.Properties properties = new KakaoUserDto.Properties(); + properties.setNickname("성우"); + properties.setProfileImage("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz0wDuJn1Y"); + properties.setThumbnailImage("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz"); + responseDto.setProperties(properties); + + KakaoUserDto.KakaoAccount kakaoAccount = new KakaoUserDto.KakaoAccount(); + kakaoAccount.setProfileNicknameNeedsAgreement(false); + kakaoAccount.setProfileImageNeedsAgreement(false); + + KakaoUserDto.KakaoAccount.Profile profile = new KakaoUserDto.KakaoAccount.Profile(); + profile.setNickname("성우"); + profile.setThumbnailImageUrl("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz"); + + kakaoAccount.setProfile(profile); + responseDto.setKakaoAccount(kakaoAccount); + + return responseDto; + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/MatchFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/MatchFixture.java new file mode 100644 index 00000000..58039380 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/MatchFixture.java @@ -0,0 +1,6 @@ +package leaguehub.leaguehubbackend.fixture; + +public class MatchFixture { + + +} diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/MatchScoreListFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/MatchScoreListFixture.java new file mode 100644 index 00000000..40abe7b2 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/MatchScoreListFixture.java @@ -0,0 +1,46 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.match.dto.MatchPlayerInfo; + +import java.util.Arrays; +import java.util.List; + +public class MatchScoreListFixture { + public static List createExpectedList() { + return Arrays.asList( + MatchPlayerInfo.builder() + .matchPlayerId(1L) + .participantId(1L) + .matchRank(1) + .profileSrc("url") + .gameId("NO_DATA") + .score(8) + .build(), + MatchPlayerInfo.builder() + .matchPlayerId(2L) + .participantId(2L) + .matchRank(2) + .profileSrc("url") + .gameId("NO_DATA") + .score(7) + .build(), + MatchPlayerInfo.builder() + .matchPlayerId(3L) + .participantId(3L) + .matchRank(2) + .profileSrc("url") + .gameId("NO_DATA") + .score(7) + .build(), + MatchPlayerInfo.builder() + .matchPlayerId(4L) + .participantId(4L) + .matchRank(4) + .profileSrc("url") + .gameId("NO_DATA") + .score(6) + .build() + ); + } +} + diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/MypageResponseFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/MypageResponseFixture.java new file mode 100644 index 00000000..27eb43f3 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/MypageResponseFixture.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.member.dto.member.MypageResponseDto; + +public class MypageResponseFixture { + public static MypageResponseDto createMypageResponse() { + return MypageResponseDto.builder() + .profileImageUrl("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz0wDuJn1Y") + .nickName("성우") + .email("성우@example.com") + .userEmailVerified(true) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/ParticipantFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/ParticipantFixture.java new file mode 100644 index 00000000..b9ee23e7 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/ParticipantFixture.java @@ -0,0 +1,14 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantDto; + +public class ParticipantFixture { + + public static ParticipantDto createParticipantResponseDto(String nickname){ + ParticipantDto participantResponseDto = new ParticipantDto(); + participantResponseDto.setGameId(nickname); + participantResponseDto.setNickname(nickname); + + return participantResponseDto; + } +} diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/ProfileFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/ProfileFixture.java new file mode 100644 index 00000000..778c7f11 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/ProfileFixture.java @@ -0,0 +1,12 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.member.dto.member.ProfileDto; + +public class ProfileFixture { + public static ProfileDto createProfile() { + return ProfileDto.builder() + .profileImageUrl("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz0wDuJn1Y") + .nickName("성우") + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/ProfileResponseFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/ProfileResponseFixture.java new file mode 100644 index 00000000..a548bf56 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/ProfileResponseFixture.java @@ -0,0 +1,15 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.member.dto.member.ProfileResponseDto; + +public class ProfileResponseFixture { + public static ProfileResponseDto createProfileResponse() { + ProfileResponseDto responseDto = ProfileResponseDto.builder() + .profileId("12345") + .profileImageUrl("http://k.kakaocdn.net/dn/dpk9l1/btqmGhA2lKL/Oz0wDuJn1Y") + .nickName("성우") + .build(); + + return responseDto; + } +} diff --git a/src/test/java/leaguehub/leaguehubbackend/fixture/UserFixture.java b/src/test/java/leaguehub/leaguehubbackend/fixture/UserFixture.java new file mode 100644 index 00000000..002fc10a --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/fixture/UserFixture.java @@ -0,0 +1,99 @@ +package leaguehub.leaguehubbackend.fixture; + +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.email.entity.EmailAuth; +import leaguehub.leaguehubbackend.domain.member.entity.BaseRole; +import leaguehub.leaguehubbackend.domain.member.entity.LoginProvider; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public class UserFixture { + + public static Member createMember() { + Member member = Member.builder() + .personalId("id").profileImageUrl("url") + .nickname("id") + .emailAuth(new EmailAuth("id@example.com", "authToken")) + .loginProvider(LoginProvider.KAKAO).baseRole(BaseRole.USER) + .build(); + member.verifyEmail(); + return member; + } + + public static Member createGuestMember() { + Member member = Member.builder() + .personalId("Guest").profileImageUrl("urlGuest") + .nickname("nickNameGuest") + .emailAuth(new EmailAuth("idGuest@example.com", "authToken")) + .loginProvider(LoginProvider.KAKAO).baseRole(BaseRole.GUEST) + .build(); + + return member; + } + + public static Member createCustomeMember(String name){ + Member member = Member.builder() + .personalId(name).profileImageUrl("url") + .nickname(name) + .loginProvider(LoginProvider.KAKAO).baseRole(BaseRole.USER) + .build(); + + return member; + } + + + + public static LoginMemberResponse createLoginResponse() { + LoginMemberResponse loginMemberResponse = LoginMemberResponse.builder() + .accessToken("accessToken") + .refreshToken("refreshToken") + .build(); + return loginMemberResponse; + } + + public static void setUpAuth() { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("id") + .password("id") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + public static void setUpCustomAuth(String name) { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username(name) + .password("id") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + public static void setUpCustomGuest(String name) { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username(name) + .password("id") + .roles("GUEST") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } +} diff --git a/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelBoardServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelBoardServiceTest.java new file mode 100644 index 00000000..8839907d --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelBoardServiceTest.java @@ -0,0 +1,280 @@ +package leaguehub.leaguehubbackend.service.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelBoardLoadDto; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelBoardService; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelBoardNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelBoardServiceTest { + + @Autowired + MemberRepository memberRepository; + @Autowired + ChannelService channelService; + @Autowired + ChannelRepository channelRepository; + @Autowired + ChannelBoardService channelBoardService; + @Autowired + ChannelBoardRepository channelBoardRepository; + @Autowired + ParticipantRepository participantRepository; + @Autowired + ChannelRuleRepository channelRuleRepository; + Member member; + private String channelLink; + + Channel createCustomChannel(Boolean tier, Boolean playCount, Integer tierMax, Integer tierMin, int playCountMin) throws Exception { + Member ironMember = memberRepository.save(UserFixture.createCustomeMember("썹맹구")); + Member unrankedMember = memberRepository.save(UserFixture.createCustomeMember("서초임")); + Member platinumMember = memberRepository.save(UserFixture.createCustomeMember("손성한")); + Member masterMember = memberRepository.save(UserFixture.createCustomeMember("채수채수밭")); + Member alreadyMember = memberRepository.save(UserFixture.createCustomeMember("요청한사람")); + Member rejectedMember = memberRepository.save(UserFixture.createCustomeMember("거절된사람")); + Member doneMember1 = memberRepository.save(UserFixture.createCustomeMember("참가된사람1")); + Member doneMember2 = memberRepository.save(UserFixture.createCustomeMember("참가된사람2")); + Member observer1 = memberRepository.save(UserFixture.createCustomeMember("관전자1")); + Member observer2 = memberRepository.save(UserFixture.createCustomeMember("관전자2")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(tier, playCount, tierMax, tierMin, playCountMin); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(member, channel)); + participantRepository.save(Participant.participateChannel(unrankedMember, channel)); + participantRepository.save(Participant.participateChannel(ironMember, channel)); + participantRepository.save(Participant.participateChannel(platinumMember, channel)); + participantRepository.save(Participant.participateChannel(masterMember, channel)); + participantRepository.save(Participant.participateChannel(observer1, channel)); + participantRepository.save(Participant.participateChannel(observer2, channel)); + + Participant alreadyParticipant = participantRepository.save(Participant.participateChannel(alreadyMember, channel)); + Participant rejectedParticipant = participantRepository.save(Participant.participateChannel(rejectedMember, channel)); + Participant doneParticipant1 = participantRepository.save(Participant.participateChannel(doneMember1, channel)); + Participant doneParticipant2 = participantRepository.save(Participant.participateChannel(doneMember2, channel)); + + alreadyParticipant.updateParticipantStatus("participantGameId1", "bronze ii", "participantNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + rejectedParticipant.rejectParticipantRequest(); + doneParticipant1.updateParticipantStatus("participantGameId2", "platinum ii", "participantNickname2", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant2.updateParticipantStatus("participantGameId3", "iron ii", "participantNickname3", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant1.approveParticipantMatch(); + doneParticipant2.approveParticipantMatch(); + + return channel; + } + + @BeforeEach + void setUp() { + member = memberRepository.save(UserFixture.createMember()); + UserFixture.setUpAuth(); + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + channelLink = channel.get().getChannelLink(); + } + + @Test + @DisplayName("게시판 만들기 테스트 - 성공") + void createChannelBoard() { + Optional channel = channelRepository.findByChannelLink(channelLink); + channelBoardService.createChannelBoard(channel.get().getChannelLink(), ChannelFixture.createChannelBoardDto()); + + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + + assertThat(channelBoards.size()).isEqualTo(4); + assertThat(channelBoards.get(channelBoards.size() - 1).getIndex()).isEqualTo(4); + } + + @Test + @DisplayName("게시판 만들기 테스트 - 실패 (권한 없음)") + void invalidMemberChannelBoard() throws Exception { + Channel customChannel = createCustomChannel(false, false, 800, null, 100); + Channel findChannel = channelRepository.save(customChannel); + + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + participantRepository.save(Participant.participateChannel(test, findChannel)); + + + Assertions.assertThatThrownBy(() -> channelBoardService.createChannelBoard(findChannel.getChannelLink(), + ChannelFixture.createChannelBoardDto())) + .isInstanceOf(InvalidParticipantAuthException.class); + } + + @Test + @DisplayName("게시판 조회 테스트 - 성공") + void loadBoard() { + Optional channel = channelRepository.findByChannelLink(channelLink); + channelBoardService.createChannelBoard(channel.get().getChannelLink(), ChannelFixture.createChannelBoardDto()); + + List findChannelBoards = channelBoardService.loadChannelBoards(channel.get().getChannelLink()).getChannelBoardLoadDtoList(); + + ChannelBoardDto channelBoardDto = channelBoardService.getChannelBoard(channel.get().getChannelLink(), findChannelBoards.get(findChannelBoards.size() - 1).getBoardId()); + + assertThat(findChannelBoards.size()).isEqualTo(4); + assertThat(channelBoardDto.getContent()).isEqualTo("test"); + } + + @Test + @DisplayName("게시판 조회 테스트 - 실패(해당 채널이 아닌 다른 채널의 게시판 ID 값으로 조회, 없는 게시판 ID로 조회)") + void failLoadBoard() throws Exception { + Optional channel = channelRepository.findByChannelLink(channelLink); + channelBoardService.createChannelBoard(channel.get().getChannelLink(), ChannelFixture.createChannelBoardDto()); + memberRepository.save(UserFixture.createCustomeMember("test2")); + UserFixture.setUpCustomAuth("test2"); + Channel customChannel = createCustomChannel(false, false, 800, null, 100); + Channel findChannel = channelRepository.save(customChannel); + + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(findChannel.getId()); + + assertThatThrownBy(() -> channelBoardService.loadChannelBoards("NO_VALID")) + .isInstanceOf(ChannelNotFoundException.class); + + assertThatThrownBy(() -> channelBoardService.getChannelBoard(channel.get().getChannelLink(), channelBoards.get(0).getId())) + .isInstanceOf(ChannelBoardNotFoundException.class); + assertThatThrownBy(() -> channelBoardService.getChannelBoard(channel.get().getChannelLink(), 1414141L)) + .isInstanceOf(ChannelBoardNotFoundException.class); + } + + @Test + @DisplayName("게시판 업데이트 테스트 - 성공") + void updateTest() { + Optional channel = channelRepository.findByChannelLink(channelLink); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + Long boardId = channelBoards.get(0).getId(); + ChannelBoardDto update = ChannelFixture.updateChannelBoardDto(); + channelBoardService.updateChannelBoard(channel.get().getChannelLink(), boardId, update); + + assertThat(channelBoards.get(0).getTitle()).isEqualTo(update.getTitle()); + assertThat(channelBoards.get(0).getContent()).isEqualTo(update.getContent()); + } + + @Test + @DisplayName("게시판 업데이트 테스트 - 실패(권한없음)") + void updateFailTest() { + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + Optional channel = channelRepository.findByChannelLink(channelLink); + participantRepository.save(Participant.participateChannel(test, channel.get())); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + Long boardId = channelBoards.get(0).getId(); + ChannelBoardDto update = ChannelFixture.updateChannelBoardDto(); + + assertThatThrownBy(() -> channelBoardService.updateChannelBoard(channel.get().getChannelLink(), boardId, update)) + .isInstanceOf(InvalidParticipantAuthException.class); + } + + @Test + @DisplayName("게시판 삭제 테스트 - 성공") + void deleteTest() { + Optional channel = channelRepository.findByChannelLink(channelLink); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + ChannelBoard channelBoard = channelBoards.get(0); + channelBoardService.deleteChannelBoard(channel.get().getChannelLink(), channelBoard.getId()); + List flushBoard = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + + assertThat(flushBoard.size()).isEqualTo(2); + assertThat(flushBoard).doesNotContain(channelBoard); + for (ChannelBoard board : flushBoard) { + assertThat(board.getIndex()).isNotEqualTo(3); + } + } + + + @Test + @DisplayName("게시판 삭제 테스트 - 실패(권한없음)") + void deleteFailTest() { + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + Optional channel = channelRepository.findByChannelLink(channelLink); + participantRepository.save(Participant.participateChannel(test, channel.get())); + List channelBoards = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + ChannelBoard channelBoard = channelBoards.get(0); + assertThatThrownBy(() -> channelBoardService.deleteChannelBoard(channel.get().getChannelLink(), channelBoard.getId())) + .isInstanceOf(InvalidParticipantAuthException.class); + } + + @Test + @DisplayName("게시판 드래그앤드랍 인덱스 테스트 - 성공") + void updateIndex() { + List channelBoardLoadDtoList = channelBoardService.loadChannelBoards(channelLink).getChannelBoardLoadDtoList(); + channelBoardLoadDtoList.get(0).setBoardIndex(3); + channelBoardLoadDtoList.get(2).setBoardIndex(1); + + channelBoardService.updateChannelBoardIndex(channelLink, channelBoardLoadDtoList); + + Optional channel = channelRepository.findByChannelLink(channelLink); + List allByChannelId = channelBoardRepository.findAllByChannel_IdOrderByIndex(channel.get().getId()); + + for (ChannelBoard channelBoard : allByChannelId) { + if (channelBoard.getIndex() == 3) { + assertThat(channelBoard.getTitle()).isEqualTo("리그 공지사항"); + } + + if (channelBoard.getIndex() == 1) { + assertThat(channelBoard.getTitle()).isEqualTo("참여하기"); + } + } + } + + @Test + @DisplayName("게시판 드래그앤드랍 인덱스 테스트 - 실패(권한없음)") + void updateIndex_NoAuth() { + List channelBoardLoadDtoList = channelBoardService.loadChannelBoards(channelLink).getChannelBoardLoadDtoList(); + channelBoardLoadDtoList.get(0).setBoardIndex(3); + channelBoardLoadDtoList.get(2).setBoardIndex(1); + + Member test = UserFixture.createCustomeMember("test231"); + memberRepository.save(test); + UserFixture.setUpCustomAuth("test231"); + + assertThatThrownBy(() -> channelBoardService.updateChannelBoardIndex(channelLink, channelBoardLoadDtoList)) + .isInstanceOf(InvalidParticipantAuthException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelDeleteServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelDeleteServiceTest.java new file mode 100644 index 00000000..56ccefbb --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelDeleteServiceTest.java @@ -0,0 +1,170 @@ +package leaguehub.leaguehubbackend.service.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelStatus; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelBoardService; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelDeleteService; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelStatusAlreadyException; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotGameHostException; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.NoSuchElementException; + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelDeleteServiceTest { + + @Autowired + MemberRepository memberRepository; + @Autowired + ChannelService channelService; + @Autowired + ChannelRepository channelRepository; + @Autowired + ChannelBoardService channelBoardService; + @Autowired + ChannelBoardRepository channelBoardRepository; + @Autowired + ParticipantRepository participantRepository; + @Autowired + ChannelRuleRepository channelRuleRepository; + @Autowired + ChannelDeleteService channelDeleteService; + @Autowired + MatchRepository matchRepository; + @Autowired + MatchPlayerRepository matchPlayerRepository; + private Member member1; + + private Match savedMatch; + private Channel channel; + + @BeforeEach + public void setUp() { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("member1") + .password("member1") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + member1 = memberRepository.save(UserFixture.createCustomeMember("member1")); + Member member2 = memberRepository.save(UserFixture.createCustomeMember("member2")); + Member member3 = memberRepository.save(UserFixture.createCustomeMember("member3")); + Member member4 = memberRepository.save(UserFixture.createCustomeMember("member4")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(false, false, 2400, null, 20); + channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + + // Participant 생성 및 저장 + Participant participant1 = participantRepository.save(Participant.createHostChannel(member1, channel)); + Participant participant2 = participantRepository.save(Participant.participateChannel(member2, channel)); + Participant participant3 = participantRepository.save(Participant.participateChannel(member3, channel)); + Participant participant4 = participantRepository.save(Participant.participateChannel(member4, channel)); + + // Match 생성 및 저장 + Integer matchRound = 1; + String matchName = "Sample Match"; + Match match = Match.createMatch(matchRound, channel, matchName); + savedMatch = matchRepository.save(match); + + // MatchPlayer 생성 + MatchPlayer matchPlayer1 = MatchPlayer.createMatchPlayer(participant1, savedMatch); + MatchPlayer matchPlayer2 = MatchPlayer.createMatchPlayer(participant2, savedMatch); + MatchPlayer matchPlayer3 = MatchPlayer.createMatchPlayer(participant3, savedMatch); + MatchPlayer matchPlayer4 = MatchPlayer.createMatchPlayer(participant4, savedMatch); + matchPlayer1.updateMatchPlayerScore(1); + matchPlayer2.updateMatchPlayerScore(2); + matchPlayer3.updateMatchPlayerScore(2); + matchPlayer4.updateMatchPlayerScore(3); + + matchPlayerRepository.save(matchPlayer1); + matchPlayerRepository.save(matchPlayer2); + matchPlayerRepository.save(matchPlayer3); + matchPlayerRepository.save(matchPlayer4); + } + + @Test + void deleteChannel() { + channelDeleteService.deleteChannel(channel.getChannelLink()); + + List allByMemberId = participantRepository.findAllByMemberId(member1.getId()); + + Assertions.assertThat(allByMemberId.size()).isEqualTo(0); + + Assertions.assertThatThrownBy(() -> channelRepository.findByChannelLink(channel.getChannelLink()).get()) + .isInstanceOf(NoSuchElementException.class); + } + + @Test + void deleteChannel_fail_not_auth() { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("member2") + .password("member2") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + Assertions.assertThatThrownBy(() -> channelDeleteService.deleteChannel(channel.getChannelLink())) + .isInstanceOf(ParticipantNotGameHostException.class); + } + + @Test + void deleteChannel_fail() { + channel.updateChannelStatus(ChannelStatus.PROCEEDING); + + Assertions.assertThatThrownBy(() -> channelDeleteService.deleteChannel(channel.getChannelLink())) + .isInstanceOf(ChannelStatusAlreadyException.class); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelRuleServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelRuleServiceTest.java new file mode 100644 index 00000000..15f2da38 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelRuleServiceTest.java @@ -0,0 +1,109 @@ +package leaguehub.leaguehubbackend.service.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelRuleDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelRuleService; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.InvalidParticipantAuthException; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelRuleServiceTest { + + @Autowired + ChannelRuleService channelRuleService; + @Autowired + ChannelRuleRepository channelRuleRepository; + @Autowired + ChannelService channelService; + @Autowired + ChannelRepository channelRepository; + @Autowired + MemberRepository memberRepository; + + Channel channel; + + @BeforeEach + void setUp() { + memberRepository.save(UserFixture.createMember()); + UserFixture.setUpAuth(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(ChannelFixture.createChannelDto()); + + Optional findChannel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + channel = findChannel.get(); + } + + @Test + @DisplayName("채널 룰 가져오기 테스트") + void getChannelRuleTest() { + ChannelRuleDto channelRule = channelRuleService.getChannelRule(channel.getChannelLink()); + assertThat(channelRule.getTier()).isFalse(); + assertThat(channelRule.getPlayCount()).isFalse(); + } + + @Test + @DisplayName("채널 룰 업데이트 테스트") + void updateChannelRuleTest() { + ChannelRuleDto channelRuleDto = ChannelFixture.updateChannelRule(); + channelRuleService.updateChannelRule(channel.getChannelLink(), channelRuleDto); + ChannelRule channelRule = channelRuleRepository.findChannelRuleByChannel_ChannelLink(channel.getChannelLink()); + assertThat(channelRule.getTierMax()).isEqualTo(channelRuleDto.getTierMax()); + assertThat(channelRule.getTierMin()).isEqualTo(channelRuleDto.getTierMin()); + assertThat(channelRule.getLimitedPlayCount()).isEqualTo(channelRuleDto.getPlayCountMin()); + } + + @Test + @DisplayName("채널 룰 업데이트 테스트 - 실패(티어 유효성)") + void updateChannelRuleTest_tierValid() { + ChannelRuleDto channelRuleDto = ChannelFixture.updateChannelRule(); + channelRuleDto.setTierMax(null); + channelRuleDto.setTierMin(null); + assertThatThrownBy(() -> channelRuleService.updateChannelRule(channel.getChannelLink(), channelRuleDto)) + .isInstanceOf(ChannelRequestException.class); + } + + @Test + @DisplayName("채널 룰 업데이트 테스트 - 실패(판수 유효성)") + void updateChannelRuleTest_playCountValid() { + ChannelRuleDto channelRuleDto = ChannelFixture.updateChannelRule(); + channelRuleDto.setPlayCountMin(null); + assertThatThrownBy(() -> channelRuleService.updateChannelRule(channel.getChannelLink(), channelRuleDto)) + .isInstanceOf(ChannelRequestException.class); + } + + @Test + @DisplayName("채널 룰 업데이트 테스트 - 권한 없음") + void updateChannelRuleTest_noAuth() { + ChannelRuleDto channelRuleDto = ChannelFixture.updateChannelRule(); + memberRepository.save(UserFixture.createCustomeMember("test1")); + UserFixture.setUpCustomAuth("test1"); + + assertThatThrownBy(() -> channelRuleService.updateChannelRule(channel.getChannelLink(), channelRuleDto)) + .isInstanceOf(InvalidParticipantAuthException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelServiceTest.java new file mode 100644 index 00000000..322ce693 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/channel/ChannelServiceTest.java @@ -0,0 +1,148 @@ +package leaguehub.leaguehubbackend.service.channel; + +import leaguehub.leaguehubbackend.domain.channel.dto.ChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.UpdateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelStatus; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelNotFoundException; +import leaguehub.leaguehubbackend.domain.channel.exception.exception.ChannelRequestException; +import leaguehub.leaguehubbackend.domain.channel.service.ChannelService; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import org.junit.jupiter.api.BeforeEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +class ChannelServiceTest { + + @Autowired + ChannelService channelService; + + @Autowired + ChannelRepository channelRepository; + + @Autowired + MemberRepository memberRepository; + @Autowired + ChannelRuleRepository channelRuleRepository; + + @BeforeEach + void setUp() { + memberRepository.save(UserFixture.createMember()); + UserFixture.setUpAuth(); + } + + @Test + @DisplayName("채널 생성 테스트 - 서비스") + void createChannel() { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + + Optional findChannel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + ChannelRule channelRule = channelRuleRepository.findChannelRuleByChannel_ChannelLink(findChannel.get().getChannelLink()); + assertThat(findChannel.get().getChannelStatus()).isEqualTo(ChannelStatus.PREPARING); + assertThat(findChannel.get().getMaxPlayer()).isEqualTo(createChannelDto.getMaxPlayer()); + assertThat(findChannel.get().getTitle()).isEqualTo(createChannelDto.getTitle()); + assertThat(channelRule.getTier()).isFalse(); + } + + @Test + @DisplayName("채널 생성 실패 테스트 - 서비스(티어 유효성)") + void createChannel_TierValid() { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + createChannelDto.setTier(true); + assertThatThrownBy(() -> channelService.createChannel(createChannelDto)) + .isInstanceOf(ChannelRequestException.class); + } + + @Test + @DisplayName("채널 생성 실패 테스트 - 서비스(판수 유효성)") + void createChannel_PlayCountValid() { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + createChannelDto.setPlayCount(true); + createChannelDto.setPlayCountMin(null); + assertThatThrownBy(() -> channelService.createChannel(createChannelDto)) + .isInstanceOf(ChannelRequestException.class); + } + + @Test + @DisplayName("findChannel 실패 - 서비스") + void invalidateTest() { + String validateChannelLink = "NoValid"; + assertThatThrownBy(() -> channelService.findChannel(validateChannelLink)) + .isInstanceOf(ChannelNotFoundException.class); + } + + @Test + @DisplayName("findChannel 성공 - 서비스") + void validateTest() { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + + Optional findChannel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + String validateChannelLink = findChannel.get().getChannelLink(); + ChannelDto channel = channelService.findChannel(validateChannelLink); + + assertThat(channel.getTitle()).isEqualTo(findChannel.get().getTitle()); + assertThat(channel.getRealPlayer()).isEqualTo(findChannel.get().getRealPlayer()); + } + + @Test + @DisplayName("참가한 채널 찾기") + void findParticipantList() { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + CreateChannelDto createChannelDto1 = ChannelFixture.createChannelDto(); + createChannelDto1.setTitle("test3"); + ParticipantChannelDto participantChannelDto1 = channelService.createChannel(createChannelDto1); + + List participantChannelList = channelService.findParticipantChannelList(); + + assertThat(participantChannelList.size()).isEqualTo(2); + } + + + @Test + @DisplayName("채널 업데이트 - 제목, 참가자수, 채널 이미지") + void updateChannel() { + CreateChannelDto createChannelDto = ChannelFixture.createChannelDto(); + ParticipantChannelDto participantChannelDto = channelService.createChannel(createChannelDto); + Optional channel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + UpdateChannelDto updateChannelDto = ChannelFixture.updateChannelDto(); + + channelService.updateChannel(participantChannelDto.getChannelLink(), updateChannelDto); + + Optional findChannel = channelRepository.findByChannelLink(participantChannelDto.getChannelLink()); + + assertThat(findChannel.get().getTitle()).isEqualTo(updateChannelDto.getTitle()); + assertThat(findChannel.get().getMaxPlayer()).isEqualTo(updateChannelDto.getMaxPlayer()); + assertThat(findChannel.get().getChannelImageUrl()).isEqualTo(updateChannelDto.getChannelImageUrl()); + + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/email/EmailServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/email/EmailServiceTest.java new file mode 100644 index 00000000..9e0d1b2d --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/email/EmailServiceTest.java @@ -0,0 +1,243 @@ +package leaguehub.leaguehubbackend.service.email; + +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.email.entity.EmailAuth; +import leaguehub.leaguehubbackend.domain.email.service.EmailService; +import leaguehub.leaguehubbackend.domain.member.entity.BaseRole; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.email.exception.exception.DuplicateEmailException; +import leaguehub.leaguehubbackend.domain.email.exception.exception.InvalidEmailAddressException; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.email.repository.EmailAuthRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.Properties; + +import static org.hibernate.validator.internal.util.Contracts.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +public class EmailServiceTest { + + @InjectMocks + private EmailService emailService; + @Mock + private MemberService memberService; + @Mock + private MemberRepository memberRepository; + @Mock + private EmailAuthRepository emailAuthRepository; + @Mock + private JavaMailSender mailSender; + private UserDetails userDetails; + private Member member; + + @BeforeEach + public void setUp() { + userDetails = mock(UserDetails.class); + member = UserFixture.createMember(); + ReflectionTestUtils.setField(emailService, "secretKey", "emailSecreetKey"); + } + + @Test + @DisplayName("유효한 이메일형식 일때 이메일을 전송한다") + void sendEmailWithConfirmation_whenValidEmailFormat() { + + String email = "id@example.com"; + + String personalId = "id"; + + when(userDetails.getUsername()).thenReturn(personalId); + when(memberRepository.findMemberByPersonalId(personalId)).thenReturn(Optional.of(member)); + when(memberService.findCurrentMember()).thenReturn(member); + when(mailSender.createMimeMessage()).thenReturn(new MimeMessage(Session.getDefaultInstance(new Properties()))); + + emailService.sendEmailWithConfirmation(email); + + verify(emailAuthRepository, times(1)).save(any(EmailAuth.class)); + } + + @Test + @DisplayName("유효하지 않은 이메일 형식 일때 InvalidEmailAddressException") + void sendEmailWithConfirmation_whenNotValidEmailFormat() { + + when(userDetails.getUsername()).thenReturn("userName"); + + String invalidEmail = "invalid-email-format"; + + + assertThrows(InvalidEmailAddressException.class, () -> emailService.sendEmailWithConfirmation(invalidEmail)); + } + + @Test + @DisplayName("이메일이 중복된 경우") + void whenEmailAlreadyExists() { + + String email = "id@example.com"; + + String personalId = "id"; + + when(userDetails.getUsername()).thenReturn(personalId); + when(memberRepository.findMemberByPersonalId(personalId)).thenReturn(Optional.of(member)); + when(memberRepository.findMemberByEmail(email)).thenReturn(Optional.of(member)); + + assertThrows(DuplicateEmailException.class, () -> emailService.sendEmailWithConfirmation(email)); + } + + + @Test + @DisplayName("이메일 인증 토큰 생성") + void generateUniqueTokenForUser() { + + String validToken = emailService.generateUniqueTokenForUser("test@example.com"); + + assertNotNull(validToken); + } + + @Test + @DisplayName("이메일 인증이 이미 된 경우") + void whenEmailAlreadyVerified() { + + String email = "id@example.com"; + + member.verifyEmail(); + + String personalId = "id"; + when(userDetails.getUsername()).thenReturn(personalId); + when(memberRepository.findMemberByPersonalId(personalId)).thenReturn(Optional.of(member)); + when(memberService.findCurrentMember()).thenReturn(member); + when(mailSender.createMimeMessage()).thenReturn(new MimeMessage(Session.getDefaultInstance(new Properties()))); + + emailService.sendEmailWithConfirmation(email); + + verify(emailAuthRepository, times(1)).delete(any(EmailAuth.class)); + } + + @Test + @DisplayName("토큰에서 이메일 추출") + public void getEmailFromValidToken() { + String expectedEmail = "test@example.com"; + String validToken = emailService.generateUniqueTokenForUser(expectedEmail); + String actualEmail = emailService.getEmailFromToken(validToken); + assertEquals(expectedEmail, actualEmail); + } + + @Test + @DisplayName("유효하지 않은 토큰에서 이메일 추출") + public void getEmailFromInvalidToken() { + String invalidToken = "invalidToken"; + String resultEmail = emailService.getEmailFromToken(invalidToken); + assertNull(resultEmail); + } + + @Test + @DisplayName("토큰 유효할시 이메일 인증 후 BaseRole User인지 확인, true 반환") + public void confirmUserEmailWithValidToken() { + + String validToken = emailService.generateUniqueTokenForUser(member.getEmailAuth().getEmail()); + + EmailAuth emailAuth = EmailAuth.builder() + .email(member.getEmailAuth().getEmail()) + .authToken(validToken) + .build(); + + when(emailAuthRepository.findAuthByEmail(member.getEmailAuth().getEmail())).thenReturn(Optional.of(emailAuth)); + when(memberRepository.findByEmailAuth(emailAuth)).thenReturn(Optional.of(member)); + + boolean confirmationResult = emailService.confirmUserEmail(validToken); + + assertTrue(confirmationResult); + assertEquals(BaseRole.USER, member.getBaseRole()); + } + + + @Test + @DisplayName("만료된 토큰일시 False 반환") + public void confirmUserEmailWithExpiredToken() { + + String validToken = emailService.generateUniqueTokenForUser(member.getEmailAuth().getEmail()); + + EmailAuth expiredEmailAuth = new EmailAuth("test@example.com", validToken); + expiredEmailAuth.changeExpireDate(LocalDateTime.now().minusHours(1)); + + when(emailAuthRepository.findAuthByEmail("test@example.com")).thenReturn(Optional.of(expiredEmailAuth)); + + boolean confirmationResult = emailService.confirmUserEmail(validToken); + + assertFalse(confirmationResult); + } + + @Test + @DisplayName("emailAuth가 없을시 False 반환") + public void confirmUserEmailWithNoEmailAuth() { + + Member member = UserFixture.createMember(); + String validToken = emailService.generateUniqueTokenForUser(member.getEmailAuth().getEmail()); + + when(emailAuthRepository.findAuthByEmail(member.getEmailAuth().getEmail())).thenReturn(Optional.empty()); + + boolean confirmationResult = emailService.confirmUserEmail(validToken); + + assertFalse(confirmationResult); + } + + @Test + @DisplayName("Member 정보가 없는 요청시 False 반환") + public void confirmUserEmailWithNoMemberInfo() { + + String validToken = "someValidToken"; + + EmailAuth emailAuth = new EmailAuth("test@example.com", validToken); + + when(emailAuthRepository.findAuthByEmail(emailAuth.getEmail())).thenReturn(Optional.of(emailAuth)); + when(memberRepository.findByEmailAuth(emailAuth)).thenReturn(Optional.empty()); + + boolean confirmationResult = emailService.confirmUserEmail(validToken); + + assertFalse(confirmationResult); + } + + @Test + @DisplayName("Guest인 Member가 인증 성공시 User 로 Role 변경") + public void confirmUserEmailWithGuestRole() { + + String validToken = emailService.generateUniqueTokenForUser("test@example.com"); + + EmailAuth emailAuth = EmailAuth.builder() + .email("test@example.com") + .authToken(validToken) + .build(); + + Member member = UserFixture.createGuestMember(); + + when(emailAuthRepository.findAuthByEmail(emailAuth.getEmail())).thenReturn(Optional.of(emailAuth)); + when(memberRepository.findByEmailAuth(emailAuth)).thenReturn(Optional.of(member)); + + boolean confirmationResult = emailService.confirmUserEmail(validToken); + + assertTrue(confirmationResult); + assertEquals(BaseRole.USER, member.getBaseRole()); + } +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/jwt/JwtServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/jwt/JwtServiceTest.java new file mode 100644 index 00000000..9ae0269e --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/jwt/JwtServiceTest.java @@ -0,0 +1,114 @@ +package leaguehub.leaguehubbackend.service.jwt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Optional; +import leaguehub.leaguehubbackend.domain.member.dto.member.LoginMemberResponse; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.annotation.Transactional; + + +@SpringBootTest +@Transactional +@TestPropertySource(locations = "classpath:application-test.properties") +public class JwtServiceTest { + @InjectMocks + private JwtService jwtService; + + @Mock + private MemberRepository memberRepository; + + + @BeforeEach + public void setup(){ + ReflectionTestUtils.setField(jwtService, "secretKey", "testSecretKey"); + ReflectionTestUtils.setField(jwtService, "accessTokenExpirationPeriod", 60L * 60 * 1000); + ReflectionTestUtils.setField(jwtService, "refreshTokenExpirationPeriod", 24L * 60 * 60 * 1000); + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("유효한 토큰일시 true 반환") + public void whenTokenValid_thenReturnTrue() { + String testToken = jwtService.createAccessToken("testId"); + assertTrue(jwtService.isTokenValid(testToken)); + } + + @Test + @DisplayName("유효하지 않은 토큰일시 false 반환") + public void whenTokenInvalid_thenReturnFalse() { + String invalidToken = "invalidToken"; + assertFalse(jwtService.isTokenValid(invalidToken)); + } + + @Test + @DisplayName("Access 토큰 생성시 주어진 personalId가 토큰에 있어야함") + public void whenCreateAccessToken_givenPersonalId_thenTokenShouldContainPersonalId() { + String personalId = "testPersonalId"; + String accessToken = jwtService.createAccessToken(personalId); + + Optional extractedPersonalId = jwtService.extractPersonalId(accessToken); + + assertTrue(extractedPersonalId.isPresent()); + assertEquals(personalId, extractedPersonalId.get()); + } + + @Test + @DisplayName("토큰들 생성시 personalId가 주어졌을 때 로그인 response는 토큰들을 가지고 있어야함") + public void whenCreateTokens_givenPersonalId_thenLoginMemberResponseShouldContainTokens() { + String personalId = "id"; + LoginMemberResponse loginMemberResponse = jwtService.createTokens(personalId); + + assertNotNull(loginMemberResponse.getAccessToken()); + assertNotNull(loginMemberResponse.getRefreshToken()); + } + + @Test + @DisplayName("헤더에서 Access 토큰 추출시 유효하다면 true 반환") + public void whenExtractAccessToken_givenValidTokenInHeader_thenShouldReturnToken() { + String testToken = "testToken"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + testToken); + + Optional extractedToken = jwtService.extractAccessToken(request); + + assertTrue(extractedToken.isPresent()); + assertEquals(testToken, extractedToken.get()); + } + + @Test + @DisplayName("헤더에서 Refresh 토큰 추출시 유효하다면 true 반환") + public void whenExtractRefreshToken_givenValidTokenInHeader_thenShouldReturnToken() { + String testToken = "testToken"; + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization-refresh", "Bearer " + testToken); + + Optional extractedToken = jwtService.extractRefreshToken(request); + + assertTrue(extractedToken.isPresent()); + assertEquals(testToken, extractedToken.get()); + } + + @Test + public void whenIsTokenExpired_givenNonExpiredToken_thenShouldReturnFalse() { + String personalId = "testPersonalId"; + String accessToken = jwtService.createAccessToken(personalId); + + assertFalse(jwtService.isTokenExpired(accessToken)); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/kakao/KakaoServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/kakao/KakaoServiceTest.java new file mode 100644 index 00000000..729b8875 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/kakao/KakaoServiceTest.java @@ -0,0 +1,123 @@ +package leaguehub.leaguehubbackend.service.kakao; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoTokenResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.JwtService; +import leaguehub.leaguehubbackend.domain.member.service.MemberAuthService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.global.exception.global.exception.GlobalServerErrorException; +import leaguehub.leaguehubbackend.domain.member.exception.kakao.exception.KakaoInvalidCodeException; +import leaguehub.leaguehubbackend.fixture.KakaoTokenResponseDtoFixture; +import leaguehub.leaguehubbackend.fixture.KakaoUserDtoFixture; +import leaguehub.leaguehubbackend.global.redis.service.RedisService; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@Transactional + +@TestPropertySource(locations = "classpath:application-test.properties") +class KakaoServiceTest { + + MockWebServer mockWebServer; + WebClient webClient; + MemberAuthService memberAuthService; + ObjectMapper objectMapper; + + @Mock + MemberRepository memberRepository; + @Mock + MemberService memberService; + @Mock + RedisService redisService; + @Mock + JwtService jwtService; + + + @BeforeEach + void setUp() throws IOException { + mockWebServer = new MockWebServer(); + mockWebServer.start(); + objectMapper = new ObjectMapper(); + webClient = WebClient.create(mockWebServer.url("/").toString()); + memberAuthService = new MemberAuthService(webClient, memberRepository, memberService, redisService, jwtService); + } + + @AfterEach + void tearDown() throws IOException { + mockWebServer.shutdown(); + } + + @Test + @DisplayName("유효한 카카오 코드일시 카카오 토큰을 받는다") + void whenValidKakaoToken_thenGetKakaoToken() throws JsonProcessingException { + KakaoTokenResponseDto mockResponse = KakaoTokenResponseDtoFixture.createKakaoTokenResponseDto(); + String mockResponseJson = objectMapper.writeValueAsString(mockResponse); + mockWebServer.enqueue(new MockResponse() + .setBody(mockResponseJson) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + KakaoTokenResponseDto actualResponse = memberAuthService.getKakaoToken("valid_code"); + + assertEquals(mockResponse, actualResponse); + } + + @Test + @DisplayName("유효한 카카오 토큰일시 카카오 유저를 받는다") + void whenValidAccessToken_thenGetUserInfo() throws JsonProcessingException { + KakaoTokenResponseDto tokenResponseDto = KakaoTokenResponseDtoFixture.createKakaoTokenResponseDto(); + KakaoUserDto mockResponse = KakaoUserDtoFixture.createKakaoUserDto(); + String mockResponseJson = objectMapper.writeValueAsString(mockResponse); + + mockWebServer.enqueue(new MockResponse() + .setBody(mockResponseJson) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + KakaoUserDto actualResponse = memberAuthService.getKakaoUser(tokenResponseDto); + + assertEquals(mockResponse, actualResponse); + } + + @Test + @DisplayName("잘못된 카카오 코드일시 KakaoInvalidCodeException Throw") + void whenInvalidKakaoCode_thenThrowKakaoInvalidCodeException() { + mockWebServer.enqueue(new MockResponse() + .setResponseCode(400) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + assertThrows(KakaoInvalidCodeException.class, () -> memberAuthService.getKakaoToken("invalid_code")); + } + + @Test + @DisplayName("카카오 서버에 문제가 있을 때 GlobalServerErrorException Throw") + void whenKakaoServerProblem_thenThrowGlobalServerErrorException() { + mockWebServer.enqueue(new MockResponse() + .setResponseCode(500) + .addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)); + + assertThrows(GlobalServerErrorException.class, () -> memberAuthService.getKakaoToken("valid_code")); + } + + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/match/MatchPlayerServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/match/MatchPlayerServiceTest.java new file mode 100644 index 00000000..a1205395 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/match/MatchPlayerServiceTest.java @@ -0,0 +1,121 @@ +package leaguehub.leaguehubbackend.service.match; + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.match.dto.GameResultDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchInfoDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchPlayerInfo; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayerResultStatus; +import leaguehub.leaguehubbackend.domain.match.entity.MatchStatus; +import leaguehub.leaguehubbackend.domain.match.entity.PlayerStatus; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchAlreadyUpdateException; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchResultIdNotFoundException; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRankRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.match.service.MatchPlayerService; +import leaguehub.leaguehubbackend.domain.match.service.MatchQueryService; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +class MatchPlayerServiceTest { + + @Autowired + MatchPlayerRepository matchPlayerRepository; + @Autowired + MatchRankRepository matchRankRepository; + @Autowired + MatchRepository matchRepository; + @Autowired + MatchPlayerService matchPlayerService; + @Autowired + MatchQueryService matchQueryService; + + @Test + @DisplayName("점수 업데이트 성공 - 동점자 로직(1등 많이 한 사람, Becrux) 진출") + void updateMatchPlayerScore() { + MatchInfoDto matchInfoDto = matchPlayerService.updateMatchPlayerScore(1L, 3, 1691910300L); + + assertThat(matchInfoDto.getMatchStatus()).isEqualTo(MatchStatus.END); + + Optional 무진스_협회장 = matchInfoDto.getMatchPlayerInfoList().stream() + .filter(matchPlayerInfo -> matchPlayerInfo.getGameId().equals("무진스 협회장")) + .findFirst(); + assertThat(무진스_협회장.get().getMatchPlayerResultStatus()).isEqualTo(MatchPlayerResultStatus.DISQUALIFICATION); + + Optional ChikaPuka = matchInfoDto.getMatchPlayerInfoList().stream() + .filter(matchPlayerInfo -> matchPlayerInfo.getGameId().equals("ChikaPuka")) + .findFirst(); + + assertThat(ChikaPuka.get().getScore()).isEqualTo(108); + assertThat(ChikaPuka.get().getMatchPlayerResultStatus()).isEqualTo(MatchPlayerResultStatus.ADVANCE); + assertThat(ChikaPuka.get().getPlayerStatus()).isEqualTo(PlayerStatus.WAITING); + + + Optional 플레이어개장줄 = matchInfoDto.getMatchPlayerInfoList().stream() + .filter(matchPlayerInfo -> matchPlayerInfo.getGameId().equals("플레이어개장줄")) + .findFirst(); + Optional doaltlqkfrpdla = matchInfoDto.getMatchPlayerInfoList().stream() + .filter(matchPlayerInfo -> matchPlayerInfo.getGameId().equals("doaltlqkfrpdla")) + .findFirst(); + Optional Becrux = matchInfoDto.getMatchPlayerInfoList().stream() + .filter(matchPlayerInfo -> matchPlayerInfo.getGameId().equals("Becrux")) + .findFirst(); + + assertThat(Becrux.get().getMatchPlayerResultStatus()).isEqualTo(MatchPlayerResultStatus.ADVANCE); + assertThat(플레이어개장줄.get().getMatchPlayerResultStatus()).isEqualTo(MatchPlayerResultStatus.ADVANCE); + assertThat(doaltlqkfrpdla.get().getMatchPlayerResultStatus()).isEqualTo(MatchPlayerResultStatus.DROPOUT); + assertThat(matchInfoDto.getMatchPlayerInfoList().get(5).getGameId()).isEqualTo("반갑쨔나"); + + } + + + @Test + @DisplayName("유효하지 않은 경기 - 가져온 결과에 모든 참가자가 있지 않음") + void updateMatchPlayerScore_fail() { + assertThatThrownBy(() -> matchPlayerService.updateMatchPlayerScore(1L, 3, 1695726000L)) + .isInstanceOf(MatchResultIdNotFoundException.class); + } + + @Test + @DisplayName("매치 플레이어 점수 업데이트 - 실패(이미 업데이트 되어있음.)") + void updateMatchPlayerScore_Fail() { + assertThatThrownBy(() -> matchPlayerService.updateMatchPlayerScore(1L, 2, 1691908000L)) + .isInstanceOf(MatchAlreadyUpdateException.class); + } + + @Test + @DisplayName("이전 경기 결과 조회 테스트") + void getGameResult() { + List gameResultList = matchQueryService.getGameResult(1L); + + assertThat(gameResultList.size()).isEqualTo(2); + assertThat(gameResultList.get(0).getMatchRankResultDtos().stream() + .filter(matchRankResultDto -> matchRankResultDto.getGameId().equals("무진스 협회장")) + .findFirst().get().getPlacement()).isEqualTo(6); + + assertThat(gameResultList.get(1).getMatchRankResultDtos().stream() + .filter(matchRankResultDto -> matchRankResultDto.getGameId().equals("무진스 협회장")) + .findFirst().get().getPlacement()).isEqualTo(2); + } + + @Test + @DisplayName("이전 경기 결과 조회 테스트-결과없음") + void getGameResult_fail() { + assertThatThrownBy(() -> matchQueryService.getGameResult(2L)).isInstanceOf(MatchResultIdNotFoundException.class); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/match/MatchServiceScoreTest.java b/src/test/java/leaguehub/leaguehubbackend/service/match/MatchServiceScoreTest.java new file mode 100644 index 00000000..09dc9916 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/match/MatchServiceScoreTest.java @@ -0,0 +1,196 @@ +package leaguehub.leaguehubbackend.service.match; + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.match.service.MatchQueryService; +import leaguehub.leaguehubbackend.domain.match.service.MatchService; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchPlayerInfo; +import leaguehub.leaguehubbackend.domain.match.dto.MatchScoreInfoDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.match.exception.exception.MatchNotFoundException; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mock; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +public class MatchServiceScoreTest { + + @Autowired + private MatchService matchService; + @Autowired + private MemberRepository memberRepository; + @Autowired + ChannelRepository channelRepository; + @Autowired + ChannelRuleRepository channelRuleRepository; + @Autowired + ParticipantRepository participantRepository; + @Autowired + MatchPlayerRepository matchPlayerRepository; + @Autowired + MatchRepository matchRepository; + @Autowired + MatchQueryService matchQueryService; + @Mock + MemberService memberService; + + private Member member1; + + private Match savedMatch; + private Channel channel; + + @BeforeEach + public void setUp() { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("member1") + .password("member1") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + member1 = memberRepository.save(UserFixture.createCustomeMember("member1")); + Member member2 = memberRepository.save(UserFixture.createCustomeMember("member2")); + Member member3 = memberRepository.save(UserFixture.createCustomeMember("member3")); + Member member4 = memberRepository.save(UserFixture.createCustomeMember("member4")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(false, false, 2400, null, 20); + channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + + // Participant 생성 및 저장 + Participant participant1 = participantRepository.save(Participant.createHostChannel(member1, channel)); + Participant participant2 = participantRepository.save(Participant.createHostChannel(member2, channel)); + Participant participant3 = participantRepository.save(Participant.createHostChannel(member3, channel)); + Participant participant4 = participantRepository.save(Participant.createHostChannel(member4, channel)); + + // Match 생성 및 저장 + Integer matchRound = 1; + String matchName = "Sample Match"; + Match match = Match.createMatch(matchRound, channel, matchName); + savedMatch = matchRepository.save(match); + + // MatchPlayer 생성 + MatchPlayer matchPlayer1 = MatchPlayer.createMatchPlayer(participant1, savedMatch); + MatchPlayer matchPlayer2 = MatchPlayer.createMatchPlayer(participant2, savedMatch); + MatchPlayer matchPlayer3 = MatchPlayer.createMatchPlayer(participant3, savedMatch); + MatchPlayer matchPlayer4 = MatchPlayer.createMatchPlayer(participant4, savedMatch); + matchPlayer1.updateMatchPlayerScore(1); + matchPlayer2.updateMatchPlayerScore(2); + matchPlayer3.updateMatchPlayerScore(2); + matchPlayer4.updateMatchPlayerScore(3); + + matchPlayerRepository.save(matchPlayer1); + matchPlayerRepository.save(matchPlayer2); + matchPlayerRepository.save(matchPlayer3); + matchPlayerRepository.save(matchPlayer4); + } + + @AfterEach + public void tearDown() { + matchPlayerRepository.deleteAll(); + participantRepository.deleteAll(); + matchRepository.deleteAll(); + channelRuleRepository.deleteAll(); + channelRepository.deleteAll(); + memberRepository.deleteAll(); + + SecurityContextHolder.clearContext(); + } + + @Test + @DisplayName("getMatchScoreInfo 테스트 - 성공") + public void getMatchScoreInfoSuccessTest() throws Exception { + + MatchScoreInfoDto result = matchQueryService.getMatchScoreInfo(channel.getChannelLink(), savedMatch.getId()); + assertNotNull(result); + assertEquals(-1, result.getRequestMatchPlayerId()); + + List scoreInfos = result.getMatchPlayerInfos(); + assertNotNull(scoreInfos); + assertEquals(4, scoreInfos.size()); + + } + + @Test + @DisplayName("순위 정렬 테스트") + public void test() throws Exception { + + Long matchId = 1L; + + MatchScoreInfoDto testDto = matchQueryService.getMatchScoreInfo(channel.getChannelLink(), savedMatch.getId()); + + assertNotNull(testDto); + assertEquals(-1, testDto.getRequestMatchPlayerId()); + + List matchPlayerInfos = testDto.getMatchPlayerInfos(); + assertNotNull(matchPlayerInfos); + assertEquals(4, matchPlayerInfos.size()); + + assertEquals(1, matchPlayerInfos.get(0).getMatchRank()); + assertEquals(2, matchPlayerInfos.get(1).getMatchRank()); + assertEquals(2, matchPlayerInfos.get(2).getMatchRank()); + assertEquals(4, matchPlayerInfos.get(3).getMatchRank()); + + } + + + @Test + @DisplayName("getMatchScoreInfo 테스트 - 유효하지 않은 matchId") + public void getMatchScoreInfoInvalidMatchIdTest() { + + Long invalidMatchId = 1234L; + + assertThrows(MatchNotFoundException.class, () -> { + matchQueryService.getMatchScoreInfo("1234", invalidMatchId); + }); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/match/MatchServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/match/MatchServiceTest.java new file mode 100644 index 00000000..0e3ce3b0 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/match/MatchServiceTest.java @@ -0,0 +1,168 @@ +package leaguehub.leaguehubbackend.service.match; + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.match.service.MatchQueryService; +import leaguehub.leaguehubbackend.domain.match.service.MatchService; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.match.dto.MatchRoundListDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.match.entity.Match; +import leaguehub.leaguehubbackend.domain.match.entity.MatchPlayer; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchPlayerRepository; +import leaguehub.leaguehubbackend.domain.match.repository.MatchRepository; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import org.junit.jupiter.api.AfterEach; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +class MatchServiceTest { + + @Autowired + MemberRepository memberRepository; + + @Autowired + ChannelRepository channelRepository; + + @Autowired + ChannelBoardRepository channelBoardRepository; + + @Autowired + ParticipantRepository participantRepository; + + @Autowired + MatchRepository matchRepository; + + @Autowired + ChannelRuleRepository channelRuleRepository; + + @Autowired + MatchQueryService matchQueryService; + + @Autowired + MatchService matchService; + @Autowired + private MatchPlayerRepository matchPlayerRepository; + + + @AfterEach + public void tearDown() { + participantRepository.deleteAll(); + matchRepository.deleteAll(); + channelRuleRepository.deleteAll(); + channelRepository.deleteAll(); + memberRepository.deleteAll(); + channelBoardRepository.deleteAll(); + + SecurityContextHolder.clearContext(); + } + Channel createCustomChannel(Boolean tier, Boolean playCount, Integer tierMax, Integer tierMin, int playCountMin) throws Exception { + Member member = memberRepository.save(UserFixture.createMember()); + Member ironMember = memberRepository.save(UserFixture.createCustomeMember("썹맹구")); + Member unrankedMember = memberRepository.save(UserFixture.createCustomeMember("서초임")); + Member platinumMember = memberRepository.save(UserFixture.createCustomeMember("손성한")); + Member masterMember = memberRepository.save(UserFixture.createCustomeMember("채수채수밭")); + Member alreadyMember = memberRepository.save(UserFixture.createCustomeMember("요청한사람")); + Member rejectedMember = memberRepository.save(UserFixture.createCustomeMember("거절된사람")); + Member doneMember1 = memberRepository.save(UserFixture.createCustomeMember("참가된사람1")); + Member doneMember2 = memberRepository.save(UserFixture.createCustomeMember("참가된사람2")); + Member observer1 = memberRepository.save(UserFixture.createCustomeMember("관전자1")); + Member observer2 = memberRepository.save(UserFixture.createCustomeMember("관전자2")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(tier, playCount, tierMax, tierMin, playCountMin); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(member, channel)); + participantRepository.save(Participant.participateChannel(unrankedMember, channel)); + participantRepository.save(Participant.participateChannel(ironMember, channel)); + participantRepository.save(Participant.participateChannel(platinumMember, channel)); + participantRepository.save(Participant.participateChannel(masterMember, channel)); + participantRepository.save(Participant.participateChannel(observer1, channel)); + participantRepository.save(Participant.participateChannel(observer2, channel)); + + Participant alreadyParticipant = participantRepository.save(Participant.participateChannel(alreadyMember, channel)); + Participant rejectedParticipant = participantRepository.save(Participant.participateChannel(rejectedMember, channel)); + Participant doneParticipant1 = participantRepository.save(Participant.participateChannel(doneMember1, channel)); + Participant doneParticipant2 = participantRepository.save(Participant.participateChannel(doneMember2, channel)); + + alreadyParticipant.updateParticipantStatus("participantGameId1", "bronze ii", "participantNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + rejectedParticipant.rejectParticipantRequest(); + doneParticipant1.updateParticipantStatus("participantGameId2", "platinum ii", "participantNickname2", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant2.updateParticipantStatus("participantGameId3", "iron ii", "participantNickname3", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant1.approveParticipantMatch(); + doneParticipant2.approveParticipantMatch(); + + return channel; + } + + @Test + @DisplayName("매치 생성 테스트 - 성공") + public void matchCreateSuccessTest() throws Exception { + //given + UserFixture.setUpCustomAuth("id"); + Channel channel = createCustomChannel(false, false, 2400, null, 20); + + matchService.createSubMatches(channel, channel.getMaxPlayer()); + + List findMatchRound16 = matchRepository.findAllByChannel_ChannelLinkAndMatchRoundOrderByMatchName(channel.getChannelLink(), 1); + List findMatchRound8 = matchRepository.findAllByChannel_ChannelLinkAndMatchRoundOrderByMatchName(channel.getChannelLink(), 2); + + assertThat(findMatchRound16.size()).isEqualTo(2); + assertThat(findMatchRound8.size()).isEqualTo(1); + assertThat(findMatchRound16.get(0).getMatchName()).isEqualTo("Group A"); + + } + + + @Test + @DisplayName("라운드 리스트 조회 테스트 - 성공") + public void matchListSuccessTest() throws Exception { + //given + UserFixture.setUpCustomAuth("id"); + Channel channel = createCustomChannel(false, false, 2400, null, 20); + + MatchRoundListDto roundList = matchQueryService.getRoundList(channel.getChannelLink()); + + assertThat(roundList.getRoundList().size()).isEqualTo(2); + } + + @Test + @DisplayName("DTO 변환 테스트") + public void convertMatchPlayerDto() { + List matchPlayerList = matchPlayerRepository.findAllByMatch_IdOrderByPlayerScoreDesc(1184L); + + matchService.convertMatchPlayerInfoList(matchPlayerList).stream().forEach(matchPlayerInfo -> + System.out.println(matchPlayerInfo.getGameId()+ " " + + matchPlayerInfo.getScore() + " " + matchPlayerInfo.getMatchRank())); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/member/MemberServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/member/MemberServiceTest.java new file mode 100644 index 00000000..cfb46846 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/member/MemberServiceTest.java @@ -0,0 +1,199 @@ +package leaguehub.leaguehubbackend.service.member; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.Optional; +import leaguehub.leaguehubbackend.domain.member.dto.kakao.KakaoUserDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.MypageResponseDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.NicknameRequestDto; +import leaguehub.leaguehubbackend.domain.member.dto.member.ProfileDto; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.exception.member.exception.MemberNotFoundException; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.MemberAuthService; +import leaguehub.leaguehubbackend.domain.member.service.MemberProfileService; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.ParticipantNotFoundException; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.fixture.KakaoUserDtoFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +class MemberServiceTest { + @Mock + private MemberRepository memberRepository; + @Mock + private ParticipantRepository participantRepository; + @InjectMocks + private MemberService memberService; + private Member member; + private Member expectedMember; + + private MemberAuthService memberAuthService; + private MemberProfileService memberProfileService; + @BeforeEach + public void setUp() { + UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder() + .username("id") + .password("id") + .roles("USER") + .build(); + + GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); + + Authentication authentication = new UsernamePasswordAuthenticationToken(userDetailsUser, null + , authoritiesMapper.mapAuthorities(userDetailsUser.getAuthorities())); + + SecurityContextHolder.getContext().setAuthentication(authentication); + + member = UserFixture.createMember(); + + expectedMember = UserFixture.createMember(); + + } + @Test + @DisplayName("개인 아이디로 멤버 찾기") + void findMemberByPersonalId() { + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.of(expectedMember)); + + Optional actualMember = memberService.findMemberByPersonalId("id"); + + assertTrue(actualMember.isPresent()); + assertEquals(expectedMember, actualMember.get()); + } + + @Test + @DisplayName("새로운 멤버 저장") + void saveMember() { + + KakaoUserDto kakaoUserDto = KakaoUserDtoFixture.createKakaoUserDto(); + expectedMember = Member.kakaoUserToMember(KakaoUserDtoFixture.createKakaoUserDto()); + when(memberRepository.save(any(Member.class))).thenReturn(expectedMember); + + Optional actualMember = memberService.saveMember(kakaoUserDto); + + assertTrue(actualMember.isPresent()); + assertEquals(expectedMember.getId(), actualMember.get().getId()); + assertEquals(expectedMember.getPersonalId(), actualMember.get().getPersonalId()); + assertEquals(expectedMember.getLoginProvider(), actualMember.get().getLoginProvider()); + + } + + @Test + @DisplayName("개인 아이디로 멤버 유효성 검사") + void validateMember() { + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.of(expectedMember)); + + Member actualMember = memberService.validateMember("id"); + assertEquals(expectedMember, actualMember); + + when(memberRepository.findMemberByPersonalId(eq("invalidId"))).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> memberService.validateMember("invalidId")); + } + + @Test + @DisplayName("개인 아이디로 멤버 마이페이지 정보 가져오기") + void getProfileTest() { + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.of(member)); + + ProfileDto profile = memberProfileService.getProfile(); + + assertEquals(member.getNickname(), profile.getNickName()); + + } + + @Test + @DisplayName("유효한 멤버의 마이페이지 프로필 검색") + void getMypageProfile_ValidMember() { + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.of(member)); + + MypageResponseDto mypageProfile = memberProfileService.getMypageProfile(); + + assertEquals(member.getProfileImageUrl(), mypageProfile.getProfileImageUrl()); + assertEquals(member.getNickname(), mypageProfile.getNickName()); + assertEquals(member.isEmailUserVerified(), mypageProfile.isUserEmailVerified()); + assertEquals(memberService.getVerifiedEmail(member), mypageProfile.getEmail()); + } + + @Test + @DisplayName("존재하지 않는 멤버의 마이페이지 프로필 검색") + void getMypageProfile_InvalidMember() { + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> memberProfileService.getMypageProfile()); + } + @Test + @DisplayName("멤버 로그아웃") + void logoutMemberTest() { + + HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = Mockito.mock(HttpServletResponse.class); + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.of(member)); + + memberAuthService.logoutMember(request, response); + + assertNull(SecurityContextHolder.getContext().getAuthentication()); + verify(memberRepository).save(any(Member.class)); + } + + @Test + @DisplayName("존재하지 않는 멤버의 닉네임 변경 시 예외 발생") + void changeMemberParticipantNickname_NotFoundMember() { + + NicknameRequestDto nicknameRequestDto = new NicknameRequestDto(); + nicknameRequestDto.setNickName("newNickname"); + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> memberProfileService.changeMemberParticipantNickname(nicknameRequestDto)); + } + + @Test + @DisplayName("존재하지 않는 참가자의 닉네임 변경 시 예외 발생") + void changeMemberParticipantNickname_NotFoundParticipant() { + + NicknameRequestDto nicknameRequestDto = new NicknameRequestDto(); + nicknameRequestDto.setNickName("newNickname"); + + when(memberRepository.findMemberByPersonalId("id")).thenReturn(Optional.of(member)); + when(participantRepository.findAllByMemberId(member.getId())).thenReturn(Collections.emptyList()); + + assertThrows(ParticipantNotFoundException.class, () -> memberProfileService.changeMemberParticipantNickname(nicknameRequestDto)); + } + +} \ No newline at end of file diff --git a/src/test/java/leaguehub/leaguehubbackend/service/notice/NoticeServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/notice/NoticeServiceTest.java new file mode 100644 index 00000000..56cbc7d4 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/notice/NoticeServiceTest.java @@ -0,0 +1,57 @@ +package leaguehub.leaguehubbackend.service.notice; + +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.notice.dto.NoticeDto; +import leaguehub.leaguehubbackend.domain.notice.entity.GameType; +import leaguehub.leaguehubbackend.domain.notice.entity.Notice; +import leaguehub.leaguehubbackend.domain.notice.exception.exception.NoticeUnsupportedException; +import leaguehub.leaguehubbackend.domain.notice.service.NoticeService; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +public class NoticeServiceTest { + + @Autowired + MemberService memberService; + + @Autowired + NoticeService noticeService; + + + @Test + @DisplayName("공지사항 추출 테스트") + void noticeTest() { + + List notices = noticeService.getNotices(GameType.MAIN); + + assertThat(notices.get(0).getNoticeTitle()).isEqualTo("리그허브 서비스 오픈"); + assertThat(notices.get(1).getNoticeTitle()).isEqualTo("리그허브 서비스 안정화"); + assertThat(notices.get(2).getNoticeTitle()).isEqualTo("리그허브 이벤트 안내"); + + } + + @Test + @DisplayName("공지사항 업데이트 테스트") + void noticeUpdateTest() throws Exception { + //given + List noticeList = noticeService.updateNotices(GameType.TFT); + + assertThat(noticeList.size()).isEqualTo(6); + assertThatThrownBy(()->noticeService.updateNotices(GameType.MAIN)).isInstanceOf(NoticeUnsupportedException.class); + } +} diff --git a/src/test/java/leaguehub/leaguehubbackend/service/participant/ParticipantServiceTest.java b/src/test/java/leaguehub/leaguehubbackend/service/participant/ParticipantServiceTest.java new file mode 100644 index 00000000..fdb30f57 --- /dev/null +++ b/src/test/java/leaguehub/leaguehubbackend/service/participant/ParticipantServiceTest.java @@ -0,0 +1,791 @@ +package leaguehub.leaguehubbackend.service.participant; + +import jakarta.transaction.Transactional; +import leaguehub.leaguehubbackend.domain.channel.dto.CreateChannelDto; +import leaguehub.leaguehubbackend.domain.channel.dto.ParticipantChannelDto; +import leaguehub.leaguehubbackend.domain.channel.entity.Channel; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelBoard; +import leaguehub.leaguehubbackend.domain.channel.entity.ChannelRule; +import leaguehub.leaguehubbackend.domain.channel.entity.GameCategory; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelBoardRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRepository; +import leaguehub.leaguehubbackend.domain.channel.repository.ChannelRuleRepository; +import leaguehub.leaguehubbackend.domain.email.exception.exception.UnauthorizedEmailException; +import leaguehub.leaguehubbackend.domain.member.entity.Member; +import leaguehub.leaguehubbackend.domain.member.repository.MemberRepository; +import leaguehub.leaguehubbackend.domain.member.service.MemberService; +import leaguehub.leaguehubbackend.domain.participant.dto.ParticipantDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseStatusPlayerDto; +import leaguehub.leaguehubbackend.domain.participant.dto.ResponseUserGameInfoDto; +import leaguehub.leaguehubbackend.domain.participant.entity.Participant; +import leaguehub.leaguehubbackend.domain.participant.entity.RequestStatus; +import leaguehub.leaguehubbackend.domain.participant.entity.Role; +import leaguehub.leaguehubbackend.domain.participant.exception.exception.*; +import leaguehub.leaguehubbackend.domain.participant.repository.ParticipantRepository; +import leaguehub.leaguehubbackend.domain.participant.service.*; +import leaguehub.leaguehubbackend.fixture.ChannelFixture; +import leaguehub.leaguehubbackend.fixture.UserFixture; +import leaguehub.leaguehubbackend.global.util.SecurityUtils; +import org.jetbrains.annotations.NotNull; +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.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.context.TestPropertySource; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@TestPropertySource(locations = "classpath:application-test.properties") +class ParticipantServiceTest { + + @Autowired + MemberService memberService; + + @Autowired + ParticipantService participantService; + + @Autowired + MemberRepository memberRepository; + + @Autowired + ChannelRepository channelRepository; + + @Autowired + ChannelBoardRepository channelBoardRepository; + + @Autowired + ParticipantRepository participantRepository; + @Autowired + ChannelRuleRepository channelRuleRepository; + + @Autowired + ParticipantWebClientService participantWebClientService; + @Autowired + ParticipantManagementService participantManagementService; + @Autowired + ParticipantQueryService participantQueryService; + @Autowired + ParticipantRoleAndPermissionService participantRoleAndPermissionService; + + Member getMemberId() throws Exception { + + UserDetails userDetails = SecurityUtils.getAuthenticatedUser(); + + String personalId = userDetails.getUsername(); + + Member member = memberService.validateMember(personalId); + + return member; + } + + + Channel createCustomChannel(Boolean tier, Boolean playCount, Integer tierMax, Integer tierMin, int playCountMin){ + //Member member = memberRepository.save(UserFixture.createMember()); + Member hostMember = memberRepository.save(UserFixture.createCustomeMember("호스트")); + Member ironMember = memberRepository.save(UserFixture.createCustomeMember("썹맹구")); + Member unrankedMember = memberRepository.save(UserFixture.createCustomeMember("서초임")); + Member platinumMember = memberRepository.save(UserFixture.createCustomeMember("연습용아이디가됨")); + Member masterMember = memberRepository.save(UserFixture.createCustomeMember("채수채수밭")); + Member alreadyMember = memberRepository.save(UserFixture.createCustomeMember("요청한사람")); + Member rejectedMember = memberRepository.save(UserFixture.createCustomeMember("거절된사람")); + Member doneMember1 = memberRepository.save(UserFixture.createCustomeMember("참가된사람1")); + Member doneMember2 = memberRepository.save(UserFixture.createCustomeMember("참가된사람2")); + Member observer1 = memberRepository.save(UserFixture.createCustomeMember("관전자1")); + Member observer2 = memberRepository.save(UserFixture.createCustomeMember("관전자2")); + + CreateChannelDto channelDto = ChannelFixture.createAllPropertiesCustomChannelDto(tier, playCount, tierMax, tierMin, playCountMin); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel, channelDto.getTier(), channelDto.getTierMax(), channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(hostMember, channel)); + participantRepository.save(Participant.participateChannel(unrankedMember, channel)); + participantRepository.save(Participant.participateChannel(ironMember, channel)); + participantRepository.save(Participant.participateChannel(platinumMember, channel)); + participantRepository.save(Participant.participateChannel(masterMember, channel)); + participantRepository.save(Participant.participateChannel(observer1, channel)); + participantRepository.save(Participant.participateChannel(observer2, channel)); + + Participant alreadyParticipant = participantRepository.save(Participant.participateChannel(alreadyMember, channel)); + Participant rejectedParticipant = participantRepository.save(Participant.participateChannel(rejectedMember, channel)); + Participant doneParticipant1 = participantRepository.save(Participant.participateChannel(doneMember1, channel)); + Participant doneParticipant2 = participantRepository.save(Participant.participateChannel(doneMember2, channel)); + + alreadyParticipant.updateParticipantStatus("participantGameId1", "bronze ii", "participantNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + rejectedParticipant.rejectParticipantRequest(); + doneParticipant1.updateParticipantStatus("participantGameId2", "platinum ii", "participantNickname2", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant2.updateParticipantStatus("participantGameId3", "iron ii", "participantNickname3", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + doneParticipant1.approveParticipantMatch(); + doneParticipant2.approveParticipantMatch(); + + return channel; + } + + @NotNull + private Participant getParticipant(String DummyName1, Channel channel, String DummyGameId1, String DummyNickname1) { + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember(DummyName1)); + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + dummy1.updateParticipantStatus(DummyGameId1, "platinum", DummyNickname1, "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + return dummy1; + } + + + @Test + @DisplayName("티어, 플레이 횟수 검색 테스트 - 성공") + void getDetailSuccessTest() throws Exception { + ResponseUserGameInfoDto testDto2 = participantWebClientService.getTierAndPlayCount("서초임#kr1"); + + + assertThat(testDto2.getTier()).isEqualTo("UNRANKED"); + + assertThat(testDto2.getPlayCount()).isEqualTo(0); + + } + + @Test + @DisplayName("티어, 플레이 횟수 검색 테스트 - 실패") + void getDetailFailTest() throws Exception { + + assertThatThrownBy(() -> participantWebClientService.getTierAndPlayCount("saovkovsk#kr1")) + .isInstanceOf(ParticipantGameIdNotFoundException.class); + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어, 판수 제한 x) - 성공") + void participateDefaultMatchSuccessTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("서초임"); + + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("서초임#kr1"); + + participantManagementService.participateMatch(responseDto, channel.getChannelLink()); + + //when + + Participant participant = participantRepository. + findParticipantByMemberIdAndChannel_ChannelLink( + getMemberId().getId(), + channel.getChannelLink()).get(); + + //then + assertThat(participant.getGameTier()).isEqualToIgnoringCase("unranked"); + assertThat(participant.getRole()).isEqualTo(Role.OBSERVER); + assertThat(participant.getRequestStatus()).isEqualTo(RequestStatus.REQUEST); + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어, 판수 제한 o) - 성공") + void participatelimitedMatchSuccessTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(true, true, 2100, null, 1); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("손성한#kr1"); + + participantManagementService.participateMatch(responseDto, channel.getChannelLink()); + + //when + + Participant participant = participantRepository. + findParticipantByMemberIdAndChannel_ChannelLink( + getMemberId().getId(), + channel.getChannelLink()).get(); + + //then + assertThat(participant.getRole()).isEqualTo(Role.OBSERVER); + assertThat(participant.getRequestStatus()).isEqualTo(RequestStatus.REQUEST); + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어 마스터 20000점 이하, 판수 제한 o) - 성공") + void participatelimitedMatchMasterSuccessTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(true, true, 2400, null, 1); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("손성한#kr1"); + + participantManagementService.participateMatch(responseDto, channel.getChannelLink()); + + //when + + Participant participant = participantRepository. + findParticipantByMemberIdAndChannel_ChannelLink( + getMemberId().getId(), + channel.getChannelLink()).get(); + + //then + assertThat(participant.getRole()).isEqualTo(Role.OBSERVER); + assertThat(participant.getRequestStatus()).isEqualTo(RequestStatus.REQUEST); + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (중복) - 실패") + void participateDuplicatedMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("participantGameId2"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantDuplicatedGameIdException.class); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (참가된사람) - 실패") + void participateDoneMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("참가된사람1"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("participantGameId2"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantInvalidRoleException.class); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (이미참가요청한사람) - 실패") + void participateAlreadyMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("요청한사람"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("participantGameId1"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantAlreadyRequestedException.class); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (거절된사람) - 실패") + void participateRejectedMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(false, false, 800, null, 100); + UserFixture.setUpCustomAuth("거절된사람"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("participantGameId4"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantRejectedRequestedException.class); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어 제한 o) - 실패") + void participatelimitedTierMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(true, false, 400, null, 100); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("손성한#kr1"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantInvalidRankException.class); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어 최소제한 o) - 실패") + void participatelimitedTierMatchFailTest_tierMin() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(true, false, null, 2400, 100); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("서초임#kr1"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantInvalidRankException.class); + + } + + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (판수 제한 o) - 실패") + void participateLimitedPlayCountMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(false, true, 800, null, 100); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("서초임#kr1"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantInvalidPlayCountException.class); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (티어 마스터 100점 이하, 판수 제한 o) - 실패") + void participatelimitedMatchMasterFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Channel channel = createCustomChannel(true, true, 400, null, 20); + UserFixture.setUpCustomAuth("서초임"); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("손성한#kr1"); + + //when + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(ParticipantInvalidRankException.class); + } + + @Test + @DisplayName("채널 경기 참여자 조회 테스트") + void loadPlayerTest() throws Exception { + //given + Member member = memberRepository.save(UserFixture.createMember()); + Member ironMember = memberRepository.save(UserFixture.createCustomeMember("썹맹구")); + Member unrankedMember = memberRepository.save(UserFixture.createCustomeMember("서초임")); + Member platinumMember = memberRepository.save(UserFixture.createCustomeMember("손성한")); + CreateChannelDto channelDto = ChannelFixture.createChannelDto(); + Channel channel = Channel.createChannel(channelDto.getTitle(), + channelDto.getGameCategory(), channelDto.getMaxPlayer(), + channelDto.getMatchFormat(), channelDto.getChannelImageUrl()); + ChannelRule channelRule = ChannelRule.createChannelRule(channel,channelDto.getTier(), channelDto.getTierMax(), + channelDto.getTierMin(), + channelDto.getPlayCount(), + channelDto.getPlayCountMin()); + channelRepository.save(channel); + channelRuleRepository.save(channelRule); + channelBoardRepository.saveAll(ChannelBoard.createDefaultBoard(channel)); + participantRepository.save(Participant.createHostChannel(member, channel)); + participantRepository.save(Participant.participateChannel(unrankedMember, channel)); + Participant part2 = participantRepository.save(Participant.participateChannel(platinumMember, channel)); + Participant part3 = participantRepository.save(Participant.participateChannel(ironMember, channel)); + + part2.updateParticipantStatus("손성한", "platinum III", "손성한", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + part3.updateParticipantStatus("썹맹구", "iron III", "썹맹구", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + + part2.approveParticipantMatch(); + part3.approveParticipantMatch(); + + + //when + List requestPlayerDto = participantQueryService.loadPlayers(channel.getChannelLink()); + + //then + assertThat(part2.getNickname()).isEqualTo(requestPlayerDto.get(0).getNickname()); + assertThat(part2.getGameId()).isEqualTo(requestPlayerDto.get(0).getGameId()); + + assertThat(part3.getNickname()).isEqualTo(requestPlayerDto.get(1).getNickname()); + assertThat(part3.getGameId()).isEqualTo(requestPlayerDto.get(1).getGameId()); + + + assertThat(part2.getRequestStatus()).isEqualTo(RequestStatus.DONE); + assertThat(part3.getRequestStatus()).isEqualTo(RequestStatus.DONE); + + } + + @Test + @DisplayName("요청된사람 조회 테스트 (관리자 x) - 실패") + public void loadRequestStatusPlayerListFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember("DummyName1")); + Member dummyMember2 = memberRepository.save(UserFixture.createCustomeMember("DummyName2")); + + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + Participant dummy2 = participantRepository.save(Participant.participateChannel(dummyMember2, channel)); + + dummy1.updateParticipantStatus("DummyGameId1", "platinum III", "DummyNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + dummy2.updateParticipantStatus("DummyGameId2", "iron III", "DummyNickname2", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + + //when + assertThatThrownBy(() -> participantQueryService.loadRequestStatusPlayerList(channel.getChannelLink())) + .isInstanceOf(InvalidParticipantAuthException.class); + + } + + @Test + @DisplayName("요청된사람 조회 테스트 (관리자 o) - 성공") + public void loadRequestStatusPlayerListTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember("DummyName1")); + Member dummyMember2 = memberRepository.save(UserFixture.createCustomeMember("DummyName2")); + + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + Participant dummy2 = participantRepository.save(Participant.participateChannel(dummyMember2, channel)); + + dummy1.updateParticipantStatus("DummyGameId1", "platinum III", "DummyNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + + //when + List DtoList = participantQueryService.loadRequestStatusPlayerList(channel.getChannelLink()); + + + //then + assertThat(dummy1.getId()).isEqualTo(DtoList.get(0).getPk()); + assertThat(dummy1.getNickname()).isEqualTo(DtoList.get(0).getNickname()); + assertThat(dummy1.getGameId()).isEqualTo(DtoList.get(0).getGameId()); + assertThat(dummy1.getGameTier()).isEqualTo(DtoList.get(0).getTier()); + assertThat(dummy1.getRequestStatus()).isEqualTo(RequestStatus.REQUEST); + + } + + + @Test + @DisplayName("요청된사람 승인 테스트 (관리자 o) - 성공") + public void approveParticipantSuccessTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + + //when + participantRoleAndPermissionService.approveParticipantRequest(channel.getChannelLink(), dummy1.getId()); + + Participant updateDummy = participantRepository.findParticipantByIdAndChannel_ChannelLink(dummy1.getId(), channel.getChannelLink()).get(); + Optional channel1 = channelRepository.findByChannelLink(channel.getChannelLink()); + int updateRealPlayerCount = 3; + + //then + assertThat(updateDummy.getId()).isEqualTo(dummy1.getId()); + assertThat(updateDummy.getRole().getNum()).isEqualTo(Role.PLAYER.getNum()); + assertThat(updateDummy.getRequestStatus().getNum()).isEqualTo(RequestStatus.DONE.getNum()); + assertThat(channel1.get().getRealPlayer()).isEqualTo(updateRealPlayerCount); + + } + + @Test + @DisplayName("요청된사람 승인 테스트 (관리자 x) - 실패") + public void approveParticipantFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + + assertThatThrownBy(() -> participantRoleAndPermissionService.approveParticipantRequest(channel.getChannelLink(), dummy1.getId())) + .isInstanceOf(InvalidParticipantAuthException.class); + + } + + @Test + @DisplayName("요청된사람 거절 테스트 (관리자 o) - 성공") + public void rejectedParticipantSuccessTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + + //when + participantRoleAndPermissionService.rejectedParticipantRequest(channel.getChannelLink(), dummy1.getId()); + + Participant updateDummy = participantRepository.findParticipantByIdAndChannel_ChannelLink(dummy1.getId(), channel.getChannelLink()).get(); + + //then + assertThat(updateDummy.getId()).isEqualTo(dummy1.getId()); + assertThat(updateDummy.getRole().getNum()).isEqualTo(Role.OBSERVER.getNum()); + assertThat(updateDummy.getRequestStatus().getNum()).isEqualTo(RequestStatus.REJECT.getNum()); + + } + + @Test + @DisplayName("요청된사람 거절 테스트 (관리자 x) - 실패") + public void rejectedParticipantFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + + + assertThatThrownBy(() -> participantRoleAndPermissionService.rejectedParticipantRequest(channel.getChannelLink(), dummy1.getId())) + .isInstanceOf(InvalidParticipantAuthException.class); + + + } + + @Test + @DisplayName("참가한사람 거절 테스트 (관리자 o) - 성공") + public void rejectedPlayerSuccessTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + dummy1.approveParticipantMatch(); + + //when + participantRoleAndPermissionService.rejectedParticipantRequest(channel.getChannelLink(), dummy1.getId()); + + Participant updateDummy = participantRepository.findParticipantByIdAndChannel_ChannelLink(dummy1.getId(), channel.getChannelLink()).get(); + + //then + assertThat(updateDummy.getId()).isEqualTo(dummy1.getId()); + assertThat(updateDummy.getRole().getNum()).isEqualTo(Role.OBSERVER.getNum()); + assertThat(updateDummy.getRequestStatus().getNum()).isEqualTo(RequestStatus.REJECT.getNum()); + + } + + @Test + @DisplayName("참가한사람 거절 테스트 (관리자 x) - 실패") + public void rejectedPlayerFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + + assertThatThrownBy(() -> participantRoleAndPermissionService.rejectedParticipantRequest(channel.getChannelLink(), dummy1.getId())) + .isInstanceOf(InvalidParticipantAuthException.class); + + } + + @Test + @DisplayName("관전자 조회 테스트 (관리자 o) - 성공") + public void loadObserverListTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember("DummyName1")); + Member dummyMember2 = memberRepository.save(UserFixture.createCustomeMember("DummyName2")); + + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + Participant dummy2 = participantRepository.save(Participant.participateChannel(dummyMember2, channel)); + + + //when + List DtoList = participantQueryService.loadObserverPlayerList(channel.getChannelLink()); + + //then + assertThat(dummy1.getId()).isEqualTo(DtoList.get(0).getPk()); + assertThat(dummy1.getNickname()).isEqualTo(DtoList.get(0).getNickname()); + assertThat(dummy1.getGameId()).isEqualTo(DtoList.get(0).getGameId()); + assertThat(dummy1.getGameTier()).isEqualTo(DtoList.get(0).getTier()); + assertThat(dummy1.getRequestStatus()).isEqualTo(RequestStatus.NO_REQUEST); + + assertThat(dummy2.getId()).isEqualTo(DtoList.get(1).getPk()); + assertThat(dummy2.getNickname()).isEqualTo(DtoList.get(1).getNickname()); + assertThat(dummy2.getGameId()).isEqualTo(DtoList.get(1).getGameId()); + assertThat(dummy2.getGameTier()).isEqualTo(DtoList.get(1).getTier()); + assertThat(dummy2.getRequestStatus()).isEqualTo(RequestStatus.NO_REQUEST); + + } + + @Test + @DisplayName("관전자 조회 테스트 (관리자 x) - 실패") + public void loadObserverListFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember("DummyName1")); + Member dummyMember2 = memberRepository.save(UserFixture.createCustomeMember("DummyName2")); + + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + Participant dummy2 = participantRepository.save(Participant.participateChannel(dummyMember2, channel)); + + //when + assertThatThrownBy(() -> participantQueryService.loadObserverPlayerList(channel.getChannelLink())) + .isInstanceOf(InvalidParticipantAuthException.class); + + } + + @Test + @DisplayName("관리자 권한 부여 테스트 (관리자 o) - 성공") + public void updateHostParticipantSuccessTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Member dummyMember1 = memberRepository.save(UserFixture.createCustomeMember("DummyName1")); + Member dummyMember2 = memberRepository.save(UserFixture.createCustomeMember("DummyName2")); + + Participant dummy1 = participantRepository.save(Participant.participateChannel(dummyMember1, channel)); + Participant dummy2 = participantRepository.save(Participant.participateChannel(dummyMember2, channel)); + dummy1.updateParticipantStatus("DummyGameId1", "platinum", "DummyNickname1", "xKzO3XyPc7DLH5n6P-XC8z0DvQqhmZy8y8JZZxjXSSvPQ5qXqohUw1sehtNdSYIpsH0ckWagN5wnOQ"); + + //when + participantRoleAndPermissionService.updateHostRole(channel.getChannelLink(), dummy1.getId()); + participantRoleAndPermissionService.updateHostRole(channel.getChannelLink(), dummy2.getId()); + + Participant updateDummy1 = participantRepository.findParticipantByIdAndChannel_ChannelLink(dummy1.getId(), channel.getChannelLink()).get(); + Participant updateDummy2 = participantRepository.findParticipantByIdAndChannel_ChannelLink(dummy2.getId(), channel.getChannelLink()).get(); + + //then + assertThat(updateDummy1.getId()).isEqualTo(dummy1.getId()); + assertThat(updateDummy1.getRole().getNum()).isEqualTo(Role.HOST.getNum()); + assertThat(updateDummy1.getRequestStatus().getNum()).isEqualTo(RequestStatus.NO_REQUEST.getNum()); + + assertThat(updateDummy2.getId()).isEqualTo(dummy2.getId()); + assertThat(updateDummy2.getRole().getNum()).isEqualTo(Role.HOST.getNum()); + assertThat(updateDummy2.getRequestStatus().getNum()).isEqualTo(RequestStatus.NO_REQUEST.getNum()); + + } + + @Test + @DisplayName("관리자권한 부여 테스트 (관리자 x) - 실패") + public void updateHostParticipantFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + Participant dummy1 = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + + assertThatThrownBy(() -> participantRoleAndPermissionService.updateHostRole(channel.getChannelLink(), dummy1.getId())) + .isInstanceOf(InvalidParticipantAuthException.class); + + } + + @Test + @DisplayName("요청된사람 승인 테스트 (최대 인원수 초과) - 실패") + public void approveParticipantCountFailTest() throws Exception { + //given + UserFixture.setUpCustomAuth("호스트"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + String[] nickName = new String[13]; + + for (int i = 0; i < nickName.length; i++) { + nickName[i] = "더미" + i; + Participant dummyParticipant = getParticipant(nickName[i], channel, nickName[i], nickName[i]); + dummyParticipant.approveParticipantMatch(); + } + + Participant dummy = getParticipant("DummyName1", channel, "DummyGameId1", "DummyNickname1"); + participantRoleAndPermissionService.approveParticipantRequest(channel.getChannelLink(), dummy.getId()); + + Participant dummy1 = getParticipant("DummyName2", channel, "DummyGameId2", "DummyNickname2"); + + assertThatThrownBy(() -> participantRoleAndPermissionService.approveParticipantRequest(channel.getChannelLink(), dummy1.getId())) + .isInstanceOf(ParticipantRealPlayerIsMaxException.class); + + } + + @Test + @DisplayName("채널 참여 - 성공") + void participantChannelSuccess() throws Exception { + //given + Member dummyMember = memberRepository.save(UserFixture.createCustomeMember("참가할사람")); + UserFixture.setUpCustomAuth("참가할사람"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + + //when + ParticipantChannelDto participantChannelDto = participantManagementService.participateChannel(channel.getChannelLink()); + //then + + assertThat(participantChannelDto.getChannelLink()).isEqualTo(channel.getChannelLink()); + assertThat(participantChannelDto.getGameCategory()).isEqualTo(GameCategory.TFT.getNum()); + assertThat(participantChannelDto.getTitle()).isEqualTo(channel.getTitle()); + } + + @Test + @DisplayName("채널 중복 참여 - 실패") + void participantChannelDuplicateFail() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + + //then + assertThatThrownBy(() -> participantManagementService.participateChannel(channel.getChannelLink())) + .isInstanceOf(ParticipantDuplicatedGameIdException.class); + + } + + @Test + @DisplayName("채널 나가기 - 성공") + void participantChannelLeaveSuccess() throws Exception { + //given + UserFixture.setUpCustomAuth("서초임"); + Channel channel = createCustomChannel(true, true, 2400, null, 20); + + UserDetails userDetails = SecurityUtils.getAuthenticatedUser(); + String personalId = userDetails.getUsername(); + Member member = memberService.validateMember(personalId); + + System.out.println("member nickname = " + member.getNickname()); + System.out.println("member nickname = " + member.getId()); + + long count = participantRepository.count(); + + //when + participantManagementService.leaveChannel(channel.getChannelLink()); + + //then + long deleteCount = participantRepository.count(); + + assertThat(deleteCount).isEqualTo(count - 1); + + } + + @Test + @DisplayName("해당 채널의 경기 참가 테스트 (이메일 미인증) - 실패") + void participateUnAuthMatchFailTest() throws Exception { + //given, 역할이 OBSERVER인 참가자, 해당 채널, 해당 채널 룰, 유저 디테일 + Member guestMember = memberRepository.save(UserFixture.createGuestMember()); + UserFixture.setUpCustomGuest("Guest"); + + Channel channel = createCustomChannel(false, false, 800, null, 100); + ParticipantDto responseDto = new ParticipantDto(); + + responseDto.setGameId("urlGuestGameId"); + + assertThatThrownBy(() -> participantManagementService.participateMatch(responseDto, channel.getChannelLink())) + .isInstanceOf(UnauthorizedEmailException.class); + + } + + @Test + @DisplayName("채널 커스텀 정렬") + void channelCustomIndexOrder() { + Member host1Member = memberRepository.save(UserFixture.createCustomeMember("호스트1")); + UserFixture.setUpCustomAuth("호스트1"); + List channels = IntStream.range(0, 3) + .mapToObj(i -> createCustomChannel(false, false, 800, null, 100)) + .peek(channel -> { + channelRepository.save(channel); + participantManagementService.participateChannel(channel.getChannelLink()); + }) + .collect(Collectors.toList()); + + List participantChannelDtos = IntStream.range(0, 3) + .mapToObj(i -> { + ParticipantChannelDto dto = new ParticipantChannelDto(); + Channel channel = channels.get(i); + dto.setChannelLink(channel.getChannelLink()); + dto.setImgSrc(channel.getChannelImageUrl()); + dto.setTitle(channel.getTitle()); + dto.setGameCategory(channel.getGameCategory().getNum()); + dto.setCustomChannelIndex(2 - i); // Reversed index + return dto; + }) + .collect(Collectors.toList()); + + participantManagementService.updateCustomChannelIndex(participantChannelDtos); + + List all = participantRepository.findAllByMemberIdOrderByIndex(host1Member.getId()); + assertThat(all.get(0).getIndex()).isEqualTo(0); + assertThat(all.get(0).getChannel()).isEqualTo(channels.get(2)); + } + + + +} \ No newline at end of file