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)**
+
+---
+
+
+### 기대효과
+
+> 1️⃣ **사용자 증가 및 참여 활성화**
+>
+
+- 아마추어 리그에 대한 적극적인 참여를 독려함으로써 플랫폼에 대한 사용자 수를 증가시킬 수 있어요.
+- 다양한 게임 지원과 유연한 리그 설정을 통해 사용자들은 자신이 원하는 게임에서 참가할 수 있으며, 자신만의 리그를 만들 수 있어요.
+
+> 2️⃣ **커뮤니티 형성과 상호 작용 강화**
+>
+
+- 아마추어 리그 플랫폼을 통해 게임 커뮤니티가 형성될 수 있어요
+- 사용자들은 자신의 게임 능력을 측정하고 비교하며, 다른 플레이어들과 소통하고 경쟁할 수 있는 경험을 즐길 수 있어요
+- 이는 사용자들 간의 상호 작용을 촉진하고, 게임에 대한 더욱 흥미로운 경험을 제공해요요
+
+> 3️⃣ **사용자 중심의 서비스 제공**
+>
+>
+
+- 사용자들은 자신의 리그를 쉽게 생성하고 관리할 수 있는 사용자 중심의 서비스를 경험할 수 있어요
+- 사용자들이 플랫폼을 편리하게 이용할 수 있어요
+- 사용자 피드백을 반영한 지속적인 서비스 개선을 통해 사용자들의 만족도를 높일 수 있어요
+
+# **핵심 기능**
+
+---
+
+## `로그인`
+
+
+
+
+❶ 카카오를 이용한 로그인
+
+❷ 추후 다른 소셜 로그인 도입 예정
+
+
+
+### `메인페이지(관리자, 사용자)`
+
+
+
+
+❶ 대회(채널) 참가
+
+❷ 이미 들어간 채널을 선택하여 채널 둘러보기
+
+❸ 해당 웹 서비스의 공지사항 확인
+
+❹ 게임의 패치노트 확인
+
+### `관리자 - 대회 관리 페이지`
+
+|  |  |
+|---|---|
+
+
+❶ 채널의 홈 화면에서 대회의 정보를 수정
+
+❷ 대회 관리에서 대회 설정(대회 시작, 경기 배정, 채널 정보 수정) 및 대회 알림 확인
+
+### `리그허브 공지사항`
+|  |  |
+|---|---|
+
+
+
+❶ 게임의 패치노트 확인
+
+### `채널 추가하기`
+
+
+
+
+❶ 대회 개최를 이용하여 자신의 채널 생성
+
+❷ 다른 채널의 참여 코드를 이용하여 채널 참여
+
+
+### `채널 생성하기`
+
+
+
+
+❶ 예시 폼을 작성하여 채널을 생성
+
+❷ 커스텀 룰, 대회 이미지를 선택하여 설정
+
+### `채널 참가하기`
+
+
+
+
+❶ 참여 코드를 입력하여 채널 참여
+
+### 경기 페이지(관리자, 사용자)
+
+### `관리자 - 경기 페이지`
+
+
+
+❶ 실격 처리 버튼을 이용하여 해당 대회 참가자 실격
+
+❷ 채팅을 통하여 참가들과 소통
+
+❸ 체크 박스(체크인)을 통한 준비 확인
+
+
+### `사용자 - 경기 페이지`
+
+
+
+❶ 준비 버튼을 통한 준비(체크 박스) 체크
+
+❷ 초록색 배경을 통한 자신의 위치 확인
+
+❸ 체크 박스(체크인)을 통한 준비 확인
+
+❹ 채팅을 통한 관리자 및 참가자와의 소통, Call Admin을 통한 관리자 호출
+
+### 채널 공지 페이지(관리자, 사용자)
+
+### `사용자 - 채널 공지 페이지`
+
+
+
+❶ 참여자 규칙 탭을 통하여 해당 대회의 규칙 확인 가능
+
+### `관리자 - 채널 공지 페이지`
+
+
+
+❶ 공지 삭제, 수정을 통한 관리
+
+### `대진표`
+
+
+
+❶ 대진표를 통하여 참여자 확인
+
+❷ 대진표의 회색 바탕을 통하여 실격자 확인
+
+❸ 자세히 버튼 또는 현재 라운드(라운드 2)를 통하여 해당 그룹의 대회 페이지로 이동 가능
+
+❹ 빨간 점을 통하여 현재 Round 확인
+
+### 시스템 아키텍처
+
+
+
+### 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
+
+| 김현준 | 박정석 | 이상엽 | 이경훈 | 홍성우 |
+|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:|
+| [](https://github.com/HyeonJun0530) | [](https://github.com/navyjeongs) | [](https://github.com/pp449) | [](https://github.com/TinyFrogs) | [](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 extends GrantedAuthority> 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