diff --git a/.github/workflows/checkstyle-validation.yml b/.github/workflows/checkstyle-validation.yml index 8b8fc328c..caba78380 100644 --- a/.github/workflows/checkstyle-validation.yml +++ b/.github/workflows/checkstyle-validation.yml @@ -19,3 +19,5 @@ jobs: run: ./gradlew --console verbose clean checkstyleMain - name: ️Test checkstyle run: ./gradlew --console verbose clean checkstyleTest + - name: ️TestFixture checkstyle + run: ./gradlew --console verbose clean checkstyleTestFixture diff --git a/README.md b/README.md index 492e93961..60214c684 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # 42arcade.gg.server.v2 -https://42gg.kr/ - +https://gg.42seoul.kr/ ## ⚡️ 프로젝트 소개 + 42 서울 내에서 탁구 경기 매칭, 전적, 상점 서비스를 제공하는 프로젝트 입니다.
향후 추가 서비스 확장 예정 @@ -27,44 +27,85 @@ https://42gg.kr/ - ## ⚡️ 프로젝트 관리 + ## ⚡️ 프로젝트 개발기간 + - 3기: 2023.04.16 ~ 2023.06.23 - 4기: 2023.08.01 ~ 2023.09.21 - 5기 : 2023.11.01 ~ 2024.01.31 +- 6기 : 2023.02.01 ~ 2024.05.10 + +- 7기 : 2024.06.03 ~ 2024.09.10 + ## ⚡️ 프로젝트 아키텍처 -![gg-5th-architecture](https://github.com/42organization/42gg.server.dev.v2/assets/33301153/f801e7b5-d579-467b-9ad0-2bfec506dcaa) +![AwsArchitecture](https://github.com/user-attachments/assets/54da941b-a8c4-4586-9489-5e1d1085d7b8) +## ⚡️ ERD 및 모듈 구조 + +
+ 모듈 구조도 + +모듈 구조도 +
+ +
+ 5기 + +![5기ERD](https://github.com/user-attachments/assets/0f889aaa-a39d-4062-8063-a495d6cd8863) +
+
+ 6기 + +![6기ERD](https://github.com/user-attachments/assets/4719ec57-64b3-42f8-8ada-a745f91c6444) +
+
+ Recurit + +![RecuritERD](https://github.com/user-attachments/assets/ad07f23e-2c99-4d21-b0b5-a5d47c28dcb1) +
+
+ 7기 + +![7기ERD](https://github.com/user-attachments/assets/c5a147b6-107c-4524-b656-6183dc04ccf6) +
## ⚡️ 팀소개 + ### 3기 +
3기 진행 사항
### ⚡️⚡ 로그인 연동 추가 -- v1에서 지원하지 않던 카카오계정 연동 기능 추가(좌 : v1, 우: v2)

- loginv1     - loginv2     +- v1에서 지원하지 않던 카카오계정 연동 기능 추가(좌 : v1, 우: v2)

+ loginv1 +      + loginv2 +      ### ⚡️⚡ DB table 구조 변경 + - v1에서 확장을 위해 열어둔 구조나 테이블마다 여러 곳에 있던 중복된 속성 제거 - v1 -> v2 테이블 수 감소 : 14 -> 12 -erdv1     +erdv1 +    
-erdv2     +erdv2 +     ### ⚡️⚡ 게임추가 기능 + - v1에서 1개의 예약만 되던 것에서 최대 3개까지 예약을 잡을 수 있도록 변경

@@ -72,15 +113,19 @@ https://42gg.kr/
### ⚡️⚡ 도커 도입 + - v2에서 도커 도입을 통해 컨테이너를 통한 서버 관리 도입 -
+
+
dockerPs    
### ⚡️⚡ 모니터링 도입 + - grafana를 통한 서버 모니터링 도입 -
+
+
dockerPs    
@@ -116,33 +161,37 @@ https://42gg.kr/ - - ### 4기 +
4기 진행 사항
### ⚡️⚡ DB table 구조 변경 + - 상점, 티어 등 서비스 확장을 위한 DB 재설계 -ERD V3 + ERD V3 ### ⚡️⚡ 재화 시스템 추가 + - 출석, 게임 승패에 연관해 재화 시스템 추가 -attendance + attendance ### ⚡️⚡ 상점, 아이템 서비스 추가 + - 유저 요구사항을 반영한 기능 확장 -스크린샷 2023-09-23 오후 11 48 01 -스크린샷 2023-09-23 오후 11 48 18 + 스크린샷 2023-09-23 오후 11 48 01 + 스크린샷 2023-09-23 오후 11 48 18 ### ⚡️⚡ 티어 시스템 추가 + - 랭킹전 활성화를 위한 티어 시스템 추가 -tier + tier ### ⚡️⚡ 관리자 페이지 구현 + - 원활한 운영을 위한 관리자 기능 추가 -admin + admin
@@ -175,28 +224,38 @@ https://42gg.kr/ ### 5기 +
5기 진행 사항
### ⚡️⚡ 토너먼트 개발 + 5th-tournament ### ⚡️⚡ 테스트 커버리지 개선 (2024-03-19 기준) + ### 전체 68% -> 74% + 5th-test-coverage-total ### 단위 테스트 0% -> 30% -5th-test-coverage-unit +5th-test-coverage-unit ### ⚡️⚡ 아키텍처 변경 + ### BEFORE - systemArchitecture     + +systemArchitecture +     + ### AFTER - ![gg-5th-architecture](https://github.com/42organization/42gg.server.dev.v2/assets/33301153/f801e7b5-d579-467b-9ad0-2bfec506dcaa) + +![gg-5th-architecture](https://github.com/42organization/42gg.server.dev.v2/assets/33301153/f801e7b5-d579-467b-9ad0-2bfec506dcaa) ### ⚡️⚡ DB table 구조 변경 + ![image](https://github.com/42organization/42gg.server.dev.v2/assets/33301153/d4c68d74-590c-41db-9c47-0bdd4f249bc3) @@ -229,12 +288,122 @@ https://42gg.kr/ +### 6기 + +
+ 6기 진행 사항 +
+ +### ⚡️⚡ 파티 서비스 개발 + +42party + +### ⚡️⚡ 테스트 커버리지 개선 (2024-04-16 기준) + +### 전체 74% -> 75.9% + +![integrationTest](https://github.com/42organization/42gg.server.dev.v2/assets/79272189/79731062-a8f4-4575-a683-61fa5dd60a15) + +### 단위 테스트 30% -> 36.7% + +![unitTest](https://github.com/42organization/42gg.server.dev.v2/assets/79272189/b0e5055b-9008-40d8-b93a-3b05fdffc710) + +### ⚡️⚡ DB table 구조 변경 + +![image](https://github.com/42organization/42gg.server.dev.v2/assets/79272189/c9c47670-b955-4e34-a589-c498008446f0) + + +
+
+ + + + + + + + + + + + + + + + + + + + + + +
🏓🏓🏓🏓
권기현 @ghyen정승수 @AreSain김정주 @JayJay-Kay 이예슬 @yes-ee
파티 서비스 개발,
테스트 커버리지 개선
팀장, 파티 서비스 개발,
테스트 커버리지 개선
파티 서비스 개발,
테스트 커버리지 개선
파티 서비스 개발,
테스트 커버리지 개선
+ +### 7기 + +
+ 7기 진행 사항 +
+ +### ⚡️⚡ 행사 서비스 개발 + +- 42서울 내 행사를 진행할 수 있는 서비스 개발 +![인덱스](https://github.com/user-attachments/assets/48966d80-337f-42d9-9024-b1f5392a81ab) + +- 행사 개최, 참가, 결과 확인, 개인 프로필 등의 기능을 제공 +![대회목록](https://github.com/user-attachments/assets/cf5fb4b3-bcad-4e89-ab8b-3f798f3cba9f) +![상세보기](https://github.com/user-attachments/assets/f6109e2c-3a93-462c-a899-cfc35989dc20) +![대회 참가](https://github.com/user-attachments/assets/f11b5c89-ebc2-4d2d-91c7-25317d33ad2d) +![프로필](https://github.com/user-attachments/assets/f9b31b71-76f6-4bf0-9b5c-d56446e292a0) + +- 평가 포인트를 티켓으로 환전해 사용해 공식 대회를 참가해 칭호와 업적 등의 보상을 받을 수 있음(현재는 기부만 가능) +![티켓 페이지](https://github.com/user-attachments/assets/fd76a962-1254-4354-a1ff-be93950d75a3) + +### ⚡️⚡ DataFlow + +![AgendaDataFlow](https://github.com/user-attachments/assets/f9fd25ee-d275-41a3-be78-501eba88df5f) + +### ⚡️⚡ DB table 구조 변경 + +![7기ERD](https://github.com/user-attachments/assets/e3d2e431-1154-43d6-8a48-dd2ac2e510a5) + +### ⚡️⚡ 테스트 커버리지 + +### 전체 75.9% -> 76.5% +![테스트 전체](https://github.com/user-attachments/assets/3c567a75-a897-483c-ba89-8c5e9caff210) + +
+
+ + + + + + + + + + + + + + + + + + + +
🏓🏓🏓
정승수 @AreSain박정우 @yhames김지은 @kimjieun0301
팀장, 아젠다 서비스 개발,
테스트 커버리지 개선
아젠다 서비스 개발,
테스트 커버리지 개선
아젠다 서비스 개발,
테스트 커버리지 개선
+ + ## ⚡️ 필요 파일 +
application.yml
다음과 같은 양식의 "application.yml"파일이 "src/main/resources/"경로에 필요합니다. + ``` spring: profiles: diff --git a/build.gradle b/build.gradle index 7233d1bff..8f713003a 100644 --- a/build.gradle +++ b/build.gradle @@ -107,10 +107,14 @@ subprojects { '*Application*', "**/config/*", "**/security/*", + "**/external/*", "**/dto/*", "**/aws/*", "*NotiMailSender*", '*SlackbotService*', + "**/file/*", + "*AwsImageHandler*", + "*SlackbotApiUtils*" ] //커버리지 리포트 생성 @@ -252,6 +256,7 @@ project(':gg-pingpong-api') { implementation project(':gg-utils') implementation project(':gg-auth') implementation project(':gg-recruit-api') + implementation project(':gg-agenda-api') } } @@ -267,6 +272,18 @@ project(':gg-recruit-api') { } } +project(':gg-agenda-api') { + bootJar { enabled = false } + jar { enabled = true } + dependencies { + implementation project(':gg-data') + implementation project(':gg-repo') + implementation project(':gg-admin-repo') + implementation project(':gg-utils') + implementation project(':gg-auth') + } +} + project(':gg-auth') { bootJar { enabled = false } jar { enabled = true } @@ -309,4 +326,3 @@ project(':gg-utils') { dependencies { } } - diff --git a/codecov.yml b/codecov.yml index 3cb47c47c..9fc75e6da 100644 --- a/codecov.yml +++ b/codecov.yml @@ -19,6 +19,7 @@ flags: - gg-auth/src/main/java/gg/auth - gg-utils/src/main/java/gg/utils - gg-recruit-api/src/main/java/gg/recruit/api + - gg-agenda-api/src/main/java/gg/agenda/api integrationTest: paths: - gg-pingpong-api/src/main/java/gg/pingpong/api @@ -28,3 +29,4 @@ flags: - gg-auth/src/main/java/gg/auth - gg-utils/src/main/java/gg/utils - gg-recruit-api/src/main/java/gg/recruit/api + - gg-agenda-api/src/main/java/gg/agenda/api diff --git a/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaAdminRepository.java b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaAdminRepository.java new file mode 100644 index 000000000..2363f2ef6 --- /dev/null +++ b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaAdminRepository.java @@ -0,0 +1,19 @@ +package gg.admin.repo.agenda; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.Agenda; + +@Repository +public interface AgendaAdminRepository extends JpaRepository { + + @Query("SELECT a FROM Agenda a WHERE a.agendaKey = :agendaKey") + Optional findByAgendaKey(UUID agendaKey); + + boolean existsByAgendaKey(UUID issuedFromKey); +} diff --git a/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaAnnouncementAdminRepository.java b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaAnnouncementAdminRepository.java new file mode 100644 index 000000000..23caaa92f --- /dev/null +++ b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaAnnouncementAdminRepository.java @@ -0,0 +1,15 @@ +package gg.admin.repo.agenda; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; + +@Repository +public interface AgendaAnnouncementAdminRepository extends JpaRepository { + + Page findAllByAgenda(Agenda agenda, Pageable pageable); +} diff --git a/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaProfileAdminRepository.java b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaProfileAdminRepository.java new file mode 100644 index 000000000..a8e52007e --- /dev/null +++ b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaProfileAdminRepository.java @@ -0,0 +1,15 @@ +package gg.admin.repo.agenda; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.AgendaProfile; + +@Repository +public interface AgendaProfileAdminRepository extends JpaRepository { + @Query("SELECT a FROM AgendaProfile a WHERE a.intraId = :intraId") + Optional findByIntraId(String intraId); +} diff --git a/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaTeamAdminRepository.java b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaTeamAdminRepository.java new file mode 100644 index 000000000..cd543ca74 --- /dev/null +++ b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaTeamAdminRepository.java @@ -0,0 +1,27 @@ +package gg.admin.repo.agenda; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaTeam; + +@Repository +public interface AgendaTeamAdminRepository extends JpaRepository { + + @Query("SELECT at FROM AgendaTeam at WHERE at.agenda = :agenda") + List findAllByAgenda(Agenda agenda); + + @Query("SELECT at FROM AgendaTeam at WHERE at.agenda = :agenda") + Page findAllByAgenda(Agenda agenda, Pageable pageable); + + @Query("SELECT at FROM AgendaTeam at WHERE at.teamKey = :teamKey") + Optional findByTeamKey(UUID teamKey); +} diff --git a/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaTeamProfileAdminRepository.java b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaTeamProfileAdminRepository.java new file mode 100644 index 000000000..898d9eb91 --- /dev/null +++ b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/AgendaTeamProfileAdminRepository.java @@ -0,0 +1,23 @@ +package gg.admin.repo.agenda; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; + +@Repository +public interface AgendaTeamProfileAdminRepository extends JpaRepository { + + List findAllByAgendaTeamAndIsExistIsTrue(AgendaTeam agendaTeam); + + @Query("SELECT atp FROM AgendaTeamProfile atp WHERE atp.agenda = :agenda AND atp.profile = :agendaProfile " + + "AND atp.isExist = true") + Optional findByAgendaAndProfileAndIsExistTrue(Agenda agenda, AgendaProfile agendaProfile); +} diff --git a/gg-admin-repo/src/main/java/gg/admin/repo/agenda/TicketAdminRepository.java b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/TicketAdminRepository.java new file mode 100644 index 000000000..ed49c744e --- /dev/null +++ b/gg-admin-repo/src/main/java/gg/admin/repo/agenda/TicketAdminRepository.java @@ -0,0 +1,18 @@ +package gg.admin.repo.agenda; + +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; + +@Repository +public interface TicketAdminRepository extends JpaRepository { + Optional findByAgendaProfile(AgendaProfile agendaProfile); + + Page findByAgendaProfile(AgendaProfile agendaProfile, Pageable pageable); +} diff --git a/gg-admin-repo/src/test/resources/application.yml b/gg-admin-repo/src/test/resources/application.yml index f28374ce9..7bad2a46d 100644 --- a/gg-admin-repo/src/test/resources/application.yml +++ b/gg-admin-repo/src/test/resources/application.yml @@ -72,6 +72,9 @@ info: image: defaultUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/small_default.jpeg' itemNotFoundUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/not_found.svg' + web: + coalitionUrl: 'https://api.intra.42.fr/v2/users/{id}/coalitions' + pointHistoryUrl: 'https://api.intra.42.fr/v2/users/{id}/correction_point_historics?sort=-id' constant: allowedMinimalStartDays: 2 diff --git a/gg-agenda-api/build.gradle b/gg-agenda-api/build.gradle new file mode 100644 index 000000000..e8a4bdd9d --- /dev/null +++ b/gg-agenda-api/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' +} + +group 'gg.api' +version '42gg' + +repositories { + mavenCentral() +} + +dependencies { + /* spring */ + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-mail' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + /* StringUtils */ + implementation 'org.apache.commons:commons-lang3:3.12.0' + + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation testFixtures(project(':gg-utils')) +} + +test { + useJUnitPlatform() +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/AgendaAdminController.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/AgendaAdminController.java new file mode 100644 index 000000000..fceba9d3e --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/AgendaAdminController.java @@ -0,0 +1,85 @@ +package gg.agenda.api.admin.agenda.controller; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import gg.agenda.api.admin.agenda.controller.request.AgendaAdminUpdateReqDto; +import gg.agenda.api.admin.agenda.controller.response.AgendaAdminResDto; +import gg.agenda.api.admin.agenda.controller.response.AgendaAdminSimpleResDto; +import gg.agenda.api.admin.agenda.service.AgendaAdminService; +import gg.data.agenda.Agenda; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.exception.custom.InvalidParameterException; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/agenda/admin") +@RequiredArgsConstructor +public class AgendaAdminController { + + private final AgendaAdminService agendaAdminService; + + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Agenda 요청 리스트 조회 성공")}) + @GetMapping("/request/list") + public ResponseEntity> agendaList( + @ModelAttribute @Valid PageRequestDto pageDto) { + int page = pageDto.getPage(); + int size = pageDto.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page agendaRequestList = agendaAdminService.getAgendaRequestList(pageable); + + List agendaDtos = agendaRequestList.stream() + .map(AgendaAdminResDto.MapStruct.INSTANCE::toAgendaAdminResDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + agendaRequestList.getTotalElements(), agendaDtos); + return ResponseEntity.ok(pageResponseDto); + } + + @GetMapping("/list") + public ResponseEntity> agendaSimpleList() { + List agendas = agendaAdminService.getAgendaSimpleList(); + return ResponseEntity.status(HttpStatus.OK).body(agendas); + } + + @ApiResponses(value = {@ApiResponse(responseCode = "204", description = "Agenda 수정 성공"), + @ApiResponse(responseCode = "400", description = "Agenda 수정 요청이 잘못됨"), + @ApiResponse(responseCode = "404", description = "Agenda를 찾을 수 없음"), + @ApiResponse(responseCode = "409", description = "Agenda 지역을 변경할 수 없음"), + @ApiResponse(responseCode = "409", description = "Agenda 팀 제한을 변경할 수 없음"), + @ApiResponse(responseCode = "409", description = "Agenda 팀 인원 제한을 변경할 수 없음")}) + @PostMapping("/request") + public ResponseEntity agendaUpdate(@RequestParam("agenda_key") UUID agendaKey, + @ModelAttribute @Valid AgendaAdminUpdateReqDto agendaDto, + @RequestParam(required = false) MultipartFile agendaPoster) { + if (Objects.nonNull(agendaPoster) && agendaPoster.getSize() > 1024 * 1024 * 2) { // 2MB + throw new InvalidParameterException(AGENDA_POSTER_SIZE_TOO_LARGE); + } + agendaAdminService.updateAgenda(agendaKey, agendaDto, agendaPoster); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/request/AgendaAdminUpdateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/request/AgendaAdminUpdateReqDto.java new file mode 100644 index 000000000..cbb94284c --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/request/AgendaAdminUpdateReqDto.java @@ -0,0 +1,70 @@ +package gg.agenda.api.admin.agenda.controller.request; + +import java.net.URL; +import java.time.LocalDateTime; + +import org.springframework.format.annotation.DateTimeFormat; + +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAdminUpdateReqDto { + + private String agendaTitle; + + private String agendaContent; + + private URL agendaPosterUri; + + private Boolean isOfficial; + + private Boolean isRanking; + + private AgendaStatus agendaStatus; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private LocalDateTime agendaDeadLine; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private LocalDateTime agendaStartTime; + + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private LocalDateTime agendaEndTime; + + private Location agendaLocation; + + private int agendaMinTeam; + + private int agendaMaxTeam; + + private int agendaMinPeople; + + private int agendaMaxPeople; + + @Builder + public AgendaAdminUpdateReqDto(String agendaTitle, String agendaContent, URL agendaPosterUri, Boolean isOfficial, + Boolean isRanking, AgendaStatus agendaStatus, LocalDateTime agendaDeadLine, LocalDateTime agendaStartTime, + LocalDateTime agendaEndTime, Location agendaLocation, int agendaMinTeam, int agendaMaxTeam, + int agendaMinPeople, int agendaMaxPeople) { + this.agendaTitle = agendaTitle; + this.agendaContent = agendaContent; + this.agendaPosterUri = agendaPosterUri; + this.isOfficial = isOfficial; + this.isRanking = isRanking; + this.agendaStatus = agendaStatus; + this.agendaDeadLine = agendaDeadLine; + this.agendaStartTime = agendaStartTime; + this.agendaEndTime = agendaEndTime; + this.agendaLocation = agendaLocation; + this.agendaMinTeam = agendaMinTeam; + this.agendaMaxTeam = agendaMaxTeam; + this.agendaMinPeople = agendaMinPeople; + this.agendaMaxPeople = agendaMaxPeople; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/response/AgendaAdminResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/response/AgendaAdminResDto.java new file mode 100644 index 000000000..c3e0d45f5 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/response/AgendaAdminResDto.java @@ -0,0 +1,105 @@ +package gg.agenda.api.admin.agenda.controller.response; + +import java.net.URL; +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAdminResDto { + + private Long agendaId; + + private UUID agendaKey; + + private String agendaTitle; + + private String agendaContent; + + private String agendaDeadLine; + + private String agendaStartTime; + + private String agendaEndTime; + + private int agendaCurrentTeam; + + private int agendaMaxTeam; + + private int agendaMinTeam; + + private int agendaMinPeople; + + private int agendaMaxPeople; + + private Location agendaLocation; + + private Boolean isRanking; + + private Boolean isOfficial; + + private AgendaStatus agendaStatus; + + private String agendaPosterUrl; + + @Builder + public AgendaAdminResDto(Long agendaId, UUID agendaKey, String agendaTitle, String agendaDeadLine, + String agendaStartTime, String agendaEndTime, int agendaCurrentTeam, int agendaMaxTeam, int agendaMinPeople, + int agendaMaxPeople, Location agendaLocation, Boolean isRanking, Boolean isOfficial, + AgendaStatus agendaStatus, String agendaPosterUrl, String agendaContent, int agendaMinTeam) { + this.agendaId = agendaId; + this.agendaKey = agendaKey; + this.agendaTitle = agendaTitle; + this.agendaContent = agendaContent; + this.agendaDeadLine = agendaDeadLine; + this.agendaStartTime = agendaStartTime; + this.agendaEndTime = agendaEndTime; + this.agendaCurrentTeam = agendaCurrentTeam; + this.agendaMaxTeam = agendaMaxTeam; + this.agendaMinTeam = agendaMinTeam; + this.agendaMinPeople = agendaMinPeople; + this.agendaMaxPeople = agendaMaxPeople; + this.agendaLocation = agendaLocation; + this.isRanking = isRanking; + this.isOfficial = isOfficial; + this.agendaStatus = agendaStatus; + this.agendaPosterUrl = agendaPosterUrl; + } + + @Mapper + public interface MapStruct { + + AgendaAdminResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaAdminResDto.MapStruct.class); + + @Mapping(target = "agendaId", source = "id") + @Mapping(target = "agendaKey", source = "agendaKey") + @Mapping(target = "agendaTitle", source = "title") + @Mapping(target = "agendaContent", source = "content") + @Mapping(target = "agendaDeadLine", source = "deadline") + @Mapping(target = "agendaStartTime", source = "startTime") + @Mapping(target = "agendaEndTime", source = "endTime") + @Mapping(target = "agendaCurrentTeam", source = "currentTeam") + @Mapping(target = "agendaMaxTeam", source = "maxTeam") + @Mapping(target = "agendaMinTeam", source = "minTeam") + @Mapping(target = "agendaMinPeople", source = "minPeople") + @Mapping(target = "agendaMaxPeople", source = "maxPeople") + @Mapping(target = "agendaLocation", source = "location") + @Mapping(target = "isRanking", source = "isRanking") + @Mapping(target = "isOfficial", source = "isOfficial") + @Mapping(target = "agendaStatus", source = "status") + @Mapping(target = "agendaPosterUrl", source = "posterUri") + AgendaAdminResDto toAgendaAdminResDto(Agenda agenda); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/response/AgendaAdminSimpleResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/response/AgendaAdminSimpleResDto.java new file mode 100644 index 000000000..8a2348929 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/controller/response/AgendaAdminSimpleResDto.java @@ -0,0 +1,38 @@ +package gg.agenda.api.admin.agenda.controller.response; + +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.Agenda; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAdminSimpleResDto { + + UUID agendaKey; + + String agendaTitle; + + @Builder + public AgendaAdminSimpleResDto(UUID agendaKey, String agendaTitle) { + this.agendaKey = agendaKey; + this.agendaTitle = agendaTitle; + } + + @Mapper + public interface MapStruct { + + AgendaAdminSimpleResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaAdminSimpleResDto.MapStruct.class); + + @Mapping(target = "agendaKey", source = "agendaKey") + @Mapping(target = "agendaTitle", source = "title") + AgendaAdminSimpleResDto toDto(Agenda agenda); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/service/AgendaAdminService.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/service/AgendaAdminService.java new file mode 100644 index 000000000..2980c7149 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agenda/service/AgendaAdminService.java @@ -0,0 +1,97 @@ +package gg.agenda.api.admin.agenda.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaTeamAdminRepository; +import gg.agenda.api.admin.agenda.controller.request.AgendaAdminUpdateReqDto; +import gg.agenda.api.admin.agenda.controller.response.AgendaAdminSimpleResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaPosterImage; +import gg.data.agenda.AgendaTeam; +import gg.repo.agenda.AgendaPosterImageRepository; +import gg.utils.exception.custom.BusinessException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.file.handler.ImageHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AgendaAdminService { + + private final AgendaAdminRepository agendaAdminRepository; + + private final AgendaTeamAdminRepository agendaTeamAdminRepository; + + private final AgendaPosterImageRepository agendaPosterImageRepository; + + private final ImageHandler imageHandler; + + @Value("${info.image.defaultUrl}") + private String defaultUri; + + @Transactional(readOnly = true) + public Page getAgendaRequestList(Pageable pageable) { + return agendaAdminRepository.findAll(pageable); + } + + @Transactional + public void updateAgenda(UUID agendaKey, AgendaAdminUpdateReqDto agendaDto, MultipartFile agendaPoster) { + Agenda agenda = agendaAdminRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + List teams = agendaTeamAdminRepository.findAllByAgenda(agenda); + + try { + if (Objects.nonNull(agendaPoster) && agendaPoster.getSize() > 0) { + URL storedUrl = imageHandler.uploadImageOrDefault(agendaPoster, agenda.getTitle(), defaultUri); + agenda.updatePosterUri(storedUrl.toString()); + Optional posterImage = agendaPosterImageRepository.findByAgendaIdAndIsCurrentTrue( + agenda.getId()); + posterImage.ifPresent(AgendaPosterImage::updateIsCurrentToFalse); + agendaPosterImageRepository.save(new AgendaPosterImage(agenda.getId(), storedUrl.toString())); + } + } catch (IOException e) { + log.error("Failed to upload image", e); + throw new BusinessException(AGENDA_CREATE_FAILED); + } + + agenda.updateInformation(agendaDto.getAgendaTitle(), agendaDto.getAgendaContent()); + agenda.updateIsOfficial(agendaDto.getIsOfficial()); + agenda.updateIsRanking(agendaDto.getIsRanking()); + agenda.updateAgendaStatus(agendaDto.getAgendaStatus()); + agenda.updateSchedule(agendaDto.getAgendaDeadLine(), agendaDto.getAgendaStartTime(), + agendaDto.getAgendaEndTime()); + agenda.updateLocation(agendaDto.getAgendaLocation(), teams); + agenda.updateAgendaCapacity(agendaDto.getAgendaMinTeam(), agendaDto.getAgendaMaxTeam(), teams); + agenda.updateAgendaTeamCapacity(agendaDto.getAgendaMinPeople(), agendaDto.getAgendaMaxPeople(), teams); + } + + @Transactional(readOnly = true) + public List getAgendaSimpleList() { + return agendaAdminRepository.findAll().stream() + .map(AgendaAdminSimpleResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public Optional getAgenda(UUID agendaKey) { + return agendaAdminRepository.findByAgendaKey(agendaKey); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/AgendaAnnouncementAdminController.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/AgendaAnnouncementAdminController.java new file mode 100644 index 000000000..1d02ab5ad --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/AgendaAnnouncementAdminController.java @@ -0,0 +1,61 @@ +package gg.agenda.api.admin.agendaannouncement.controller; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.admin.agendaannouncement.controller.request.AgendaAnnouncementAdminUpdateReqDto; +import gg.agenda.api.admin.agendaannouncement.controller.response.AgendaAnnouncementAdminResDto; +import gg.agenda.api.admin.agendaannouncement.service.AgendaAnnouncementAdminService; +import gg.data.agenda.AgendaAnnouncement; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/agenda/admin/announcement") +@RequiredArgsConstructor +public class AgendaAnnouncementAdminController { + + private final AgendaAnnouncementAdminService agendaAnnouncementAdminService; + + @GetMapping() + public ResponseEntity> agendaAnnouncementList( + @RequestParam("agenda_key") UUID agendaKey, @ModelAttribute @Valid PageRequestDto pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page agendaAnnouncementList = agendaAnnouncementAdminService + .getAgendaAnnouncementList(agendaKey, pageable); + + List announceDtos = agendaAnnouncementList.stream() + .map(AgendaAnnouncementAdminResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + agendaAnnouncementList.getTotalElements(), announceDtos); + return ResponseEntity.ok(pageResponseDto); + } + + @PatchMapping() + public ResponseEntity updateAgendaAnnouncement( + @RequestBody @Valid AgendaAnnouncementAdminUpdateReqDto updateReqDto) { + agendaAnnouncementAdminService.updateAgendaAnnouncement(updateReqDto); + return ResponseEntity.noContent().build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/request/AgendaAnnouncementAdminUpdateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/request/AgendaAnnouncementAdminUpdateReqDto.java new file mode 100644 index 000000000..667875730 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/request/AgendaAnnouncementAdminUpdateReqDto.java @@ -0,0 +1,38 @@ +package gg.agenda.api.admin.agendaannouncement.controller.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.constraints.Length; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAnnouncementAdminUpdateReqDto { + + @NotNull + private Long id; + + @NotBlank + @Length(max = 50) + private String title; + + @NotBlank + @Length(max = 1000) + private String content; + + @NotNull + private Boolean isShow; + + @Builder + public AgendaAnnouncementAdminUpdateReqDto(Long id, String title, String content, Boolean isShow) { + this.id = id; + this.title = title; + this.content = content; + this.isShow = isShow; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/response/AgendaAnnouncementAdminResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/response/AgendaAnnouncementAdminResDto.java new file mode 100644 index 000000000..d70d1cdbe --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/controller/response/AgendaAnnouncementAdminResDto.java @@ -0,0 +1,45 @@ +package gg.agenda.api.admin.agendaannouncement.controller.response; + +import java.time.LocalDateTime; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.AgendaAnnouncement; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAnnouncementAdminResDto { + + private Long id; + + private String title; + + private String content; + + private Boolean isShow; + + private LocalDateTime createdAt; + + @Builder + public AgendaAnnouncementAdminResDto(Long id, String title, String content, Boolean isShow, + LocalDateTime createdAt) { + this.id = id; + this.title = title; + this.content = content; + this.isShow = isShow; + this.createdAt = createdAt; + } + + @Mapper + public interface MapStruct { + + MapStruct INSTANCE = Mappers.getMapper(MapStruct.class); + + AgendaAnnouncementAdminResDto toDto(AgendaAnnouncement announcement); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/service/AgendaAnnouncementAdminService.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/service/AgendaAnnouncementAdminService.java new file mode 100644 index 000000000..7b4488a2b --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaannouncement/service/AgendaAnnouncementAdminService.java @@ -0,0 +1,42 @@ +package gg.agenda.api.admin.agendaannouncement.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaAnnouncementAdminRepository; +import gg.agenda.api.admin.agendaannouncement.controller.request.AgendaAnnouncementAdminUpdateReqDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AgendaAnnouncementAdminService { + + private final AgendaAdminRepository agendaAdminRepository; + + private final AgendaAnnouncementAdminRepository agendaAnnouncementAdminRepository; + + @Transactional(readOnly = true) + public Page getAgendaAnnouncementList(UUID agendaKey, Pageable pageable) { + Agenda agenda = agendaAdminRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + return agendaAnnouncementAdminRepository.findAllByAgenda(agenda, pageable); + } + + @Transactional + public void updateAgendaAnnouncement(AgendaAnnouncementAdminUpdateReqDto updateReqDto) { + AgendaAnnouncement announcement = agendaAnnouncementAdminRepository.findById(updateReqDto.getId()) + .orElseThrow(() -> new NotExistException(AGENDA_ANNOUNCEMENT_NOT_FOUND)); + announcement.updateByAdmin(updateReqDto.getTitle(), updateReqDto.getContent(), updateReqDto.getIsShow()); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/controller/AgendaProfileAdminController.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/controller/AgendaProfileAdminController.java new file mode 100644 index 000000000..9625759c4 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/controller/AgendaProfileAdminController.java @@ -0,0 +1,38 @@ +package gg.agenda.api.admin.agendaprofile.controller; + +import javax.validation.Valid; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.admin.agendaprofile.controller.request.AgendaProfileChangeAdminReqDto; +import gg.agenda.api.admin.agendaprofile.service.AgendaProfileAdminService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda/admin/profile") +public class AgendaProfileAdminController { + private final AgendaProfileAdminService agendaProfileAdminService; + + /** + * 관리자 개인 프로필 변경 API + * + * @param intraId 수정할 사용자의 intra_id + * @param reqDto 변경할 프로필 정보 + * @return HTTP 상태 코드와 빈 응답 + */ + @PatchMapping + public ResponseEntity agendaProfileModify( + @RequestParam String intraId, + @RequestBody @Valid AgendaProfileChangeAdminReqDto reqDto) { + agendaProfileAdminService.modifyAgendaProfile(intraId, reqDto); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} + diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/controller/request/AgendaProfileChangeAdminReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/controller/request/AgendaProfileChangeAdminReqDto.java new file mode 100644 index 000000000..d5dcf76e8 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/controller/request/AgendaProfileChangeAdminReqDto.java @@ -0,0 +1,33 @@ +package gg.agenda.api.admin.agendaprofile.controller.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import org.hibernate.validator.constraints.URL; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class AgendaProfileChangeAdminReqDto { + + @NotBlank + @Size(max = 50, message = "userContent의 길이가 허용된 범위를 초과합니다.") + private String userContent; + + @URL + @Size(max = 200, message = "userGithub의 길이가 허용된 범위를 초과합니다.") + private String userGithub; + + @NotBlank + private String userLocation; + + @Builder + public AgendaProfileChangeAdminReqDto(String userContent, String userGithub, String userLocation) { + this.userContent = userContent; + this.userGithub = userGithub; + this.userLocation = userLocation; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/service/AgendaProfileAdminService.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/service/AgendaProfileAdminService.java new file mode 100644 index 000000000..b5718a539 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendaprofile/service/AgendaProfileAdminService.java @@ -0,0 +1,35 @@ +package gg.agenda.api.admin.agendaprofile.service; + +import static gg.utils.exception.ErrorCode.*; + +import javax.transaction.Transactional; + +import org.springframework.stereotype.Service; + +import gg.admin.repo.agenda.AgendaProfileAdminRepository; +import gg.agenda.api.admin.agendaprofile.controller.request.AgendaProfileChangeAdminReqDto; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Location; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AgendaProfileAdminService { + private final AgendaProfileAdminRepository agendaProfileAdminRepository; + + /** + * AgendaProfile 변경 메서드 + * @param intraId 로그인한 유저의 id + * @param reqDto 변경할 프로필 정보 + */ + @Transactional + public void modifyAgendaProfile(String intraId, AgendaProfileChangeAdminReqDto reqDto) { + AgendaProfile agendaProfile = agendaProfileAdminRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + agendaProfile.updateProfileAdmin(reqDto.getUserContent(), reqDto.getUserGithub(), + Location.valueOfLocation(reqDto.getUserLocation())); + agendaProfileAdminRepository.save(agendaProfile); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/AgendaTeamAdminController.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/AgendaTeamAdminController.java new file mode 100644 index 000000000..9beaa7816 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/AgendaTeamAdminController.java @@ -0,0 +1,78 @@ +package gg.agenda.api.admin.agendateam.controller; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamUpdateDto; +import gg.agenda.api.admin.agendateam.controller.response.AgendaTeamDetailResDto; +import gg.agenda.api.admin.agendateam.controller.response.AgendaTeamResDto; +import gg.agenda.api.admin.agendateam.service.AgendaTeamAdminService; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/agenda/admin/team") +@RequiredArgsConstructor +public class AgendaTeamAdminController { + + private final AgendaTeamAdminService agendaTeamAdminService; + + @GetMapping("/list") + public ResponseEntity> agendaTeamList(@RequestParam("agenda_key") UUID agendaKey, + @ModelAttribute @Valid PageRequestDto pageRequestDto) { + int page = pageRequestDto.getPage(); + int size = pageRequestDto.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page agendaTeamList = agendaTeamAdminService.getAgendaTeamList(agendaKey, pageable); + + List agendaTeamResDtos = agendaTeamList.stream() + .map(AgendaTeamResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + agendaTeamList.getTotalElements(), agendaTeamResDtos); + return ResponseEntity.ok(pageResponseDto); + } + + @GetMapping + public ResponseEntity agendaTeamDetail(@RequestParam("team_key") UUID agendaTeamKey) { + AgendaTeam agendaTeam = agendaTeamAdminService.getAgendaTeamByTeamKey(agendaTeamKey); + List participants = agendaTeamAdminService.getAgendaProfileListByAgendaTeam(agendaTeam); + AgendaTeamDetailResDto agendaTeamDetailResDto = AgendaTeamDetailResDto.MapStruct.INSTANCE + .toDto(agendaTeam, participants); + return ResponseEntity.ok(agendaTeamDetailResDto); + } + + @PatchMapping + public ResponseEntity agendaTeamUpdate(@RequestBody @Valid AgendaTeamUpdateDto agendaTeamUpdateDto) { + agendaTeamAdminService.updateAgendaTeam(agendaTeamUpdateDto); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + @PatchMapping("/cancel") + public ResponseEntity agendaTeamCancel(@RequestParam("team_key") UUID teamKey) { + AgendaTeam agendaTeam = agendaTeamAdminService.getAgendaTeamByTeamKey(teamKey); + agendaTeamAdminService.cancelAgendaTeam(agendaTeam); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/request/AgendaTeamMateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/request/AgendaTeamMateReqDto.java new file mode 100644 index 000000000..f994c5e15 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/request/AgendaTeamMateReqDto.java @@ -0,0 +1,21 @@ +package gg.agenda.api.admin.agendateam.controller.request; + +import javax.validation.constraints.NotNull; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaTeamMateReqDto { + + @NotNull + private String intraId; + + @Builder + public AgendaTeamMateReqDto(String intraId) { + this.intraId = intraId; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/request/AgendaTeamUpdateDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/request/AgendaTeamUpdateDto.java new file mode 100644 index 000000000..be8d494c4 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/request/AgendaTeamUpdateDto.java @@ -0,0 +1,68 @@ +package gg.agenda.api.admin.agendateam.controller.request; + +import java.util.List; +import java.util.UUID; + +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaTeamUpdateDto { + + @NotNull + private UUID teamKey; + + @NotBlank + @Size(max = 30) + private String teamName; + + @NotBlank + private String teamContent; + + @NotNull + private AgendaTeamStatus teamStatus; + + @NotNull + private Boolean teamIsPrivate; + + @NotNull + private Location teamLocation; + + @NotNull + private String teamAward; + + @Min(1) + @Max(1000) + private Integer teamAwardPriority; + + @Valid + @NotNull + private List teamMates; + + @Builder + public AgendaTeamUpdateDto(UUID teamKey, String teamName, String teamContent, AgendaTeamStatus teamStatus, + Location teamLocation, String teamAward, Integer teamAwardPriority, Boolean teamIsPrivate, + List teamMates) { + this.teamKey = teamKey; + this.teamName = teamName; + this.teamContent = teamContent; + this.teamStatus = teamStatus; + this.teamAward = teamAward; + this.teamAwardPriority = teamAwardPriority; + this.teamIsPrivate = teamIsPrivate; + this.teamLocation = teamLocation; + this.teamMates = teamMates; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamDetailResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamDetailResDto.java new file mode 100644 index 000000000..784595040 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamDetailResDto.java @@ -0,0 +1,73 @@ +package gg.agenda.api.admin.agendateam.controller.response; + +import java.util.List; +import java.util.stream.Collectors; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaTeamDetailResDto { + + private String teamName; + + private String teamContent; + + private String teamLeaderIntraId; + + private String teamStatus; + + private String teamAward; + + private int teamAwardPriority; + + private boolean teamIsPrivate; + + private List teamMates; + + @Builder + public AgendaTeamDetailResDto(String teamName, String teamLeaderIntraId, String teamStatus, String teamAward, + int teamAwardPriority, boolean teamIsPrivate, List teamMates, String teamContent) { + this.teamName = teamName; + this.teamContent = teamContent; + this.teamLeaderIntraId = teamLeaderIntraId; + this.teamStatus = teamStatus; + this.teamAward = teamAward; + this.teamAwardPriority = teamAwardPriority; + this.teamIsPrivate = teamIsPrivate; + this.teamMates = teamMates; + } + + @Mapper + public interface MapStruct { + + AgendaTeamDetailResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaTeamDetailResDto.MapStruct.class); + + @Mapping(target = "teamName", source = "team.name") + @Mapping(target = "teamContent", source = "team.content") + @Mapping(target = "teamLeaderIntraId", source = "team.leaderIntraId") + @Mapping(target = "teamStatus", source = "team.status") + @Mapping(target = "teamAward", source = "team.award") + @Mapping(target = "teamAwardPriority", source = "team.awardPriority") + @Mapping(target = "teamIsPrivate", source = "team.isPrivate") + @Mapping(target = "teamMates", source = "teamMates", qualifiedByName = "toAgendaProfileResDtoList") + AgendaTeamDetailResDto toDto(AgendaTeam team, List teamMates); + + @Named("toAgendaProfileResDtoList") + default List toAgendaProfileResDtoList(List teamMates) { + return teamMates.stream() + .map(AgendaTeamMateResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + } + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamMateResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamMateResDto.java new file mode 100644 index 000000000..fbf4d320f --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamMateResDto.java @@ -0,0 +1,37 @@ +package gg.agenda.api.admin.agendateam.controller.response; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Coalition; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaTeamMateResDto { + + private String intraId; + + private Coalition coalition; + + @Builder + public AgendaTeamMateResDto(String intraId, Coalition coalition) { + this.intraId = intraId; + this.coalition = coalition; + } + + @Mapper + public interface MapStruct { + + AgendaTeamMateResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaTeamMateResDto.MapStruct.class); + + @Mapping(target = "intraId", source = "intraId") + @Mapping(target = "coalition", source = "coalition") + AgendaTeamMateResDto toDto(AgendaProfile profile); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamResDto.java new file mode 100644 index 000000000..850b2e1b6 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/controller/response/AgendaTeamResDto.java @@ -0,0 +1,73 @@ +package gg.agenda.api.admin.agendateam.controller.response; + +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaTeamResDto { + + private String teamName; + + private String teamContent; + + private String teamStatus; + + private boolean teamIsPrivate; + + private String teamLeaderIntraId; + + private int teamMateCount; + + private UUID teamKey; + + private String teamAward; + + private Location teamLocation; + + private Integer teamAwardPriority; + + @Builder + public AgendaTeamResDto(String teamName, String teamStatus, boolean teamIsPrivate, String teamLeaderIntraId, + int teamMateCount, UUID teamKey, String teamAward, Integer teamAwardPriority, String teamContent, + Location teamLocation) { + this.teamName = teamName; + this.teamContent = teamContent; + this.teamStatus = teamStatus; + this.teamIsPrivate = teamIsPrivate; + this.teamLeaderIntraId = teamLeaderIntraId; + this.teamMateCount = teamMateCount; + this.teamKey = teamKey; + this.teamAward = teamAward; + this.teamAwardPriority = teamAwardPriority; + this.teamLocation = teamLocation; + } + + @Mapper + public interface MapStruct { + + AgendaTeamResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaTeamResDto.MapStruct.class); + + @Mapping(target = "teamName", source = "name") + @Mapping(target = "teamContent", source = "content") + @Mapping(target = "teamStatus", source = "status") + @Mapping(target = "teamIsPrivate", source = "isPrivate") + @Mapping(target = "teamLeaderIntraId", source = "leaderIntraId") + @Mapping(target = "teamMateCount", source = "mateCount") + @Mapping(target = "teamKey", source = "teamKey") + @Mapping(target = "teamLocation", source = "location") + @Mapping(target = "teamAward", source = "award", defaultValue = "AgendaTeam.DEFAULT_AWARD") + @Mapping(target = "teamAwardPriority", source = "awardPriority", defaultValue = "0") + AgendaTeamResDto toDto(AgendaTeam agendaTeam); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/service/AgendaTeamAdminService.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/service/AgendaTeamAdminService.java new file mode 100644 index 000000000..7fd7cde5f --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/agendateam/service/AgendaTeamAdminService.java @@ -0,0 +1,115 @@ +package gg.agenda.api.admin.agendateam.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaProfileAdminRepository; +import gg.admin.repo.agenda.AgendaTeamAdminRepository; +import gg.admin.repo.agenda.AgendaTeamProfileAdminRepository; +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamMateReqDto; +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamUpdateDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.utils.exception.custom.ForbiddenException; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AgendaTeamAdminService { + + private final AgendaAdminRepository agendaAdminRepository; + + private final AgendaTeamAdminRepository agendaTeamAdminRepository; + + private final AgendaProfileAdminRepository agendaProfileAdminRepository; + + private final AgendaTeamProfileAdminRepository agendaTeamProfileAdminRepository; + + @Transactional(readOnly = true) + public Page getAgendaTeamList(UUID agendaKey, Pageable pageable) { + Agenda agenda = agendaAdminRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + return agendaTeamAdminRepository.findAllByAgenda(agenda, pageable); + } + + @Transactional(readOnly = true) + public AgendaTeam getAgendaTeamByTeamKey(UUID teamKey) { + return agendaTeamAdminRepository.findByTeamKey(teamKey) + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + } + + @Transactional(readOnly = true) + public List getAgendaProfileListByAgendaTeam(AgendaTeam agendaTeam) { + return agendaTeamProfileAdminRepository.findAllByAgendaTeamAndIsExistIsTrue(agendaTeam).stream() + .map(AgendaTeamProfile::getProfile).collect(Collectors.toList()); + } + + @Transactional + public void updateAgendaTeam(AgendaTeamUpdateDto agendaTeamUpdateDto) { + AgendaTeam team = agendaTeamAdminRepository.findByTeamKey(agendaTeamUpdateDto.getTeamKey()) + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + List profiles = agendaTeamProfileAdminRepository.findAllByAgendaTeamAndIsExistIsTrue(team); + List updatedTeamMates = agendaTeamUpdateDto.getTeamMates().stream() + .map(AgendaTeamMateReqDto::getIntraId) + .collect(Collectors.toList()); + List currentTeamMates = profiles.stream() + .map(profile -> profile.getProfile().getIntraId()) + .collect(Collectors.toList()); + + // AgendaTeam 정보 변경 + team.updateTeamAdmin(agendaTeamUpdateDto.getTeamName(), agendaTeamUpdateDto.getTeamContent(), + agendaTeamUpdateDto.getTeamIsPrivate(), agendaTeamUpdateDto.getTeamStatus()); + team.getAgenda().adminConfirmTeam(team.getStatus()); + team.updateLocation(agendaTeamUpdateDto.getTeamLocation(), profiles); + team.acceptAward(agendaTeamUpdateDto.getTeamAward(), agendaTeamUpdateDto.getTeamAwardPriority()); + + // AgendaTeam 팀원 내보내기 + profiles.stream().filter(profile -> !updatedTeamMates.contains(profile.getProfile().getIntraId())) + .forEach(agentTeamProfile -> { + String intraId = agentTeamProfile.getProfile().getIntraId(); + agentTeamProfile.getAgendaTeam().leaveTeamMateAdmin(intraId); + agentTeamProfile.changeExistFalse(); + }); + + // AgendaTeam 팀원 추가하기 + updatedTeamMates.stream().filter(intraId -> !currentTeamMates.contains(intraId)) + .forEach(intraId -> { + AgendaProfile profile = agendaProfileAdminRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + agendaTeamProfileAdminRepository.findByAgendaAndProfileAndIsExistTrue(team.getAgenda(), profile) + .ifPresent(agendaTeamProfile -> { + throw new ForbiddenException(AGENDA_TEAM_FORBIDDEN); + }); + team.attendTeamAdmin(team.getAgenda()); + AgendaTeamProfile agendaTeamProfile = new AgendaTeamProfile(team, team.getAgenda(), profile); + agendaTeamProfileAdminRepository.save(agendaTeamProfile); + }); + + // 팀장이 없는지 확인하는 로직 + if (!updatedTeamMates.contains(team.getLeaderIntraId())) { + throw new NotExistException(TEAM_LEADER_NOT_FOUND); + } + } + + @Transactional + public void cancelAgendaTeam(AgendaTeam agendaTeam) { + List agendaTeamProfiles = agendaTeamProfileAdminRepository + .findAllByAgendaTeamAndIsExistIsTrue(agendaTeam); + agendaTeamProfiles.forEach(AgendaTeamProfile::changeExistFalse); + Agenda agenda = agendaTeam.getAgenda(); + agenda.adminCancelTeam(agendaTeam.getStatus()); + agendaTeam.adminCancelTeam(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/TicketAdminController.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/TicketAdminController.java new file mode 100644 index 000000000..73d3675b9 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/TicketAdminController.java @@ -0,0 +1,107 @@ +package gg.agenda.api.admin.ticket.controller; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.admin.agenda.service.AgendaAdminService; +import gg.agenda.api.admin.ticket.controller.request.TicketAddAdminReqDto; +import gg.agenda.api.admin.ticket.controller.request.TicketChangeAdminReqDto; +import gg.agenda.api.admin.ticket.controller.response.TicketAddAdminResDto; +import gg.agenda.api.admin.ticket.controller.response.TicketFindResDto; +import gg.agenda.api.admin.ticket.service.TicketAdminFindService; +import gg.agenda.api.admin.ticket.service.TicketAdminService; +import gg.data.agenda.Agenda; +import gg.data.agenda.Ticket; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda/admin/ticket") +public class TicketAdminController { + private final TicketAdminService ticketAdminService; + private final TicketAdminFindService ticketAdminFindService; + private final AgendaAdminService agendaAdminService; + + /** + * 티켓 설정 추가 + * @param intraId 사용자 정보 + */ + @PostMapping + public ResponseEntity ticketAdminAdd(@RequestParam String intraId, + @RequestBody TicketAddAdminReqDto ticketAddAdminReqDto) { + Long ticketId = ticketAdminService.addTicket(intraId, ticketAddAdminReqDto); + TicketAddAdminResDto ticketAddAdminResDto = new TicketAddAdminResDto(ticketId); + return ResponseEntity.status(HttpStatus.CREATED).body(ticketAddAdminResDto); + } + + /** + * 티켓 변경(관리자) API + * + * @param ticketId 수정할 ticketId + * @param ticketChangeAdminReqDto 변경할 프로필 정보 + * @return HTTP 상태 코드와 빈 응답 + */ + @PatchMapping + public ResponseEntity agendaProfileModify( + @RequestParam Long ticketId, + @RequestBody @Valid TicketChangeAdminReqDto ticketChangeAdminReqDto) { + ticketAdminService.modifyTicket(ticketId, ticketChangeAdminReqDto); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + /** + * 티켓 목록 조회하는 메서드 + * @param pageRequest 페이지네이션 요청 정보 + */ + @GetMapping("/list/{intraId}") + public ResponseEntity> getTicketList( + @PathVariable String intraId, @ModelAttribute @Valid PageRequestDto pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page ticketList = ticketAdminFindService.findTicket(intraId, pageable); + + List ticketFindResDto = ticketList.stream() + .map(ticket -> { + TicketFindResDto dto = new TicketFindResDto(ticket); + + if (dto.getIssuedFromKey() != null) { + Agenda agendaIssuedFrom = agendaAdminService.getAgenda(dto.getIssuedFromKey()).orElse(null); + dto.changeIssuedFrom(agendaIssuedFrom); + } + + if (dto.getUsedToKey() != null) { + Agenda agendaUsedTo = agendaAdminService.getAgenda(dto.getUsedToKey()).orElse(null); + dto.changeUsedTo(agendaUsedTo); + } + + return dto; + }) + .collect(Collectors.toList()); + + PageResponseDto pageResponseDto = PageResponseDto.of( + ticketList.getTotalElements(), ticketFindResDto); + return ResponseEntity.ok(pageResponseDto); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/request/TicketAddAdminReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/request/TicketAddAdminReqDto.java new file mode 100644 index 000000000..d931522a5 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/request/TicketAddAdminReqDto.java @@ -0,0 +1,19 @@ +package gg.agenda.api.admin.ticket.controller.request; + +import java.util.UUID; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TicketAddAdminReqDto { + + private UUID issuedFromKey; + + @Builder + public TicketAddAdminReqDto(UUID issuedFromKey) { + this.issuedFromKey = issuedFromKey; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/request/TicketChangeAdminReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/request/TicketChangeAdminReqDto.java new file mode 100644 index 000000000..e809d7b51 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/request/TicketChangeAdminReqDto.java @@ -0,0 +1,36 @@ +package gg.agenda.api.admin.ticket.controller.request; + +import java.time.LocalDateTime; +import java.util.UUID; + +import org.springframework.format.annotation.DateTimeFormat; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TicketChangeAdminReqDto { + private UUID issuedFromKey; + private UUID usedToKey; + private Boolean isApproved; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private LocalDateTime approvedAt; + private Boolean isUsed; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + private LocalDateTime usedAt; + + @Builder + public TicketChangeAdminReqDto(UUID issuedFromKey, UUID usedToKey, Boolean isApproved, + LocalDateTime approvedAt, + Boolean isUsed, LocalDateTime usedAt) { + this.issuedFromKey = issuedFromKey; + this.usedToKey = usedToKey; + this.isApproved = isApproved; + this.approvedAt = approvedAt; + this.isUsed = isUsed; + this.usedAt = usedAt; + + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/response/TicketAddAdminResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/response/TicketAddAdminResDto.java new file mode 100644 index 000000000..cf412b27d --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/response/TicketAddAdminResDto.java @@ -0,0 +1,15 @@ +package gg.agenda.api.admin.ticket.controller.response; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class TicketAddAdminResDto { + private Long ticketId; + + public TicketAddAdminResDto(Long ticketId) { + this.ticketId = ticketId; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/response/TicketFindResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/response/TicketFindResDto.java new file mode 100644 index 000000000..9be123d29 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/controller/response/TicketFindResDto.java @@ -0,0 +1,61 @@ +package gg.agenda.api.admin.ticket.controller.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +import gg.data.agenda.Agenda; +import gg.data.agenda.Ticket; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TicketFindResDto { + private Long ticketId; + private LocalDateTime createdAt; + private String issuedFrom; + private UUID issuedFromKey; + private String usedTo; + private UUID usedToKey; + private Boolean isApproved; + private LocalDateTime approvedAt; + private Boolean isUsed; + private LocalDateTime usedAt; + + public TicketFindResDto(Ticket ticket) { + this.ticketId = ticket.getId(); + this.createdAt = ticket.getCreatedAt(); + this.issuedFrom = "42Intra"; + this.issuedFromKey = ticket.getIssuedFrom(); + if (ticket.getIsApproved()) { + this.usedTo = "NotUsed"; + } else { + this.usedTo = "NotApproved"; + } + this.usedToKey = ticket.getUsedTo(); + this.isApproved = ticket.getIsApproved(); + this.approvedAt = ticket.getApprovedAt(); + this.isUsed = ticket.getIsUsed(); + this.usedAt = ticket.getUsedAt(); + } + + public void changeIssuedFrom(Agenda agenda) { + if (agenda == null) { + this.issuedFrom = "42Intra"; + return; + } + this.issuedFrom = agenda.getTitle(); + } + + public void changeUsedTo(Agenda agenda) { + if (agenda == null && !this.isApproved) { + this.usedTo = "NotApproved"; + return; + } + if (agenda == null) { + this.usedTo = "NotUsed"; + return; + } + this.usedTo = agenda.getTitle(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/service/TicketAdminFindService.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/service/TicketAdminFindService.java new file mode 100644 index 000000000..243a146e1 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/service/TicketAdminFindService.java @@ -0,0 +1,31 @@ +package gg.agenda.api.admin.ticket.service; + +import static gg.utils.exception.ErrorCode.*; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.TicketAdminRepository; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; +import gg.repo.agenda.AgendaProfileRepository; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class TicketAdminFindService { + private final AgendaProfileRepository agendaProfileRepository; + private final TicketAdminRepository ticketAdminRepository; + private final AgendaAdminRepository agendaAdminRepository; + + @Transactional(readOnly = true) + public Page findTicket(String intraId, Pageable pageable) { + AgendaProfile agendaProfile = agendaProfileRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + return ticketAdminRepository.findByAgendaProfile(agendaProfile, pageable); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/service/TicketAdminService.java b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/service/TicketAdminService.java new file mode 100644 index 000000000..5502443b0 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/admin/ticket/service/TicketAdminService.java @@ -0,0 +1,79 @@ +package gg.agenda.api.admin.ticket.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.Objects; +import java.util.UUID; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaProfileAdminRepository; +import gg.admin.repo.agenda.TicketAdminRepository; +import gg.agenda.api.admin.ticket.controller.request.TicketAddAdminReqDto; +import gg.agenda.api.admin.ticket.controller.request.TicketChangeAdminReqDto; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class TicketAdminService { + + private final TicketAdminRepository ticketAdminRepository; + private final AgendaProfileAdminRepository agendaProfileAdminRepository; + private final AgendaAdminRepository agendaAdminRepository; + + /** + * 티켓 설정 추가 + * @param intraId 사용자 정보 + */ + @Transactional + public Long addTicket(String intraId, TicketAddAdminReqDto ticketAddAdminReqDto) { + AgendaProfile profile = agendaProfileAdminRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + UUID issuedFromKey = ticketAddAdminReqDto.getIssuedFromKey(); + + if (isRefundedTicket(issuedFromKey)) { + boolean result = agendaAdminRepository.existsByAgendaKey(issuedFromKey); + if (!result) { + throw new NotExistException(AGENDA_NOT_FOUND); + } + } + + Ticket ticket = Ticket.createAdminTicket(profile, issuedFromKey); + return ticketAdminRepository.save(ticket).getId(); + } + + private boolean isRefundedTicket(UUID issuedFromKey) { + return Objects.nonNull(issuedFromKey); + } + + /** + * AgendaProfile 변경 메서드 + * @param ticketId 로그인한 유저의 id + * @param reqDto 변경할 프로필 정보 + */ + @Transactional + public void modifyTicket(Long ticketId, TicketChangeAdminReqDto reqDto) { + Ticket ticket = ticketAdminRepository.findById(ticketId) + .orElseThrow(() -> new NotExistException(TICKET_NOT_FOUND)); + + UUID issuedFromKey = reqDto.getIssuedFromKey(); + if (Objects.nonNull(issuedFromKey) && !agendaAdminRepository.existsByAgendaKey(issuedFromKey)) { + throw new NotExistException(AGENDA_NOT_FOUND); + } + + UUID usedToKey = reqDto.getUsedToKey(); + if (Objects.nonNull(usedToKey) && !agendaAdminRepository.existsByAgendaKey(usedToKey)) { + throw new NotExistException(AGENDA_NOT_FOUND); + } + + ticket.updateTicketAdmin(reqDto.getIssuedFromKey(), reqDto.getUsedToKey(), + reqDto.getIsApproved(), reqDto.getApprovedAt(), reqDto.getIsUsed(), reqDto.getUsedAt()); + ticketAdminRepository.save(ticket); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/AgendaController.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/AgendaController.java new file mode 100644 index 000000000..5d1957815 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/AgendaController.java @@ -0,0 +1,153 @@ +package gg.agenda.api.user.agenda.controller; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import gg.agenda.api.user.agenda.controller.request.AgendaAwardsReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; +import gg.agenda.api.user.agenda.controller.response.AgendaKeyResDto; +import gg.agenda.api.user.agenda.controller.response.AgendaResDto; +import gg.agenda.api.user.agenda.controller.response.AgendaSimpleResDto; +import gg.agenda.api.user.agenda.service.AgendaService; +import gg.agenda.api.user.agendaannouncement.service.AgendaAnnouncementService; +import gg.agenda.api.utils.AgendaSlackService; +import gg.auth.UserDto; +import gg.auth.argumentresolver.Login; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaTeam; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.exception.custom.InvalidParameterException; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda") +public class AgendaController { + + private final AgendaService agendaService; + private final AgendaSlackService agendaSlackService; + private final AgendaAnnouncementService agendaAnnouncementService; + + @GetMapping + public ResponseEntity agendaDetails(@RequestParam("agenda_key") UUID agendaKey) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + String announcementTitle = agendaAnnouncementService + .findLatestAnnounceTitleByAgendaOrDefault(agenda, ""); + AgendaResDto agendaResDto = AgendaResDto.MapStruct.INSTANCE.toDto(agenda, announcementTitle); + return ResponseEntity.ok(agendaResDto); + } + + /** + * OPEN인데 deadline이 지나지 않은 대회 반환 + */ + @GetMapping("/open") + public ResponseEntity> agendaListOpen() { + List agendaList = agendaService.findOpenAgendaList(); + List agendaSimpleResDtoList = agendaList.stream() + .map(AgendaSimpleResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + return ResponseEntity.ok(agendaSimpleResDtoList); + } + + /** + * OPEN인데 deadline이 지난 대회와 CONFIRM인 대회 반환 + */ + @GetMapping("/confirm") + public ResponseEntity> agendaListConfirm() { + List agendaList = agendaService.findConfirmAgendaList(); + List agendaSimpleResDtoList = agendaList.stream() + .map(AgendaSimpleResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + return ResponseEntity.ok(agendaSimpleResDtoList); + } + + /** + * FINISH 상태인 대회 반환, 페이지네이션 + */ + @GetMapping("/history") + public ResponseEntity> agendaListHistory( + @ModelAttribute @Valid PageRequestDto pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("startTime").ascending()); + + Page agendas = agendaService.findHistoryAgendaList(pageable); + + List agendaSimpleResDtoList = agendas.stream() + .map(AgendaSimpleResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + agendas.getTotalElements(), agendaSimpleResDtoList); + return ResponseEntity.ok(pageResponseDto); + } + + @PostMapping("/request") + public ResponseEntity agendaAdd(@Login @Parameter(hidden = true) UserDto user, + @ModelAttribute @Valid AgendaCreateReqDto agendaCreateReqDto, + @RequestParam(required = false) MultipartFile agendaPoster) { + if (Objects.nonNull(agendaPoster) && agendaPoster.getSize() > 1024 * 1024) { // 1MB + throw new InvalidParameterException(AGENDA_POSTER_SIZE_TOO_LARGE); + } + UUID agendaKey = agendaService.addAgenda(agendaCreateReqDto, agendaPoster, user).getAgendaKey(); + AgendaKeyResDto responseDto = AgendaKeyResDto.builder().agendaKey(agendaKey).build(); + return ResponseEntity.status(HttpStatus.CREATED).body(responseDto); + } + + @PatchMapping("/finish") + public ResponseEntity agendaEndWithAwards(@RequestParam("agenda_key") UUID agendaKey, + @RequestBody @Valid AgendaAwardsReqDto agendaAwardsReqDto, @Login @Parameter(hidden = true) UserDto user) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + agenda.mustModifiedByHost(user.getIntraId()); + if (agenda.getIsRanking()) { + agendaService.awardAgenda(agendaAwardsReqDto, agenda); + } + agendaService.finishAgenda(agenda); + agendaSlackService.slackFinishAgenda(agenda); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + @PatchMapping("/confirm") + public ResponseEntity agendaConfirm(@RequestParam("agenda_key") UUID agendaKey, + @Login @Parameter(hidden = true) UserDto user) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + agenda.mustModifiedByHost(user.getIntraId()); + List failTeam = agendaService.confirmAgendaAndRefundTicketForOpenTeam(agenda); + agendaSlackService.slackConfirmAgenda(agenda); + agendaSlackService.slackCancelByAgendaConfirm(agenda, failTeam); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + @PatchMapping("/cancel") + public ResponseEntity agendaCancel(@RequestParam("agenda_key") UUID agendaKey, + @Login @Parameter(hidden = true) UserDto user) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + agenda.mustModifiedByHost(user.getIntraId()); + agendaService.cancelAgenda(agenda); + agendaSlackService.slackCancelAgenda(agenda); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaAwardsReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaAwardsReqDto.java new file mode 100644 index 000000000..22f8492de --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaAwardsReqDto.java @@ -0,0 +1,26 @@ +package gg.agenda.api.user.agenda.controller.request; + +import java.util.List; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAwardsReqDto { + + @Valid + @NotNull + private List awards; + + @Builder + public AgendaAwardsReqDto(List awards) { + this.awards = awards; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaCreateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaCreateReqDto.java new file mode 100644 index 000000000..44c7c81bb --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaCreateReqDto.java @@ -0,0 +1,124 @@ +package gg.agenda.api.user.agenda.controller.request; + +import java.net.URL; +import java.time.LocalDateTime; + +import javax.validation.constraints.Future; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; +import org.springframework.format.annotation.DateTimeFormat; + +import gg.agenda.api.user.agenda.controller.request.validator.AgendaCapacityValid; +import gg.agenda.api.user.agenda.controller.request.validator.AgendaScheduleValid; +import gg.data.agenda.Agenda; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@AgendaCapacityValid +@AgendaScheduleValid +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaCreateReqDto { + + @NotBlank + private String agendaTitle; + + @NotBlank + private String agendaContent; + + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Future(message = "마감일은 현재 시간 이후여야 합니다.") + private LocalDateTime agendaDeadLine; + + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Future(message = "시작 시간은 현재 시간 이후여야 합니다.") + private LocalDateTime agendaStartTime; + + @NotNull + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Future(message = "종료 시간은 현재 시간 이후여야 합니다.") + private LocalDateTime agendaEndTime; + + @Min(2) + @Max(1000) + private int agendaMinTeam; + + @Min(2) + @Max(1000) + private int agendaMaxTeam; + + @Min(1) + @Max(100) + private int agendaMinPeople; + + @Min(1) + @Max(100) + private int agendaMaxPeople; + + private URL agendaPosterUri; + + @NotNull + private Location agendaLocation; + + @NotNull + private Boolean agendaIsRanking; + + @Builder + public AgendaCreateReqDto(String agendaTitle, String agendaContent, LocalDateTime agendaDeadLine, + LocalDateTime agendaStartTime, LocalDateTime agendaEndTime, int agendaMinTeam, int agendaMaxTeam, + int agendaMinPeople, int agendaMaxPeople, URL agendaPosterUri, Location agendaLocation, + Boolean agendaIsRanking) { + this.agendaTitle = agendaTitle; + this.agendaContent = agendaContent; + this.agendaDeadLine = agendaDeadLine; + this.agendaStartTime = agendaStartTime; + this.agendaEndTime = agendaEndTime; + this.agendaMinTeam = agendaMinTeam; + this.agendaMaxTeam = agendaMaxTeam; + this.agendaMinPeople = agendaMinPeople; + this.agendaMaxPeople = agendaMaxPeople; + this.agendaPosterUri = agendaPosterUri; + this.agendaLocation = agendaLocation; + this.agendaIsRanking = agendaIsRanking; + } + + public void updatePosterUri(URL storedUrl) { + this.agendaPosterUri = storedUrl; + } + + @Mapper + public interface MapStruct { + + AgendaCreateReqDto.MapStruct INSTANCE = Mappers.getMapper(AgendaCreateReqDto.MapStruct.class); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "title", source = "dto.agendaTitle") + @Mapping(target = "content", source = "dto.agendaContent") + @Mapping(target = "deadline", source = "dto.agendaDeadLine") + @Mapping(target = "startTime", source = "dto.agendaStartTime") + @Mapping(target = "endTime", source = "dto.agendaEndTime") + @Mapping(target = "minTeam", source = "dto.agendaMinTeam") + @Mapping(target = "maxTeam", source = "dto.agendaMaxTeam") + @Mapping(target = "currentTeam", constant = "0") + @Mapping(target = "minPeople", source = "dto.agendaMinPeople") + @Mapping(target = "maxPeople", source = "dto.agendaMaxPeople") + @Mapping(target = "hostIntraId", source = "userIntraId") + @Mapping(target = "location", source = "dto.agendaLocation") + @Mapping(target = "isRanking", source = "dto.agendaIsRanking") + @Mapping(target = "status", constant = "OPEN") + @Mapping(target = "isOfficial", constant = "false") + @Mapping(target = "posterUri", source = "dto.agendaPosterUri") + Agenda toEntity(AgendaCreateReqDto dto, String userIntraId); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaTeamAward.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaTeamAward.java new file mode 100644 index 000000000..897ffe39f --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/AgendaTeamAward.java @@ -0,0 +1,38 @@ +package gg.agenda.api.user.agenda.controller.request; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.constraints.Length; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaTeamAward { + + @NotNull + @NotEmpty + private String teamName; + + @NotBlank + @Length(max = 30) + private String awardName; + + @Min(1) + @Max(1000) + private int awardPriority; + + @Builder + public AgendaTeamAward(String teamName, String awardName, int awardPriority) { + this.teamName = teamName; + this.awardName = awardName; + this.awardPriority = awardPriority; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaCapacityValid.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaCapacityValid.java new file mode 100644 index 000000000..4afb00eed --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaCapacityValid.java @@ -0,0 +1,21 @@ +package gg.agenda.api.user.agenda.controller.request.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = AgendaCapacityValidator.class) +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AgendaCapacityValid { + + String message() default "올바르지 않은 대회 정보입니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaCapacityValidator.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaCapacityValidator.java new file mode 100644 index 000000000..923dbd7f9 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaCapacityValidator.java @@ -0,0 +1,29 @@ +package gg.agenda.api.user.agenda.controller.request.validator; + +import java.util.Objects; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; + +public class AgendaCapacityValidator implements ConstraintValidator { + + @Override + public void initialize(AgendaCapacityValid constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(AgendaCreateReqDto value, ConstraintValidatorContext context) { + if (Objects.isNull(value)) { + return true; + } + return mustHaveValidTeam(value); + } + + private boolean mustHaveValidTeam(AgendaCreateReqDto value) { + return value.getAgendaMinTeam() <= value.getAgendaMaxTeam() + && value.getAgendaMinPeople() <= value.getAgendaMaxPeople(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaScheduleValid.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaScheduleValid.java new file mode 100644 index 000000000..1bff895d6 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaScheduleValid.java @@ -0,0 +1,21 @@ +package gg.agenda.api.user.agenda.controller.request.validator; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = AgendaScheduleValidator.class) +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AgendaScheduleValid { + + String message() default "올바르지 않은 대회 일정입니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaScheduleValidator.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaScheduleValidator.java new file mode 100644 index 000000000..d87e92b36 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/request/validator/AgendaScheduleValidator.java @@ -0,0 +1,34 @@ +package gg.agenda.api.user.agenda.controller.request.validator; + +import java.util.Objects; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; + +public class AgendaScheduleValidator implements ConstraintValidator { + + @Override + public void initialize(AgendaScheduleValid constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(AgendaCreateReqDto value, ConstraintValidatorContext context) { + if (Objects.isNull(value)) { + return true; + } + if (Objects.isNull(value.getAgendaDeadLine()) + || Objects.isNull(value.getAgendaStartTime()) + || Objects.isNull(value.getAgendaEndTime())) { + return false; + } + return mustHaveValidSchedule(value); + } + + private boolean mustHaveValidSchedule(AgendaCreateReqDto value) { + return value.getAgendaDeadLine().isBefore(value.getAgendaStartTime()) + && value.getAgendaStartTime().isBefore(value.getAgendaEndTime()); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaKeyResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaKeyResDto.java new file mode 100644 index 000000000..fa61675cb --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaKeyResDto.java @@ -0,0 +1,20 @@ +package gg.agenda.api.user.agenda.controller.response; + +import java.util.UUID; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaKeyResDto { + + private UUID agendaKey; + + @Builder + public AgendaKeyResDto(UUID agendaKey) { + this.agendaKey = agendaKey; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaResDto.java new file mode 100644 index 000000000..ba54e4475 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaResDto.java @@ -0,0 +1,109 @@ +package gg.agenda.api.user.agenda.controller.response; + +import java.net.URL; +import java.time.LocalDateTime; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaResDto { + + private String agendaTitle; + + private String agendaContent; + + private LocalDateTime agendaDeadLine; + + private LocalDateTime agendaStartTime; + + private LocalDateTime agendaEndTime; + + private int agendaMinTeam; + + private int agendaMaxTeam; + + private int agendaCurrentTeam; + + private int agendaMinPeople; + + private int agendaMaxPeople; + + private String agendaPosterUrl; + + private String agendaHost; + + private Location agendaLocation; + + private AgendaStatus agendaStatus; + + private LocalDateTime createdAt; + + private Boolean isOfficial; + + private Boolean isRanking; + + private String announcementTitle; + + @Builder + public AgendaResDto(String agendaTitle, String agendaContent, LocalDateTime agendaDeadLine, + LocalDateTime agendaStartTime, LocalDateTime agendaEndTime, int agendaMinTeam, int agendaMaxTeam, + int agendaCurrentTeam, int agendaMinPeople, int agendaMaxPeople, String agendaPosterUrl, String agendaHost, + Location agendaLocation, AgendaStatus agendaStatus, LocalDateTime createdAt, String announcementTitle, + Boolean isOfficial, Boolean isRanking) { + this.agendaTitle = agendaTitle; + this.agendaContent = agendaContent; + this.agendaDeadLine = agendaDeadLine; + this.agendaStartTime = agendaStartTime; + this.agendaEndTime = agendaEndTime; + this.agendaMinTeam = agendaMinTeam; + this.agendaMaxTeam = agendaMaxTeam; + this.agendaCurrentTeam = agendaCurrentTeam; + this.agendaMinPeople = agendaMinPeople; + this.agendaMaxPeople = agendaMaxPeople; + this.agendaPosterUrl = agendaPosterUrl; + this.agendaHost = agendaHost; + this.agendaLocation = agendaLocation; + this.agendaStatus = agendaStatus; + this.createdAt = createdAt; + this.announcementTitle = announcementTitle; + this.isOfficial = isOfficial; + this.isRanking = isRanking; + } + + @Mapper + public interface MapStruct { + + AgendaResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaResDto.MapStruct.class); + + @Mapping(target = "agendaTitle", source = "agenda.title") + @Mapping(target = "agendaContent", source = "agenda.content") + @Mapping(target = "agendaDeadLine", source = "agenda.deadline") + @Mapping(target = "agendaStartTime", source = "agenda.startTime") + @Mapping(target = "agendaEndTime", source = "agenda.endTime") + @Mapping(target = "agendaMinTeam", source = "agenda.minTeam") + @Mapping(target = "agendaMaxTeam", source = "agenda.maxTeam") + @Mapping(target = "agendaCurrentTeam", source = "agenda.currentTeam") + @Mapping(target = "agendaMinPeople", source = "agenda.minPeople") + @Mapping(target = "agendaMaxPeople", source = "agenda.maxPeople") + @Mapping(target = "agendaPosterUrl", source = "agenda.posterUri") + @Mapping(target = "agendaHost", source = "agenda.hostIntraId") + @Mapping(target = "agendaLocation", source = "agenda.location") + @Mapping(target = "agendaStatus", source = "agenda.status") + @Mapping(target = "createdAt", source = "agenda.createdAt") + @Mapping(target = "isOfficial", source = "agenda.isOfficial") + @Mapping(target = "isRanking", source = "agenda.isRanking") + @Mapping(target = "announcementTitle", source = "announcementTitle") + AgendaResDto toDto(Agenda agenda, String announcementTitle); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaSimpleResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaSimpleResDto.java new file mode 100644 index 000000000..b172c3a82 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/controller/response/AgendaSimpleResDto.java @@ -0,0 +1,87 @@ +package gg.agenda.api.user.agenda.controller.response; + +import java.net.URL; +import java.time.LocalDateTime; +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.Agenda; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaSimpleResDto { + + private String agendaTitle; + + private LocalDateTime agendaDeadLine; + + private LocalDateTime agendaStartTime; + + private LocalDateTime agendaEndTime; + + private int agendaCurrentTeam; + + private int agendaMaxTeam; + + private int agendaMinPeople; + + private int agendaMaxPeople; + + private Location agendaLocation; + + private UUID agendaKey; + + private Boolean isOfficial; + + private Boolean isRanking; + + private String agendaPosterUrl; + + @Builder + public AgendaSimpleResDto(String agendaTitle, LocalDateTime agendaDeadLine, LocalDateTime agendaStartTime, + LocalDateTime agendaEndTime, int agendaCurrentTeam, int agendaMaxTeam, int agendaMinPeople, + int agendaMaxPeople, Location agendaLocation, UUID agendaKey, Boolean isOfficial, Boolean isRanking, + String agendaPosterUrl) { + this.agendaTitle = agendaTitle; + this.agendaDeadLine = agendaDeadLine; + this.agendaStartTime = agendaStartTime; + this.agendaEndTime = agendaEndTime; + this.agendaCurrentTeam = agendaCurrentTeam; + this.agendaMaxTeam = agendaMaxTeam; + this.agendaMinPeople = agendaMinPeople; + this.agendaMaxPeople = agendaMaxPeople; + this.agendaLocation = agendaLocation; + this.agendaKey = agendaKey; + this.isOfficial = isOfficial; + this.isRanking = isRanking; + this.agendaPosterUrl = agendaPosterUrl; + } + + @Mapper + public interface MapStruct { + AgendaSimpleResDto.MapStruct INSTANCE = Mappers.getMapper(AgendaSimpleResDto.MapStruct.class); + + @Mapping(target = "agendaTitle", source = "title") + @Mapping(target = "agendaDeadLine", source = "deadline") + @Mapping(target = "agendaStartTime", source = "startTime") + @Mapping(target = "agendaEndTime", source = "endTime") + @Mapping(target = "agendaCurrentTeam", source = "currentTeam") + @Mapping(target = "agendaMaxTeam", source = "maxTeam") + @Mapping(target = "agendaMinPeople", source = "minPeople") + @Mapping(target = "agendaMaxPeople", source = "maxPeople") + @Mapping(target = "agendaLocation", source = "location") + @Mapping(target = "agendaKey", source = "agendaKey") + @Mapping(target = "isOfficial", source = "isOfficial") + @Mapping(target = "isRanking", source = "isRanking") + @Mapping(target = "agendaPosterUrl", source = "posterUri") + AgendaSimpleResDto toDto(Agenda agenda); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/service/AgendaService.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/service/AgendaService.java new file mode 100644 index 000000000..694c61bce --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agenda/service/AgendaService.java @@ -0,0 +1,169 @@ +package gg.agenda.api.user.agenda.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.io.IOException; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import gg.agenda.api.user.agenda.controller.request.AgendaAwardsReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaTeamAward; +import gg.agenda.api.user.agendateam.service.AgendaTeamService; +import gg.auth.UserDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaPosterImage; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.repo.agenda.AgendaPosterImageRepository; +import gg.repo.agenda.AgendaRepository; +import gg.repo.agenda.AgendaTeamRepository; +import gg.utils.exception.custom.BusinessException; +import gg.utils.exception.custom.ForbiddenException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.file.handler.ImageHandler; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AgendaService { + + private final AgendaRepository agendaRepository; + + private final AgendaTeamRepository agendaTeamRepository; + + private final AgendaPosterImageRepository agendaPosterImageRepository; + + private final AgendaTeamService agendaTeamService; + + private final ImageHandler imageHandler; + + @Value("${info.image.defaultUrl}") + private String defaultUri; + + @Transactional(readOnly = true) + public Agenda findAgendaByAgendaKey(UUID agendaKey) { + return agendaRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + } + + /** + * OPEN인데 deadline이 지나지 않은 대회 반환 + */ + @Transactional(readOnly = true) + public List findOpenAgendaList() { + return agendaRepository.findAllByStatusIs(AgendaStatus.OPEN).stream() + .filter(agenda -> agenda.getDeadline().isAfter(LocalDateTime.now())) + .sorted(agendaComparatorWithDeadlineThenIsOfficial()) + .collect(Collectors.toList()); + } + + private Comparator agendaComparatorWithDeadlineThenIsOfficial() { + return Comparator.comparing(Agenda::getDeadline) + .thenComparing(Agenda::getIsOfficial); + } + + /** + * OPEN인데 deadline이 지난 대회와 CONFIRM인 대회 반환 + */ + @Transactional(readOnly = true) + public List findConfirmAgendaList() { + return agendaRepository.findAllByStatusIs(AgendaStatus.OPEN, AgendaStatus.CONFIRM).stream() + .filter(agenda -> agenda.getDeadline().isBefore(LocalDateTime.now()) + || agenda.getStatus() == AgendaStatus.CONFIRM) + .sorted(agendaComparatorWithStartTimeThenIsOfficial()) + .collect(Collectors.toList()); + } + + private Comparator agendaComparatorWithStartTimeThenIsOfficial() { + return Comparator.comparing(Agenda::getDeadline) + .thenComparing(Agenda::getIsOfficial); + } + + @Transactional + public Agenda addAgenda(AgendaCreateReqDto createDto, MultipartFile agendaPoster, UserDto user) { + try { + if (Objects.nonNull(agendaPoster) && agendaPoster.getSize() > 0) { + URL storedUrl = imageHandler.uploadImageOrDefault(agendaPoster, createDto.getAgendaTitle(), defaultUri); + createDto.updatePosterUri(storedUrl); + } + Agenda newAgenda = AgendaCreateReqDto.MapStruct.INSTANCE.toEntity(createDto, user.getIntraId()); + newAgenda = agendaRepository.save(newAgenda); + if (newAgenda.getPosterUri() != null) { + agendaPosterImageRepository.save(new AgendaPosterImage(newAgenda.getId(), newAgenda.getPosterUri())); + } + return newAgenda; + } catch (IOException e) { + log.error("Failed to upload image for agenda poster", e); + throw new BusinessException(AGENDA_CREATE_FAILED); + } + } + + /** + * FINISH 상태인 대회 반환, 페이지네이션 + */ + @Transactional(readOnly = true) + public Page findHistoryAgendaList(Pageable pageable) { + return agendaRepository.findAllByStatusIs(AgendaStatus.FINISH, pageable); + } + + @Transactional + public void finishAgenda(Agenda agenda) { + agenda.finishAgenda(); + } + + @Transactional + public void awardAgenda(AgendaAwardsReqDto agendaAwardsReqDto, Agenda agenda) { + List teams = agendaTeamRepository.findAllByAgendaAndStatus(agenda, AgendaTeamStatus.CONFIRM); + for (AgendaTeamAward agendaTeamAward : agendaAwardsReqDto.getAwards()) { + AgendaTeam matchedTeam = teams.stream() + .filter(team -> team.getName().equals(agendaTeamAward.getTeamName())) + .findFirst() + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + matchedTeam.acceptAward(agendaTeamAward.getAwardName(), agendaTeamAward.getAwardPriority()); + } + } + + @Transactional + public List confirmAgendaAndRefundTicketForOpenTeam(Agenda agenda) { + if (agenda.getCurrentTeam() < agenda.getMinTeam()) { + throw new ForbiddenException("팀이 모두 구성되지 않았습니다."); + } + + List openTeams = agendaTeamRepository.findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN); + for (AgendaTeam openTeam : openTeams) { + agendaTeamService.leaveTeamAll(openTeam); + } + agenda.confirmAgenda(); + return openTeams; + } + + @Transactional + public void cancelAgenda(Agenda agenda) { + List attendTeams = agendaTeamRepository.findAllByAgendaAndStatus(agenda, + AgendaTeamStatus.OPEN, AgendaTeamStatus.CONFIRM); + attendTeams.forEach(agendaTeamService::leaveTeamAll); + agenda.cancelAgenda(); + } + + @Transactional(readOnly = true) + public Optional getAgenda(UUID agendaKey) { + return agendaRepository.findByAgendaKey(agendaKey); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/AgendaAnnouncementController.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/AgendaAnnouncementController.java new file mode 100644 index 000000000..c67941230 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/AgendaAnnouncementController.java @@ -0,0 +1,74 @@ +package gg.agenda.api.user.agendaannouncement.controller; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.user.agenda.service.AgendaService; +import gg.agenda.api.user.agendaannouncement.controller.request.AgendaAnnouncementCreateReqDto; +import gg.agenda.api.user.agendaannouncement.controller.response.AgendaAnnouncementResDto; +import gg.agenda.api.user.agendaannouncement.service.AgendaAnnouncementService; +import gg.agenda.api.utils.AgendaSlackService; +import gg.auth.UserDto; +import gg.auth.argumentresolver.Login; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/agenda/announcement") +@RequiredArgsConstructor +public class AgendaAnnouncementController { + + private final AgendaService agendaService; + private final AgendaSlackService agendaSlackService; + private final AgendaAnnouncementService agendaAnnouncementService; + + @PostMapping + public ResponseEntity agendaAnnouncementAdd(@Login UserDto user, @RequestParam("agenda_key") UUID agendaKey, + @RequestBody @Valid AgendaAnnouncementCreateReqDto agendaAnnouncementCreateReqDto) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + agenda.mustModifiedByHost(user.getIntraId()); + AgendaAnnouncement newAnnounce = agendaAnnouncementService + .addAgendaAnnouncement(agendaAnnouncementCreateReqDto, agenda); + agendaSlackService.slackAddAgendaAnnouncement(agenda, newAnnounce); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @GetMapping + public ResponseEntity> agendaAnnouncementList( + @RequestParam("agenda_key") UUID agendaKey, @ModelAttribute @Valid PageRequestDto pageRequest) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page announcementList = agendaAnnouncementService + .findAnnouncementListByAgenda(pageable, agenda); + + List announceDto = announcementList.stream() + .map(AgendaAnnouncementResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + announcementList.getTotalElements(), announceDto); + return ResponseEntity.ok(pageResponseDto); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/request/AgendaAnnouncementCreateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/request/AgendaAnnouncementCreateReqDto.java new file mode 100644 index 000000000..77a682f85 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/request/AgendaAnnouncementCreateReqDto.java @@ -0,0 +1,47 @@ +package gg.agenda.api.user.agendaannouncement.controller.request; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAnnouncementCreateReqDto { + + @NotNull + @NotEmpty + private String title; + + @NotNull + @NotEmpty + private String content; + + @Builder + public AgendaAnnouncementCreateReqDto(String title, String content) { + this.title = title; + this.content = content; + } + + @Mapper + public interface MapStruct { + + MapStruct INSTANCE = Mappers.getMapper(MapStruct.class); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "title", source = "dto.title") + @Mapping(target = "content", source = "dto.content") + @Mapping(target = "isShow", constant = "true") + @Mapping(target = "agenda", source = "agenda") + AgendaAnnouncement toEntity(AgendaAnnouncementCreateReqDto dto, Agenda agenda); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/response/AgendaAnnouncementResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/response/AgendaAnnouncementResDto.java new file mode 100644 index 000000000..97dc5f879 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/controller/response/AgendaAnnouncementResDto.java @@ -0,0 +1,40 @@ +package gg.agenda.api.user.agendaannouncement.controller.response; + +import java.time.LocalDateTime; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.AgendaAnnouncement; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaAnnouncementResDto { + + long id; + + String title; + + String content; + + LocalDateTime createdAt; + + @Builder + public AgendaAnnouncementResDto(long id, String title, String content, LocalDateTime createdAt) { + this.id = id; + this.title = title; + this.content = content; + this.createdAt = createdAt; + } + + @Mapper + public interface MapStruct { + MapStruct INSTANCE = Mappers.getMapper(MapStruct.class); + + AgendaAnnouncementResDto toDto(AgendaAnnouncement announcement); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/service/AgendaAnnouncementService.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/service/AgendaAnnouncementService.java new file mode 100644 index 000000000..e34e789d1 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaannouncement/service/AgendaAnnouncementService.java @@ -0,0 +1,48 @@ +package gg.agenda.api.user.agendaannouncement.service; + +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agenda.api.user.agendaannouncement.controller.request.AgendaAnnouncementCreateReqDto; +import gg.agenda.api.utils.SnsMessageUtil; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.repo.agenda.AgendaAnnouncementRepository; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.utils.sns.MessageSender; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AgendaAnnouncementService { + + private final MessageSender messageSender; + private final SnsMessageUtil snsMessageUtil; + private final AgendaTeamProfileRepository agendaTeamProfileRepository; + private final AgendaAnnouncementRepository agendaAnnouncementRepository; + + @Transactional + public AgendaAnnouncement addAgendaAnnouncement(AgendaAnnouncementCreateReqDto announceCreateDto, Agenda agenda) { + AgendaAnnouncement newAnnounce = AgendaAnnouncementCreateReqDto + .MapStruct.INSTANCE.toEntity(announceCreateDto, agenda); + return agendaAnnouncementRepository.save(newAnnounce); + } + + @Transactional(readOnly = true) + public Page findAnnouncementListByAgenda(Pageable pageable, Agenda agenda) { + return agendaAnnouncementRepository.findListByAgenda(pageable, agenda); + } + + @Transactional(readOnly = true) + public String findLatestAnnounceTitleByAgendaOrDefault(Agenda agenda, String defaultTitle) { + Optional latestAnnounce = agendaAnnouncementRepository.findLatestByAgenda(agenda); + if (latestAnnounce.isEmpty()) { + return defaultTitle; + } + return latestAnnounce.get().getTitle(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/AgendaHostController.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/AgendaHostController.java new file mode 100644 index 000000000..87062f5c8 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/AgendaHostController.java @@ -0,0 +1,70 @@ +package gg.agenda.api.user.agendaprofile.controller; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.user.agendaprofile.controller.response.HostedAgendaResDto; +import gg.agenda.api.user.agendaprofile.service.AgendaProfileFindService; +import gg.auth.UserDto; +import gg.auth.argumentresolver.Login; +import gg.data.agenda.Agenda; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda/host") +public class AgendaHostController { + + private final AgendaProfileFindService agendaProfileFindService; + + @GetMapping("/history/list/{intraId}") + public ResponseEntity> hostedAgendaList( + @PathVariable String intraId, @ModelAttribute @Valid PageRequestDto pageRequestDto) { + int page = pageRequestDto.getPage(); + int size = pageRequestDto.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page hostedAgendas = agendaProfileFindService.findHostedAgenda(intraId, pageable); + + List agendaResDtos = hostedAgendas.stream() + .map(HostedAgendaResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + hostedAgendas.getTotalElements(), agendaResDtos); + return ResponseEntity.ok(pageResponseDto); + } + + @GetMapping("/current/list/{intraId}") + public ResponseEntity> hostingAgendaList( + @PathVariable String intraId, + @ModelAttribute @Valid PageRequestDto pageRequestDto) { + int page = pageRequestDto.getPage(); + int size = pageRequestDto.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page hostedAgendas = agendaProfileFindService.findHostingAgenda(intraId, pageable); + + List agendaResDtos = hostedAgendas.stream() + .map(HostedAgendaResDto.MapStruct.INSTANCE::toDto) + .collect(Collectors.toList()); + PageResponseDto pageResponseDto = PageResponseDto.of( + hostedAgendas.getTotalElements(), agendaResDtos); + return ResponseEntity.ok(pageResponseDto); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/AgendaProfileController.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/AgendaProfileController.java new file mode 100644 index 000000000..0e3c67745 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/AgendaProfileController.java @@ -0,0 +1,154 @@ +package gg.agenda.api.user.agendaprofile.controller; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.user.agendaprofile.controller.request.AgendaProfileChangeReqDto; +import gg.agenda.api.user.agendaprofile.controller.response.AgendaProfileDetailsResDto; +import gg.agenda.api.user.agendaprofile.controller.response.AgendaProfileInfoDetailsResDto; +import gg.agenda.api.user.agendaprofile.controller.response.AttendedAgendaListResDto; +import gg.agenda.api.user.agendaprofile.controller.response.CurrentAttendAgendaListResDto; +import gg.agenda.api.user.agendaprofile.controller.response.IntraProfileResDto; +import gg.agenda.api.user.agendaprofile.controller.response.MyAgendaProfileDetailsResDto; +import gg.agenda.api.user.agendaprofile.service.AgendaProfileFindService; +import gg.agenda.api.user.agendaprofile.service.AgendaProfileService; +import gg.agenda.api.user.agendaprofile.service.IntraProfileUtils; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraProfile; +import gg.agenda.api.user.ticket.service.TicketService; +import gg.auth.UserDto; +import gg.auth.argumentresolver.Login; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.user.type.RoleType; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda/profile") +public class AgendaProfileController { + private final AgendaProfileFindService agendaProfileFindService; + private final AgendaProfileService agendaProfileService; + private final TicketService ticketService; + private final IntraProfileUtils intraProfileUtils; + + /** + * AgendaProfile admin 여부 조회 API + * @param user 로그인한 사용자 정보 + */ + @GetMapping("/info") + public ResponseEntity myAgendaProfileInfoDetails( + @Login @Parameter(hidden = true) UserDto user) { + String intraId = user.getIntraId(); + Boolean isAdmin = user.getRoleType() == RoleType.ADMIN; + + AgendaProfileInfoDetailsResDto agendaProfileInfoDetails = new AgendaProfileInfoDetailsResDto(intraId, isAdmin); + + return ResponseEntity.ok(agendaProfileInfoDetails); + } + + /** + * AgendaProfile 상세 조회 API + * @param user 로그인한 사용자 정보 + * @return AgendaProfileDetailsResDto 객체와 HTTP 상태 코드를 포함한 ResponseEntity + */ + @GetMapping + public ResponseEntity myAgendaProfileDetails( + @Login @Parameter(hidden = true) UserDto user, HttpServletResponse response) { + AgendaProfile profile = agendaProfileFindService.findAgendaProfileByIntraId(user.getIntraId()); + int ticketCount = ticketService.findUsedTrueApproveTrueTicketList(profile).size(); + IntraProfile intraProfile = intraProfileUtils.getIntraProfile(response); + MyAgendaProfileDetailsResDto agendaProfileDetails = MyAgendaProfileDetailsResDto.toDto( + profile, ticketCount, intraProfile); + return ResponseEntity.ok(agendaProfileDetails); + } + + /** + * AgendaProfile 변경 API + * @param user 로그인한 사용자 정보 + * @param reqDto 변경할 프로필 정보 + * @return HTTP 상태 코드와 빈 응답 + */ + @PatchMapping + public ResponseEntity agendaProfileModify(@Login @Parameter(hidden = true) UserDto user, + @RequestBody @Valid AgendaProfileChangeReqDto reqDto) { + agendaProfileService.modifyAgendaProfile(user.getId(), reqDto); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + + /** + * 현재 참여중인 Agenda 목록 조회하는 메서드 + * @param user 로그인한 유저의 id + * @return List 객체 + */ + @GetMapping("/current/list") + public ResponseEntity> getCurrentAttendAgendaList( + @Login @Parameter(hidden = true) UserDto user) { + List currentAttendAgendaList = agendaProfileFindService + .findCurrentAttendAgenda(user.getIntraId()); + return ResponseEntity.ok(currentAttendAgendaList); + } + + @GetMapping("{intraId}") + public ResponseEntity agendaProfileDetails(@PathVariable String intraId) { + AgendaProfile profile = agendaProfileFindService.findAgendaProfileByIntraId(intraId); + AgendaProfileDetailsResDto resDto = AgendaProfileDetailsResDto.toDto(profile); + return ResponseEntity.ok(resDto); + } + + @GetMapping("/intra/{intraId}") + public ResponseEntity intraProfileDetails(@PathVariable String intraId, + HttpServletResponse response) { + IntraProfile intraProfile = intraProfileUtils.getIntraProfile(intraId, response); + IntraProfileResDto resDto = IntraProfileResDto.toDto(intraProfile); + return ResponseEntity.ok(resDto); + } + + /** + * 과거에 참여했던 Agenda 목록 조회하는 메서드 + * @param pageRequest 페이지네이션 요청 정보, agendaId 아젠다 아이디 + */ + @GetMapping("/history/list/{intraId}") + public ResponseEntity> getAttendedAgendaList( + @PathVariable String intraId, @ModelAttribute @Valid PageRequestDto pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page attendedAgendaList = agendaProfileFindService + .findAttendedAgenda(intraId, pageable); + + List attendedAgendaDtos = attendedAgendaList.stream() + .map(agendaTeamProfile -> { + List teamMates = agendaProfileFindService + .findTeamMatesFromAgendaTeam(agendaTeamProfile.getAgendaTeam()); + return new AttendedAgendaListResDto(agendaTeamProfile, teamMates); + }) + .collect(Collectors.toList()); + + PageResponseDto pageResponseDto = PageResponseDto.of( + attendedAgendaList.getTotalElements(), attendedAgendaDtos); + return ResponseEntity.ok(pageResponseDto); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/request/AgendaProfileChangeReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/request/AgendaProfileChangeReqDto.java new file mode 100644 index 000000000..b8d415b3d --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/request/AgendaProfileChangeReqDto.java @@ -0,0 +1,29 @@ +package gg.agenda.api.user.agendaprofile.controller.request; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import org.hibernate.validator.constraints.URL; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class AgendaProfileChangeReqDto { + + @NotBlank + @Size(max = 50, message = "userContent의 길이가 허용된 범위를 초과합니다.") + private String userContent; + + @URL + @Size(max = 200, message = "userGithub의 길이가 허용된 범위를 초과합니다.") + private String userGithub; + + @Builder + public AgendaProfileChangeReqDto(String userContent, String userGithub) { + this.userContent = userContent; + this.userGithub = userGithub; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AgendaProfileDetailsResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AgendaProfileDetailsResDto.java new file mode 100644 index 000000000..104d8332c --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AgendaProfileDetailsResDto.java @@ -0,0 +1,39 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Coalition; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaProfileDetailsResDto { + private String userIntraId; + private String userContent; + private String userGithub; + private Coalition userCoalition; + private Location userLocation; + + @Builder + public AgendaProfileDetailsResDto(String userIntraId, String userContent, String userGithub, + Coalition userCoalition, Location userLocation) { + this.userIntraId = userIntraId; + this.userContent = userContent; + this.userGithub = userGithub; + this.userCoalition = userCoalition; + this.userLocation = userLocation; + } + + public static AgendaProfileDetailsResDto toDto(AgendaProfile profile) { + return AgendaProfileDetailsResDto.builder() + .userIntraId(profile.getIntraId()) + .userContent(profile.getContent()) + .userGithub(profile.getGithubUrl()) + .userCoalition(profile.getCoalition()) + .userLocation(profile.getLocation()) + .build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AgendaProfileInfoDetailsResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AgendaProfileInfoDetailsResDto.java new file mode 100644 index 000000000..67f7e403c --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AgendaProfileInfoDetailsResDto.java @@ -0,0 +1,16 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class AgendaProfileInfoDetailsResDto { + private String intraId; + private Boolean isAdmin; + + public AgendaProfileInfoDetailsResDto(String intraId, Boolean isAdmin) { + this.intraId = intraId; + this.isAdmin = isAdmin; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AttendedAgendaListResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AttendedAgendaListResDto.java new file mode 100644 index 000000000..138bdd549 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/AttendedAgendaListResDto.java @@ -0,0 +1,46 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import gg.agenda.api.user.agendateam.controller.response.TeamMateDto; +import gg.data.agenda.AgendaTeamProfile; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class AttendedAgendaListResDto { + private String agendaId; + private UUID agendaKey; + private String agendaTitle; + private LocalDateTime agendaStartTime; + private LocalDateTime agendaEndTime; + private int agendaCurrentTeam; + private String agendaLocation; + private UUID teamKey; + private Boolean isOfficial; + private int agendaMaxPeople; + private String teamName; + private List teamMates; + + public AttendedAgendaListResDto(AgendaTeamProfile agendaTeamProfile, + List agendaTeamProfileList) { + this.agendaId = agendaTeamProfile.getAgenda().getId().toString(); + this.agendaKey = agendaTeamProfile.getAgenda().getAgendaKey(); + this.agendaTitle = agendaTeamProfile.getAgenda().getTitle(); + this.agendaStartTime = agendaTeamProfile.getAgenda().getStartTime(); + this.agendaEndTime = agendaTeamProfile.getAgenda().getEndTime(); + this.agendaCurrentTeam = agendaTeamProfile.getAgenda().getCurrentTeam(); + this.agendaLocation = agendaTeamProfile.getAgenda().getLocation().toString(); + this.teamKey = agendaTeamProfile.getAgendaTeam().getTeamKey(); + this.isOfficial = agendaTeamProfile.getAgenda().getIsOfficial(); + this.agendaMaxPeople = agendaTeamProfile.getAgenda().getMaxPeople(); + this.teamName = agendaTeamProfile.getAgendaTeam().getName(); + this.teamMates = agendaTeamProfileList.stream() + .map(TeamMateDto::new) + .collect(Collectors.toList()); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/CurrentAttendAgendaListResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/CurrentAttendAgendaListResDto.java new file mode 100644 index 000000000..6d20c6651 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/CurrentAttendAgendaListResDto.java @@ -0,0 +1,34 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +import gg.data.agenda.AgendaTeamProfile; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class CurrentAttendAgendaListResDto { + private String agendaId; + private UUID agendaKey; + private String agendaTitle; + private String agendaLocation; + private UUID teamKey; + private Boolean isOfficial; + private String teamName; + private LocalDateTime agendaStartTime; + private String teamStatus; + + public CurrentAttendAgendaListResDto(AgendaTeamProfile agendaTeamProfile) { + this.agendaId = agendaTeamProfile.getAgenda().getId().toString(); + this.agendaKey = agendaTeamProfile.getAgenda().getAgendaKey(); + this.agendaTitle = agendaTeamProfile.getAgenda().getTitle(); + this.agendaLocation = agendaTeamProfile.getAgenda().getLocation().toString(); + this.teamKey = agendaTeamProfile.getAgendaTeam().getTeamKey(); + this.isOfficial = agendaTeamProfile.getAgenda().getIsOfficial(); + this.teamName = agendaTeamProfile.getAgendaTeam().getName(); + this.agendaStartTime = agendaTeamProfile.getAgenda().getStartTime(); + this.teamStatus = agendaTeamProfile.getAgendaTeam().getStatus().toString(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/HostedAgendaResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/HostedAgendaResDto.java new file mode 100644 index 000000000..3ac8534b6 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/HostedAgendaResDto.java @@ -0,0 +1,76 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HostedAgendaResDto { + private String agendaKey; + private String agendaTitle; + private LocalDateTime agendaDeadLine; + private LocalDateTime agendaStartTime; + private LocalDateTime agendaEndTime; + private int agendaCurrentTeam; + private int agendaMaxTeam; + private int agendaMinTeam; + private int agendaMinPeople; + private int agendaMaxPeople; + private String agendaLocation; + private Boolean isRanking; + private Boolean isOfficial; + private AgendaStatus agendaStatus; + + @Builder + public HostedAgendaResDto(String agendaKey, String agendaTitle, LocalDateTime agendaDeadLine, + LocalDateTime agendaStartTime, LocalDateTime agendaEndTime, int agendaCurrentTeam, int agendaMaxTeam, + int agendaMinTeam, int agendaMinPeople, int agendaMaxPeople, String agendaLocation, Boolean isRanking, + Boolean isOfficial, AgendaStatus agendaStatus) { + this.agendaKey = agendaKey; + this.agendaTitle = agendaTitle; + this.agendaDeadLine = agendaDeadLine; + this.agendaStartTime = agendaStartTime; + this.agendaEndTime = agendaEndTime; + this.agendaCurrentTeam = agendaCurrentTeam; + this.agendaMaxTeam = agendaMaxTeam; + this.agendaMinTeam = agendaMinTeam; + this.agendaMinPeople = agendaMinPeople; + this.agendaMaxPeople = agendaMaxPeople; + this.agendaLocation = agendaLocation; + this.isRanking = isRanking; + this.isOfficial = isOfficial; + this.agendaStatus = agendaStatus; + } + + @Mapper + public interface MapStruct { + HostedAgendaResDto.MapStruct INSTANCE = Mappers.getMapper(HostedAgendaResDto.MapStruct.class); + + @Mapping(target = "agendaKey", source = "agendaKey") + @Mapping(target = "agendaTitle", source = "title") + @Mapping(target = "agendaDeadLine", source = "deadline") + @Mapping(target = "agendaStartTime", source = "startTime") + @Mapping(target = "agendaEndTime", source = "endTime") + @Mapping(target = "agendaCurrentTeam", source = "currentTeam") + @Mapping(target = "agendaMaxTeam", source = "maxTeam") + @Mapping(target = "agendaMinTeam", source = "minTeam") + @Mapping(target = "agendaMinPeople", source = "minPeople") + @Mapping(target = "agendaMaxPeople", source = "maxPeople") + @Mapping(target = "agendaLocation", source = "location") + @Mapping(target = "isRanking", source = "isRanking") + @Mapping(target = "isOfficial", source = "isOfficial") + @Mapping(target = "agendaStatus", source = "status") + HostedAgendaResDto toDto(Agenda agenda); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/IntraProfileResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/IntraProfileResDto.java new file mode 100644 index 000000000..b3b7a791b --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/IntraProfileResDto.java @@ -0,0 +1,33 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import java.net.URL; +import java.util.List; + +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraAchievement; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraProfile; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class IntraProfileResDto { + private String intraId; + private URL imageUrl; + private List achievements; + + @Builder + public IntraProfileResDto(String intraId, URL imageUrl, List achievements) { + this.intraId = intraId; + this.imageUrl = imageUrl; + this.achievements = achievements; + } + + public static IntraProfileResDto toDto(IntraProfile intraProfile) { + return IntraProfileResDto.builder() + .intraId(intraProfile.getIntraId()) + .imageUrl(intraProfile.getImageUrl()) + .achievements(intraProfile.getAchievements()) + .build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/MyAgendaProfileDetailsResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/MyAgendaProfileDetailsResDto.java new file mode 100644 index 000000000..abb0d37fa --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/controller/response/MyAgendaProfileDetailsResDto.java @@ -0,0 +1,55 @@ +package gg.agenda.api.user.agendaprofile.controller.response; + +import java.net.URL; +import java.util.List; + +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraAchievement; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraProfile; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Coalition; +import gg.data.agenda.type.Location; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class MyAgendaProfileDetailsResDto { + + private String userIntraId; + private String userContent; + private String userGithub; + private Coalition userCoalition; + private Location userLocation; + private int ticketCount; + private URL imageUrl; + private List achievements; + + @Builder + public MyAgendaProfileDetailsResDto(String userIntraId, String userContent, String userGithub, + Coalition userCoalition, Location userLocation, int ticketCount, URL imageUrl, + List achievements) { + this.userIntraId = userIntraId; + this.userContent = userContent; + this.userGithub = userGithub; + this.userCoalition = userCoalition; + this.userLocation = userLocation; + this.ticketCount = ticketCount; + this.imageUrl = imageUrl; + this.achievements = achievements; + } + + public static MyAgendaProfileDetailsResDto toDto(AgendaProfile profile, int ticketCount, + IntraProfile intraProfile) { + return MyAgendaProfileDetailsResDto.builder() + .userIntraId(profile.getIntraId()) + .userContent(profile.getContent()) + .userGithub(profile.getGithubUrl()) + .userCoalition(profile.getCoalition()) + .userLocation(profile.getLocation()) + .ticketCount(ticketCount) + .imageUrl(intraProfile.getImageUrl()) + .achievements(intraProfile.getAchievements()) + .build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/AgendaProfileFindService.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/AgendaProfileFindService.java new file mode 100644 index 000000000..1ab8710eb --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/AgendaProfileFindService.java @@ -0,0 +1,87 @@ +package gg.agenda.api.user.agendaprofile.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agenda.api.user.agendaprofile.controller.response.CurrentAttendAgendaListResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaStatus; +import gg.repo.agenda.AgendaProfileRepository; +import gg.repo.agenda.AgendaRepository; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AgendaProfileFindService { + + private final AgendaProfileRepository agendaProfileRepository; + private final AgendaTeamProfileRepository agendaTeamProfileRepository; + private final AgendaRepository agendaRepository; + + @Transactional(readOnly = true) + public AgendaProfile findAgendaProfileByIntraId(String intraId) { + return agendaProfileRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + } + + /** + * 자기가 참여중인 Agenda 목록 조회하는 메서드 + * @param intraId 로그인한 유저의 id + * @return AgendaProfileDetailsResDto 객체 + */ + @Transactional(readOnly = true) + public List findCurrentAttendAgenda(String intraId) { + AgendaProfile agendaProfile = agendaProfileRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + List agendaTeamProfiles = agendaTeamProfileRepository + .findByProfileAndIsExistTrue(agendaProfile); + + return agendaTeamProfiles.stream() + .filter(agendaTeamProfile -> { + AgendaStatus status = agendaTeamProfile.getAgenda().getStatus(); + return status == AgendaStatus.OPEN || status == AgendaStatus.CONFIRM; + }) + .map(CurrentAttendAgendaListResDto::new) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public Page findAttendedAgenda(String intraId, Pageable pageable) { + AgendaProfile agendaProfile = agendaProfileRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + return agendaTeamProfileRepository.findByProfileAndIsExistTrueAndAgendaStatus( + agendaProfile, AgendaStatus.FINISH, pageable); + } + + @Transactional(readOnly = true) + public List findTeamMatesFromAgendaTeam(AgendaTeam agendaTeam) { + return agendaTeamProfileRepository.findByAgendaTeamAndIsExistTrue(agendaTeam); + } + + @Transactional(readOnly = true) + public Page findHostedAgenda(String intraId, Pageable pageable) { + return agendaRepository.findAllByHostIntraIdAndStatus( + intraId, AgendaStatus.FINISH, AgendaStatus.CANCEL, pageable); + } + + @Transactional(readOnly = true) + public Page findHostingAgenda(String intraId, Pageable pageable) { + return agendaRepository.findAllByHostIntraIdAndStatus( + intraId, AgendaStatus.OPEN, AgendaStatus.CONFIRM, pageable); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/AgendaProfileService.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/AgendaProfileService.java new file mode 100644 index 000000000..6fffc7363 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/AgendaProfileService.java @@ -0,0 +1,38 @@ +package gg.agenda.api.user.agendaprofile.service; + +import static gg.utils.exception.ErrorCode.*; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agenda.api.user.agendaprofile.controller.request.AgendaProfileChangeReqDto; +import gg.data.agenda.AgendaProfile; +import gg.data.user.User; +import gg.repo.agenda.AgendaProfileRepository; +import gg.repo.user.UserRepository; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AgendaProfileService { + + private final UserRepository userRepository; + private final AgendaProfileRepository agendaProfileRepository; + + /** + * AgendaProfile 변경 메서드 + * @param userId 로그인한 유저의 id + * @param reqDto 변경할 프로필 정보 + */ + @Transactional + public void modifyAgendaProfile(Long userId, AgendaProfileChangeReqDto reqDto) { + User user = userRepository.getById(userId); + + AgendaProfile agendaProfile = agendaProfileRepository.findByUserId(user.getId()) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + agendaProfile.updateProfile(reqDto.getUserContent(), reqDto.getUserGithub()); + agendaProfileRepository.save(agendaProfile); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/IntraProfileUtils.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/IntraProfileUtils.java new file mode 100644 index 000000000..e342ed9cb --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/IntraProfileUtils.java @@ -0,0 +1,107 @@ +package gg.agenda.api.user.agendaprofile.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.Objects; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; + +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraAchievement; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraImage; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraProfile; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraProfileResponse; +import gg.auth.FortyTwoAuthUtil; +import gg.utils.cookie.CookieUtil; +import gg.utils.exception.custom.AuthenticationException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.external.ApiUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class IntraProfileUtils { + + private static final String INTRA_PROFILE_URL = "https://api.intra.42.fr/v2/me"; + private static final String INTRA_USERS_URL = "https://api.intra.42.fr/v2/users/"; + + private final FortyTwoAuthUtil fortyTwoAuthUtil; + + private final ApiUtil apiUtil; + + private final CookieUtil cookieUtil; + + public IntraProfile getIntraProfile(HttpServletResponse response) { + try { + IntraProfileResponse intraProfileResponse = requestIntraProfile(INTRA_PROFILE_URL); + intraProfileResponseValidation(intraProfileResponse); + IntraImage intraImage = intraProfileResponse.getImage(); + List intraAchievements = intraProfileResponse.getAchievements(); + return new IntraProfile(intraProfileResponse.getLogin(), intraImage.getLink(), intraAchievements); + } catch (Exception e) { + log.error("42 Intra Profile API 호출 실패", e); + cookieUtil.deleteCookie(response, "refresh_token"); + throw new AuthenticationException(AUTH_NOT_FOUND); + } + } + + public IntraProfile getIntraProfile(String intraId, HttpServletResponse response) { + try { + IntraProfileResponse intraProfileResponse = requestIntraProfile(INTRA_USERS_URL + intraId); + intraProfileResponseValidation(intraProfileResponse); + IntraImage intraImage = intraProfileResponse.getImage(); + List intraAchievements = intraProfileResponse.getAchievements(); + return new IntraProfile(intraProfileResponse.getLogin(), intraImage.getLink(), intraAchievements); + } catch (Exception e) { + if (e instanceof NotExistException) { + throw new NotExistException(AUTH_NOT_FOUND); + } + log.error("42 Intra Profile API 호출 실패", e); + cookieUtil.deleteCookie(response, "refresh_token"); + throw new AuthenticationException(AUTH_NOT_VALID); + } + } + + private IntraProfileResponse requestIntraProfile(String requestUrl) { + try { + String accessToken = fortyTwoAuthUtil.getAccessToken(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + return apiUtil.apiCall(requestUrl, IntraProfileResponse.class, headers, HttpMethod.GET); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + throw new NotExistException(AUTH_NOT_FOUND); + } + String accessToken = fortyTwoAuthUtil.refreshAccessToken(); + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + return apiUtil.apiCall(requestUrl, IntraProfileResponse.class, headers, HttpMethod.GET); + } + } + + private void intraProfileResponseValidation(IntraProfileResponse intraProfileResponse) { + if (Objects.isNull(intraProfileResponse)) { + throw new AuthenticationException(AUTH_NOT_FOUND); + } + if (Objects.isNull(intraProfileResponse.getLogin())) { + throw new AuthenticationException(AUTH_NOT_FOUND); + } + if (Objects.isNull(intraProfileResponse.getImage())) { + throw new AuthenticationException(AUTH_NOT_FOUND); + } + if (Objects.isNull(intraProfileResponse.getAchievements())) { + throw new AuthenticationException(AUTH_NOT_FOUND); + } + if (Objects.isNull(intraProfileResponse.getImage().getLink())) { + throw new AuthenticationException(AUTH_NOT_FOUND); + } + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraAchievement.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraAchievement.java new file mode 100644 index 000000000..ade777add --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraAchievement.java @@ -0,0 +1,53 @@ +package gg.agenda.api.user.agendaprofile.service.intraprofile; + +import java.net.URL; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IntraAchievement { + + private static final String IMAGE_URL = "https://cdn.intra.42.fr"; + + private Long id; + + private String name; + + private String description; + + private String tier; + + private String kind; + + private boolean visible; + + private String image; + + @JsonProperty("nbr_of_success") + private String nbrOfSuccess; + + @JsonProperty("users_url") + private URL usersUrl; + + @Builder + public IntraAchievement(Long id, String name, String description, String tier, String kind, boolean visible, + String image, String nbrOfSuccess, URL usersUrl) { + this.id = id; + this.name = name; + this.description = description; + this.tier = tier; + this.kind = kind; + this.visible = visible; + this.image = image; + this.nbrOfSuccess = nbrOfSuccess; + this.usersUrl = usersUrl; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraImage.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraImage.java new file mode 100644 index 000000000..1f17a0a2f --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraImage.java @@ -0,0 +1,23 @@ +package gg.agenda.api.user.agendaprofile.service.intraprofile; + +import java.net.URL; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IntraImage { + + private URL link; + + private IntraImageVersion versions; + + @Builder + public IntraImage(URL link, IntraImageVersion versions) { + this.link = link; + this.versions = versions; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraImageVersion.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraImageVersion.java new file mode 100644 index 000000000..f5b00d2ce --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraImageVersion.java @@ -0,0 +1,29 @@ +package gg.agenda.api.user.agendaprofile.service.intraprofile; + +import java.net.URL; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IntraImageVersion { + + private URL large; + + private URL medium; + + private URL small; + + private URL micro; + + @Builder + public IntraImageVersion(URL large, URL medium, URL small, URL micro) { + this.large = large; + this.medium = medium; + this.small = small; + this.micro = micro; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraProfile.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraProfile.java new file mode 100644 index 000000000..801b304f1 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraProfile.java @@ -0,0 +1,26 @@ +package gg.agenda.api.user.agendaprofile.service.intraprofile; + +import java.net.URL; +import java.util.List; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IntraProfile { + private String intraId; + + private URL imageUrl; + + private List achievements; + + @Builder + public IntraProfile(String intraId, URL imageUrl, List achievements) { + this.intraId = intraId; + this.imageUrl = imageUrl; + this.achievements = achievements; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraProfileResponse.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraProfileResponse.java new file mode 100644 index 000000000..86e295a10 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendaprofile/service/intraprofile/IntraProfileResponse.java @@ -0,0 +1,25 @@ +package gg.agenda.api.user.agendaprofile.service.intraprofile; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class IntraProfileResponse { + String login; + + IntraImage image; + + List achievements; + + @Builder + public IntraProfileResponse(String login, IntraImage image, List achievements) { + this.login = login; + this.image = image; + this.achievements = achievements; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/AgendaTeamController.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/AgendaTeamController.java new file mode 100644 index 000000000..3674e8b57 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/AgendaTeamController.java @@ -0,0 +1,220 @@ +package gg.agenda.api.user.agendateam.controller; + +import static gg.data.agenda.type.AgendaTeamStatus.*; +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.user.agenda.service.AgendaService; +import gg.agenda.api.user.agendateam.controller.request.TeamCreateReqDto; +import gg.agenda.api.user.agendateam.controller.request.TeamKeyReqDto; +import gg.agenda.api.user.agendateam.controller.request.TeamUpdateReqDto; +import gg.agenda.api.user.agendateam.controller.response.ConfirmTeamResDto; +import gg.agenda.api.user.agendateam.controller.response.MyTeamSimpleResDto; +import gg.agenda.api.user.agendateam.controller.response.OpenTeamResDto; +import gg.agenda.api.user.agendateam.controller.response.TeamDetailsResDto; +import gg.agenda.api.user.agendateam.controller.response.TeamKeyResDto; +import gg.agenda.api.user.agendateam.service.AgendaTeamService; +import gg.agenda.api.utils.AgendaSlackService; +import gg.auth.UserDto; +import gg.auth.argumentresolver.Login; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.Coalition; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.exception.custom.ForbiddenException; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda/team") +public class AgendaTeamController { + private final AgendaService agendaService; + private final AgendaTeamService agendaTeamService; + private final AgendaSlackService agendaSlackService; + + /** + * 내 팀 간단 정보 조회 + * @param user 사용자 정보, agendaId 아젠다 아이디 + * @return 내 팀 간단 정보 + */ + @GetMapping("/my") + public ResponseEntity> myTeamSimpleDetails( + @Parameter(hidden = true) @Login UserDto user, @RequestParam("agenda_key") UUID agendaKey) { + Optional myTeamSimpleResDto = agendaTeamService.detailsMyTeamSimple(user, agendaKey); + if (myTeamSimpleResDto.isEmpty()) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.ok(myTeamSimpleResDto); + } + + /* + * 아젠다 팀 상세 정보 조회 + * @param user 사용자 정보, teamDetailsReqDto 팀 상세 정보 요청 정보, agendaId 아젠다 아이디 + * @return 팀 상세 정보 + */ + @GetMapping + public ResponseEntity agendaTeamDetails(@Parameter(hidden = true) @Login UserDto user, + @ModelAttribute @Valid TeamKeyReqDto teamKeyReqDto, @RequestParam("agenda_key") UUID agendaKey) { + TeamDetailsResDto teamDetailsResDto = agendaTeamService.detailsAgendaTeam(user, agendaKey, teamKeyReqDto); + return ResponseEntity.ok(teamDetailsResDto); + } + + /** + * 아젠다 팀 생성하기 + * @param user 사용자 정보, teamCreateReqDto 팀 생성 요청 정보, agendaId 아젠다 아이디 + * @return 만들어진 팀 KEY + */ + @PostMapping + public ResponseEntity agendaTeamAdd(@Parameter(hidden = true) @Login UserDto user, + @RequestBody @Valid TeamCreateReqDto teamCreateReqDto, @RequestParam("agenda_key") UUID agendaKey) { + TeamKeyResDto teamKeyReqDto = agendaTeamService.addAgendaTeam(user, teamCreateReqDto, agendaKey); + return ResponseEntity.status(HttpStatus.CREATED).body(teamKeyReqDto); + } + + /** + * 아젠다 팀 확정하기 + * @param user 사용자 정보, teamKeyReqDto 팀 KEY 요청 정보, agendaId 아젠다 아이디 + */ + @PatchMapping("/confirm") + public ResponseEntity confirmTeam(@Parameter(hidden = true) @Login UserDto user, + @ModelAttribute @Valid TeamKeyReqDto teamKeyReqDto, @RequestParam("agenda_key") UUID agendaKey) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + AgendaTeam agendaTeam = agendaTeamService.confirmTeam(user, agenda, teamKeyReqDto.getTeamKey()); + agendaSlackService.slackConfirmAgendaTeam(agenda, agendaTeam); + return ResponseEntity.ok().build(); + } + + /** + * 아젠다 팀장 나가기 + * @param user 사용자 정보, teamKeyReqDto 팀 KEY 요청 정보, agendaId 아젠다 아이디 + */ + @PatchMapping("/cancel") + public ResponseEntity leaveAgendaTeam(@Parameter(hidden = true) @Login UserDto user, + @ModelAttribute @Valid TeamKeyReqDto teamKeyReqDto) { + AgendaTeam agendaTeam = agendaTeamService.getAgendaTeam(teamKeyReqDto.getTeamKey()); + agendaTeam.getAgenda().agendaStatusMustBeOpen(); + agendaTeam.agendaTeamStatusMustBeOpenAndConfirm(); + if (!agendaTeam.getLeaderIntraId().equals(user.getIntraId())) { + throw new ForbiddenException(TEAM_LEADER_FORBIDDEN); + } + agendaTeamService.leaveTeamAll(agendaTeam); + agendaSlackService.slackCancelAgendaTeam(agendaTeam.getAgenda(), agendaTeam); + return ResponseEntity.noContent().build(); + } + + /** + * 아젠다 팀원 나가기 + * @param user 사용자 정보, teamKeyReqDto 팀 KEY 요청 정보, agendaId 아젠다 아이디 + */ + @PatchMapping("/drop") + public ResponseEntity dropAgendaTeamMate(@Parameter(hidden = true) @Login UserDto user, + @ModelAttribute @Valid TeamKeyReqDto teamKeyReqDto) { + AgendaTeam agendaTeam = agendaTeamService.getAgendaTeam(teamKeyReqDto.getTeamKey()); + agendaTeam.getAgenda().agendaStatusMustBeOpen(); + agendaTeam.agendaTeamStatusMustBeOpen(); + if (agendaTeam.getLeaderIntraId().equals(user.getIntraId())) { + throw new ForbiddenException(NOT_TEAM_MATE); + } + agendaTeamService.leaveTeamMate(agendaTeam, user); + agendaSlackService.slackLeaveTeamMate(agendaTeam.getAgenda(), agendaTeam, user.getIntraId()); + return ResponseEntity.noContent().build(); + } + + /** + * 아젠다 팀 공개 모집인 팀 목록 조회 + * @param pageRequest 페이지네이션 요청 정보, agendaId 아젠다 아이디 + */ + @GetMapping("/open/list") + public ResponseEntity> openTeamList( + @ModelAttribute @Valid PageRequestDto pageRequest, @RequestParam("agenda_key") UUID agendaKey) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page openTeams = agendaTeamService.findAgendaTeamWithStatus(agendaKey, OPEN, pageable); + + List openTeamResDtoList = openTeams.stream() + .map(agendaTeam -> { + List coalitions = agendaTeamService.getCoalitionsFromAgendaTeam(agendaTeam); + return new OpenTeamResDto(agendaTeam, coalitions); + }) + .collect(Collectors.toList()); + + PageResponseDto pageResponseDto = PageResponseDto.of( + openTeams.getTotalElements(), openTeamResDtoList); + return ResponseEntity.ok(pageResponseDto); + } + + /** + * 아젠다 팀 확정된 팀 목록 조회 + * @param pageRequest 페이지네이션 요청 정보, agendaId 아젠다 아이디 + */ + @GetMapping("/confirm/list") + public ResponseEntity> confirmTeamList( + @ModelAttribute @Valid PageRequestDto pageRequest, @RequestParam("agenda_key") UUID agendaKey) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page confirmTeams = agendaTeamService.findAgendaTeamWithStatus(agendaKey, CONFIRM, pageable); + + List confirmTeamResDtoList = confirmTeams.stream() + .map(agendaTeam -> { + List coalitions = agendaTeamService.getCoalitionsFromAgendaTeam(agendaTeam); + return new ConfirmTeamResDto(agendaTeam, coalitions); + }) + .collect(Collectors.toList()); + + PageResponseDto pageResponseDto = PageResponseDto.of( + confirmTeams.getTotalElements(), confirmTeamResDtoList); + return ResponseEntity.ok(pageResponseDto); + } + + /** + * 아젠다 팀 참여하기 + * @param user 사용자 정보, teamKeyReqDto 팀 KEY 요청 정보, agendaId 아젠다 아이디 + */ + @PostMapping("/join") + public ResponseEntity attendTeamModify(@Parameter(hidden = true) @Login UserDto user, + @ModelAttribute @Valid TeamKeyReqDto teamKeyReqDto, @RequestParam("agenda_key") UUID agendaKey) { + Agenda agenda = agendaService.findAgendaByAgendaKey(agendaKey); + AgendaTeam agendaTeam = agendaTeamService.getAgendaTeam(teamKeyReqDto.getTeamKey()); + agendaTeamService.modifyAttendTeam(user, agendaTeam, agenda); + agendaSlackService.slackAttendTeamMate(agenda, agendaTeam, user.getIntraId()); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + /** + * 아젠다 팀 수정하기 + * @param user 사용자 정보, teamUpdateReqDto 팀 update 요청 정보, agendaId 아젠다 아이디 + */ + @PatchMapping + public ResponseEntity agendaTeamModify(@Parameter(hidden = true) @Login UserDto user, + @RequestBody @Valid TeamUpdateReqDto teamUpdateReqDto, @RequestParam("agenda_key") UUID agendaKey) { + agendaTeamService.modifyAgendaTeam(user, teamUpdateReqDto, agendaKey); + return ResponseEntity.noContent().build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamCreateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamCreateReqDto.java new file mode 100644 index 000000000..865a20c71 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamCreateReqDto.java @@ -0,0 +1,58 @@ +package gg.agenda.api.user.agendateam.controller.request; + +import static gg.data.agenda.type.AgendaTeamStatus.*; + +import java.util.UUID; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.Location; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TeamCreateReqDto { + @NotBlank + @Size(max = 30) + private String teamName; + + @NotNull + private Boolean teamIsPrivate; + + @NotBlank + @Size(max = 10) + private String teamLocation; + + @NotBlank + @Size(max = 500) + private String teamContent; + + @Builder + public TeamCreateReqDto(String teamName, Boolean teamIsPrivate, String teamLocation, String teamContent) { + this.teamName = teamName; + this.teamIsPrivate = teamIsPrivate; + this.teamLocation = teamLocation; + this.teamContent = teamContent; + } + + public static AgendaTeam toEntity(TeamCreateReqDto teamCreateReqDto, Agenda agenda, String intraId) { + return AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name(teamCreateReqDto.getTeamName()) + .content(teamCreateReqDto.getTeamContent()) + .leaderIntraId(intraId) + .status(OPEN) + .location(Location.valueOf(teamCreateReqDto.getTeamLocation())) + .mateCount(1) + .awardPriority(1) + .isPrivate(teamCreateReqDto.getTeamIsPrivate()) + .build(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamKeyReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamKeyReqDto.java new file mode 100644 index 000000000..80cac539d --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamKeyReqDto.java @@ -0,0 +1,19 @@ +package gg.agenda.api.user.agendateam.controller.request; + +import java.util.UUID; + +import javax.validation.constraints.NotNull; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TeamKeyReqDto { + @NotNull + private UUID teamKey; + + public TeamKeyReqDto(UUID teamKey) { + this.teamKey = teamKey; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamUpdateReqDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamUpdateReqDto.java new file mode 100644 index 000000000..810d4afcb --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/request/TeamUpdateReqDto.java @@ -0,0 +1,39 @@ +package gg.agenda.api.user.agendateam.controller.request; + +import java.util.UUID; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TeamUpdateReqDto { + @NotNull + private UUID teamKey; + @NotBlank + @Size(max = 30) + private String teamName; + @NotBlank + @Size(max = 500) + private String teamContent; + @NotNull + private Boolean teamIsPrivate; + @NotBlank + @Size(max = 10) + private String teamLocation; + + @Builder + public TeamUpdateReqDto(UUID teamKey, String teamName, String teamContent, Boolean teamIsPrivate, + String teamLocation) { + this.teamKey = teamKey; + this.teamName = teamName; + this.teamContent = teamContent; + this.teamIsPrivate = teamIsPrivate; + this.teamLocation = teamLocation; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/ConfirmTeamResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/ConfirmTeamResDto.java new file mode 100644 index 000000000..e73d2a2af --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/ConfirmTeamResDto.java @@ -0,0 +1,28 @@ +package gg.agenda.api.user.agendateam.controller.response; + +import java.util.List; + +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.Coalition; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class ConfirmTeamResDto { + private String teamName; + private String teamLeaderIntraId; + private int teamMateCount; + private String teamAward; + private int awardPriority; + private List coalitions; + + public ConfirmTeamResDto(AgendaTeam agendaTeam, List coalitions) { + this.teamName = agendaTeam.getName(); + this.teamLeaderIntraId = agendaTeam.getLeaderIntraId(); + this.teamMateCount = agendaTeam.getMateCount(); + this.teamAward = agendaTeam.getAward(); + this.awardPriority = agendaTeam.getAwardPriority(); + this.coalitions = coalitions; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/MyTeamSimpleResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/MyTeamSimpleResDto.java new file mode 100644 index 000000000..0a9ec04c0 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/MyTeamSimpleResDto.java @@ -0,0 +1,36 @@ +package gg.agenda.api.user.agendateam.controller.response; + +import java.util.List; +import java.util.UUID; + +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Coalition; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MyTeamSimpleResDto { + private String teamName; + private String teamLeaderIntraId; + private int teamMateCount; + private AgendaTeamStatus teamStatus; + private UUID teamKey; + private Location teamLocation; + private String teamAward; + private List coalitions; + + public MyTeamSimpleResDto(AgendaTeam agendaTeam, List coalitions) { + this.teamName = agendaTeam.getName(); + this.teamLeaderIntraId = agendaTeam.getLeaderIntraId(); + this.teamMateCount = agendaTeam.getMateCount(); + this.teamStatus = agendaTeam.getStatus(); + this.teamKey = agendaTeam.getTeamKey(); + this.teamLocation = agendaTeam.getLocation(); + this.teamAward = agendaTeam.getAward(); + this.coalitions = coalitions; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/OpenTeamResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/OpenTeamResDto.java new file mode 100644 index 000000000..d7970a91f --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/OpenTeamResDto.java @@ -0,0 +1,26 @@ +package gg.agenda.api.user.agendateam.controller.response; + +import java.util.List; + +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.Coalition; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class OpenTeamResDto { + private String teamName; + private String teamLeaderIntraId; + private int teamMateCount; + private String teamKey; + private List coalitions; + + public OpenTeamResDto(AgendaTeam agendaTeam, List coalitions) { + this.teamName = agendaTeam.getName(); + this.teamLeaderIntraId = agendaTeam.getLeaderIntraId(); + this.teamMateCount = agendaTeam.getMateCount(); + this.teamKey = agendaTeam.getTeamKey().toString(); + this.coalitions = coalitions; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamDetailsResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamDetailsResDto.java new file mode 100644 index 000000000..adc435f15 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamDetailsResDto.java @@ -0,0 +1,35 @@ +package gg.agenda.api.user.agendateam.controller.response; + +import java.util.List; +import java.util.stream.Collectors; + +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TeamDetailsResDto { + private String teamName; + private String teamLeaderIntraId; + private AgendaTeamStatus teamStatus; + private Location teamLocation; + private String teamContent; + private boolean teamIsPrivate; + private List teamMates; + + public TeamDetailsResDto(AgendaTeam agendaTeam, List agendaTeamProfileList) { + this.teamName = agendaTeam.getName(); + this.teamLeaderIntraId = agendaTeam.getLeaderIntraId(); + this.teamStatus = agendaTeam.getStatus(); + this.teamLocation = agendaTeam.getLocation(); + this.teamContent = agendaTeam.getContent(); + this.teamIsPrivate = agendaTeam.getIsPrivate(); + this.teamMates = agendaTeamProfileList.stream() + .map(TeamMateDto::new) + .collect(Collectors.toList()); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamKeyResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamKeyResDto.java new file mode 100644 index 000000000..04814be7b --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamKeyResDto.java @@ -0,0 +1,14 @@ +package gg.agenda.api.user.agendateam.controller.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TeamKeyResDto { + String teamKey; + + public TeamKeyResDto(String teamKey) { + this.teamKey = teamKey; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamMateDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamMateDto.java new file mode 100644 index 000000000..3a0eb667b --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/controller/response/TeamMateDto.java @@ -0,0 +1,18 @@ +package gg.agenda.api.user.agendateam.controller.response; + +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.Coalition; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class TeamMateDto { + private String intraId; + private Coalition coalition; + + public TeamMateDto(AgendaTeamProfile agendaTeamProfile) { + this.intraId = agendaTeamProfile.getProfile().getIntraId(); + this.coalition = agendaTeamProfile.getProfile().getCoalition(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/service/AgendaTeamService.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/service/AgendaTeamService.java new file mode 100644 index 000000000..4ad2897e3 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/agendateam/service/AgendaTeamService.java @@ -0,0 +1,298 @@ +package gg.agenda.api.user.agendateam.service; + +import static gg.data.agenda.type.AgendaTeamStatus.*; +import static gg.utils.exception.ErrorCode.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import gg.agenda.api.user.agendateam.controller.request.TeamCreateReqDto; +import gg.agenda.api.user.agendateam.controller.request.TeamKeyReqDto; +import gg.agenda.api.user.agendateam.controller.request.TeamUpdateReqDto; +import gg.agenda.api.user.agendateam.controller.response.MyTeamSimpleResDto; +import gg.agenda.api.user.agendateam.controller.response.TeamDetailsResDto; +import gg.agenda.api.user.agendateam.controller.response.TeamKeyResDto; +import gg.agenda.api.user.ticket.service.TicketService; +import gg.agenda.api.utils.SnsMessageUtil; +import gg.auth.UserDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.Ticket; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Coalition; +import gg.data.agenda.type.Location; +import gg.repo.agenda.AgendaProfileRepository; +import gg.repo.agenda.AgendaRepository; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.repo.agenda.AgendaTeamRepository; +import gg.repo.agenda.TicketRepository; +import gg.utils.exception.custom.BusinessException; +import gg.utils.exception.custom.DuplicationException; +import gg.utils.exception.custom.ForbiddenException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.sns.MessageSender; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class AgendaTeamService { + private final TicketService ticketService; + private final MessageSender messageSender; + private final SnsMessageUtil snsMessageUtil; + private final AgendaRepository agendaRepository; + private final TicketRepository ticketRepository; + private final AgendaTeamRepository agendaTeamRepository; + private final AgendaProfileRepository agendaProfileRepository; + private final AgendaTeamProfileRepository agendaTeamProfileRepository; + + /** + * 내 팀 간단 정보 조회 + * @param user 사용자 정보, agendaId 아젠다 아이디 + * @return 내 팀 간단 정보 + */ + public Optional detailsMyTeamSimple(UserDto user, UUID agendaKey) { + Agenda agenda = agendaRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + + AgendaProfile agendaProfile = agendaProfileRepository.findByUserId(user.getId()) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + Optional agendaTeam = agendaTeamProfileRepository.findByAgendaAndAgendaProfileAndIsExistTrue(agenda, + agendaProfile) + .map(AgendaTeamProfile::getAgendaTeam); + if (agendaTeam.isEmpty()) { + return Optional.empty(); + } + + List agendaTeamProfileList = agendaTeamProfileRepository + .findByAgendaTeamAndIsExistTrue(agendaTeam.get()); + + List coalitions = agendaTeamProfileList.stream() + .map(AgendaTeamProfile::getProfile) + .map(AgendaProfile::getCoalition) + .collect(Collectors.toList()); + + return Optional.of(new MyTeamSimpleResDto(agendaTeam.get(), coalitions)); + } + + /** + * 아젠다 팀 상세 정보 조회 + * @param user 사용자 정보, teamCreateReqDto 팀 키, agendaKey 아젠다 키 + * @return 만들어진 팀 상세 정보 + */ + @Transactional(readOnly = true) + public TeamDetailsResDto detailsAgendaTeam(UserDto user, UUID agendaKey, TeamKeyReqDto teamKeyReqDto) { + Agenda agenda = agendaRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + + AgendaTeam agendaTeam = agendaTeamRepository + .findByAgendaAndTeamKeyAndStatus(agenda, teamKeyReqDto.getTeamKey(), OPEN, CONFIRM) + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + + List agendaTeamProfileList = agendaTeamProfileRepository + .findByAgendaTeamAndIsExistTrue(agendaTeam); + + if (agendaTeam.getStatus().equals(CONFIRM)) { // 팀이 확정 상태인 경우에 + if (agendaTeamProfileList.stream() // 팀에 속한 유저가 아닌 경우 + .noneMatch(profile -> profile.getProfile().getUserId().equals(user.getId()))) { + throw new ForbiddenException(NOT_TEAM_MATE); // 조회 불가 + } + } + return new TeamDetailsResDto(agendaTeam, agendaTeamProfileList); + } + + /** + * 아젠다 팀 생성하기 + * @param user 사용자 정보, teamCreateReqDto 팀 생성 요청 정보, agendaId 아젠다 아이디 + * @return 만들어진 팀 KEY + */ + @Transactional + public TeamKeyResDto addAgendaTeam(UserDto user, TeamCreateReqDto teamCreateReqDto, UUID agendaKey) { + AgendaProfile agendaProfile = agendaProfileRepository.findByUserId(user.getId()) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + Agenda agenda = agendaRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + + agenda.addTeam(Location.valueOfLocation(teamCreateReqDto.getTeamLocation()), LocalDateTime.now()); + + if (agenda.getHostIntraId().equals(user.getIntraId())) { + throw new ForbiddenException(HOST_FORBIDDEN); + } + + if (agenda.getLocation() != Location.MIX && agenda.getLocation() != agendaProfile.getLocation()) { + throw new BusinessException(LOCATION_NOT_VALID); + } + + agendaTeamProfileRepository.findByAgendaAndProfileAndIsExistTrue(agenda, agendaProfile) + .ifPresent(teamProfile -> { + throw new DuplicationException(AGENDA_TEAM_FORBIDDEN); + }); + + if (agenda.getIsOfficial()) { + Ticket ticket = ticketRepository.findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + agendaProfile) + .orElseThrow(() -> new ForbiddenException(TICKET_NOT_EXIST)); + ticket.useTicket(agenda.getAgendaKey()); + } + + agendaTeamRepository.findByAgendaAndTeamNameAndStatus(agenda, teamCreateReqDto.getTeamName(), OPEN, CONFIRM) + .ifPresent(team -> { + throw new DuplicationException(TEAM_NAME_EXIST); + }); + + AgendaTeam agendaTeam = TeamCreateReqDto.toEntity(teamCreateReqDto, agenda, user.getIntraId()); + AgendaTeamProfile agendaTeamProfile = new AgendaTeamProfile(agendaTeam, agenda, agendaProfile); + agendaRepository.save(agenda); + agendaTeamRepository.save(agendaTeam); + agendaTeamProfileRepository.save(agendaTeamProfile); + return new TeamKeyResDto(agendaTeam.getTeamKey().toString()); + } + + /** + * 아젠다 팀 확정하기 + * @param user 사용자 정보, teamKeyReqDto 팀 KEY 요청 정보, agendaId 아젠다 아이디 + */ + @Transactional + public AgendaTeam confirmTeam(UserDto user, Agenda agenda, UUID teamKey) { + AgendaTeam agendaTeam = agendaTeamRepository + .findByAgendaAndTeamKeyAndStatus(agenda, teamKey, OPEN, CONFIRM) + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + + if (!agendaTeam.getLeaderIntraId().equals(user.getIntraId())) { + throw new ForbiddenException(TEAM_LEADER_FORBIDDEN); + } + if (agendaTeam.getMateCount() < agenda.getMinPeople()) { + throw new BusinessException(NOT_ENOUGH_TEAM_MEMBER); + } + agenda.confirmTeam(agendaTeam.getLocation(), LocalDateTime.now()); + agendaTeam.confirm(); + return agendaTeamRepository.save(agendaTeam); + } + + /** + * 아젠다 팀 찾기 + * @param teamKey 팀 KEY + */ + @Transactional(readOnly = true) + public AgendaTeam getAgendaTeam(UUID teamKey) { + return agendaTeamRepository.findByTeamKey(teamKey) + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + } + + /** + * 팀장이 팀 나가기 + * @param agendaTeam 팀 + */ + @Transactional + public void leaveTeamAll(AgendaTeam agendaTeam) { + List agendaTeamProfiles = agendaTeamProfileRepository + .findByAgendaTeamAndIsExistTrue(agendaTeam); + agendaTeamProfiles.forEach(agendaTeamProfile -> leaveTeam(agendaTeam, agendaTeamProfile)); + agendaTeam.cancelTeam(agendaTeam.getStatus()); + } + + /** + * 아젠다 팀원 나가기 + * @param agendaTeam 아젠다 팀, user 사용자 정보 + */ + @Transactional + public void leaveTeamMate(AgendaTeam agendaTeam, UserDto user) { + List agendaTeamProfiles = agendaTeamProfileRepository + .findByAgendaTeamAndIsExistTrue(agendaTeam); + AgendaTeamProfile agendaTeamProfile = agendaTeamProfiles.stream() + .filter(profile -> profile.getProfile().getUserId().equals(user.getId())) + .findFirst() + .orElseThrow(() -> new ForbiddenException(NOT_TEAM_MATE)); + leaveTeam(agendaTeam, agendaTeamProfile); + } + + /** + * 팀원이 팀 나가기 + * @param agendaTeamProfile 팀 프로필 + */ + @Transactional(propagation = Propagation.MANDATORY) + public void leaveTeam(AgendaTeam agendaTeam, AgendaTeamProfile agendaTeamProfile) { + agendaTeam.leaveTeamMate(); + agendaTeamProfile.changeExistFalse(); + if (agendaTeamProfile.getAgenda().getIsOfficial()) { + ticketService.refundTicket(agendaTeamProfile); + } + } + + @Transactional(readOnly = true) + public Page findAgendaTeamWithStatus(UUID agendaKey, AgendaTeamStatus status, Pageable pageable) { + Agenda agenda = agendaRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + return agendaTeamRepository.findByAgendaAndStatus(agenda, status, pageable); + } + + @Transactional(readOnly = true) + public List getCoalitionsFromAgendaTeam(AgendaTeam agendaTeam) { + return agendaTeamProfileRepository.findByAgendaTeamAndIsExistTrue(agendaTeam).stream() + .map(agendaTeamProfile -> agendaTeamProfile.getProfile().getCoalition()) + .collect(Collectors.toList()); + } + + /** + * 아젠다 팀 참여하기 + */ + @Transactional + public void modifyAttendTeam(UserDto user, AgendaTeam agendaTeam, Agenda agenda) { + AgendaProfile agendaProfile = agendaProfileRepository.findByUserId(user.getId()) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + + agendaTeamProfileRepository.findByAgendaAndProfileAndIsExistTrue(agenda, agendaProfile) + .ifPresent(profile -> { + throw new ForbiddenException(AGENDA_TEAM_FORBIDDEN); + }); + + if (agenda.getIsOfficial()) { + Ticket ticket = ticketRepository + .findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc(agendaProfile) + .orElseThrow(() -> new ForbiddenException(TICKET_NOT_EXIST)); + ticket.useTicket(agenda.getAgendaKey()); + } + agenda.attendTeam(agendaProfile.getLocation(), LocalDateTime.now()); + agendaTeam.attendTeam(agenda); + agendaTeamProfileRepository.save(new AgendaTeamProfile(agendaTeam, agenda, agendaProfile)); + } + + /** + * 아젠다 팀 수정하기 + * @param user 사용자 정보, teamUpdateReqDto 팀 수정 요청 정보, agendaId 아젠다 아이디 + */ + @Transactional + public void modifyAgendaTeam(UserDto user, TeamUpdateReqDto teamUpdateReqDto, UUID agendaKey) { + Agenda agenda = agendaRepository.findByAgendaKey(agendaKey) + .orElseThrow(() -> new NotExistException(AGENDA_NOT_FOUND)); + + AgendaTeam agendaTeam = agendaTeamRepository + .findByAgendaAndTeamKeyAndStatus(agenda, teamUpdateReqDto.getTeamKey(), OPEN, CONFIRM) + .orElseThrow(() -> new NotExistException(AGENDA_TEAM_NOT_FOUND)); + + if (!agendaTeam.getLeaderIntraId().equals(user.getIntraId())) { + throw new ForbiddenException(TEAM_LEADER_FORBIDDEN); + } + + List profiles = agendaTeamProfileRepository + .findAllByAgendaTeamAndIsExistTrue(agendaTeam); + + agenda.updateTeam(Location.valueOfLocation(teamUpdateReqDto.getTeamLocation()), LocalDateTime.now()); + agendaTeam.updateTeam(teamUpdateReqDto.getTeamName(), teamUpdateReqDto.getTeamContent(), + teamUpdateReqDto.getTeamIsPrivate(), Location.valueOfLocation(teamUpdateReqDto.getTeamLocation()), + profiles); + agendaTeamRepository.save(agendaTeam); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/TicketController.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/TicketController.java new file mode 100644 index 000000000..1c6b503aa --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/TicketController.java @@ -0,0 +1,131 @@ +package gg.agenda.api.user.ticket.controller; + +import static gg.utils.exception.ErrorCode.*; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import gg.agenda.api.user.agenda.service.AgendaService; +import gg.agenda.api.user.agendaprofile.service.AgendaProfileFindService; +import gg.agenda.api.user.ticket.controller.response.TicketCountResDto; +import gg.agenda.api.user.ticket.controller.response.TicketHistoryResDto; +import gg.agenda.api.user.ticket.service.TicketService; +import gg.auth.UserDto; +import gg.auth.argumentresolver.Login; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; +import gg.utils.cookie.CookieUtil; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.exception.custom.AuthenticationException; +import gg.utils.exception.user.TokenNotValidException; +import io.swagger.v3.oas.annotations.Parameter; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/agenda/ticket") +public class TicketController { + private final CookieUtil cookieUtil; + private final TicketService ticketService; + private final AgendaProfileFindService agendaProfileFindService; + private final AgendaService agendaService; + + /** + * 티켓 설정 추가 + * @param user 사용자 정보 + */ + @PostMapping + public ResponseEntity ticketSetupAdd(@Parameter(hidden = true) @Login UserDto user) { + ticketService.addTicketSetup(user); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + /** + * 티켓 수 조회 + * boolean setupTicket = tickets.size() > approvedCount; setupTicket이 있는지 확인하는 부분 + * @param user 사용자 정보 + */ + @GetMapping + public ResponseEntity ticketCountFind(@Parameter(hidden = true) @Login UserDto user) { + AgendaProfile profile = agendaProfileFindService.findAgendaProfileByIntraId(user.getIntraId()); + List tickets = ticketService.findUsedFalseTicketList(profile); + long approvedCount = tickets.stream() + .filter(Ticket::getIsApproved) + .count(); + boolean setupTicket = tickets.size() > approvedCount; + + return ResponseEntity.ok(new TicketCountResDto(approvedCount, setupTicket)); + } + + /** + * 티켓 승인/거절 + * @param user 사용자 정보 + */ + @PatchMapping + public ResponseEntity ticketApproveModify(@Parameter(hidden = true) @Login UserDto user, + HttpServletResponse response) { + try { + AgendaProfile profile = agendaProfileFindService.findAgendaProfileByIntraId(user.getIntraId()); + ticketService.modifyTicketApprove(profile); + return ResponseEntity.noContent().build(); + } catch (TokenNotValidException e) { + cookieUtil.deleteCookie(response, "refresh_token"); + throw new AuthenticationException(REFRESH_TOKEN_EXPIRED); + } + } + + /** + * 티켓 이력 조회 + * @param user 사용자 정보 + * @param pageRequest 페이지 정보 + */ + @GetMapping("/history") + public ResponseEntity> ticketHistoryList( + @Parameter(hidden = true) @Login UserDto user, @ModelAttribute @Valid PageRequestDto pageRequest) { + int page = pageRequest.getPage(); + int size = pageRequest.getSize(); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("id").descending()); + + Page tickets = ticketService.findTicketsByUserId(user.getId(), pageable); + + List ticketDtos = tickets.stream() + .map(ticket -> { + TicketHistoryResDto dto = new TicketHistoryResDto(ticket); + + if (dto.getIssuedFromKey() != null) { + Agenda agendaIssuedFrom = agendaService.getAgenda(dto.getIssuedFromKey()).orElse(null); + dto.changeIssuedFrom(agendaIssuedFrom); + } + + if (dto.getUsedToKey() != null) { + Agenda agendaUsedTo = agendaService.getAgenda(dto.getUsedToKey()).orElse(null); + dto.changeUsedTo(agendaUsedTo); + } + + return dto; + }) + .collect(Collectors.toList()); + + PageResponseDto pageResponseDto = PageResponseDto.of( + tickets.getTotalElements(), ticketDtos); + return ResponseEntity.ok(pageResponseDto); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/response/TicketCountResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/response/TicketCountResDto.java new file mode 100644 index 000000000..bbf872f53 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/response/TicketCountResDto.java @@ -0,0 +1,16 @@ +package gg.agenda.api.user.ticket.controller.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TicketCountResDto { + private long ticketCount; + private boolean setupTicket; + + public TicketCountResDto(long ticketCount, boolean setupTicket) { + this.ticketCount = ticketCount; + this.setupTicket = setupTicket; + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/response/TicketHistoryResDto.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/response/TicketHistoryResDto.java new file mode 100644 index 000000000..32617aa59 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/controller/response/TicketHistoryResDto.java @@ -0,0 +1,59 @@ +package gg.agenda.api.user.ticket.controller.response; + +import java.time.LocalDateTime; +import java.util.UUID; + +import gg.data.agenda.Agenda; +import gg.data.agenda.Ticket; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +public class TicketHistoryResDto { + private LocalDateTime createdAt; + private String issuedFrom; + private UUID issuedFromKey; + private String usedTo; + private UUID usedToKey; + private boolean isApproved; + private LocalDateTime approvedAt; + private boolean isUsed; + private LocalDateTime usedAt; + + public TicketHistoryResDto(Ticket ticket) { + this.createdAt = ticket.getCreatedAt(); + this.issuedFrom = "42Intra"; + this.issuedFromKey = ticket.getIssuedFrom(); + if (ticket.getIsApproved()) { + this.usedTo = "NotUsed"; + } else { + this.usedTo = "NotApproved"; + } + this.usedToKey = ticket.getUsedTo(); + this.isApproved = ticket.getIsApproved(); + this.approvedAt = ticket.getApprovedAt(); + this.isUsed = ticket.getIsUsed(); + this.usedAt = ticket.getUsedAt(); + } + + public void changeIssuedFrom(Agenda agenda) { + if (agenda == null) { + this.issuedFrom = "42Intra"; + return; + } + this.issuedFrom = agenda.getTitle(); + } + + public void changeUsedTo(Agenda agenda) { + if (agenda == null && !this.isApproved) { + this.usedTo = "NotApproved"; + return; + } + if (agenda == null) { + this.usedTo = "NotUsed"; + return; + } + this.usedTo = agenda.getTitle(); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/service/TicketService.java b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/service/TicketService.java new file mode 100644 index 000000000..e69cf6613 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/user/ticket/service/TicketService.java @@ -0,0 +1,162 @@ +package gg.agenda.api.user.ticket.service; + +import static gg.utils.exception.ErrorCode.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpClientErrorException; + +import gg.auth.FortyTwoAuthUtil; +import gg.auth.UserDto; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.Ticket; +import gg.repo.agenda.AgendaProfileRepository; +import gg.repo.agenda.TicketRepository; +import gg.utils.DateTimeUtil; +import gg.utils.exception.custom.DuplicationException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.external.ApiUtil; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class TicketService { + private final ApiUtil apiUtil; + private final FortyTwoAuthUtil fortyTwoAuthUtil; + private final TicketRepository ticketRepository; + private final AgendaProfileRepository agendaProfileRepository; + + @Value("https://api.intra.42.fr/v2/users/{id}/correction_point_historics?sort=-id") + private String pointHistoryUrl; + + private static final String selfDonation = "Provided points to the pool"; + private static final String autoDonation = "correction points trimming weekly"; + + @Transactional(propagation = Propagation.MANDATORY) + public void refundTicket(AgendaTeamProfile changedTeamProfile) { + Ticket.createRefundedTicket(changedTeamProfile); + } + + /** + * 티켓 설정 추가 + * @param user 사용자 정보 + */ + @Transactional + public void addTicketSetup(UserDto user) { + AgendaProfile profile = agendaProfileRepository.findByUserId(user.getId()) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + Optional optionalTicket = ticketRepository.findByAgendaProfileAndIsApprovedFalse(profile); + if (optionalTicket.isPresent()) { + throw new DuplicationException(ALREADY_TICKET_SETUP); + } + ticketRepository.save(Ticket.createNotApporveTicket(profile)); + } + + /** + * 티켓 수 조회 + * @param profile AgendaProfile + * @return 티켓 수 + */ + @Transactional(readOnly = true) + public List findUsedTrueApproveTrueTicketList(AgendaProfile profile) { + return ticketRepository.findByAgendaProfileAndIsUsedFalseAndIsApprovedTrue(profile); + } + + @Transactional(readOnly = true) + public List findUsedFalseTicketList(AgendaProfile profile) { + return ticketRepository.findByAgendaProfileAndIsUsedFalse(profile); + } + + /** + * 티켓 승인/거절 + * @param profile 사용자 정보 + */ + @Transactional + public void modifyTicketApprove(AgendaProfile profile) { + Ticket setUpTicket = getSetUpTicket(profile); + List> pointHistory = getPointHistory(profile); + processTicketApproval(profile, setUpTicket, pointHistory); + } + + /** + * 티켓 setup 조회 + * @param profile AgendaProfile + * @return 티켓 설정 + */ + public Ticket getSetUpTicket(AgendaProfile profile) { + return ticketRepository.findByAgendaProfileAndIsApprovedFalse(profile) + .orElseThrow(() -> new NotExistException(NOT_SETUP_TICKET)); + } + + /** + * 포인트 이력 조회 + * @param profile AgendaProfile + * @return 포인트 이력 + */ + private List> getPointHistory(AgendaProfile profile) { + String url = pointHistoryUrl.replace("{id}", profile.getFortyTwoId().toString()); + ParameterizedTypeReference>> responseType = new ParameterizedTypeReference<>() { + }; + try { + String accessToken = fortyTwoAuthUtil.getAccessToken(); + return apiUtil.callApiWithAccessToken(url, accessToken, responseType); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) { + String accessToken = fortyTwoAuthUtil.refreshAccessToken(); + return apiUtil.callApiWithAccessToken(url, accessToken, responseType); + } + throw e; + } + } + + /** + * 티켓 승인 처리 + * @param profile AgendaProfile + * @param setUpTicket Ticket + * @param pointHistory 포인트 이력 + */ + private void processTicketApproval(AgendaProfile profile, Ticket setUpTicket, + List> pointHistory) { + LocalDateTime cutoffTime = setUpTicket.getCreatedAt(); + + int ticketSum = pointHistory.stream() + .takeWhile( + history -> DateTimeUtil.convertToSeoulDateTime(history.get("created_at")).isAfter(cutoffTime)) + .filter(history -> { + String reason = history.get("reason"); + return reason.contains(selfDonation) || reason.contains(autoDonation); + }) + .mapToInt(history -> Integer.parseInt(history.get("sum")) * (-1)) + .sum(); + + if (ticketSum == 0) { + throw new NotExistException(POINT_HISTORY_NOT_FOUND); + } + + if (ticketSum >= 2) { + ticketRepository.save(Ticket.createApproveTicket(profile)); + } + + setUpTicket.changeIsApproved(); + ticketRepository.save(setUpTicket); + } + + @Transactional(readOnly = true) + public Page findTicketsByUserId(Long userId, Pageable pageable) { + AgendaProfile profile = agendaProfileRepository.findByUserId(userId) + .orElseThrow(() -> new NotExistException(AGENDA_PROFILE_NOT_FOUND)); + return ticketRepository.findByAgendaProfileId(profile.getId(), pageable); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/utils/AgendaSlackService.java b/gg-agenda-api/src/main/java/gg/agenda/api/utils/AgendaSlackService.java new file mode 100644 index 000000000..70158dcad --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/utils/AgendaSlackService.java @@ -0,0 +1,97 @@ +package gg.agenda.api.utils; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.utils.sns.MessageSender; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaSlackService { + private final MessageSender messageSender; + private final SnsMessageUtil snsMessageUtil; + private final AgendaTeamProfileRepository agendaTeamProfileRepository; + + public void slackAddAgendaAnnouncement(Agenda agenda, AgendaAnnouncement newAnnounce) { + List agendaTeamProfiles = agendaTeamProfileRepository.findAllByAgendaAndIsExistTrue(agenda); + String message = snsMessageUtil.addAgendaAnnouncementMessage(agenda, newAnnounce); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackCancelAgenda(Agenda agenda) { + List agendaTeamProfiles = agendaTeamProfileRepository.findAllByAgendaAndIsExistTrue(agenda); + String message = snsMessageUtil.cancelAgendaMessage(agenda); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackFinishAgenda(Agenda agenda) { + List agendaTeamProfiles = agendaTeamProfileRepository.findAllByAgendaAndIsExistTrue(agenda); + String message = snsMessageUtil.finishAgendaMessage(agenda); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackConfirmAgenda(Agenda agenda) { + List agendaTeamProfiles = agendaTeamProfileRepository.findAllByAgendaAndIsExistTrue(agenda); + String message = snsMessageUtil.confirmAgendaMessage(agenda); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackConfirmAgendaTeam(Agenda agenda, AgendaTeam newTeam) { + List agendaTeamProfiles = agendaTeamProfileRepository.findByAgendaTeamAndIsExistTrue( + newTeam); + String message = snsMessageUtil.confirmTeamMessage(agenda, newTeam); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + if (agenda.getMaxTeam() == agenda.getCurrentTeam()) { + String toHostMessage = snsMessageUtil.agendaHostMinTeamSatisfiedMessage(agenda); + messageSender.send(agenda.getHostIntraId(), toHostMessage); + } else if (agenda.getMinTeam() == agenda.getCurrentTeam()) { + String toHostMessage = snsMessageUtil.agendaHostMaxTeamSatisfiedMessage(agenda); + messageSender.send(agenda.getHostIntraId(), toHostMessage); + } + } + + public void slackCancelAgendaTeam(Agenda agenda, AgendaTeam newTeam) { + List agendaTeamProfiles = agendaTeamProfileRepository.findByAgendaTeamAndIsExistTrue( + newTeam); + String message = snsMessageUtil.cancelTeamMessage(agenda, newTeam); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackCancelByAgendaConfirm(Agenda agenda, List failTeam) { + List agendaTeamProfiles = agendaTeamProfileRepository.findByAgendaTeamInAndIsExistTrue( + failTeam); + String message = snsMessageUtil.failTeamMessage(agenda); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackAttendTeamMate(Agenda agenda, AgendaTeam agendaTeam, String userIntraId) { + List agendaTeamProfiles = agendaTeamProfileRepository.findByAgendaTeamAndIsExistTrue( + agendaTeam); + String message = snsMessageUtil.attendTeamMateMessage(agenda, agendaTeam, userIntraId); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .filter(intraId -> !intraId.equals(userIntraId)) + .forEach(intraId -> messageSender.send(intraId, message)); + } + + public void slackLeaveTeamMate(Agenda agenda, AgendaTeam agendaTeam, String userIntraId) { + List agendaTeamProfiles = agendaTeamProfileRepository.findByAgendaTeamAndIsExistTrue( + agendaTeam); + String message = snsMessageUtil.leaveTeamMateMessage(agenda, agendaTeam, userIntraId); + agendaTeamProfiles.stream().map(atp -> atp.getProfile().getIntraId()) + .forEach(intraId -> messageSender.send(intraId, message)); + } +} diff --git a/gg-agenda-api/src/main/java/gg/agenda/api/utils/SnsMessageUtil.java b/gg-agenda-api/src/main/java/gg/agenda/api/utils/SnsMessageUtil.java new file mode 100644 index 000000000..290355125 --- /dev/null +++ b/gg-agenda-api/src/main/java/gg/agenda/api/utils/SnsMessageUtil.java @@ -0,0 +1,91 @@ +package gg.agenda.api.utils; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.data.agenda.AgendaTeam; + +@Component +public class SnsMessageUtil { + private static final String URL = "https://gg.42seoul.kr/agenda/"; + private static final String SUBJECT = "행사요정🧚으로부터 도착한 편지"; + + public String addAgendaAnnouncementMessage(Agenda agenda, AgendaAnnouncement newAnnounce) { + String link = URL + "agenda_key=" + agenda.getAgendaKey() + "/announcement/" + newAnnounce.getId(); + return SUBJECT + + "\n" + agenda.getTitle() + "의 새로운 공지사항이 도착했습니다." + + "\n" + newAnnounce.getTitle() + + "\n" + "$$" + link + "$$"; + } + + public String confirmAgendaMessage(Agenda agenda) { + String link = URL + "agenda_key=" + agenda.getAgendaKey(); + return SUBJECT + + "\n" + agenda.getTitle() + "이 확정되었습니다." + + "\n" + "행사가 확정되었습니다. 시작일자와 장소를 확인해주세요!" + + "\n" + "$$" + link + "$$"; + } + + public String cancelAgendaMessage(Agenda agenda) { + return SUBJECT + + "\n" + agenda.getTitle() + "이 취소되었습니다." + + "\n" + "아쉽게도 행사가 취소되었습니다. 다음에 다시 만나요!"; + } + + public String finishAgendaMessage(Agenda agenda) { + String link = URL + "agenda_key=" + agenda.getAgendaKey(); + if (agenda.getIsRanking()) { + return SUBJECT + + "\n" + agenda.getTitle() + "이 종료되었습니다." + + "\n" + "행사가 성공적으로 종료되었습니다. 수고하셨습니다!" + + "\n" + "결과 확인 $$" + link + "$$"; + } else { + return SUBJECT + + "\n" + agenda.getTitle() + "이 종료되었습니다." + + "\n" + "행사가 성공적으로 종료되었습니다. 수고하셨습니다!"; + } + } + + public String confirmTeamMessage(Agenda agenda, AgendaTeam agendaTeam) { + return SUBJECT + + "\n" + agenda.getTitle() + "의" + agendaTeam.getName() + "팀이 확정되었습니다." + + "\n" + "행사 확정을 기다려주세요!"; + } + + public String cancelTeamMessage(Agenda agenda, AgendaTeam agendaTeam) { + return SUBJECT + + "\n" + agenda.getTitle() + "의" + agendaTeam.getName() + "팀이 취소되었습니다."; + } + + public String failTeamMessage(Agenda agenda) { + return SUBJECT + + "\n" + agenda.getTitle() + "의 팀이 취소되었습니다." + + "\n" + "행사가 확정되어 확정되지 않은 팀은 취소됩니다."; + } + + public String attendTeamMateMessage(Agenda agenda, AgendaTeam agendaTeam, String intraId) { + return SUBJECT + + "\n" + agenda.getTitle() + "의" + agendaTeam.getName() + "팀에" + intraId + "님이 참가했습니다."; + } + + public String leaveTeamMateMessage(Agenda agenda, AgendaTeam agendaTeam, String intraId) { + return SUBJECT + + "\n" + agenda.getTitle() + "의" + agendaTeam.getName() + "팀에서" + intraId + "님이 탈퇴했습니다."; + } + + public String agendaHostMinTeamSatisfiedMessage(Agenda agenda) { + return SUBJECT + + "\n" + agenda.getTitle() + "행사가 최소 팀 개수를 충족했습니다." + + "\n" + "행사를 확정할 수 있습니다." + + "\n" + "확정시엔 다른 팀들이 참가 할 수 없으니, 주의하세요!" + + "\n" + "$$" + URL + "agenda_key=" + agenda.getAgendaKey() + "$$"; + } + + public String agendaHostMaxTeamSatisfiedMessage(Agenda agenda) { + return SUBJECT + + "\n" + agenda.getTitle() + "행사가 최대 팀 개수를 충족했습니다." + + "\n" + "행사를 확정하고 진행 시간과 장소를 공지사항으로 전달해주세요." + + "\n" + "$$" + URL + "agenda_key=" + agenda.getAgendaKey() + "$$"; + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/AgendaMockData.java b/gg-agenda-api/src/test/java/gg/agenda/api/AgendaMockData.java new file mode 100644 index 000000000..3072f4402 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/AgendaMockData.java @@ -0,0 +1,789 @@ +package gg.agenda.api; + +import static gg.data.agenda.type.AgendaStatus.*; +import static gg.data.agenda.type.Coalition.*; +import static gg.data.agenda.type.Location.*; +import static java.util.UUID.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.persistence.EntityManager; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.Ticket; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.repo.agenda.AgendaAnnouncementRepository; +import gg.repo.agenda.AgendaProfileRepository; +import gg.repo.agenda.AgendaRepository; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.repo.agenda.AgendaTeamRepository; +import gg.repo.agenda.TicketRepository; +import gg.utils.TestDataUtils; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaMockData { + + private final EntityManager em; + private final TicketRepository ticketRepository; + private final AgendaRepository agendaRepository; + private final AgendaTeamRepository agendaTeamRepository; + private final AgendaProfileRepository agendaProfileRepository; + private final AgendaTeamProfileRepository agendaTeamProfileRepository; + private final AgendaAnnouncementRepository agendaAnnouncementRepository; + private final TestDataUtils testDataUtils; + + public Agenda createOfficialAgenda() { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(AgendaStatus.OPEN) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createNonOfficialAgenda() { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(AgendaStatus.OPEN) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public List createOfficialAgendaList(int size, AgendaStatus status) { + List agendas = IntStream.range(0, size).mapToObj(i -> Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(i + 3)) + .startTime(LocalDateTime.now().plusDays(i + 5)) + .endTime(LocalDateTime.now().plusDays(i + 6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(status) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) // true + .isRanking(true) + .build() + ) + .collect(Collectors.toList()); + return agendaRepository.saveAll(agendas); + } + + public List createNonOfficialAgendaList(int size, AgendaStatus status) { + List agendas = IntStream.range(0, size).mapToObj(i -> Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(i + 3)) + .startTime(LocalDateTime.now().plusDays(i + 5)) + .endTime(LocalDateTime.now().plusDays(i + 6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(status) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(false) // false + .isRanking(true) + .build() + ) + .collect(Collectors.toList()); + return agendaRepository.saveAll(agendas); + } + + public AgendaAnnouncement createAgendaAnnouncement(Agenda agenda) { + AgendaAnnouncement announcement = AgendaAnnouncement.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .isShow(true) + .agenda(agenda) + .build(); + em.persist(announcement); + em.flush(); + em.clear(); + return announcement; + } + + public List createAgendaAnnouncementList(Agenda agenda, int size) { + List announcements = new ArrayList<>(); + for (int i = 0; i < size; i++) { + announcements.add(AgendaAnnouncement.builder() + .title("title " + i) + .content("content " + i) + .isShow(true) + .agenda(agenda) + .build()); + } + return agendaAnnouncementRepository.saveAll(announcements); + } + + public List createAgendaAnnouncementList(Agenda agenda, int size, boolean isShow) { + List announcements = new ArrayList<>(); + for (int i = 0; i < size; i++) { + announcements.add(AgendaAnnouncement.builder() + .title("title " + i) + .content("content " + i) + .isShow(isShow) + .agenda(agenda) + .build()); + } + return agendaAnnouncementRepository.saveAll(announcements); + } + + public List createAgendaHistory(int size) { + List agendas = IntStream.range(0, size).mapToObj(i -> Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().minusDays(i + 6)) + .startTime(LocalDateTime.now().minusDays(i + 4)) + .endTime(LocalDateTime.now().minusDays(i + 2)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(FINISH) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build() + ) + .collect(Collectors.toList()); + return agendaRepository.saveAll(agendas); + } + + public Agenda createAgenda() { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(String intraId) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId(intraId) + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(String intraId, LocalDateTime startTime, boolean rank) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(startTime.minusDays(1)) + .startTime(startTime) + .endTime(startTime.plusDays(1)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId(intraId) + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(rank) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(String intraId, LocalDateTime startTime, boolean rank, AgendaStatus status) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(startTime.minusDays(1)) + .startTime(startTime) + .endTime(startTime.plusDays(1)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId(intraId) + .location(SEOUL) + .status(status) + .isOfficial(true) + .isRanking(rank) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(String intraId, LocalDateTime startTime, AgendaStatus status) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(startTime.minusDays(1)) + .startTime(startTime) + .endTime(startTime.plusDays(1)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId(intraId) + .location(SEOUL) + .status(status) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(LocalDateTime deadline) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(deadline) + .startTime(deadline.plusDays(1)) + .endTime(deadline.plusDays(2)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgendaWithAgendaCapacity(int min, int max) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(min) + .maxTeam(max) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgendaWithAgendaCapacityAndStatus(int min, int max, AgendaStatus status) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(min) + .maxTeam(max) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(status) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgendaWithAgendaTeamCapacity(int min, int max) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(2) + .maxTeam(10) + .currentTeam(0) + .minPeople(min) + .maxPeople(max) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(Location location) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(2) + .maxTeam(20) + .currentTeam(1) + .minPeople(1) + .maxPeople(20) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(location) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(int curruentTeam) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(1) + .maxTeam(5) + .currentTeam(curruentTeam) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createNeedMorePeopleAgenda(int curruentTeam) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(1) + .maxTeam(5) + .currentTeam(curruentTeam) + .minPeople(3) + .maxPeople(5) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(AgendaStatus.OPEN) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(AgendaStatus status) { + Agenda agenda = Agenda.builder() + .title("title") + .content("content") + .deadline(LocalDateTime.now().plusDays(1)) + .startTime(LocalDateTime.now().plusDays(2)) + .endTime(LocalDateTime.now().plusDays(3)) + .minTeam(1) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(3) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(SEOUL) + .status(status) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public AgendaProfile createAgendaProfile(User user, Location location) { + AgendaProfile agendaProfile = AgendaProfile.builder() + .content("content") + .githubUrl("githubUrl") + .coalition(LEE) + .location(location) + .intraId(user.getIntraId()) + .userId(user.getId()) + .fortyTwoId(user.getId()) + .build(); + return agendaProfileRepository.save(agendaProfile); + } + + public Ticket createTicket(AgendaProfile agendaProfile) { + Ticket ticket = Ticket.builder() + .agendaProfile(agendaProfile) + .issuedFrom(null) + .usedTo(null) + .isApproved(true) + .approvedAt(LocalDateTime.now().minusDays(1)) + .isUsed(false) + .usedAt(null) + .build(); + return ticketRepository.save(ticket); + } + + public AgendaTeam createAgendaTeam(Agenda agenda) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId("leaderIntraId") + .status(AgendaTeamStatus.OPEN) + .location(SEOUL) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public List createAgendaTeamList(Agenda agenda, int size, AgendaTeamStatus status) { + List agendaTeams = IntStream.range(0, size).mapToObj(i -> AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId("leaderIntraId") + .status(status) + .location(SEOUL) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build() + ) + .collect(Collectors.toList()); + return agendaTeamRepository.saveAll(agendaTeams); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, String teamName) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name(teamName) + .content("content") + .leaderIntraId("leaderIntraId") + .status(AgendaTeamStatus.OPEN) + .location(SEOUL) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, String teamName, AgendaTeamStatus status) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name(teamName) + .content("content") + .leaderIntraId("leaderIntraId") + .status(status) + .location(SEOUL) + .mateCount(3) + .awardPriority(-1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(AgendaTeamStatus.OPEN) + .location(SEOUL) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user, int mateCount) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(AgendaTeamStatus.OPEN) + .location(SEOUL) + .mateCount(mateCount) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user, Location location) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(AgendaTeamStatus.OPEN) + .location(location) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user, Location location, AgendaTeamStatus status) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(status) + .location(location) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(int currentTeam, Agenda agenda, User user, Location location) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(AgendaTeamStatus.OPEN) + .location(location) + .mateCount(currentTeam) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user, Location location, AgendaTeamStatus status, + Boolean isPrivate) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(status) + .location(location) + .mateCount(3) + .awardPriority(1) + .isPrivate(isPrivate) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeamProfile createAgendaTeamProfile(AgendaTeam agendaTeam, AgendaProfile agendaProfile) { + AgendaTeamProfile agendaTeamProfile = AgendaTeamProfile.builder() + .agendaTeam(agendaTeam) + .agenda(agendaTeam.getAgenda()) + .profile(agendaProfile) + .isExist(true) + .build(); + return agendaTeamProfileRepository.save(agendaTeamProfile); + } + + public Agenda createAgendaWithTeam(int teamCount) { + Agenda agenda = createAgenda(SEOUL); + for (int i = 0; i < teamCount; i++) { + AgendaTeam agendaTeam = createAgendaTeam(agenda); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + return agenda; + } + + public Agenda createAgendaWithTeamGyeongsan(int teamCount) { + Agenda agenda = createAgenda(GYEONGSAN); + for (int i = 0; i < teamCount; i++) { + User user = testDataUtils.createNewUser(); + AgendaTeam agendaTeam = createAgendaTeam(agenda, user, GYEONGSAN); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + return agenda; + } + + public Agenda createAgendaWithTeamMix(int teamCount) { + Agenda agenda = createAgenda(MIX); + int half = teamCount / 2; + for (int i = 0; i < teamCount - half; i++) { + User user = testDataUtils.createNewUser(); + AgendaTeam agendaTeam = createAgendaTeam(agenda, user, SEOUL); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + for (int i = 0; i < half; i++) { + User user = testDataUtils.createNewUser(); + AgendaTeam agendaTeam = createAgendaTeam(agenda, user, GYEONGSAN); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + return agenda; + } + + public Agenda createAgendaWithTeamAndAgendaCapacity(int teamCount, int min, int max) { + Agenda agenda = createAgendaWithAgendaCapacity(min, max); + for (int i = 0; i < teamCount; i++) { + AgendaTeam agendaTeam = createAgendaTeam(agenda); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + return agenda; + } + + public Agenda createAgendaWithTeamAndAgendaCapacityAndFinish(int teamCount, int min, int max) { + Agenda agenda = createAgendaWithAgendaCapacity(min, max); + for (int i = 0; i < teamCount; i++) { + AgendaTeam agendaTeam = createAgendaTeam(agenda); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + agenda.updateSchedule(LocalDateTime.now().minusDays(2), + LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1)); + agenda.confirmAgenda(); + agenda.finishAgenda(); + em.persist(agenda); + em.flush(); + em.clear(); + return agenda; + } + + public Agenda createAgendaWithTeamAndAgendaTeamCapacity(int teamCount, int min, int max) { + Agenda agenda = createAgendaWithAgendaTeamCapacity(min, max); + for (int i = 0; i < teamCount; i++) { + User user = testDataUtils.createNewUser(); + AgendaTeam agendaTeam = createAgendaTeam(agenda, user, 10); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + return agenda; + } + + public Agenda createAgendaWithTeamAndAgendaTeamCapacityAndFinish(int teamCount, int min, int max) { + Agenda agenda = createAgendaWithAgendaCapacityAndStatus(min, max, CONFIRM); + for (int i = 0; i < teamCount; i++) { + User user = testDataUtils.createNewUser(); + AgendaTeam agendaTeam = createAgendaTeam(agenda, user, 3); + agendaTeam.confirm(); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + em.persist(agendaTeam); + em.flush(); + em.clear(); + } + return agenda; + } + + public Agenda createAgendaWithStatusAndTeamWithAgendaTeamCapacity(AgendaStatus status, + int teamCount, int min, int max) { + Agenda agenda = createAgendaWithAgendaCapacityAndStatus(min, max, status); + for (int i = 0; i < teamCount; i++) { + User user = testDataUtils.createNewUser(); + AgendaTeam agendaTeam = createAgendaTeam(agenda, user, 3); + agendaTeam.confirm(); + agenda.addTeam(agendaTeam.getLocation(), LocalDateTime.now()); + em.persist(agendaTeam); + em.flush(); + em.clear(); + } + return agenda; + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agenda/controller/AgendaAdminControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agenda/controller/AgendaAdminControllerTest.java new file mode 100644 index 000000000..901d705c6 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agenda/controller/AgendaAdminControllerTest.java @@ -0,0 +1,662 @@ +package gg.agenda.api.admin.agenda.controller; + +import static gg.data.agenda.type.AgendaStatus.*; +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.MultiValueMap; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.agenda.api.AgendaMockData; +import gg.agenda.api.admin.agenda.controller.request.AgendaAdminUpdateReqDto; +import gg.agenda.api.admin.agenda.controller.response.AgendaAdminResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.repo.agenda.AgendaPosterImageRepository; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.converter.MultiValueMapConverter; +import gg.utils.dto.PageResponseDto; +import gg.utils.file.handler.AwsImageHandler; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaPosterImageFixture; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaAdminControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private TestDataUtils testDataUtils; + + @Autowired + private AgendaMockData agendaMockData; + + @Autowired + private AgendaFixture agendaFixture; + + @Autowired + private AgendaPosterImageFixture agendaPosterImageFixture; + + @Autowired + EntityManager em; + + @Autowired + AgendaAdminRepository agendaAdminRepository; + + @Autowired + AgendaPosterImageRepository agendaPosterImageRepository; + + @Value("${info.image.defaultUrl}") + private String defaultUri; + + @MockBean + private AwsImageHandler imageHandler; + + private User user; + + private String accessToken; + + @BeforeEach + void setUp() { + user = testDataUtils.createAdminUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Nested + @DisplayName("Admin Agenda 상세 조회") + class GetAgendaAdmin { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("Admin Agenda 상세 조회 성공") + void findAgendaByAgendaKeySuccessAdmin(int page) throws Exception { + // given + int size = 10; + List agendas = new ArrayList<>(); + agendas.addAll(agendaMockData.createOfficialAgendaList(5, AgendaStatus.OPEN)); + agendas.addAll(agendaMockData.createOfficialAgendaList(5, AgendaStatus.FINISH)); + agendas.addAll(agendaMockData.createOfficialAgendaList(5, AgendaStatus.CANCEL)); + agendas.addAll(agendaMockData.createNonOfficialAgendaList(5, AgendaStatus.OPEN)); + agendas.addAll(agendaMockData.createNonOfficialAgendaList(5, AgendaStatus.FINISH)); + agendas.addAll(agendaMockData.createNonOfficialAgendaList(5, AgendaStatus.CANCEL)); + + // when + String response = mockMvc.perform(get("/agenda/admin/request/list") + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo( + ((page - 1) * size) < agendas.size() ? Math.min(size, agendas.size() - (page - 1) * size) : 0); + agendas.sort((a, b) -> b.getId().compareTo(a.getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getAgendaId()).isEqualTo(agendas.get(i + (page - 1) * size).getId()); + } + } + + @Test + @DisplayName("Admin Agenda 상세 조회 성공 - 대회가 존재하지 않는 경우") + void findAgendaByAgendaKeySuccessAdminWithNoContent() throws Exception { + // given + int page = 1; + int size = 10; + + // when + String response = mockMvc.perform(get("/agenda/admin/request/list") + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(0); + } + } + + @Nested + @DisplayName("Admin Agenda 수정 및 삭제") + class UpdateAgendaAdmin { + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - 기본 정보") + void updateAgendaAdminSuccessWithInformation() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaTitle("updated title").agendaContent("updated content") + .agendaStatus(FINISH).isOfficial(!agenda.getIsOfficial()) + .isRanking(!agenda.getIsRanking()).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getTitle()).isEqualTo(agendaDto.getAgendaTitle()); + assertThat(updated.get().getContent()).isEqualTo(agendaDto.getAgendaContent()); + assertThat(updated.get().getStatus()).isEqualTo(agendaDto.getAgendaStatus()); + assertThat(updated.get().getIsOfficial()).isEqualTo(agendaDto.getIsOfficial()); + assertThat(updated.get().getIsRanking()).isEqualTo(agendaDto.getIsRanking()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - 스케쥴 정보") + void updateAgendaAdminSuccessWithSchedule() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder() + .agendaDeadLine(agenda.getDeadline().plusDays(1)) + .agendaStartTime(agenda.getStartTime().plusDays(1)) + .agendaEndTime(agenda.getEndTime().plusDays(1)) + .build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + params.get("agendaDeadLine").remove(0); + params.get("agendaDeadLine").add(agendaDto.getAgendaDeadLine().toString().substring(0, 19)); + params.get("agendaStartTime").remove(0); + params.get("agendaStartTime").add(agendaDto.getAgendaStartTime().toString().substring(0, 19)); + params.get("agendaEndTime").remove(0); + params.get("agendaEndTime").add(agendaDto.getAgendaEndTime().toString().substring(0, 19)); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getDeadline()) + .isEqualTo(agendaDto.getAgendaDeadLine().toString().substring(0, 19)); + assertThat(updated.get().getStartTime()) + .isEqualTo(agendaDto.getAgendaStartTime().toString().substring(0, 19)); + assertThat(updated.get().getEndTime()) + .isEqualTo(agendaDto.getAgendaEndTime().toString().substring(0, 19)); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - 서울 대회를 MIX로 변경") + void updateAgendaAdminSuccessWithLocationSeoulToMix() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); // SEOUL + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(MIX).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getLocation()).isEqualTo(agendaDto.getAgendaLocation()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - 서울 대회를 경산으로 변경") + void updateAgendaAdminSuccessWithLocationSeoulToGyeongsan() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgenda(); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(GYEONGSAN).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getLocation()).isEqualTo(agendaDto.getAgendaLocation()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - 경산 대회를 서울로 변경") + void updateAgendaAdminSuccessWithLocationGyeongsanToSeoul() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgenda(GYEONGSAN); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(GYEONGSAN).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getLocation()).isEqualTo(agendaDto.getAgendaLocation()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - 경산 대회를 MIX로 변경") + void updateAgendaAdminSuccessWithLocationGyeongsanToMix() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamGyeongsan(10); // SEOUL + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(MIX).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getLocation()).isEqualTo(agendaDto.getAgendaLocation()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - Agenda 팀 제한 정보") + void updateAgendaAdminSuccessWithAgendaCapacity() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaMinTeam(agenda.getMinTeam() + 1) + .agendaMaxTeam(agenda.getMaxTeam() + 1).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getMinTeam()).isEqualTo(agendaDto.getAgendaMinTeam()); + assertThat(updated.get().getMaxTeam()).isEqualTo(agendaDto.getAgendaMaxTeam()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 - Agenda 팀 허용 인원 제한 정보") + void updateAgendaAdminSuccessWithAgendaTeamCapacity() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinPeople(agenda.getMinPeople() + 1) + .agendaMaxPeople(agenda.getMaxPeople() + 1).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getMinPeople()).isEqualTo(agendaDto.getAgendaMinPeople()); + assertThat(updated.get().getMaxPeople()).isEqualTo(agendaDto.getAgendaMaxPeople()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 성공 -Poster URI 변경") + void updateAgendaAdminSuccessWithAgendaPosterUri() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + URL newMockUrl = new URL("https://newPosterUri.com"); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(newMockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); + agendaPosterImageFixture.createAgendaPosterImage(agenda, mockUrl.toString()); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder() + .agendaPosterUri(newMockUrl) + .build(); + MockMultipartFile multipartFile = new MockMultipartFile("file", "test.jpg", "image/jpeg", + "test".getBytes()); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // when + mockMvc.perform(multipart("/agenda/admin/request") + .file("agendaPoster", multipartFile.getBytes()) + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional updated = agendaAdminRepository.findByAgendaKey(agenda.getAgendaKey()); + + // then + assert (updated.isPresent()); + assertThat(updated.get().getPosterUri()).isEqualTo(agendaDto.getAgendaPosterUri().toString()); + agendaPosterImageRepository.findByAgendaIdAndIsCurrentTrue(agenda.getId()) + .ifPresentOrElse(posterImage -> assertThat(posterImage.getImageUri()) + .isEqualTo(agendaDto.getAgendaPosterUri().toString()), + () -> fail("Agenda Poster Image not found")); + agendaPosterImageRepository.findByAgendaIdAndIsCurrentFalse(agenda.getId()) + .ifPresentOrElse(posterImage -> assertThat(posterImage.getIsCurrent()) + .isEqualTo(false), + () -> fail("Agenda Poster Image not found")); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 대회가 존재하지 않는 경우") + void updateAgendaAdminFailedWithNoAgenda() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", UUID.randomUUID().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 서울 대회를 경산으로 변경할 수 없는 경우") + void updateAgendaAdminFailedWithLocationSeoulToGyeongSan() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeam(10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(GYEONGSAN).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 경산 대회를 서울 대회로 변경할 수 없는 경우") + void updateAgendaAdminFailedWithLocationGyeongSanToSeoul() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamGyeongsan(10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(SEOUL).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @EnumSource(value = Location.class, names = {"SEOUL", "GYEONGSAN"}) + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 혼합 대회를 다른 지역 대회로 변경할 수 없는 경우") + void updateAgendaAdminFailedWithLocationMixToSeoul() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamMix(10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(SEOUL).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .params(params) + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 이미 maxTeam 이상의 팀이 존재하는 경우") + void updateAgendaAdminFailedWithAgendaInvalidCapacity() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamAndAgendaCapacity(10, 2, 10); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinTeam(10).agendaMaxTeam(2).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 이미 maxTeam 이상의 팀이 존재하는 경우") + void updateAgendaAdminFailedWithMaxTeam() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamAndAgendaCapacity(10, 2, 10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaMinTeam(agenda.getMinTeam()) + .agendaMaxTeam(agenda.getMaxTeam() - 5).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 이미 종료한 대회에 minTeam 이하의 팀이 참여한 경우") + void updateAgendaAdminFailedWithMinTeam() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamAndAgendaCapacityAndFinish(5, 5, 10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaMinTeam(agenda.getMinTeam() + 2) + .agendaMaxTeam((agenda.getMaxTeam())).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - minPeople이 maxPeople보다 큰 경우") + void updateAgendaAdminFailedWithAgendaTeamInvalidCapacity() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamAndAgendaTeamCapacity(10, 2, 10); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinPeople(10).agendaMaxPeople(2).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 이미 팀에 maxPeople 이상의 인원이 참여한 경우") + void updateAgendaAdminFailedWithMaxPeople() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithTeamAndAgendaTeamCapacity(10, 2, 10); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaMinPeople(agenda.getMinPeople()) + .agendaMaxPeople(agenda.getMaxPeople() - 5).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin Agenda 수정 및 삭제 실패 - 이미 확정된 팀에 minPeople 이하의 인원이 참여한 경우") + void updateAgendaAdminFailedWithMinPeople() throws Exception { + // given + URL mockUrl = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); + Agenda agenda = agendaMockData.createAgendaWithStatusAndTeamWithAgendaTeamCapacity(OPEN, + 10, 3, 10); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinPeople(5).agendaMaxPeople(agenda.getMaxPeople()).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, agendaDto); + + // expected + mockMvc.perform(multipart("/agenda/admin/request") + .param("agenda_key", agenda.getAgendaKey().toString()) + .params(params) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("Admin Agenda 목록 간단 조회") + class GetAgendaAdminSimple { + @Test + @DisplayName("Admin Agenda 목록 간단 조회 성공") + void getAgendaAdminSimpleSuccess() throws Exception { + // given + agendaFixture.createAgenda(OPEN); + agendaFixture.createAgenda(FINISH); + agendaFixture.createAgenda(CANCEL); + + // when + String response = mockMvc.perform(get("/agenda/admin/list") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaAdminResDto[] result = objectMapper.readValue(response, AgendaAdminResDto[].class); + + // then + assertThat(result).hasSize(3); + } + + @Test + @DisplayName("Admin Agenda 목록 간단 조회 성공 - 빈 리스트 반환") + void getAgendaAdminSimpleSuccessWithEmtpyList() throws Exception { + // given + // when + String response = mockMvc.perform(get("/agenda/admin/list") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaAdminResDto[] result = objectMapper.readValue(response, AgendaAdminResDto[].class); + + // then + assertThat(result).isEmpty(); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agenda/service/AgendaAdminServiceTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agenda/service/AgendaAdminServiceTest.java new file mode 100644 index 000000000..006740e30 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agenda/service/AgendaAdminServiceTest.java @@ -0,0 +1,386 @@ +package gg.agenda.api.admin.agenda.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.net.URL; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.mock.web.MockMultipartFile; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaTeamAdminRepository; +import gg.agenda.api.admin.agenda.controller.request.AgendaAdminUpdateReqDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.repo.agenda.AgendaPosterImageRepository; +import gg.utils.annotation.UnitTest; +import gg.utils.exception.custom.InvalidParameterException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.file.handler.AwsImageHandler; + +@UnitTest +public class AgendaAdminServiceTest { + + @Mock + AgendaAdminRepository agendaAdminRepository; + + @Mock + AgendaTeamAdminRepository agendaTeamAdminRepository; + + @Mock + AgendaPosterImageRepository agendaPosterImageRepository; + + @Mock + AwsImageHandler imageHandler; + + @InjectMocks + AgendaAdminService agendaAdminService; + + @Nested + @DisplayName("Admin Agenda 상세 조회") + class GetAgendaAdmin { + + @Test + @DisplayName("Admin Agenda 상세 조회 성공") + void findAgendaByAgendaKeySuccessAdmin() { + // given + Pageable pageable = mock(Pageable.class); + List agendas = new ArrayList<>(); + agendas.add(Agenda.builder().build()); + Page page = new PageImpl<>(agendas); + when(agendaAdminRepository.findAll(pageable)).thenReturn(page); + + // when + Page result = agendaAdminService.getAgendaRequestList(pageable); + + // then + verify(agendaAdminRepository, times(1)).findAll(pageable); + assertThat(result).isNotEmpty(); + } + + @Test + @DisplayName("Admin Agenda 상세 조회 성공 - 빈 리스트인 경우") + void findAgendaByAgendaKeySuccessAdminWithNoContent() { + // given + Pageable pageable = mock(Pageable.class); + Page page = new PageImpl<>(List.of()); + when(agendaAdminRepository.findAll(pageable)).thenReturn(page); + + // when + Page result = agendaAdminService.getAgendaRequestList(pageable); + + // then + verify(agendaAdminRepository, times(1)).findAll(pageable); + assertThat(result).isEmpty(); + } + } + + @Nested + @DisplayName("Admin Agenda 수정") + class UpdateAgenda { + + private final String defaultUrl = "http://localhost:8080/images/image.jpeg"; + + @Test + @DisplayName("Admin Agenda 수정 성공 - 기본 정보") + void updateAgendaSuccessWithInformation() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + Agenda agenda = Agenda.builder().title("title").content("content").posterUri("posterUri").isOfficial(false) + .isRanking(true).status(AgendaStatus.FINISH).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaTitle("Updated title").agendaContent("Updated content") + .isOfficial(true).isRanking(true) + .agendaStatus(AgendaStatus.CANCEL).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // when + agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any()); + verify(agendaTeamAdminRepository, times(1)).findAllByAgenda(any()); + assertThat(agenda.getTitle()).isEqualTo(agendaDto.getAgendaTitle()); + assertThat(agenda.getContent()).isEqualTo(agendaDto.getAgendaContent()); + assertThat(agenda.getPosterUri()).isEqualTo(defaultUrl); + assertThat(agenda.getIsOfficial()).isEqualTo(agendaDto.getIsOfficial()); + assertThat(agenda.getIsRanking()).isEqualTo(agendaDto.getIsRanking()); + assertThat(agenda.getStatus()).isEqualTo(agendaDto.getAgendaStatus()); + } + + @Test + @DisplayName("Admin Agenda 수정 성공 - 스케줄 정보") + void updateAgendaSuccessWithSchedule() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + Agenda agenda = + Agenda.builder().deadline(LocalDateTime.now().minusDays(3)).startTime(LocalDateTime.now().plusDays(1)) + .endTime(LocalDateTime.now().plusDays(3)).build(); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaDeadLine(LocalDateTime.now()) + .agendaStartTime(LocalDateTime.now().plusDays(3)).agendaEndTime(LocalDateTime.now().plusDays(5)) + .build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // when + agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any()); + verify(agendaTeamAdminRepository, times(1)).findAllByAgenda(any()); + assertThat(agenda.getDeadline()).isEqualTo(agendaDto.getAgendaDeadLine()); + assertThat(agenda.getStartTime()).isEqualTo(agendaDto.getAgendaStartTime()); + assertThat(agenda.getEndTime()).isEqualTo(agendaDto.getAgendaEndTime()); + } + + @Test + @DisplayName("Admin Agenda 수정 성공 - 지역 정보") + void updateAgendaSuccessWithLocation() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + Agenda agenda = Agenda.builder().location(Location.SEOUL).build(); + AgendaAdminUpdateReqDto agendaDto = AgendaAdminUpdateReqDto.builder().agendaLocation(Location.MIX).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // when + agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any()); + verify(agendaTeamAdminRepository, times(1)).findAllByAgenda(any()); + assertThat(agenda.getLocation()).isEqualTo(agendaDto.getAgendaLocation()); + } + + @Test + @DisplayName("Admin Agenda 수정 성공 - Agenda 팀 제한 정보") + void updateAgendaSuccessWithAgendaCapacity() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + Agenda agenda = Agenda.builder().minTeam(5).maxTeam(10).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinTeam(2).agendaMaxTeam(20).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // when + agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any()); + verify(agendaTeamAdminRepository, times(1)).findAllByAgenda(any()); + assertThat(agenda.getMinTeam()).isEqualTo(agendaDto.getAgendaMinTeam()); + assertThat(agenda.getMaxTeam()).isEqualTo(agendaDto.getAgendaMaxTeam()); + } + + @Test + @DisplayName("Admin Agenda 수정 성공 - Agenda 팀 인원 제한 정보") + void updateAgendaSuccessWithAgendaTeamCapacity() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + Agenda agenda = Agenda.builder().minPeople(1).maxPeople(10).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinPeople(2).agendaMaxPeople(20).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // when + agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any()); + verify(agendaTeamAdminRepository, times(1)).findAllByAgenda(any()); + assertThat(agenda.getMinTeam()).isEqualTo(agendaDto.getAgendaMinTeam()); + assertThat(agenda.getMaxTeam()).isEqualTo(agendaDto.getAgendaMaxTeam()); + } + + @Test + @DisplayName("Admin Agenda 수정 실패 - Agenda를 찾을 수 없음") + void updateAgendaFailAdminWithNotExistAgenda() { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + AgendaAdminUpdateReqDto agendaDto = mock(AgendaAdminUpdateReqDto.class); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaAdminService.updateAgenda(UUID.randomUUID(), agendaDto, file)); + } + + @Test + @DisplayName("Admin Agenda 수정 실패 - Agenda 지역을 변경할 수 없음") + void updateAgendaFailAdminWithCannotChangeLocation() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } // SEOUL + teams.add(AgendaTeam.builder().location(Location.GYEONGSAN).mateCount(3).build()); // GYEONGSAN + Agenda agenda = Agenda.builder().currentTeam(teams.size()).location(Location.MIX).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaLocation(Location.SEOUL).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file)); + } + + @Test + @DisplayName("Admin Agenda 수정 실패 - Agenda 팀 제한을 변경할 수 없음") + void updateAgendaFailAdminWithCannotChangeMinTeam() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 5; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } // 10개 팀 + Agenda agenda = + Agenda.builder().currentTeam(teams.size()).minTeam(5).maxTeam(10).status(AgendaStatus.FINISH).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinTeam(10).agendaMaxTeam(20).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file)); + } + + @Test + @DisplayName("Admin Agenda 수정 실패 - Agenda 팀 제한을 변경할 수 없음") + void updateAgendaFailAdminWithCannotChangeMaxTeam() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 10; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } // 10개 팀 + Agenda agenda = Agenda.builder().currentTeam(teams.size()).minTeam(5).maxTeam(10).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinTeam(2).agendaMaxTeam(5).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file)); + } + + @Test + @DisplayName("Admin Agenda 수정 실패 - Agenda 팀 인원 제한을 변경할 수 없음") + void updateAgendaFailAdminWithCannotChangeMaxPeople() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(10).status(AgendaTeamStatus.CONFIRM) + .build()); // mateCount 10 + Agenda agenda = Agenda.builder().currentTeam(teams.size()).minPeople(1).maxPeople(10).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinPeople(2).agendaMaxPeople(5).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file)); + } + + @Test + @DisplayName("Admin Agenda 수정 실패 - Agenda 팀 인원 제한을 변경할 수 없음") + void updateAgendaFailAdminWithCannotChangeMinPeople() throws IOException { + // given + MockMultipartFile file = new MockMultipartFile("file", "test.jpg", + "image/jpeg", "test".getBytes()); + int teamCount = 3; + List teams = new ArrayList<>(); + for (int i = 0; i < teamCount; i++) { + teams.add(AgendaTeam.builder().location(Location.SEOUL).mateCount(3).build()); + } + teams.add( + AgendaTeam.builder().location(Location.SEOUL).mateCount(3).status(AgendaTeamStatus.CONFIRM).build()); + Agenda agenda = Agenda.builder().currentTeam(teams.size()).minPeople(1).maxPeople(10).build(); + AgendaAdminUpdateReqDto agendaDto = + AgendaAdminUpdateReqDto.builder().agendaMinPeople(5).agendaMaxPeople(20).build(); + when(agendaAdminRepository.findByAgendaKey(any())).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any())).thenReturn(teams); + when(imageHandler.uploadImageOrDefault(any(), any(), any())).thenReturn(new URL(defaultUrl)); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaAdminService.updateAgenda(agenda.getAgendaKey(), agendaDto, file)); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaannouncement/controller/AgendaAnnouncementAdminControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaannouncement/controller/AgendaAnnouncementAdminControllerTest.java new file mode 100644 index 000000000..25f1049a8 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaannouncement/controller/AgendaAnnouncementAdminControllerTest.java @@ -0,0 +1,261 @@ +package gg.agenda.api.admin.agendaannouncement.controller; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaAnnouncementAdminRepository; +import gg.agenda.api.admin.agendaannouncement.controller.request.AgendaAnnouncementAdminUpdateReqDto; +import gg.agenda.api.user.agendaannouncement.controller.response.AgendaAnnouncementResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.data.user.User; +import gg.utils.AgendaTestDataUtils; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.fixture.agenda.AgendaAnnouncementFixture; +import gg.utils.fixture.agenda.AgendaFixture; + +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaAnnouncementAdminControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private TestDataUtils testDataUtils; + + @Autowired + private AgendaFixture agendaFixture; + + @Autowired + private AgendaAnnouncementFixture agendaAnnouncementFixture; + + @Autowired + private AgendaTestDataUtils agendaTestDataUtils; + + @Autowired + EntityManager em; + + @Autowired + AgendaAdminRepository agendaAdminRepository; + + @Autowired + AgendaAnnouncementAdminRepository agendaAnnouncementAdminRepository; + + private User user; + + private String accessToken; + + @BeforeEach + void setUp() { + user = testDataUtils.createAdminUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Nested + @DisplayName("Admin AgendaAnnouncement 상세 조회") + class GetAgendaAnnouncementListAdmin { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5, 6}) + @DisplayName("Admin AgendaAnnouncement 상세 조회 성공") + void getAgendaAnnouncementAdminSuccess(int page) throws Exception { + // given + int size = 10; + Agenda agenda = agendaFixture.createAgenda(); + List announcements = + agendaAnnouncementFixture.createAgendaAnnouncementList(agenda, 37); + + // when + String response = mockMvc.perform(get("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue( + response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(((page - 1) * size) < announcements.size() + ? Math.min(size, announcements.size() - (page - 1) * size) : 0); + announcements.sort((a, b) -> b.getId().compareTo(a.getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getId()).isEqualTo(announcements.get(i + (page - 1) * size).getId()); + } + } + + @Test + @DisplayName("Admin AgendaAnnouncement 상세 조회 성공 - 빈 리스트 반환") + void getAgendaAnnouncementAdminSuccessWithNoContent() throws Exception { + // given + int page = 1; + int size = 10; + Agenda agenda = agendaFixture.createAgenda(); + + // when + String response = mockMvc.perform(get("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue( + response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(0); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 상세 조회 실패 - Agenda가 없는 경우") + void getAgendaAnnouncementAdminFailedWithNoAgenda() throws Exception { + // given + int page = 1; + int size = 10; + + // expected + mockMvc.perform(get("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", UUID.randomUUID().toString()) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("Admin AgendaAnnouncement 수정 및 삭제") + class UpdateAgendaAnnouncementAdmin { + @Test + @DisplayName("Admin AgendaAnnouncement 수정 성공") + void updateAgendaAnnouncementAdminSuccess() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaAnnouncement announcement = agendaAnnouncementFixture.createAgendaAnnouncement(agenda); + AgendaAnnouncementAdminUpdateReqDto updateReqDto = AgendaAnnouncementAdminUpdateReqDto.builder() + .id(announcement.getId()).isShow(!announcement.getIsShow()) + .title("수정된 공지사항 제목").content("수정된 공지사항 내용").build(); + String request = objectMapper.writeValueAsString(updateReqDto); + + // when + mockMvc.perform(patch("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNoContent()); + AgendaAnnouncement result = agendaAnnouncementAdminRepository + .findById(announcement.getId()).orElseThrow(); + + // then + assertThat(result.getTitle()).isEqualTo(updateReqDto.getTitle()); + assertThat(result.getContent()).isEqualTo(updateReqDto.getContent()); + assertThat(result.getIsShow()).isEqualTo(updateReqDto.getIsShow()); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 수정 실패 - 존재하지 않는 공지사항") + void updateAgendaAnnouncementAdminFailedWithNoAnnouncement() throws Exception { + // given + AgendaAnnouncementAdminUpdateReqDto updateReqDto = AgendaAnnouncementAdminUpdateReqDto.builder() + .id(1L).isShow(true).title("수정된 공지사항 제목").content("수정된 공지사항 내용").build(); + String request = objectMapper.writeValueAsString(updateReqDto); + + // expected + mockMvc.perform(patch("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 수정 실패 - Update Dto에 id가 없는 경우") + void updateAgendaAnnouncementAdminFailedWithNoReqId() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaAnnouncement announcement = agendaAnnouncementFixture.createAgendaAnnouncement(agenda); + AgendaAnnouncementAdminUpdateReqDto updateReqDto = AgendaAnnouncementAdminUpdateReqDto.builder() + .isShow(!announcement.getIsShow()).title("수정된 공지사항 제목").content("수정된 공지사항 내용").build(); + String request = objectMapper.writeValueAsString(updateReqDto); + + // expected + mockMvc.perform(patch("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 수정 실패 - title이 너무 긴 경우") + void updateAgendaAnnouncementAdminFailedWithTooLongTitle() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaAnnouncement announcement = agendaAnnouncementFixture.createAgendaAnnouncement(agenda); + AgendaAnnouncementAdminUpdateReqDto updateReqDto = AgendaAnnouncementAdminUpdateReqDto.builder() + .id(announcement.getId()).isShow(!announcement.getIsShow()) + .title("수정된 공지사항 제목".repeat(10)).content("수정된 공지사항 내용").build(); + String request = objectMapper.writeValueAsString(updateReqDto); + + // expected + mockMvc.perform(patch("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 수정 실패 - content이 너무 긴 경우") + void updateAgendaAnnouncementAdminFailedWithTooLongContent() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaAnnouncement announcement = agendaAnnouncementFixture.createAgendaAnnouncement(agenda); + AgendaAnnouncementAdminUpdateReqDto updateReqDto = AgendaAnnouncementAdminUpdateReqDto.builder() + .id(announcement.getId()).isShow(!announcement.getIsShow()) + .title("수정된 공지사항 제목").content("수정된 공지사항 내용".repeat(100)).build(); + String request = objectMapper.writeValueAsString(updateReqDto); + + // expected + mockMvc.perform(patch("/agenda/admin/announcement") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaannouncement/service/AgendaAnnouncementAdminServiceTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaannouncement/service/AgendaAnnouncementAdminServiceTest.java new file mode 100644 index 000000000..e934f3a59 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaannouncement/service/AgendaAnnouncementAdminServiceTest.java @@ -0,0 +1,127 @@ +package gg.agenda.api.admin.agendaannouncement.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaAnnouncementAdminRepository; +import gg.agenda.api.admin.agendaannouncement.controller.request.AgendaAnnouncementAdminUpdateReqDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.utils.annotation.UnitTest; +import gg.utils.exception.custom.NotExistException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@UnitTest +public class AgendaAnnouncementAdminServiceTest { + + @Mock + private AgendaAdminRepository agendaAdminRepository; + + @Mock + private AgendaAnnouncementAdminRepository agendaAnnouncementAdminRepository; + + @InjectMocks + private AgendaAnnouncementAdminService agendaAnnouncementAdminService; + + @Nested + @DisplayName("Admin AgendaAnnouncement 상세 조회") + class GetAgendaAnnouncementListAdmin { + + @Test + @DisplayName("Admin AgendaAnnouncement 상세 조회 성공") + void getAgendaAnnouncementAdminSuccess() { + // given + Agenda agenda = Agenda.builder().build(); + List announcements = new ArrayList<>(); + for (int i = 0; i < 30; i++) { + announcements.add(AgendaAnnouncement.builder().agenda(agenda).build()); + } + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + when(agendaAdminRepository.findByAgendaKey(any(UUID.class))).thenReturn(Optional.of(agenda)); + when(agendaAnnouncementAdminRepository.findAllByAgenda(any(Agenda.class), any(Pageable.class))) + .thenReturn(new PageImpl<>(announcements)); + + // when + agendaAnnouncementAdminService.getAgendaAnnouncementList(agenda.getAgendaKey(), pageable); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any(UUID.class)); + verify(agendaAnnouncementAdminRepository, times(1)).findAllByAgenda(any(Agenda.class), any(Pageable.class)); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 상세 조회 성공 - 빈 리스트 반환") + void getAgendaAnnouncementAdminSuccessWithNoContent() { + // given + Agenda agenda = Agenda.builder().build(); + List announcements = new ArrayList<>(); + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + when(agendaAdminRepository.findByAgendaKey(any(UUID.class))).thenReturn(Optional.of(agenda)); + when(agendaAnnouncementAdminRepository.findAllByAgenda(any(Agenda.class), any(Pageable.class))) + .thenReturn(new PageImpl<>(announcements)); + + // when + agendaAnnouncementAdminService.getAgendaAnnouncementList(agenda.getAgendaKey(), pageable); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any(UUID.class)); + verify(agendaAnnouncementAdminRepository, times(1)).findAllByAgenda(any(Agenda.class), any(Pageable.class)); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 상세 조회 실패 - Agenda가 없는 경우") + void getAgendaAnnouncementAdminFailedWithNoAgenda() { + // given + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + when(agendaAdminRepository.findByAgendaKey(any(UUID.class))).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaAnnouncementAdminService.getAgendaAnnouncementList(UUID.randomUUID(), pageable)); + } + } + + @Nested + @DisplayName("Admin AgendaAnnouncement 수정 및 삭제") + class UpdateAgendaAnnouncementAdmin { + @Test + @DisplayName("Admin AgendaAnnouncement 수정 성공") + void updateAgendaAnnouncementAdminSuccess() { + // given + AgendaAnnouncement announcement = AgendaAnnouncement.builder().build(); + when(agendaAnnouncementAdminRepository.findById(any(Long.class))).thenReturn(Optional.of(announcement)); + + // expected + assertDoesNotThrow(() -> agendaAnnouncementAdminService.updateAgendaAnnouncement( + AgendaAnnouncementAdminUpdateReqDto.builder().id(1L).build())); + } + + @Test + @DisplayName("Admin AgendaAnnouncement 수정 실패 - AgendaAnnouncement가 없는 경우") + void updateAgendaAnnouncementAdminFailed() { + // given + when(agendaAnnouncementAdminRepository.findById(any(Long.class))).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, () -> agendaAnnouncementAdminService.updateAgendaAnnouncement( + AgendaAnnouncementAdminUpdateReqDto.builder().id(1L).build())); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaprofile/AgendaProfileAdminControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaprofile/AgendaProfileAdminControllerTest.java new file mode 100644 index 000000000..ce482e597 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendaprofile/AgendaProfileAdminControllerTest.java @@ -0,0 +1,170 @@ +package gg.agenda.api.admin.agendaprofile; + +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.admin.repo.agenda.AgendaProfileAdminRepository; +import gg.agenda.api.AgendaMockData; +import gg.agenda.api.admin.agendaprofile.controller.request.AgendaProfileChangeAdminReqDto; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.data.user.type.RoleType; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; + +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaProfileAdminControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private TestDataUtils testDataUtils; + @Autowired + private AgendaMockData agendaMockData; + @Autowired + private AgendaProfileAdminRepository agendaProfileAdminRepository; + User user; + String accessToken; + + @Nested + @DisplayName("개인 프로필 정보 변경") + class UpdateAgendaProfile { + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewAdminUser(RoleType.ADMIN); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Test + @DisplayName("유효한 정보로 개인 프로필을 변경합니다.") + void updateProfileWithValidData() throws Exception { + // Given + AgendaProfile agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + agendaMockData.createTicket(agendaProfile); + AgendaProfileChangeAdminReqDto requestDto = new AgendaProfileChangeAdminReqDto("Valid user content", + "https://github.com/validUser", "SEOUL"); + String content = objectMapper.writeValueAsString(requestDto); + // When + mockMvc.perform(patch("/agenda/admin/profile") + .param("intraId", user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNoContent()); + // Then + AgendaProfile result = agendaProfileAdminRepository.findByIntraId(user.getIntraId()).orElseThrow(null); + assertThat(result.getContent()).isEqualTo(requestDto.getUserContent()); + assertThat(result.getGithubUrl()).isEqualTo(requestDto.getUserGithub()); + assertThat(result.getLocation().name()).isEqualTo(requestDto.getUserLocation()); + } + + @Test + @DisplayName("ENUM 이외의 지역 정보가 들어온 경우 MIX로 저장합니다.") + void updateProfileWithInvalidLocation() throws Exception { + // Given + agendaMockData.createAgendaProfile(user, SEOUL); + AgendaProfileChangeAdminReqDto requestDto = new AgendaProfileChangeAdminReqDto("Valid user content", + "https://github.com/validUser", "INVALID_LOCATION"); + String content = objectMapper.writeValueAsString(requestDto); + + // When + mockMvc.perform(patch("/agenda/admin/profile") + .param("intraId", user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNoContent()); + + // Then + AgendaProfile result = agendaProfileAdminRepository.findByIntraId(user.getIntraId()).orElseThrow(); + assertThat(result.getLocation()).isEqualTo(Location.MIX); + } + + @Test + @DisplayName("userContent 없이 개인 프로필을 변경합니다.") + void updateProfileWithoutUserContent() throws Exception { + // Given + AgendaProfileChangeAdminReqDto requestDto = new AgendaProfileChangeAdminReqDto("", + "https://github.com/validUser", "SEOUL"); + String content = objectMapper.writeValueAsString(requestDto); + // When & Then + mockMvc.perform(patch("/agenda/admin/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("잘못된 형식의 userGithub로 개인 프로필을 변경합니다.") + void updateProfileWithInvalidUserGithub() throws Exception { + // Given + AgendaProfileChangeAdminReqDto requestDto = new AgendaProfileChangeAdminReqDto("Valid user content", + "invalidGithubUrl", "SEOUL"); + String content = objectMapper.writeValueAsString(requestDto); + // When & Then + mockMvc.perform(patch("/agenda/admin/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("userContent가 허용된 길이를 초과하여 개인 프로필을 변경합니다.") + void updateProfileWithExceededUserContentLength() throws Exception { + // Given + String longContent = "a".repeat(1001); // Assuming the limit is 1000 characters + AgendaProfileChangeAdminReqDto requestDto = new AgendaProfileChangeAdminReqDto(longContent, + "https://github.com/validUser", "SEOUL"); + String content = objectMapper.writeValueAsString(requestDto); + // When & Then + mockMvc.perform(patch("/agenda/admin/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("userGithub가 허용된 길이를 초과하여 개인 프로필을 변경합니다.") + void updateProfileWithExceededUserGithubLength() throws Exception { + // Given + String longGithubUrl = "https://github.com/" + "a".repeat(256); // Assuming the limit is 255 characters + AgendaProfileChangeAdminReqDto requestDto = new AgendaProfileChangeAdminReqDto("Valid user content", + longGithubUrl, "SEOUL"); + + String content = objectMapper.writeValueAsString(requestDto); + + // When & Then + mockMvc.perform(patch("/agenda/admin/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + } + +} + + + diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendateam/controller/AgendaTeamAdminControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendateam/controller/AgendaTeamAdminControllerTest.java new file mode 100644 index 000000000..27b777c1b --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendateam/controller/AgendaTeamAdminControllerTest.java @@ -0,0 +1,670 @@ +package gg.agenda.api.admin.agendateam.controller; + +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaTeamAdminRepository; +import gg.admin.repo.agenda.AgendaTeamProfileAdminRepository; +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamMateReqDto; +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamUpdateDto; +import gg.agenda.api.admin.agendateam.controller.response.AgendaTeamDetailResDto; +import gg.agenda.api.admin.agendateam.controller.response.AgendaTeamMateResDto; +import gg.agenda.api.admin.agendateam.controller.response.AgendaTeamResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.utils.AgendaTestDataUtils; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageResponseDto; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaProfileFixture; +import gg.utils.fixture.agenda.AgendaTeamFixture; +import gg.utils.fixture.agenda.AgendaTeamProfileFixture; + +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaTeamAdminControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private TestDataUtils testDataUtils; + + @Autowired + private AgendaFixture agendaFixture; + + @Autowired + private AgendaTeamFixture agendaTeamFixture; + + @Autowired + private AgendaProfileFixture agendaProfileFixture; + + @Autowired + private AgendaTeamProfileFixture agendaTeamProfileFixture; + + @Autowired + private AgendaTestDataUtils agendaTestDataUtils; + + @Autowired + EntityManager em; + + @Autowired + AgendaAdminRepository agendaAdminRepository; + + @Autowired + AgendaTeamAdminRepository agendaTeamAdminRepository; + + @Autowired + AgendaTeamProfileAdminRepository agendaTeamProfileAdminRepository; + + private User user; + + private String accessToken; + + @BeforeEach + void setUp() { + user = testDataUtils.createAdminUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Nested + @DisplayName("Admin AgendaTeam 전체 조회") + class GetAgendaTeamListAdmin { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("Admin AgendaTeam 전체 조회 성공") + void getAgendaTeamListAdminSuccess(int page) throws Exception { + // given + int size = 10; + int total = 37; + Agenda agenda = agendaFixture.createAgenda(2, 50, 1, 10); + List teams = agendaTeamFixture + .createAgendaTeamList(agenda, AgendaTeamStatus.CONFIRM, total); + + // when + String response = mockMvc.perform(get("/agenda/admin/team/list") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result).isNotNull(); + assertThat(result.size()).isEqualTo(((page - 1) * size) < teams.size() + ? Math.min(size, teams.size() - (page - 1) * size) : 0); + teams.sort((a, b) -> b.getId().compareTo(a.getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getTeamKey()).isEqualTo(teams.get(i + (page - 1) * size).getTeamKey()); + } + } + + @Test + @DisplayName("Admin AgendaTeam 전체 조회 실패 - Agenda 없음") + void getAgendaTeamListAdminFailedWithNoAgenda() throws Exception { + // given + int page = 1; + int size = 10; + + // expected + mockMvc.perform(get("/agenda/admin/team/list") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", UUID.randomUUID().toString()) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("Admin AgendaTeam 상세 조회") + class GetAgendaTeamDetailAdmin { + @Test + @DisplayName("Admin AgendaTeam 상세 조회 성공") + void getAgendaTeamDetailAdminSuccess() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + // when + String response = mockMvc.perform(get("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .param("team_key", team.getTeamKey().toString())) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + AgendaTeamDetailResDto result = objectMapper.readValue(response, AgendaTeamDetailResDto.class); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTeamName()).isEqualTo(team.getName()); + assertThat(result.getTeamMates()).isNotNull(); + assertThat(result.getTeamMates().size()).isEqualTo(profiles.size()); + for (int i = 0; i < profiles.size(); i++) { + AgendaTeamMateResDto profile = result.getTeamMates().get(i); + assertThat(profile.getIntraId()).isEqualTo(profiles.get(i).getIntraId()); + } + } + + @Test + @DisplayName("Admin AgendaTeam 상세 조회 실패 - Team Key 없음") + void getAgendaTeamDetailAdminFailedWithNoTeamKey() throws Exception { + // given + // expected + mockMvc.perform(get("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin AgendaTeam 상세 조회 실패 - 존재하지 않는 Team") + void getAgendaTeamDetailAdminFailedWithNotFoundTeam() throws Exception { + // given + // expected + mockMvc.perform(get("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .param("team_key", UUID.randomUUID().toString())) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Admin AgendaTeam 상세 조회 성공 - teamMate 없는 경우 빈 리스트") + void getAgendaTeamDetailAdminSuccessWithNoTeamMates() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + + // when + String response = mockMvc.perform(get("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .param("team_key", team.getTeamKey().toString())) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + AgendaTeamDetailResDto result = objectMapper.readValue(response, AgendaTeamDetailResDto.class); + + // then + assertThat(result).isNotNull(); + assertThat(result.getTeamName()).isEqualTo(team.getName()); + assertThat(result.getTeamMates()).isNotNull(); + assertThat(result.getTeamMates().size()).isEqualTo(0); + } + } + + @Nested + @DisplayName("Admin AgendaTeam 수정") + class UpdateAgendaTeamAdmin { + @Test + @DisplayName("Admin AgendaTeam 수정 성공") + void updateAgendaTeamAdminSuccess() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaProfile seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUserAgendaProfile); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + updateTeamMates.add(new AgendaTeamMateReqDto(seoulUserAgendaProfile.getIntraId())); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNoContent()); + AgendaTeam updatedAgendaTeam = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + + // then + assertThat(updatedAgendaTeam.getName()).isEqualTo(updateDto.getTeamName()); + assertThat(updatedAgendaTeam.getContent()).isEqualTo(updateDto.getTeamContent()); + assertThat(updatedAgendaTeam.getIsPrivate()).isEqualTo(updateDto.getTeamIsPrivate()); + assertThat(updatedAgendaTeam.getStatus()).isEqualTo(updateDto.getTeamStatus()); + assertThat(updatedAgendaTeam.getAward()).isEqualTo(updateDto.getTeamAward()); + assertThat(updatedAgendaTeam.getAwardPriority()).isEqualTo(updateDto.getTeamAwardPriority()); + assertThat(updatedAgendaTeam.getLocation()).isEqualTo(updateDto.getTeamLocation()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - Location을 변경할 수 없는 경우") + void updateAgendaTeamAdminFailedWithLocation() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(Location.MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.GYEONGSAN) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + AgendaTeam result = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + + // then + assertThat(result.getLocation()).isEqualTo(team.getLocation()); + assertThat(result.getName()).isEqualTo(team.getName()); + assertThat(result.getContent()).isEqualTo(team.getContent()); + assertThat(result.getIsPrivate()).isEqualTo(team.getIsPrivate()); + assertThat(result.getStatus()).isEqualTo(team.getStatus()); + assertThat(result.getAward()).isEqualTo(team.getAward()); + assertThat(result.getAwardPriority()).isEqualTo(team.getAwardPriority()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 성공 - 팀원 추가하기") + void updateAgendaTeamAdminSuccessWithAddTeammate() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaProfile seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUserAgendaProfile); + List profiles = agendaProfileFixture.createAgendaProfileList(3); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + AgendaProfile newProfile = agendaProfileFixture.createAgendaProfile(); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + updateTeamMates.add(new AgendaTeamMateReqDto(seoulUserAgendaProfile.getIntraId())); + updateTeamMates.add(new AgendaTeamMateReqDto(newProfile.getIntraId())); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNoContent()); + AgendaTeam updatedAgendaTeam = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + List participants = agendaTeamProfileAdminRepository + .findAllByAgendaTeamAndIsExistIsTrue(updatedAgendaTeam); + + // then + assertThat(updatedAgendaTeam.getName()).isEqualTo(updateDto.getTeamName()); + assertThat(updatedAgendaTeam.getContent()).isEqualTo(updateDto.getTeamContent()); + assertThat(updatedAgendaTeam.getIsPrivate()).isEqualTo(updateDto.getTeamIsPrivate()); + assertThat(updatedAgendaTeam.getStatus()).isEqualTo(updateDto.getTeamStatus()); + assertThat(updatedAgendaTeam.getAward()).isEqualTo(updateDto.getTeamAward()); + assertThat(updatedAgendaTeam.getAwardPriority()).isEqualTo(updateDto.getTeamAwardPriority()); + assertThat(updatedAgendaTeam.getLocation()).isEqualTo(updateDto.getTeamLocation()); + assertThat(participants.size()).isEqualTo(updateTeamMates.size()); + // Check new participant + assertThat(participants.stream() + .anyMatch(participant -> participant.getProfile().getIntraId().equals(newProfile.getIntraId()))) + .isTrue(); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - AgendaTeamStatus는 Cancel로 변경할 수 없음") + void updateAgendaTeamAdminFailedWithCancelStatus() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CANCEL).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 이미 꽉 찬 팀에 팀원 추가하기") + void updateAgendaTeamAdminFailedWithMaxPeople() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + AgendaProfile newProfile = agendaProfileFixture.createAgendaProfile(); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + updateTeamMates.add(new AgendaTeamMateReqDto(newProfile.getIntraId())); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 성공 - 팀원 삭제하기") + void updateAgendaTeamAdminSuccessWithRemoveTeammate() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaProfile seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUserAgendaProfile); + List profiles = agendaProfileFixture.createAgendaProfileList(3); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + AgendaProfile wrongProfile = agendaProfileFixture.createAgendaProfile(); + agendaTeamProfileFixture.createAgendaTeamProfile(agenda, team, wrongProfile); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + updateTeamMates.add(new AgendaTeamMateReqDto(seoulUserAgendaProfile.getIntraId())); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNoContent()); + AgendaTeam updatedAgendaTeam = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + List participants = agendaTeamProfileAdminRepository + .findAllByAgendaTeamAndIsExistIsTrue(updatedAgendaTeam); + + // then + assertThat(updatedAgendaTeam.getName()).isEqualTo(updateDto.getTeamName()); + assertThat(updatedAgendaTeam.getContent()).isEqualTo(updateDto.getTeamContent()); + assertThat(updatedAgendaTeam.getIsPrivate()).isEqualTo(updateDto.getTeamIsPrivate()); + assertThat(updatedAgendaTeam.getStatus()).isEqualTo(updateDto.getTeamStatus()); + assertThat(updatedAgendaTeam.getAward()).isEqualTo(updateDto.getTeamAward()); + assertThat(updatedAgendaTeam.getAwardPriority()).isEqualTo(updateDto.getTeamAwardPriority()); + assertThat(updatedAgendaTeam.getLocation()).isEqualTo(updateDto.getTeamLocation()); + assertThat(participants.size()).isEqualTo(updateTeamMates.size()); + // Check wrong participant + assertThat(participants.stream() + .noneMatch(participant -> participant.getProfile().getIntraId().equals(wrongProfile.getIntraId()))) + .isTrue(); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 리더를 삭제하는 경우") + void updateAgendaTeamAdminFailedWithRemoveLeader() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaProfile seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUserAgendaProfile); + List profiles = agendaProfileFixture.createAgendaProfileList(3); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 팀장이 존재하지 않음") + void updateAgendaTeamAdminFailedWithNoLeader() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(3); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 존재하지 않는 Team Key") + void updateAgendaTeamAdminFailedWithInvalidTeamKey() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(UUID.randomUUID()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 존재하지 않는 Intra ID") + void updateAgendaTeamAdminFailedWithInvalidIntraId() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + List updateTeamMates = profiles.stream() + .map(profile -> new AgendaTeamMateReqDto(profile.getIntraId())) + .collect(Collectors.toList()); + updateTeamMates.add(new AgendaTeamMateReqDto("invalid")); + AgendaTeamUpdateDto updateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates) + .teamStatus(AgendaTeamStatus.CONFIRM).teamLocation(Location.MIX) + .teamName("newName").teamContent("newContent").teamIsPrivate(true) + .teamAward("newAward").teamAwardPriority(team.getAwardPriority() + 1).build(); + String request = objectMapper.writeValueAsString(updateDto); + + // when + mockMvc.perform(patch("/agenda/admin/team") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + AgendaTeam result = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + + // then + assertThat(result.getLocation()).isEqualTo(team.getLocation()); + assertThat(result.getName()).isEqualTo(team.getName()); + assertThat(result.getContent()).isEqualTo(team.getContent()); + assertThat(result.getIsPrivate()).isEqualTo(team.getIsPrivate()); + assertThat(result.getStatus()).isEqualTo(team.getStatus()); + assertThat(result.getAward()).isEqualTo(team.getAward()); + assertThat(result.getAwardPriority()).isEqualTo(team.getAwardPriority()); + } + } + + @Nested + @DisplayName("Admin AgendaTeam 취소") + class CancelAgendaTeamAdmin { + @Test + @DisplayName("Admin AgendaTeam 취소 성공") + void cancelAgendaTeamAdminSuccess() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(agenda, team, profile)); + + // when + mockMvc.perform(patch("/agenda/admin/team/cancel") + .header("Authorization", "Bearer " + accessToken) + .param("team_key", team.getTeamKey().toString())) + .andExpect(status().isNoContent()); + AgendaTeam result = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + + // then + assertThat(result.getStatus()).isEqualTo(AgendaTeamStatus.CANCEL); + assertThat(agendaTeamProfileAdminRepository + .findAllByAgendaTeamAndIsExistIsTrue(result).size()).isEqualTo(0); + assertThat(agenda.getCurrentTeam()).isEqualTo(1); + } + + @Nested + @DisplayName("Admin Confirm 상태의 AgendaTeam 취소") + class CancelConfirmAgendaTeamAdmin { + @Test + @DisplayName("Admin AgendaTeam 취소 성공") + void cancelAgendaTeamAdminSuccess() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL, AgendaTeamStatus.CONFIRM); + List profiles = agendaProfileFixture.createAgendaProfileList(5); + profiles.forEach(profile -> agendaTeamProfileFixture + .createAgendaTeamProfile(team, profile)); + + // when + mockMvc.perform(patch("/agenda/admin/team/cancel") + .header("Authorization", "Bearer " + accessToken) + .param("team_key", team.getTeamKey().toString())) + .andExpect(status().isNoContent()); + AgendaTeam result = agendaTeamAdminRepository.findByTeamKey(team.getTeamKey()) + .orElseThrow(() -> new AssertionError("AgendaTeam not found")); + + // then + assertThat(result.getStatus()).isEqualTo(AgendaTeamStatus.CANCEL); + assertThat(agendaTeamProfileAdminRepository + .findAllByAgendaTeamAndIsExistIsTrue(result).size()).isEqualTo(0); + assertThat(agenda.getCurrentTeam()).isEqualTo(0); + } + } + + @Test + @DisplayName("Admin AgendaTeam 취소 실패 - 존재하지 않는 Team Key") + void cancelAgendaTeamAdminFailedWithInvalidTeamKey() throws Exception { + // given + // expected + mockMvc.perform(patch("/agenda/admin/team/cancel") + .header("Authorization", "Bearer " + accessToken) + .param("team_key", UUID.randomUUID().toString())) + .andExpect(status().isNotFound()); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendateam/service/AgendaTeamAdminServiceTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendateam/service/AgendaTeamAdminServiceTest.java new file mode 100644 index 000000000..1c5f7fc63 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/agendateam/service/AgendaTeamAdminServiceTest.java @@ -0,0 +1,223 @@ +package gg.agenda.api.admin.agendateam.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import gg.admin.repo.agenda.AgendaAdminRepository; +import gg.admin.repo.agenda.AgendaProfileAdminRepository; +import gg.admin.repo.agenda.AgendaTeamAdminRepository; +import gg.admin.repo.agenda.AgendaTeamProfileAdminRepository; +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamMateReqDto; +import gg.agenda.api.admin.agendateam.controller.request.AgendaTeamUpdateDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.utils.annotation.UnitTest; +import gg.utils.exception.custom.NotExistException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@UnitTest +public class AgendaTeamAdminServiceTest { + + @Mock + private AgendaAdminRepository agendaAdminRepository; + + @Mock + private AgendaTeamAdminRepository agendaTeamAdminRepository; + + @Mock + private AgendaProfileAdminRepository agendaProfileAdminRepository; + + @Mock + private AgendaTeamProfileAdminRepository agendaTeamProfileAdminRepository; + + @InjectMocks + private AgendaTeamAdminService agendaTeamAdminService; + + @Nested + @DisplayName("Admin AgendaTeam 전체 조회") + class GetAgendaTeamListAdmin { + + @Test + @DisplayName("Admin AgendaTeam 전체 조회 성공") + void getAgendaTeamListAdminSuccess() { + // given + Agenda agenda = Agenda.builder().build(); + List announcements = new ArrayList<>(); + Pageable pageable = PageRequest.of(0, 10, Sort.by("id").descending()); + when(agendaAdminRepository.findByAgendaKey(any(UUID.class))).thenReturn(Optional.of(agenda)); + when(agendaTeamAdminRepository.findAllByAgenda(any(Agenda.class), any(Pageable.class))) + .thenReturn(new PageImpl<>(announcements)); + + // when + Page result = agendaTeamAdminService.getAgendaTeamList(agenda.getAgendaKey(), pageable); + + // then + verify(agendaAdminRepository, times(1)).findByAgendaKey(any(UUID.class)); + verify(agendaTeamAdminRepository, times(1)) + .findAllByAgenda(any(Agenda.class), any(Pageable.class)); + assertThat(result).isNotNull(); + } + + @Test + @DisplayName("Admin AgendaTeam 전체 조회 실패 - Agenda 없음") + void getAgendaTeamListAdminFailedWithNoAgenda() { + // given + Pageable pageable = mock(Pageable.class); + when(agendaAdminRepository.findByAgendaKey(any(UUID.class))).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaTeamAdminService.getAgendaTeamList(UUID.randomUUID(), pageable)); + } + } + + @Nested + @DisplayName("Admin AgendaTeam Team Key로 조회") + class GetAgendaTeamByTeamKey { + + @Test + @DisplayName("Admin AgendaTeam Team Key로 조회 성공") + void getAgendaTeamByTeamKeySuccess() { + // given + UUID teamKey = UUID.randomUUID(); + AgendaTeam agendaTeam = mock(AgendaTeam.class); + when(agendaTeamAdminRepository.findByTeamKey(teamKey)).thenReturn(Optional.of(agendaTeam)); + + // when + AgendaTeam agendaTeamByTeamKey = agendaTeamAdminService.getAgendaTeamByTeamKey(teamKey); + + // then + verify(agendaTeamAdminRepository, times(1)).findByTeamKey(teamKey); + assertThat(agendaTeamByTeamKey).isNotNull(); + } + + @Test + @DisplayName("Admin AgendaTeam 상세 조회 실패 - Team Key 없음") + void getAgendaTeamByTeamKeyFailedWithNoTeam() { + // given + UUID teamKey = UUID.randomUUID(); + when(agendaTeamAdminRepository.findByTeamKey(teamKey)).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaTeamAdminService.getAgendaTeamByTeamKey(teamKey)); + } + } + + @Nested + @DisplayName("Admin AgendaTeam teamMates 조회") + class GetAgendaProfileListByAgendaTeam { + + @Test + @DisplayName("Admin AgendaTeam teamMates 조회 성공") + void getAgendaProfileListByAgendaTeamSuccess() { + // given + AgendaTeam agendaTeam = mock(AgendaTeam.class); + List participants = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + AgendaProfile profile = AgendaProfile.builder().build(); + AgendaTeamProfile participant = AgendaTeamProfile.builder() + .profile(profile).build(); + participants.add(participant); + } + when(agendaTeamProfileAdminRepository.findAllByAgendaTeamAndIsExistIsTrue(agendaTeam)) + .thenReturn(participants); + + // when + List result = agendaTeamAdminService.getAgendaProfileListByAgendaTeam(agendaTeam); + + // then + verify(agendaTeamProfileAdminRepository, times(1)) + .findAllByAgendaTeamAndIsExistIsTrue(agendaTeam); + assertThat(result).isNotNull(); + } + + @Test + @DisplayName("Admin AgendaTeam teamMates 조회 성공 - teamMates 없는 경우 빈 리스트 반환") + void getAgendaProfileListByAgendaTeamFailedWithNoTeam() { + // given + AgendaTeam agendaTeam = mock(AgendaTeam.class); + List participants = new ArrayList<>(); + when(agendaTeamProfileAdminRepository.findAllByAgendaTeamAndIsExistIsTrue(agendaTeam)) + .thenReturn(participants); + + // when + List result = agendaTeamAdminService.getAgendaProfileListByAgendaTeam(agendaTeam); + + // then + verify(agendaTeamProfileAdminRepository, times(1)) + .findAllByAgendaTeamAndIsExistIsTrue(agendaTeam); + assertThat(result).isNotNull(); + assertThat(result).isEmpty(); + + } + } + + @Nested + @DisplayName("Admin AgendaTeam 수정") + class UpdateAgendaTeamAdmin { + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 존재하지 않는 Team Key") + void updateAgendaTeamAdminFailedWithInvalidTeamKey() { + // given + AgendaTeamUpdateDto agendaTeamUpdateDto = AgendaTeamUpdateDto.builder() + .teamKey(UUID.randomUUID()).teamMates(List.of()).build(); + when(agendaTeamAdminRepository.findByTeamKey(any(UUID.class))).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaTeamAdminService.updateAgendaTeam(agendaTeamUpdateDto)); + } + + @Test + @DisplayName("Admin AgendaTeam 수정 실패 - 존재하지 않는 Intra ID") + void updateAgendaTeamAdminFailedWithInvalidIntraId() { + // given + Agenda agenda = Agenda.builder().build(); + AgendaTeam team = AgendaTeam.builder().teamKey(UUID.randomUUID()).agenda(agenda).build(); + AgendaProfile profile = AgendaProfile.builder().intraId("intra").build(); + AgendaTeamProfile participant = AgendaTeamProfile.builder() + .agendaTeam(team).agenda(agenda).profile(profile).build(); + + AgendaProfile newProfile = AgendaProfile.builder().intraId("newIntra").build(); + List updateTeamMates = new ArrayList<>(); + updateTeamMates.add(AgendaTeamMateReqDto.builder() + .intraId(profile.getIntraId()).build()); + updateTeamMates.add(AgendaTeamMateReqDto.builder() + .intraId(newProfile.getIntraId()).build()); + + AgendaTeamUpdateDto agendaTeamUpdateDto = AgendaTeamUpdateDto.builder() + .teamKey(team.getTeamKey()).teamMates(updateTeamMates).build(); + + when(agendaTeamAdminRepository.findByTeamKey(any(UUID.class))).thenReturn(Optional.of(team)); + when(agendaTeamProfileAdminRepository.findAllByAgendaTeamAndIsExistIsTrue(any(AgendaTeam.class))) + .thenReturn(List.of(participant)); + when(agendaProfileAdminRepository.findByIntraId("newIntra")) + .thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaTeamAdminService.updateAgendaTeam(agendaTeamUpdateDto)); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/admin/ticket/TicketAdminControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/admin/ticket/TicketAdminControllerTest.java new file mode 100644 index 000000000..9f662b6fd --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/admin/ticket/TicketAdminControllerTest.java @@ -0,0 +1,432 @@ +package gg.agenda.api.admin.ticket; + +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.admin.repo.agenda.TicketAdminRepository; +import gg.agenda.api.admin.ticket.controller.request.TicketAddAdminReqDto; +import gg.agenda.api.admin.ticket.controller.request.TicketChangeAdminReqDto; +import gg.agenda.api.admin.ticket.controller.response.TicketAddAdminResDto; +import gg.agenda.api.admin.ticket.controller.response.TicketFindResDto; +import gg.agenda.api.user.ticket.controller.response.TicketHistoryResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.data.user.type.RoleType; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaProfileFixture; +import gg.utils.fixture.agenda.TicketFixture; + +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class TicketAdminControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private TestDataUtils testDataUtils; + @Autowired + private AgendaProfileFixture agendaProfileFixture; + @Autowired + private AgendaFixture agendaFixture; + @Autowired + private TicketFixture ticketFixture; + @Autowired + private TicketAdminRepository ticketAdminRepository; + User user; + String accessToken; + AgendaProfile agendaProfile; + + @Nested + @DisplayName("티켓 발급 요청") + class AddTicket { + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewAdminUser(RoleType.ADMIN); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + agendaProfile = agendaProfileFixture.createAgendaProfile(user, Location.SEOUL); + } + + @Test + @DisplayName("issuedFromKey = null로 티켓이 잘 생성되어 201 반환 및 ticketId 반환") + void createTicketWithNullIssuedFromKey() throws Exception { + // Given + String content = objectMapper.writeValueAsString(new TicketAddAdminReqDto(null)); + + // When + String responseContent = mockMvc.perform( + post("/agenda/admin/ticket") + .param("intraId", user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + + TicketAddAdminResDto response = objectMapper.readValue(responseContent, TicketAddAdminResDto.class); + Ticket createdTicket = ticketAdminRepository.findByAgendaProfile(agendaProfile) + .orElseThrow(); + // Then + assertThat(response.getTicketId()).isNotNull(); + assertThat(createdTicket.getAgendaProfile().getId()).isEqualTo(agendaProfile.getId()); + assertThat(createdTicket.getIssuedFrom()).isNull(); + assertThat(createdTicket.getIsApproved()).isTrue(); + assertThat(createdTicket.getIsUsed()).isFalse(); + } + + @Test + @DisplayName("존재하는 대회의 issuedFromKey 넣어 티켓(환불티켓생성)이 잘 생성되어 201 반환 및 ticketId 반환") + void createTicketWithNotNullIssuedFromKey() throws Exception { + // Given + User agendaCreateUser = testDataUtils.createNewUser(); + Agenda agenda = agendaFixture.createAgenda(agendaCreateUser.getIntraId(), AgendaStatus.OPEN); + String content = objectMapper.writeValueAsString(new TicketAddAdminReqDto(agenda.getAgendaKey())); + + // When + String responseContent = mockMvc.perform( + post("/agenda/admin/ticket") + .param("intraId", user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + + TicketAddAdminResDto response = objectMapper.readValue(responseContent, TicketAddAdminResDto.class); + Ticket createdTicket = ticketAdminRepository.findByAgendaProfile(agendaProfile) + .orElseThrow(); + // Then + assertThat(response.getTicketId()).isNotNull(); + assertThat(createdTicket.getAgendaProfile().getId()).isEqualTo(agendaProfile.getId()); + assertThat(createdTicket.getIssuedFrom()).isEqualTo(agenda.getAgendaKey()); + assertThat(createdTicket.getIsApproved()).isTrue(); + assertThat(createdTicket.getIsUsed()).isFalse(); + } + + @Test + @DisplayName("존재하지않는 대회의 issuedFromKey 넣어 404 반환") + void createTicketWithAgendaNotExit() throws Exception { + // Given + UUID nonExistentAgendaKey = UUID.randomUUID(); + String content = objectMapper.writeValueAsString(new TicketAddAdminReqDto(nonExistentAgendaKey)); + + // When & Then + mockMvc.perform( + post("/agenda/admin/ticket") + .param("intraId", user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("티켓 정보 변경") + class UpdateTicket { + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewAdminUser(RoleType.ADMIN); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + agendaProfile = agendaProfileFixture.createAgendaProfile(user, Location.SEOUL); + } + + @Test + @DisplayName("유효한 정보로 티켓 정보를 변경합니다.") + void updateTicketWithValidData() throws Exception { + // Given + User agendaCreateUser = testDataUtils.createNewUser(); + Ticket ticket = ticketFixture.createNotApporveTicket(agendaProfile); + Agenda usedAgenda = agendaFixture.createAgenda(agendaCreateUser.getIntraId(), AgendaStatus.OPEN); + Agenda refundedAgenda = agendaFixture.createAgenda(agendaCreateUser.getIntraId(), AgendaStatus.OPEN); + String content = objectMapper.writeValueAsString( + new TicketChangeAdminReqDto(refundedAgenda.getAgendaKey(), usedAgenda.getAgendaKey(), Boolean.TRUE, + LocalDateTime.now(), Boolean.TRUE, LocalDateTime.now())); + + // When + mockMvc.perform( + patch("/agenda/admin/ticket") + .param("ticketId", String.valueOf(ticket.getId())) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNoContent()); + + // Then + Ticket updatedTicket = ticketAdminRepository.findById(ticket.getId()).orElseThrow(); + assertThat(updatedTicket.getIssuedFrom()).isEqualTo(refundedAgenda.getAgendaKey()); + assertThat(updatedTicket.getUsedTo()).isEqualTo(usedAgenda.getAgendaKey()); + assertThat(updatedTicket.getIsApproved()).isTrue(); + assertThat(updatedTicket.getIsUsed()).isTrue(); + } + + @Test + @DisplayName("존재하지않는 대회의 issuedFromKey 넣어 404 반환") + void updateTicketWithInvalidIssuedFromKey() throws Exception { + // Given + Ticket ticket = ticketFixture.createNotApporveTicket(agendaProfile); + String content = objectMapper.writeValueAsString( + new TicketChangeAdminReqDto(UUID.randomUUID(), null, Boolean.TRUE, + LocalDateTime.now(), Boolean.FALSE, LocalDateTime.now())); + + // When & Then + mockMvc.perform( + patch("/agenda/admin/ticket") + .param("ticketId", String.valueOf(ticket.getId())) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("존재하지않는 대회의 usedToKey 넣어 404 반환") + void updateTicketWithInvalidUsedToKey() throws Exception { + // Given + Ticket ticket = ticketFixture.createNotApporveTicket(agendaProfile); + String content = objectMapper.writeValueAsString( + new TicketChangeAdminReqDto(null, UUID.randomUUID(), Boolean.TRUE, + LocalDateTime.now(), Boolean.TRUE, LocalDateTime.now())); + + // When & Then + mockMvc.perform( + patch("/agenda/admin/ticket") + .param("ticketId", String.valueOf(ticket.getId())) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("티켓 목록 조회 테스트") + class GetTicketList { + + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewAdminUser(RoleType.ADMIN); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + agendaProfile = agendaProfileFixture.createAgendaProfile(user, Location.SEOUL); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3}) + @DisplayName("200 모든 티켓 목록 조회 성공") + void getAllTicketsSuccess(int page) throws Exception { + // Given + int size = 10; + int total = 25; + List tickets = new ArrayList<>(); + for (int i = 0; i < total; i++) { + tickets.add(ticketFixture.createTicket(agendaProfile)); + } + tickets.sort((o1, o2) -> Long.compare(o2.getId(), o1.getId())); + + PageRequestDto pageRequest = new PageRequestDto(page, size); + String request = objectMapper.writeValueAsString(pageRequest); + + // When + String res = mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // Then + int expectedSize = Math.min(size, total - (page - 1) * size); + assertThat(result).hasSize(expectedSize); + + for (int i = 0; i < result.size(); i++) { + TicketFindResDto actual = result.get(i); + + int ticketIndex = (page - 1) * size + i; + if (ticketIndex >= tickets.size()) { + break; + } + Ticket expected = tickets.get(ticketIndex); + + // 검증 + assertThat(actual.getTicketId()).isEqualTo(expected.getId()); + assertThat(actual.getCreatedAt()).isEqualTo(expected.getCreatedAt()); + assertThat(actual.getIsApproved()).isEqualTo(expected.getIsApproved()); + assertThat(actual.getApprovedAt()).isEqualTo(expected.getApprovedAt()); + assertThat(actual.getIsUsed()).isEqualTo(expected.getIsUsed()); + assertThat(actual.getUsedAt()).isEqualTo(expected.getUsedAt()); + } + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - approve 되어있지 않은 경우") + void findTicketHistorySuccessToNotApprove() throws Exception { + //given + ticketFixture.createTicket(agendaProfile, false, false, null, null); + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo("42Intra"); + assertThat(result.get(0).getUsedTo()).isEqualTo("NotApproved"); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - approve 되어있고 used 되어있는 경우") + void findTicketHistorySuccessToUsed() throws Exception { + //given + Agenda seoulAgenda = agendaFixture.createAgenda(SEOUL); + Ticket ticket = ticketFixture.createTicket(agendaProfile, true, true, null, + seoulAgenda.getAgendaKey()); + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo("42Intra"); + assertThat(result.get(0).getUsedTo()).isEqualTo(seoulAgenda.getTitle()); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - approve 되어있고 used 되어있지 않은 경우") + void findTicketHistorySuccessToNotUsed() throws Exception { + //given + Agenda seoulAgenda = agendaFixture.createAgenda(SEOUL); + Ticket ticket = ticketFixture.createTicket(agendaProfile, true, false, null, + null); + PageRequestDto req = new PageRequestDto(1, 5); + String content = objectMapper.writeValueAsString(req); + //when + String res = mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo("42Intra"); + assertThat(result.get(0).getUsedTo()).isEqualTo("NotUsed"); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - refund 되어있고 used 되어있지 않은 경우") + void findTicketHistorySuccessToRefund() throws Exception { + //given + Agenda seoulAgenda = agendaFixture.createAgenda(SEOUL); + Ticket ticket = ticketFixture.createTicket(agendaProfile, true, false, seoulAgenda.getAgendaKey(), + null); + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo(seoulAgenda.getTitle()); + assertThat(result.get(0).getUsedTo()).isEqualTo("NotUsed"); + } + + @Test + @DisplayName("200 티켓이 없는 경우") + void getAllTicketsEmpty() throws Exception { + //Given + PageRequestDto req = new PageRequestDto(1, 5); + // When + String res = mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", "1") + .param("size", "10") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("400 유효하지 않은 페이지 요청") + void getAllTicketsInvalidPageRequest() throws Exception { + // Given + // When & Then + mockMvc.perform( + get("/agenda/admin/ticket/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", "-1") + .param("size", "10") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/AgendaControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/AgendaControllerTest.java new file mode 100644 index 000000000..6b4dab4bb --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/AgendaControllerTest.java @@ -0,0 +1,1384 @@ +package gg.agenda.api.user.agenda.controller; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.net.URL; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.MultiValueMap; +import org.springframework.web.multipart.MultipartFile; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.agenda.api.AgendaMockData; +import gg.agenda.api.user.agenda.controller.request.AgendaAwardsReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaTeamAward; +import gg.agenda.api.user.agenda.controller.response.AgendaKeyResDto; +import gg.agenda.api.user.agenda.controller.response.AgendaResDto; +import gg.agenda.api.user.agenda.controller.response.AgendaSimpleResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.repo.agenda.AgendaPosterImageRepository; +import gg.repo.agenda.AgendaRepository; +import gg.repo.agenda.AgendaTeamRepository; +import gg.utils.AgendaTestDataUtils; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.converter.MultiValueMapConverter; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.exception.custom.BusinessException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.file.handler.AwsImageHandler; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaTeamFixture; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private TestDataUtils testDataUtils; + + @Autowired + private AgendaMockData agendaMockData; + + @Autowired + private AgendaFixture agendaFixture; + + @Autowired + private AgendaTeamFixture agendaTeamFixture; + + @Autowired + private AgendaTestDataUtils agendaTestDataUtils; + + @MockBean + private AwsImageHandler imageHandler; + + @Autowired + EntityManager em; + + @Autowired + AgendaRepository agendaRepository; + + @Autowired + AgendaTeamRepository agendaTeamRepository; + + @Autowired + AgendaPosterImageRepository agendaPosterImageRepository; + + @Value("${info.image.defaultUrl}") + private String defaultUri; + + private User user; + + private String accessToken; + + @BeforeEach + void setUp() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Nested + @DisplayName("Agenda 상세 조회") + class GetAgenda { + + @Test + @DisplayName("agenda_id에 해당하는 Agenda를 상세 조회합니다.") + void getAgendaSuccess() throws Exception { + // given + Agenda agenda = agendaMockData.createOfficialAgenda(); + AgendaAnnouncement announcement = agendaMockData.createAgendaAnnouncement(agenda); + + // when + String response = mockMvc.perform(get("/agenda") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaResDto result = objectMapper.readValue(response, AgendaResDto.class); + + // then + assertThat(result.getAgendaTitle()).isEqualTo(agenda.getTitle()); + assertThat(result.getAgendaMinPeople()).isEqualTo(agenda.getMinPeople()); + assertThat(result.getAnnouncementTitle()).isEqualTo(announcement.getTitle()); + } + + @Test + @DisplayName("announce가 없는 경우 announcementTitle를 빈 문자열로 반환합니다.") + void getAgendaWithNoAnnounce() throws Exception { + // given + Agenda agenda = agendaMockData.createOfficialAgenda(); + + // when + String response = mockMvc.perform(get("/agenda") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaResDto result = objectMapper.readValue(response, AgendaResDto.class); + + // then + assertThat(result.getAgendaTitle()).isEqualTo(agenda.getTitle()); + assertThat(result.getAnnouncementTitle()).isEqualTo(""); + } + + @Test + @DisplayName("announce가 여러 개인 경우 가장 최근 작성된 announce를 반환합니다.") + void getAgendaWithLatestAnnounce() throws Exception { + // given + Agenda agenda = agendaMockData.createOfficialAgenda(); + AgendaAnnouncement announcement1 = agendaMockData.createAgendaAnnouncement(agenda); + AgendaAnnouncement announcement2 = agendaMockData.createAgendaAnnouncement(agenda); + AgendaAnnouncement announcement3 = agendaMockData.createAgendaAnnouncement(agenda); + + // when + String response = mockMvc.perform(get("/agenda") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaResDto result = objectMapper.readValue(response, AgendaResDto.class); + + // then + assertThat(result.getAgendaTitle()).isEqualTo(agenda.getTitle()); + assertThat(result.getAnnouncementTitle()).isNotEqualTo(announcement1.getTitle()); + assertThat(result.getAnnouncementTitle()).isNotEqualTo(announcement2.getTitle()); + assertThat(result.getAnnouncementTitle()).isEqualTo(announcement3.getTitle()); + } + + @Test + @DisplayName("agenda_key가 잘못된 경우 400를 반환합니다.") + void getAgendaFailedWhenInvalidKey() throws Exception { + // given + Agenda agenda = agendaMockData.createOfficialAgenda(); + AgendaAnnouncement announcement = agendaMockData.createAgendaAnnouncement(agenda); + + // expected + mockMvc.perform(get("/agenda") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", "invalid_key")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("agenda_key에 해당하는 agenda가 없는 경우 404를 반환합니다.") + void getAgendaFailedWhenNoContent() throws Exception { + // given + UUID invalidKey = UUID.randomUUID(); + + // expected + mockMvc.perform(get("/agenda") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", invalidKey.toString())) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("모집중 Agenda 전체 조회") + class GetAgendaListOpen { + + @Test + @DisplayName("모집중인 대회를 Official과 Deadline이 빠른 순으로 정렬하여 반환합니다.") + void getAgendaListOpenSuccess() throws Exception { + // given + List officialAgendaList = agendaMockData.createOfficialAgendaList(3, AgendaStatus.OPEN); + List nonOfficialAgendaList = agendaMockData + .createNonOfficialAgendaList(3, AgendaStatus.OPEN); + + // when + String response = mockMvc.perform(get("/agenda/open") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaSimpleResDto[] result = objectMapper.readValue(response, AgendaSimpleResDto[].class); + + // then + assertThat(result.length).isEqualTo(officialAgendaList.size() + nonOfficialAgendaList.size()); + for (int i = 0; i < result.length; i++) { + if (i == 0 || i == officialAgendaList.size()) { + continue; + } + assertThat(result[i].getAgendaDeadLine()).isAfter(result[i - 1].getAgendaDeadLine()); + } + } + + @Test + @DisplayName("모집 중인 Agenda가 없는 경우 빈 리스트를 반환합니다.") + void getAgendaListOpenSuccessWithNoAgenda() throws Exception { + // given + agendaMockData.createOfficialAgendaList(3, AgendaStatus.FINISH); + agendaMockData.createNonOfficialAgendaList(6, AgendaStatus.CANCEL); + + // when + String response = mockMvc.perform(get("/agenda/open") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaSimpleResDto[] result = objectMapper.readValue(response, AgendaSimpleResDto[].class); + + // then + assertThat(result.length).isEqualTo(0); + } + } + + @Nested + @DisplayName("진행중 Agenda 전체 조회") + class GetAgendaListConfirm { + + @Test + @DisplayName("진행중인 대회를 Official과 Deadline이 빠른 순으로 정렬하여 반환합니다.") + void getAgendaListConfirmSuccess() throws Exception { + // given + List officialAgendaList = agendaMockData + .createOfficialAgendaList(3, AgendaStatus.OPEN); + for (Agenda agenda : officialAgendaList) { + agenda.updateSchedule(LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2)); + } + List nonOfficialAgendaList = agendaMockData + .createNonOfficialAgendaList(3, AgendaStatus.OPEN); + for (Agenda agenda : nonOfficialAgendaList) { + agenda.updateSchedule(LocalDateTime.now().minusDays(1), LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(2)); + } + List confirmedAgendaList = agendaMockData + .createNonOfficialAgendaList(3, AgendaStatus.CONFIRM); + int totalSize = officialAgendaList.size() + nonOfficialAgendaList.size() + confirmedAgendaList.size(); + + // when + String response = mockMvc.perform(get("/agenda/confirm") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaSimpleResDto[] result = objectMapper.readValue(response, AgendaSimpleResDto[].class); + + // then + assertThat(result.length).isEqualTo(totalSize); + for (int i = 0; i < result.length; i++) { + if (i == 0 || i == officialAgendaList.size()) { + continue; + } + assertThat(result[i].getAgendaStartTime()).isAfter(result[i - 1].getAgendaDeadLine()); + } + } + + @Test + @DisplayName("진행 중인 Agenda가 없는 경우 빈 리스트를 반환합니다.") + void getAgendaListConfirmSuccessWithNoAgenda() throws Exception { + // given + agendaMockData.createOfficialAgendaList(3, AgendaStatus.OPEN); + agendaMockData.createOfficialAgendaList(3, AgendaStatus.FINISH); + agendaMockData.createNonOfficialAgendaList(3, AgendaStatus.CANCEL); + + // when + String response = mockMvc.perform(get("/agenda/confirm") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaSimpleResDto[] result = objectMapper.readValue(response, AgendaSimpleResDto[].class); + + // then + assertThat(result.length).isEqualTo(0); + } + } + + @Nested + @DisplayName("Agenda 생성하기") + class CreateAgenda { + + @Test + @DisplayName("Agenda 생성하기 성공 - 포스터가 없는 경우 기본 이미지로 생성합니다.") + void createAgendaSuccess() throws Exception { + // given + URL mockS3Path = new URL(defaultUri); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockS3Path); + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, dto); + + // when + String response = mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .params(params)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + AgendaKeyResDto result = objectMapper.readValue(response, AgendaKeyResDto.class); + Agenda agenda = agendaRepository.findByAgendaKey(result.getAgendaKey()) + .orElseThrow(() -> new NotExistException("Agenda not found")); + + // then + assertThat(agenda.getTitle()).isEqualTo(dto.getAgendaTitle()); + assertThat(agenda.getContent()).isEqualTo(dto.getAgendaContent()); + assertThat(agenda.getMinPeople()).isEqualTo(dto.getAgendaMinPeople()); + assertThat(agenda.getPosterUri()).isNull(); + } + + @Test + @DisplayName("Agenda 생성하기 성공 - 포스터가 있는 경우 해당 이미지로 생성합니다.") + void createAgendaSuccessWithPosterImage() throws Exception { + // given + URL mockS3Path = new URL("https://test.com/test.jpeg"); + Mockito.when(imageHandler.uploadImageOrDefault( + Mockito.any(MultipartFile.class), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockS3Path); + + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + MockMultipartFile agendaPoster = new MockMultipartFile( + "agendaPoster", + "test.jpg", + "image/jpeg", + "test".getBytes() + ); + MultiValueMap params = MultiValueMapConverter.convert(objectMapper, dto); + + // when + String response = mockMvc.perform(multipart("/agenda/request") + .file(agendaPoster) + .header("Authorization", "Bearer " + accessToken) + .params(params)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + AgendaKeyResDto result = objectMapper.readValue(response, AgendaKeyResDto.class); + Agenda agenda = agendaRepository.findByAgendaKey(result.getAgendaKey()) + .orElseThrow(() -> new NotExistException("Agenda not found")); + + // then + assertThat(agenda.getTitle()).isEqualTo(dto.getAgendaTitle()); + assertThat(agenda.getContent()).isEqualTo(dto.getAgendaContent()); + assertThat(agenda.getMinPeople()).isEqualTo(dto.getAgendaMinPeople()); + assertThat(agenda.getPosterUri()).isNotEqualTo(defaultUri); + assertThat(agenda.getPosterUri()).isEqualTo(mockS3Path.toString()); + agendaPosterImageRepository.findByAgendaIdAndIsCurrentTrue(agenda.getId()) + .orElseThrow(() -> new NotExistException("Agenda poster image not found")); + } + + @Test + @DisplayName("deadline이 startTime보다 미래인 경우 400을 반환합니다.") + void createAgendaFailedWhenDeadlineIsAfterStartTime() throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(6)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("deadline이 endTime보다 미래인 경우 400을 반환합니다.") + void createAgendaFailedWhenDeadlineIsAfterEndTime() throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(4)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("startTime이 endTime보다 미래인 경우 400을 반환합니다.") + void createAgendaFailedWhenStartTimeIsAfterEndTime() throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(7)) + .agendaEndTime(LocalDateTime.now().plusDays(5)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("min team이 max team보다 큰 경우 400을 반환합니다.") + void createAgendaFailedWhenMinTeamGreaterThanMaxTeam() throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(7).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("min people이 max people보다 큰 경우 400을 반환합니다.") + void createAgendaFailedWhenMinPeopleGreaterThanMaxPeople() throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(6).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @ValueSource(ints = {1, 0, -1}) + @DisplayName("min team이 1 이하인 경우 400을 반환합니다.") + void createAgendaFailedWhenNegativeMinTeam(int value) throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(value).agendaMaxTeam(5).agendaMinPeople(1).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + @DisplayName("min people이 0 이하인 경우 400을 반환합니다.") + void createAgendaFailedWhenNegativeMinPeople(int value) throws Exception { + // given + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaTitle("title").agendaContent("content") + .agendaDeadLine(LocalDateTime.now().plusDays(3)) + .agendaStartTime(LocalDateTime.now().plusDays(5)) + .agendaEndTime(LocalDateTime.now().plusDays(7)) + .agendaMinTeam(2).agendaMaxTeam(5).agendaMinPeople(value).agendaMaxPeople(5) + .agendaIsRanking(true).agendaLocation(Location.SEOUL).build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/request") + .header("Authorization", "Bearer " + accessToken) + .contentType("application/json") + .content(request)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("Agenda 지난 목록 조회") + class GetAgendaListHistory { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4}) + @DisplayName("지난 Agenda 목록을 조회합니다.") + void getAgendaListHistorySuccess(int page) throws Exception { + // given + int totalCount = 35; + int size = 10; + List agendaHistory = agendaMockData.createAgendaHistory(totalCount); + + // when + String response = mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(size * page < totalCount ? size : totalCount % size); + for (int i = 0; i < result.size(); i++) { + if (i == 0) { + continue; + } + assertThat(result.get(i).getAgendaStartTime()).isAfter(result.get(i - 1).getAgendaStartTime()); + } + } + + @Test + @DisplayName("지난 Agenda가 없는 경우 빈 리스트를 반환합니다.") + void getAgendaListHistoryWithNoContent() throws Exception { + // given + int page = 1; + int size = 10; + + // when + String response = mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue( + response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + // then + assertThat(result.size()).isEqualTo(0); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1}) + @DisplayName("page가 1보다 작은 경우 400을 반환합니다.") + void getAgendaListHistoryWithInvalidPage(int page) throws Exception { + // given + int size = 10; + PageRequestDto pageRequestDto = new PageRequestDto(page, size); + String req = objectMapper.writeValueAsString(pageRequestDto); + + // expected + mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(req)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("page가 null인 경우 400을 반환합니다.") + void getAgendaListHistoryWithoutPage() throws Exception { + // given + int size = 10; + PageRequestDto pageRequestDto = new PageRequestDto(null, size); + String req = objectMapper.writeValueAsString(pageRequestDto); + + // expected + mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(req)) + .andExpect(status().isBadRequest()); + } + + @ParameterizedTest + @ValueSource(ints = {5, 6, 7, 8}) + @DisplayName("page가 실제 페이지 수보다 큰 경우 빈 리스트를 반환합니다.") + void getAgendaListHistoryWithExcessPage(int page) throws Exception { + // given + int totalCount = 35; + int size = 10; + List agendaHistory = agendaMockData.createAgendaHistory(totalCount); + + // when + String response = mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue( + response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(0); + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, 31}) + @DisplayName("size가 1 미만, 30 초과인 경우 400을 반환합니다.") + void getAgendaListHistoryWithInvalidSize(int size) throws Exception { + // given + int page = 1; + PageRequestDto pageRequestDto = new PageRequestDto(page, size); + String req = objectMapper.writeValueAsString(pageRequestDto); + + // expected + mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(req)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("size가 null인 경우 size=20으로 조회합니다.") + void getAgendaListHistoryWithoutSize() throws Exception { + // given + int page = 1; + List agendaHistory = agendaMockData.createAgendaHistory(30); + + // when + String response = mockMvc.perform(get("/agenda/history") + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue( + response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(20); + for (int i = 0; i < result.size(); i++) { + if (i == 0) { + continue; + } + assertThat(result.get(i).getAgendaStartTime()).isAfter(result.get(i - 1).getAgendaStartTime()); + } + } + } + + @Nested + @DisplayName("Agenda 종료 및 시상하기") + class FinishAgenda { + + @Test + @DisplayName("Agenda 종료 및 시상하기 성공 - 시상 대회인 경우") + void finishAgendaSuccess() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), true, AgendaStatus.CONFIRM); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // when + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isNoContent()); + Agenda result = em.createQuery("select a from Agenda a where a.agendaKey = :agendaKey", Agenda.class) + .setParameter("agendaKey", agenda.getAgendaKey()).getSingleResult(); + + // then + assertThat(result.getStatus()).isEqualTo(AgendaStatus.FINISH); + awards.forEach(award -> { + AgendaTeam agendaTeam = em.createQuery( + "select at from AgendaTeam at where at.agenda = :agenda and at.name = :teamName", + AgendaTeam.class) + .setParameter("agenda", agenda) + .setParameter("teamName", award.getTeamName()) + .getSingleResult(); + assertThat(agendaTeam.getAward()).isEqualTo(award.getAwardName()); + assertThat(agendaTeam.getAwardPriority()).isEqualTo(award.getAwardPriority()); + }); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 시상 대회에 시상 내역이 없는 경우") + void finishAgendaFailedWithNoAwards() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), true, AgendaStatus.CONFIRM); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + IntStream.range(0, awardSize).mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 성공 - 시상하지 않는 대회인 경우") + void finishAgendaSuccessWithNoRanking() throws Exception { + // given + int teamSize = 10; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), false, AgendaStatus.CONFIRM); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder() + .awards(List.of()) // 시상하지 않는 대회도 빈 리스트를 전송해야합니다. + .build(); + IntStream.range(0, teamSize) + .forEach(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)); + String request = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // when + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNoContent()); + Agenda result = em.createQuery("select a from Agenda a where a.agendaKey = :agendaKey", Agenda.class) + .setParameter("agendaKey", agenda.getAgendaKey()).getSingleResult(); + + // then + assertThat(result.getStatus()).isEqualTo(AgendaStatus.FINISH); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 성공 - 시상하지 않는 대회에 시상 내역이 들어온 경우") + void finishAgendaSuccessWithNoRankAndAwards() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), false, AgendaStatus.CONFIRM); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // when + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isNoContent()); + Agenda result = em.createQuery("select a from Agenda a where a.agendaKey = :agendaKey", Agenda.class) + .setParameter("agendaKey", agenda.getAgendaKey()).getSingleResult(); + + // then + assertThat(result.getStatus()).isEqualTo(AgendaStatus.FINISH); + awards.forEach(award -> { + AgendaTeam agendaTeam = em.createQuery( + "select at from AgendaTeam at where at.agenda = :agenda and at.name = :teamName", + AgendaTeam.class) + .setParameter("agenda", agenda) + .setParameter("teamName", award.getTeamName()) + .getSingleResult(); + assertThat(agendaTeam.getAward()).isNotEqualTo(award.getAwardName()); + assertThat(agendaTeam.getAwardPriority()).isNotEqualTo(award.getAwardPriority()); + }); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 존재하지 않는 팀에 대한 시상인 경우") + void finishAgendaFailedWithInvalidTeam() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), true, AgendaStatus.CONFIRM); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + awards.add(AgendaTeamAward.builder() + .teamName("invalid_team").awardName("prize").awardPriority(1).build()); // invalid team + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - Agenda가 없는 경우") + void finishAgendaFailedWithNoAgenda() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + UUID invalidAgendaKey = UUID.randomUUID(); // invalid agenda key + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", invalidAgendaKey.toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 시상 내역이 없는 경우") + void finishAgendaFailedWithoutAwards() throws Exception { + // given + int teamSize = 10; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + IntStream.range(0, teamSize).forEach(i -> + agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().build(); // null + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // when + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 시상 내역이 빈 리스트인 경우") + void finishAgendaFailedWithEmptyAwards() throws Exception { + // given + int teamSize = 10; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + IntStream.range(0, teamSize).forEach(i -> + agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder() + .awards(List.of()) // empty + .build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // when + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 개최자가 아닌 경우") + void finishAgendaFailedNotHost() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + User another = testDataUtils.createNewUser(); + Agenda agenda = agendaMockData.createAgenda(another.getIntraId(), LocalDateTime.now().minusDays(10), true); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // when + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 이미 확정된 경우") + void finishAgendaFailedAlreadyConfirm() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), AgendaStatus.FINISH); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 이미 취소된 경우") + void finishAgendaFailedAlreadyCancel() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().minusDays(10), AgendaStatus.CANCEL); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - 아직 시작하지 않은 경우") + void finishAgendaFailedBeforeStartTime() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), + LocalDateTime.now().plusDays(1), AgendaStatus.OPEN); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - empty awardName") + void finishAgendaFailedWithEmptyAwardName() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + awards.add(AgendaTeamAward.builder().teamName(agendaTeams.get(awardSize).getName()) + .awardName("").awardPriority(awardSize).build()); // empty award name + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - null awardName") + void finishAgendaFailedWithNullAwardName() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + awards.add(AgendaTeamAward.builder().teamName(agendaTeams.get(awardSize).getName()) + .awardPriority(awardSize).build()); // null award name + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - empty teamName") + void finishAgendaFailedWithEmptyTeamName() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + awards.add(AgendaTeamAward.builder().awardName("prize" + awardSize) + .teamName("").awardPriority(awardSize).build()); // empty award name + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 종료 및 시상하기 실패 - null teamName") + void finishAgendaFailedWithNullTeamName() throws Exception { + // given + int teamSize = 10; + int awardSize = 3; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId(), LocalDateTime.now().minusDays(10), true); + List agendaTeams = + IntStream.range(0, teamSize) + .mapToObj(i -> agendaMockData.createAgendaTeam(agenda, "team" + i, AgendaTeamStatus.CONFIRM)) + .collect(Collectors.toList()); + List awards = + IntStream.range(0, awardSize) + .mapToObj(i -> AgendaTeamAward.builder().teamName(agendaTeams.get(i).getName()) + .awardName("prize" + i).awardPriority(i + 1).build()) + .collect(Collectors.toList()); + awards.add(AgendaTeamAward.builder().awardName("prize" + awardSize) + .awardPriority(awardSize).build()); // null award name + AgendaAwardsReqDto agendaAwardsReqDto = AgendaAwardsReqDto.builder().awards(awards).build(); + String response = objectMapper.writeValueAsString(agendaAwardsReqDto); + + // expected + mockMvc.perform(patch("/agenda/finish") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(response)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("Agenda 확정하기") + class ConfirmAgenda { + + @Test + @DisplayName("Agenda 확정하기 성공") + void confirmAgendaSuccess() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaAndAgendaTeams(user.getIntraId(), 5, AgendaStatus.OPEN); + List openTeams = agendaTeamFixture.createAgendaTeamList(agenda, AgendaTeamStatus.OPEN, 3); + + // when + mockMvc.perform(patch("/agenda/confirm") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + Optional confirmedAgenda = agendaRepository.findById(agenda.getId()); + List openedTeams = agendaTeamRepository + .findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN); + List canceledTeams = agendaTeamRepository + .findAllByAgendaAndStatus(agenda, AgendaTeamStatus.CANCEL); + + // then + assert (confirmedAgenda.isPresent()); + assertThat(confirmedAgenda.get().getStatus()).isEqualTo(AgendaStatus.CONFIRM); + assertThat(openedTeams.size()).isEqualTo(0); + assertThat(canceledTeams.size()).isEqualTo(openTeams.size()); + } + + @Test + @DisplayName("Agenda 확정하기 실패 - 존재하지 않는 Agenda인 경우") + void confirmAgendaFailedWithNoAgenda() throws Exception { + // given + // expected + mockMvc.perform(patch("/agenda/confirm") + .param("agenda_key", UUID.randomUUID().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Agenda 확정하기 실패 - 개최자가 아닌 경우") + void confirmAgendaFailedWithNotHost() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaAndAgendaTeams("not host", 5, AgendaStatus.OPEN); + + // when + mockMvc.perform(patch("/agenda/confirm") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isForbidden()); + } + + @ParameterizedTest + @EnumSource(value = AgendaStatus.class, names = {"CANCEL", "CONFIRM", "FINISH"}) + @DisplayName("Agenda 확정하기 실패 - 대회의 상태가 OPEN이 아닌 경우") + void confirmAgendaFailedWithNotOpenAgenda(AgendaStatus status) throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(user.getIntraId(), AgendaStatus.OPEN); + agendaTeamFixture.createAgendaTeamList(agenda, AgendaTeamStatus.CONFIRM, 5); + agenda.updateAgendaStatus(status); + + // expected + mockMvc.perform(patch("/agenda/confirm") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("Agenda 취소하기") + class CancelAgenda { + @Test + @DisplayName("Agenda 취소하기 성공") + void cancelAgendaSuccess() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaTeamProfiles(user, AgendaStatus.OPEN); + + // when + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + + // then + Agenda canceledAgenda = agendaRepository.findById(agenda.getId()) + .orElseThrow(() -> new BusinessException("cancelAgendaSuccess - 테스트 실패")); + assertThat(canceledAgenda.getStatus()).isEqualTo(AgendaStatus.CANCEL); + + em.createQuery("select at from AgendaTeam at where at.agenda = :agenda", + AgendaTeam.class).setParameter("agenda", agenda) + .getResultStream() + .forEach(agendaTeam -> assertThat(agendaTeam.getStatus()).isEqualTo(AgendaTeamStatus.CANCEL)); + } + + @Test + @DisplayName("Agenda 취소하기 실패 - 존재하지 않는 Agenda인 경우") + void cancelAgendaFailedWithInvalidAgenda() throws Exception { + // given + // No Agenda + + // expected + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", UUID.randomUUID().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Agenda 취소하기 실패 - 개최자가 아닌 경우") + void cancelAgendaFailedWithNotHost() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaTeamProfiles(user, AgendaStatus.OPEN); + User other = testDataUtils.createNewUser(); + String otherAccessToken = testDataUtils.getLoginAccessTokenFromUser(other); + // expected + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + otherAccessToken)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("Agenda 취소하기 실패 - 이미 취소된 경우") + void cancelAgendaFailedWithAlreadyCancel() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaTeamProfiles(user, AgendaStatus.OPEN); + agenda.cancelAgenda(); + + // expected + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 취소하기 실패 - 이미 확정된 경우") + void cancelAgendaFailedWithAlreadyConfirm() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaTeamProfiles(user, AgendaStatus.OPEN); + agenda.confirmAgenda(); + + // expected + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 취소하기 실패 - 이미 종료된 경우") + void cancelAgendaFailedWithAlreadyFinish() throws Exception { + // given + Agenda agenda = agendaTestDataUtils.createAgendaTeamProfiles(user, AgendaStatus.OPEN); + agenda.confirmAgenda(); + agenda.finishAgenda(); + + // expected + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("Agenda 취소하기 성공 - 참여한 팀이 없는 경우") + void cancelAgendaSuccessWithNoTeams() throws Exception { + // given + Agenda agenda = agendaFixture.createAgenda(user.getIntraId(), AgendaStatus.OPEN); + + // when + mockMvc.perform(patch("/agenda/cancel") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNoContent()); + + // then + Agenda result = agendaRepository.findById(agenda.getId()) + .orElseThrow(() -> new BusinessException("cancelAgendaSuccessWithNoTeams - 테스트 실패")); + assertThat(result.getStatus()).isEqualTo(AgendaStatus.CANCEL); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/dto/AgendaCreateReqDtoTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/dto/AgendaCreateReqDtoTest.java new file mode 100644 index 000000000..4bdd20599 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/dto/AgendaCreateReqDtoTest.java @@ -0,0 +1,39 @@ +package gg.agenda.api.user.agenda.controller.dto; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; +import gg.auth.UserDto; +import gg.data.agenda.Agenda; +import gg.utils.annotation.UnitTest; +import gg.utils.exception.custom.InvalidParameterException; + +@UnitTest +class AgendaCreateReqDtoTest { + + @Test + @DisplayName("Agenda 생성 성공") + void createAgendaSuccess() { + //given + UserDto user = UserDto.builder().intraId("intraId").build(); + AgendaCreateReqDto dto = AgendaCreateReqDto.builder() + .agendaDeadLine(LocalDateTime.now().plusDays(5)) + .agendaStartTime(LocalDateTime.now().plusDays(8)) + .agendaEndTime(LocalDateTime.now().plusDays(10)) + .build(); + + // when + Agenda agenda = AgendaCreateReqDto.MapStruct.INSTANCE.toEntity(dto, user.getIntraId()); + + // then + assertNotNull(agenda); + assertThat(agenda.getHostIntraId()).isEqualTo(user.getIntraId()); + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/dto/AgendaSimpleResDtoTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/dto/AgendaSimpleResDtoTest.java new file mode 100644 index 000000000..b8e1ef055 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/controller/dto/AgendaSimpleResDtoTest.java @@ -0,0 +1,40 @@ +package gg.agenda.api.user.agenda.controller.dto; + +import static org.assertj.core.api.AssertionsForClassTypes.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import gg.agenda.api.user.agenda.controller.response.AgendaSimpleResDto; +import gg.data.agenda.Agenda; +import gg.utils.annotation.UnitTest; + +@UnitTest +class AgendaSimpleResDtoTest { + + @Nested + @DisplayName("AgendaSimpleResponseDto 생성") + class CreateAgendaSimpleResDto { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + @DisplayName("AgendaSimpleResponseDto 생성 성공") + void createAgendaSimpleResponseDtoSuccess(boolean value) { + // when + Agenda agenda = Agenda.builder() + .isOfficial(value) + .build(); + + // given + AgendaSimpleResDto dto = AgendaSimpleResDto.MapStruct.INSTANCE.toDto(agenda); + + // then + assertThat(dto).isNotNull(); + assertThat(dto.getAgendaKey()).isEqualTo(agenda.getAgendaKey()); + assertThat(dto.getIsOfficial()).isEqualTo(value); + } + } + +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/service/AgendaServiceTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/service/AgendaServiceTest.java new file mode 100644 index 000000000..7ac7d12d0 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agenda/service/AgendaServiceTest.java @@ -0,0 +1,425 @@ +package gg.agenda.api.user.agenda.service; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + +import gg.agenda.api.user.agenda.controller.request.AgendaAwardsReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaCreateReqDto; +import gg.agenda.api.user.agenda.controller.request.AgendaTeamAward; +import gg.agenda.api.user.agendateam.service.AgendaTeamService; +import gg.agenda.api.user.ticket.service.TicketService; +import gg.auth.UserDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.repo.agenda.AgendaRepository; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.repo.agenda.AgendaTeamRepository; +import gg.utils.annotation.UnitTest; +import gg.utils.exception.custom.InvalidParameterException; +import gg.utils.exception.custom.NotExistException; +import gg.utils.file.handler.ImageHandler; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@UnitTest +class AgendaServiceTest { + + @Mock + AgendaRepository agendaRepository; + + @Mock + AgendaTeamRepository agendaTeamRepository; + + @Mock + AgendaTeamProfileRepository agendaTeamProfileRepository; + + @Mock + AgendaTeamService agendaTeamService; + + @Mock + TicketService ticketService; + + @Mock + ImageHandler imageHandler; + + @InjectMocks + AgendaService agendaService; + + @Nested + @DisplayName("Agenda 상세 조회") + class GetAgenda { + @Test + @DisplayName("AgendaKey로 Agenda 상세 조회") + void findAgendaByAgendaKeySuccess() { + // given + Agenda agenda = Agenda.builder().build(); + UUID agendaKey = agenda.getAgendaKey(); + when(agendaRepository.findByAgendaKey(agendaKey)).thenReturn(Optional.of(agenda)); + + // when + Agenda result = agendaService.findAgendaByAgendaKey(agendaKey); + + // then + verify(agendaRepository, times(1)).findByAgendaKey(agendaKey); + assertThat(result).isEqualTo(agenda); + } + + @Test + @DisplayName("AgendaKey로 Agenda 상세 조회 - 존재하지 않는 AgendaKey인 경우") + void findAgendaByAgendaKeyFailedWithNoAgenda() { + // given + UUID agendaKey = UUID.randomUUID(); + when(agendaRepository.findByAgendaKey(agendaKey)).thenReturn(Optional.empty()); + + // expected + assertThrows(NotExistException.class, + () -> agendaService.findAgendaByAgendaKey(agendaKey)); + } + } + + @Nested + @DisplayName("Agenda 현황 전체 조회") + class GetAgendaListCurrent { + + @Test + @DisplayName("Agenda 현황 전체를 반환합니다.") + void getAgendaListSuccess() { + // given + int officialSize = 3; + int nonOfficialSize = 3; + List agendas = new ArrayList<>(); + IntStream.range(0, officialSize).forEach(i -> agendas.add(Agenda.builder().isOfficial(true) + .deadline(LocalDateTime.now().plusDays(i + 5)).build())); + IntStream.range(0, nonOfficialSize).forEach(i -> agendas.add(Agenda.builder().isOfficial(false) + .deadline(LocalDateTime.now().plusDays(i + 2)).build())); + when(agendaRepository.findAllByStatusIs(AgendaStatus.OPEN)).thenReturn(agendas); + + // when + List result = agendaService.findOpenAgendaList(); + + // then + verify(agendaRepository, times(1)).findAllByStatusIs(any()); + for (int i = 0; i < result.size(); i++) { + if (i == 0 || i == officialSize) { + continue; + } + assertThat(result.get(i).getDeadline()).isAfter(result.get(i - 1).getDeadline()); + } + } + + @Test + @DisplayName("생성된 Agenda가 없는 경우 빈 리스트를 반환합니다.") + void getAgendaListWithNoContent() { + // given + List agendas = new ArrayList<>(); + when(agendaRepository.findAllByStatusIs(AgendaStatus.OPEN)).thenReturn(agendas); + + // when + agendaService.findOpenAgendaList(); + + // then + verify(agendaRepository, times(1)).findAllByStatusIs(any()); + } + } + + @Nested + @DisplayName("Agenda 생성") + class CreateAgenda { + + @Test + @DisplayName("Agenda 생성 성공") + void createAgendaSuccess() throws IOException { + // given + AgendaCreateReqDto agendaCreateReqDto = AgendaCreateReqDto.builder().build(); + UserDto user = UserDto.builder().intraId("intraId").build(); + Agenda agenda = Agenda.builder().build(); + when(agendaRepository.save(any())).thenReturn(agenda); + + // when + Agenda result = agendaService.addAgenda(agendaCreateReqDto, null, user); + + // then + verify(agendaRepository, times(1)).save(any()); + assertThat(result).isEqualTo(agenda); + } + } + + @Nested + @DisplayName("지난 Agenda 조회") + class GetAgendaListHistory { + + @Test + @DisplayName("지난 Agenda 조회 성공") + void getAgendaListHistorySuccess() { + // given + int page = 1; + int size = 10; + Pageable pageable = PageRequest.of(page - 1, size, Sort.by("startTime").descending()); + List agendas = new ArrayList<>(); + IntStream.range(0, size * 2).forEach(i -> agendas.add(Agenda.builder() + .startTime(LocalDateTime.now().minusDays(i)) + .build() + )); + Page agendaPage = new PageImpl<>(agendas.subList(0, 10), pageable, size); + when(agendaRepository.findAllByStatusIs(eq(AgendaStatus.FINISH), any(Pageable.class))) + .thenReturn(agendaPage); + + // when + Page res = agendaService.findHistoryAgendaList(pageable); + List result = res.getContent(); + + // then + verify(agendaRepository, times(1)) + .findAllByStatusIs(AgendaStatus.FINISH, pageable); + assertThat(result.size()).isEqualTo(size); + for (int i = 1; i < result.size(); i++) { + assertThat(result.get(i).getStartTime()) + .isBefore(result.get(i - 1).getStartTime()); + } + } + } + + @Nested + @DisplayName("Agenda 시상 및 확정") + class FinishAgenda { + + int seq; + + @BeforeEach + void setUp() { + seq = 0; + } + + @Test + @DisplayName("Agenda 시상 및 확정 성공") + void finishAgendaSuccess() { + // given + Agenda agenda = Agenda.builder() + .hostIntraId("intraId").startTime(LocalDateTime.now().minusDays(1)) + .status(AgendaStatus.CONFIRM).isRanking(true).build(); + List agendaTeams = new ArrayList<>(); + IntStream.range(0, 10).forEach(i -> agendaTeams.add(AgendaTeam.builder().name("team" + i).build())); + AgendaTeamAward awardDto = AgendaTeamAward.builder() + .teamName("team1").awardName("award").awardPriority(1).build(); + AgendaAwardsReqDto confirmDto = AgendaAwardsReqDto.builder() + .awards(List.of(awardDto)).build(); + + when(agendaTeamRepository.findAllByAgendaAndStatus(any(), any())) + .thenReturn(agendaTeams); + + // when + agendaService.awardAgenda(confirmDto, agenda); + + // then + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(any(), any()); + assertThat(agenda.getStatus()).isEqualTo(AgendaStatus.CONFIRM); + } + + @Test + @DisplayName("Agenda 시상 및 확정 성공 - 시상하지 않는 대회에 시상 내역이 빈 리스트로 들어온 경우") + void finishAgendaSuccessWithNoRankAndEmptyAwards() { + // given + Agenda agenda = Agenda.builder() + .hostIntraId("intraId").startTime(LocalDateTime.now().minusDays(1)) + .status(AgendaStatus.CONFIRM).isRanking(false).build(); + AgendaAwardsReqDto confirmDto = AgendaAwardsReqDto.builder() + .awards(List.of()).build(); + when(agendaTeamRepository.findAllByAgendaAndStatus(any(), any())) + .thenReturn(List.of()); + + // when + agendaService.awardAgenda(confirmDto, agenda); + + // then + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(any(), any()); + assertThat(agenda.getStatus()).isEqualTo(AgendaStatus.CONFIRM); + } + + @Test + @DisplayName("Agenda 시상 및 확정 실패 - 시상 내역이 null인 경우") + void finishAgendaFailedWithoutAwards() { + // given + Agenda agenda = Agenda.builder() + .hostIntraId("intraId").startTime(LocalDateTime.now().minusDays(1)) + .status(AgendaStatus.CONFIRM).isRanking(true).build(); + + AgendaAwardsReqDto confirmDto = AgendaAwardsReqDto.builder().build(); + + // expected + assertThrows(NullPointerException.class, + () -> agendaService.awardAgenda(confirmDto, agenda)); + } + + @Test + @DisplayName("Agenda 시상 및 확정 실패 - 매개변수가 null인 경우") + void finishAgendaFailedWithNullDto() { + // given + Agenda agenda = Agenda.builder() + .hostIntraId("intraId").startTime(LocalDateTime.now().minusDays(1)) + .status(AgendaStatus.CONFIRM).isRanking(true).build(); + + // expected + assertThrows(NullPointerException.class, + () -> agendaService.awardAgenda(null, agenda)); + } + + @Test + @DisplayName("Agenda 시상 및 확정 실패 - 존재하지 않는 팀에 대한 시상인 경우") + void finishAgendaFailedWithInvalidTeam() { + // given + Agenda agenda = Agenda.builder() + .hostIntraId("intraId").startTime(LocalDateTime.now().minusDays(1)) + .status(AgendaStatus.CONFIRM).isRanking(true).build(); + AgendaTeamAward awardDto = AgendaTeamAward.builder() + .teamName("invalidTeam").awardName("award").awardPriority(1).build(); + AgendaAwardsReqDto confirmDto = AgendaAwardsReqDto.builder() + .awards(List.of(awardDto)).build(); + + when(agendaTeamRepository.findAllByAgendaAndStatus(any(), any())) + .thenReturn(List.of()); + + // expected + assertThrows(NotExistException.class, + () -> agendaService.awardAgenda(confirmDto, agenda)); + } + } + + @Nested + @DisplayName("Agenda 확정하기") + class ConfirmAgenda { + + @Test + @DisplayName("Agenda 확정하기 성공") + void confirmAgendaSuccess() { + // given + Agenda agenda = Agenda.builder().hostIntraId("intraId").status(AgendaStatus.OPEN).build(); + AgendaTeam agendaTeam = AgendaTeam.builder().status(AgendaTeamStatus.OPEN).build(); + AgendaTeamProfile participant = AgendaTeamProfile.builder() + .profile(AgendaProfile.builder().build()).build(); + when(agendaTeamRepository.findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN)) + .thenReturn(List.of(agendaTeam)); + doNothing().when(agendaTeamService).leaveTeamAll(any()); + + // when + agendaService.confirmAgendaAndRefundTicketForOpenTeam(agenda); + + // then + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN); + assertThat(agenda.getStatus()).isEqualTo(AgendaStatus.CONFIRM); + } + + @Test + @DisplayName("Agenda 확정하기 실패 - AgendaTeam이 없는 경우") + void confirmAgendaFailedWithNoAgenda() { + // given + Agenda agenda = Agenda.builder().hostIntraId("intraId").status(AgendaStatus.OPEN).build(); + when(agendaTeamRepository.findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN)) + .thenReturn(List.of()); + + // when + agendaService.confirmAgendaAndRefundTicketForOpenTeam(agenda); + + // then + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN); + assertThat(agenda.getStatus()).isEqualTo(AgendaStatus.CONFIRM); + } + + @ParameterizedTest + @EnumSource(value = AgendaStatus.class, names = {"CONFIRM", "FINISH", "CANCEL"}) + @DisplayName("Agenda 확정하기 실패 - 대회의 상태가 OPEN이 아닌 경우") + void confirmAgendaFailedWithAlreadyConfirm(AgendaStatus status) { + // given + Agenda agenda = Agenda.builder().hostIntraId("intraId").status(status).build(); + AgendaTeam agendaTeam = AgendaTeam.builder().status(AgendaTeamStatus.OPEN).build(); + when(agendaTeamRepository.findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN)) + .thenReturn(List.of(agendaTeam)); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaService.confirmAgendaAndRefundTicketForOpenTeam(agenda)); + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(agenda, AgendaTeamStatus.OPEN); + } + } + + @Nested + @DisplayName("Agenda 취소하기") + class CancelAgenda { + @Test + @DisplayName("Agenda 취소하기 성공") + void cancelAgendaSuccess() { + // given + Agenda agenda = Agenda.builder().status(AgendaStatus.OPEN).build(); + List agendaTeams = List.of(mock(AgendaTeam.class)); + when(agendaTeamRepository.findAllByAgendaAndStatus(any(), any(), any())) + .thenReturn(agendaTeams); + doNothing().when(agendaTeamService).leaveTeamAll(any()); + + // when + agendaService.cancelAgenda(agenda); + + // then + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(any(), any(), any()); + verify(agendaTeamService, times(agendaTeams.size())).leaveTeamAll(any()); + assertThat(agenda.getStatus()).isEqualTo(AgendaStatus.CANCEL); + } + + @ParameterizedTest + @EnumSource(value = AgendaStatus.class, names = {"CONFIRM", "FINISH", "CANCEL"}) + @DisplayName("Agenda 취소하기 실패 - AgendaStatus가 OPEN이 아닌 경우") + void cancelAgendaFailedWithNotOpen(AgendaStatus status) { + // given + Agenda agenda = Agenda.builder().status(status).build(); + List agendaTeam = List.of(mock(AgendaTeam.class)); + when(agendaTeamRepository.findAllByAgendaAndStatus(any(), any(), any())) + .thenReturn(agendaTeam); + doNothing().when(agendaTeamService).leaveTeamAll(any()); + + // expected + assertThrows(InvalidParameterException.class, + () -> agendaService.cancelAgenda(agenda)); + } + + @Test + @DisplayName("Agenda 취소하기 성공 - AgendaTeam이 없는 경우") + void cancelAgendaSuccessWithNoAgendaTeam() { + // given + Agenda agenda = Agenda.builder().status(AgendaStatus.OPEN).build(); + when(agendaTeamRepository.findAllByAgendaAndStatus(any(), any(), any())) + .thenReturn(List.of()); + + // when + agendaService.cancelAgenda(agenda); + + verify(agendaTeamRepository, times(1)).findAllByAgendaAndStatus(any(), any(), any()); + verify(agendaTeamService, never()).leaveTeamAll(any()); + assertThat(agenda.getStatus()).isEqualTo(AgendaStatus.CANCEL); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaannouncement/controller/AgendaAnnouncementControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaannouncement/controller/AgendaAnnouncementControllerTest.java new file mode 100644 index 000000000..be367f70f --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaannouncement/controller/AgendaAnnouncementControllerTest.java @@ -0,0 +1,295 @@ +package gg.agenda.api.user.agendaannouncement.controller; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.agenda.api.AgendaMockData; +import gg.agenda.api.user.agendaannouncement.controller.request.AgendaAnnouncementCreateReqDto; +import gg.agenda.api.user.agendaannouncement.controller.response.AgendaAnnouncementResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.data.user.User; +import gg.repo.agenda.AgendaAnnouncementRepository; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageResponseDto; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaAnnouncementControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private TestDataUtils testDataUtils; + + @Autowired + private AgendaMockData agendaMockData; + + @Autowired + EntityManager em; + + @Autowired + AgendaAnnouncementRepository agendaAnnouncementRepository; + + private User user; + + private String accessToken; + + @BeforeEach + void setUp() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Nested + @DisplayName("AgendaAnnouncement 생성") + class CreateAgendaAnnouncement { + + @Test + @DisplayName("AgendaAnnouncement 생성 성공") + void createAgendaAnnouncementSuccess() throws Exception { + // given + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("title").content("content").build(); + String request = objectMapper.writeValueAsString(dto); + + // when + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isCreated()); + Optional latestAnnounce = agendaAnnouncementRepository.findLatestByAgenda(agenda); + + // then + assertThat(latestAnnounce).isPresent(); + latestAnnounce.ifPresent(announcement -> assertThat(announcement.getTitle()).isEqualTo(dto.getTitle())); + latestAnnounce.ifPresent(announcement -> assertThat(announcement.getContent()).isEqualTo(dto.getContent())); + } + + @Test + @DisplayName("AgendaAnnouncement 생성 실패 - title이 null인 경우") + void createAgendaAnnouncementFailedWithNoTitle() throws Exception { + // given + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .content("content").build(); // title이 null인 경우 + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("AgendaAnnouncement 생성 실패 - title이 빈 문자열 경우") + void createAgendaAnnouncementFailedWithEmptyTitle() throws Exception { + // given + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("").content("content").build(); // title이 empty인 경우 + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("AgendaAnnouncement 생성 실패 - Content가 null인 경우") + void createAgendaAnnouncementFailedWithNoContent() throws Exception { + // given + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("title").build(); // content가 null인 경우 + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("AgendaAnnouncement 생성 실패 - Content가 빈 문자열 경우") + void createAgendaAnnouncementFailedWithEmptyContent() throws Exception { + // given + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("title").content("").build(); // content가 empty인 경우 + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("AgendaAnnouncement 생성 실패 - Agenda가 없는 경우") + void createAgendaAnnouncementFailedWithNoAgenda() throws Exception { + // given + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("title").content("content").build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("AgendaAnnouncement 생성 실패 - 개최자가 아닌 경우") + void createAgendaAnnouncementFailedWithNotHost() throws Exception { + // given + Agenda agenda = agendaMockData.createAgenda("another"); // 다른 사용자가 생성한 Agenda + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("title").content("content").build(); + String request = objectMapper.writeValueAsString(dto); + + // expected + mockMvc.perform(post("/agenda/announcement") + .header("Authorization", "Bearer " + accessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isForbidden()); + } + } + + @Nested + @DisplayName("AgendaAnnouncement 전체 조회") + class GetAgendaAnnouncementList { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3}) + @DisplayName("AgendaAnnouncement 전체 조회 성공") + void getAgendaAnnouncementListSuccess(int page) throws Exception { + // given + int total = 35; + int size = 10; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + agendaMockData.createAgendaAnnouncementList(agenda, 30, false); + List announcements = agendaMockData + .createAgendaAnnouncementList(agenda, total, true); + + // when + String response = mockMvc.perform(get("/agenda/announcement") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result).hasSize(size * page < total ? size : total % size); + announcements.sort((o1, o2) -> Long.compare(o2.getId(), o1.getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getId()).isEqualTo(announcements.get(i + (page - 1) * size).getId()); + assertThat(result.get(i).getTitle()).isEqualTo(announcements.get(i + (page - 1) * size).getTitle()); + assertThat(result.get(i).getContent()).isEqualTo(announcements.get(i + (page - 1) * size).getContent()); + if (i == 0) { + continue; + } + assertThat(result.get(i).getId()).isLessThan(result.get(i - 1).getId()); + } + } + + @Test + @DisplayName("AgendaAnnouncement 전체 조회 성공 - 데이터 없는 경우") + void getAgendaAnnouncementListSuccessWhenNoEntity() throws Exception { + // given + int page = 1; + int size = 10; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + agendaMockData.createAgendaAnnouncementList(agenda, 30, false); + + // when + String response = mockMvc.perform(get("/agenda/announcement") + .param("agenda_key", agenda.getAgendaKey().toString()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + // then + assertThat(result).hasSize(0); + } + + @Test + @DisplayName("AgendaAnnouncement 전체 조회 실패 - Agenda가 없는 경우") + void getAgendaAnnouncementListFailedWithInvalidAgenda() throws Exception { + // given + int page = 1; + int size = 10; + Agenda agenda = agendaMockData.createAgenda(user.getIntraId()); + agendaMockData.createAgendaAnnouncementList(agenda, 30, false); + agendaMockData.createAgendaAnnouncementList(agenda, 30, true); + + // when + mockMvc.perform(get("/agenda/announcement") + .param("agenda_key", UUID.randomUUID().toString()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isNotFound()); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaannouncement/service/AgendaAnnouncementServiceTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaannouncement/service/AgendaAnnouncementServiceTest.java new file mode 100644 index 000000000..feb4b53f9 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaannouncement/service/AgendaAnnouncementServiceTest.java @@ -0,0 +1,95 @@ +package gg.agenda.api.user.agendaannouncement.service; + +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import gg.agenda.api.user.agendaannouncement.controller.request.AgendaAnnouncementCreateReqDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.repo.agenda.AgendaAnnouncementRepository; +import gg.utils.annotation.UnitTest; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@UnitTest +public class AgendaAnnouncementServiceTest { + + @Mock + private AgendaAnnouncementRepository agendaAnnouncementRepository; + + @InjectMocks + private AgendaAnnouncementService agendaAnnouncementService; + + @Nested + @DisplayName("AgendaAnnouncement 생성") + class CreateAgendaAnnouncement { + + @Test + @DisplayName("AgendaAnnouncement 생성 성공") + void createAgendaAnnouncementSuccess() { + // given + Agenda agenda = mock(Agenda.class); + AgendaAnnouncement newAnnounce = mock(AgendaAnnouncement.class); + AgendaAnnouncementCreateReqDto dto = AgendaAnnouncementCreateReqDto.builder() + .title("title").content("content").build(); + when(agendaAnnouncementRepository.save(any())).thenReturn(newAnnounce); + + // when + agendaAnnouncementService.addAgendaAnnouncement(dto, agenda); + + // then + verify(agendaAnnouncementRepository, times(1)).save(any()); + } + } + + @Nested + @DisplayName("AgendaAnnouncement 전체 조회") + class GetAgendaAnnouncementList { + + @Test + @DisplayName("AgendaAnnouncement 전체 조회 성공") + void getAgendaAnnouncementListSuccess() { + // given + Agenda agenda = mock(Agenda.class); + Pageable pageable = mock(Pageable.class); + when(agendaAnnouncementRepository.findListByAgenda(pageable, agenda)) + .thenReturn(Page.empty()); + + // when + agendaAnnouncementService.findAnnouncementListByAgenda(pageable, agenda); + + // then + verify(agendaAnnouncementRepository, times(1)).findListByAgenda(pageable, agenda); + } + } + + @Nested + @DisplayName("마지막 AgendaAnnouncement 조회") + class GetAgendaAnnouncementLatest { + + @Test + @DisplayName("마지막 AgendaAnnouncement 조회 성공") + void getAgendaAnnouncementLatestSuccess() { + // given + Agenda agenda = mock(Agenda.class); + AgendaAnnouncement announcement = mock(AgendaAnnouncement.class); + when(agendaAnnouncementRepository.findLatestByAgenda(agenda)).thenReturn(Optional.of(announcement)); + + // when + agendaAnnouncementService.findLatestAnnounceTitleByAgendaOrDefault(agenda, ""); + + // then + verify(agendaAnnouncementRepository, times(1)).findLatestByAgenda(agenda); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaprofile/AgendaHostControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaprofile/AgendaHostControllerTest.java new file mode 100644 index 000000000..4dbe4935f --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaprofile/AgendaHostControllerTest.java @@ -0,0 +1,190 @@ +package gg.agenda.api.user.agendaprofile; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; +import java.util.stream.Collectors; + +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.agenda.api.user.agendaprofile.controller.response.HostedAgendaResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; +import gg.data.user.User; +import gg.utils.AgendaTestDataUtils; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageResponseDto; + +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaHostControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private TestDataUtils testDataUtils; + + @Autowired + private AgendaTestDataUtils agendaTestDataUtils; + + User user; + + String accessToken; + + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Nested + @DisplayName("내가 주최했던 Agenda 목록 조회") + class HostedAgendaList { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("내가 주최했던 Agenda 목록 조회 성공") + void hostedAgendaListSuccess(int page) throws Exception { + // given + int size = 10; + int eachCount = 10; + List agendas = agendaTestDataUtils.createAgendasWithAllStatus(user, eachCount).stream() + .filter(agenda -> + agenda.getStatus() == AgendaStatus.FINISH || agenda.getStatus() == AgendaStatus.CANCEL) + .sorted((a1, a2) -> a2.getId().compareTo(a1.getId())) + .collect(Collectors.toList()); + + // when + String response = mockMvc.perform(get("/agenda/host/history/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(pageResponseDto.getTotalSize()).isEqualTo(agendas.size()); + assertThat(result.size()).isEqualTo(size * page <= agendas.size() ? size : agendas.size() % size); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getAgendaTitle()) + .isEqualTo(agendas.get(size * (page - 1) + i).getTitle()); + assertThat(result.get(i).getAgendaStatus()).isNotEqualTo(AgendaStatus.OPEN); + assertThat(result.get(i).getAgendaStatus()).isNotEqualTo(AgendaStatus.CONFIRM); + } + } + + @Test + @DisplayName("내가 주최했던 Agenda 목록 조회 성공 - 빈 리스트인 경우") + void hostedAgendaListSuccessWithEmptyAgenda() throws Exception { + // given + + // when + String response = mockMvc.perform(get("/agenda/host/history/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", "1") + .param("size", "10")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(pageResponseDto.getTotalSize()).isEqualTo(0); + assertThat(result.size()).isEqualTo(0); + } + } + + @Nested + @DisplayName("내가 주최하고 있는 Agenda 목록 조회") + class HostingAgendaList { + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("내가 주최하고 있는 Agenda 목록 조회 성공") + void hostingAgendaListSuccess(int page) throws Exception { + // given + int size = 10; + int eachCount = 10; + List agendas = agendaTestDataUtils.createAgendasWithAllStatus(user, eachCount).stream() + .filter(agenda -> + agenda.getStatus() == AgendaStatus.OPEN || agenda.getStatus() == AgendaStatus.CONFIRM) + .sorted((a1, a2) -> a2.getId().compareTo(a1.getId())) + .collect(Collectors.toList()); + + // when + String response = mockMvc.perform(get("/agenda/host/current/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size))) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(pageResponseDto.getTotalSize()).isEqualTo(agendas.size()); + assertThat(result.size()).isEqualTo(size * page <= agendas.size() ? size : agendas.size() % size); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getAgendaTitle()) + .isEqualTo(agendas.get(size * (page - 1) + i).getTitle()); + assertThat(result.get(i).getAgendaStatus()).isNotEqualTo(AgendaStatus.FINISH); + assertThat(result.get(i).getAgendaStatus()).isNotEqualTo(AgendaStatus.CANCEL); + } + } + + @Test + @DisplayName("내가 주최하고 있는 Agenda 목록 조회 성공 - 빈 리스트인 경우") + void hostingAgendaListSuccessWithEmptyList() throws Exception { + // given + + // when + String response = mockMvc.perform(get("/agenda/host/current/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", "1") + .param("size", "10")) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(pageResponseDto.getTotalSize()).isEqualTo(0); + assertThat(result.size()).isEqualTo(0); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaprofile/AgendaProfileControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaprofile/AgendaProfileControllerTest.java new file mode 100644 index 000000000..53cbe832b --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendaprofile/AgendaProfileControllerTest.java @@ -0,0 +1,522 @@ +package gg.agenda.api.user.agendaprofile; + +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.net.URL; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; +import javax.transaction.Transactional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.agenda.api.AgendaMockData; +import gg.agenda.api.user.agendaprofile.controller.request.AgendaProfileChangeReqDto; +import gg.agenda.api.user.agendaprofile.controller.response.AgendaProfileDetailsResDto; +import gg.agenda.api.user.agendaprofile.controller.response.AgendaProfileInfoDetailsResDto; +import gg.agenda.api.user.agendaprofile.controller.response.AttendedAgendaListResDto; +import gg.agenda.api.user.agendaprofile.controller.response.CurrentAttendAgendaListResDto; +import gg.agenda.api.user.agendaprofile.controller.response.MyAgendaProfileDetailsResDto; +import gg.agenda.api.user.agendaprofile.service.IntraProfileUtils; +import gg.agenda.api.user.agendaprofile.service.intraprofile.IntraProfile; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.data.user.type.RoleType; +import gg.repo.agenda.AgendaProfileRepository; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; + +@IntegrationTest +@Transactional +@AutoConfigureMockMvc +public class AgendaProfileControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private TestDataUtils testDataUtils; + @Autowired + private AgendaMockData agendaMockData; + @Autowired + private AgendaProfileRepository agendaProfileRepository; + + @MockBean + private IntraProfileUtils intraProfileUtils; + + User user; + String accessToken; + AgendaProfile agendaProfile; + + @Nested + @DisplayName("나의 agenda profile 상세 조회") + class GetMyAgendaProfile { + + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Test + @DisplayName("로그인된 유저에 해당하는 Agenda profile를 상세 조회합니다.") + void test() throws Exception { + //given + URL url = new URL("http://localhost:8080"); + IntraProfile intraProfile = new IntraProfile(user.getIntraId(), url, List.of()); + Mockito.when(intraProfileUtils.getIntraProfile(any(HttpServletResponse.class))) + .thenReturn(intraProfile); + AgendaProfile agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + agendaMockData.createTicket(agendaProfile); + // when + String response = mockMvc.perform(get("/agenda/profile") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + MyAgendaProfileDetailsResDto result = objectMapper.readValue(response, MyAgendaProfileDetailsResDto.class); + // then + assertThat(result.getUserIntraId()).isEqualTo(user.getIntraId()); + assertThat(result.getUserContent()).isEqualTo(agendaProfile.getContent()); + assertThat(result.getUserGithub()).isEqualTo(agendaProfile.getGithubUrl()); + assertThat(result.getUserCoalition()).isEqualTo(agendaProfile.getCoalition()); + assertThat(result.getUserLocation()).isEqualTo(agendaProfile.getLocation()); + assertThat(result.getTicketCount()).isEqualTo(1); + } + + @Test + @DisplayName("로그인된 유저가 유효하지 않을 때") + void testInvalidUser() throws Exception { + // given: 유효하지 않은 유저의 액세스 토큰 + String invalidAccessToken = "invalid-access-token"; + // when & then: 예외가 발생해야 함 + mockMvc.perform(get("/agenda/profile") + .header("Authorization", "Bearer " + invalidAccessToken)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("해당 로그인 유저의 아젠다 프로필이 없을 때") + void testAgendaProfileNotFound() throws Exception { + // given: 특정 유저와 관련된 AgendaProfile이 없음 + + // when & then: 예외가 발생해야 함 + mockMvc.perform(get("/agenda/profile") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("agenda profile 상세 조회") + class GetAgendaProfile { + + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Test + @DisplayName("agenda profile 상세 조회 성공") + void getAgendaProfileSuccess() throws Exception { + //given + URL url = new URL("http://localhost:8080"); + IntraProfile intraProfile = new IntraProfile(user.getIntraId(), url, List.of()); + AgendaProfile agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + agendaMockData.createTicket(agendaProfile); + Mockito.when(intraProfileUtils.getIntraProfile(any(String.class), any(HttpServletResponse.class))) + .thenReturn(intraProfile); + // when + String response = mockMvc.perform(get("/agenda/profile/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaProfileDetailsResDto result = objectMapper.readValue(response, + AgendaProfileDetailsResDto.class); + + // then + assertThat(result.getUserIntraId()).isEqualTo(user.getIntraId()); + assertThat(result.getUserContent()).isEqualTo(agendaProfile.getContent()); + assertThat(result.getUserGithub()).isEqualTo(agendaProfile.getGithubUrl()); + assertThat(result.getUserCoalition()).isEqualTo(agendaProfile.getCoalition()); + assertThat(result.getUserLocation()).isEqualTo(agendaProfile.getLocation()); + } + + @Test + @DisplayName("agenda profile 상세 조회 성공 - 존재하지 않는 사용자인 경우") + void getAgendaProfileFailedWithInvalidIntraId() throws Exception { + //given + URL url = new URL("http://localhost:8080"); + IntraProfile intraProfile = new IntraProfile(user.getIntraId(), url, List.of()); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + Mockito.when(intraProfileUtils.getIntraProfile(res)).thenReturn(intraProfile); + AgendaProfile agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + agendaMockData.createTicket(agendaProfile); + + // when + mockMvc.perform(get("/agenda/profile/" + "invalidIntraId") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("개인 프로필 정보 변경") + class UpdateAgendaProfile { + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Test + @DisplayName("유효한 정보로 개인 프로필을 변경합니다.") + void updateProfileWithValidData() throws Exception { + // Given + AgendaProfile agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + agendaMockData.createTicket(agendaProfile); + AgendaProfileChangeReqDto requestDto = new AgendaProfileChangeReqDto("Valid user content", + "https://github.com/validUser"); + String content = objectMapper.writeValueAsString(requestDto); + // When + mockMvc.perform(patch("/agenda/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isNoContent()); + // Then + AgendaProfile result = agendaProfileRepository.findByUserId(user.getId()).orElseThrow(null); + assertThat(result.getContent()).isEqualTo(requestDto.getUserContent()); + assertThat(result.getGithubUrl()).isEqualTo(requestDto.getUserGithub()); + } + + @Test + @DisplayName("userContent 없이 개인 프로필을 변경합니다.") + void updateProfileWithoutUserContent() throws Exception { + // Given + AgendaProfileChangeReqDto requestDto = new AgendaProfileChangeReqDto("", "https://github.com/validUser"); + String content = objectMapper.writeValueAsString(requestDto); + // When & Then + mockMvc.perform(patch("/agenda/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("잘못된 형식의 userGithub로 개인 프로필을 변경합니다.") + void updateProfileWithInvalidUserGithub() throws Exception { + // Given + AgendaProfileChangeReqDto requestDto = new AgendaProfileChangeReqDto("Valid user content", + "invalidGithubUrl"); + String content = objectMapper.writeValueAsString(requestDto); + // When & Then + mockMvc.perform(patch("/agenda/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("userContent가 허용된 길이를 초과하여 개인 프로필을 변경합니다.") + void updateProfileWithExceededUserContentLength() throws Exception { + // Given + String longContent = "a".repeat(1001); // Assuming the limit is 1000 characters + AgendaProfileChangeReqDto requestDto = new AgendaProfileChangeReqDto(longContent, + "https://github.com/validUser"); + String content = objectMapper.writeValueAsString(requestDto); + // When & Then + mockMvc.perform(patch("/agenda/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("userGithub가 허용된 길이를 초과하여 개인 프로필을 변경합니다.") + void updateProfileWithExceededUserGithubLength() throws Exception { + // Given + String longGithubUrl = "https://github.com/" + "a".repeat(256); // Assuming the limit is 255 characters + AgendaProfileChangeReqDto requestDto = new AgendaProfileChangeReqDto("Valid user content", longGithubUrl); + + String content = objectMapper.writeValueAsString(requestDto); + + // When & Then + mockMvc.perform(patch("/agenda/profile") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(content)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("해당 로그인 유저의 아젠다 프로필이 없을 때") + void testAgendaProfileNotFound() throws Exception { + // given: 특정 유저와 관련된 AgendaProfile이 없음 + // when & then: 예외가 발생해야 함 + mockMvc.perform(get("/agenda/profile") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("개인 프로필 admin 여부 조회") + class GetAgendaProfileInfo { + + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Test + @DisplayName("로그인된 유저에 해당하는 Admin 여부를 조회합니다.") + void test() throws Exception { + //given + // when + String response = mockMvc.perform(get("/agenda/profile/info") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + AgendaProfileInfoDetailsResDto result = objectMapper.readValue(response, + AgendaProfileInfoDetailsResDto.class); + // then + Boolean isAdmin = user.getRoleType() == RoleType.ADMIN; + assertThat(result.getIntraId()).isEqualTo(user.getIntraId()); + assertThat(result.getIsAdmin()).isEqualTo(isAdmin); + } + } + + @Nested + @DisplayName("내가 참여 중인 대회 보기") + class GetCurrentAttendAgendaList { + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @Test + @DisplayName("200 내가 참여 중인 대회 조회 성공") + public void getCurrentAttendAgendaListSuccess() throws Exception { + //given + agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + List agendaTeamList = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + User agendaCreateUser = testDataUtils.createNewUser(); + User otherUser = testDataUtils.createNewUser(); + LocalDateTime startTime = LocalDateTime.now().plusDays(i); + Agenda agenda = agendaMockData.createAgenda(agendaCreateUser.getIntraId(), startTime, + i % 2 == 0 ? AgendaStatus.OPEN : AgendaStatus.CONFIRM); + AgendaTeam agendaTeam = agendaMockData.createAgendaTeam(agenda, otherUser, Location.SEOUL); + agendaMockData.createAgendaTeamProfile(agendaTeam, agendaProfile); + agendaTeamList.add(agendaTeam); + } + + // when + String res = mockMvc.perform( + get("/agenda/profile/current/list") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + CurrentAttendAgendaListResDto[] result = objectMapper.readValue(res, CurrentAttendAgendaListResDto[].class); + + // then + assertThat(result.length).isEqualTo(agendaTeamList.size()); + for (int i = 0; i < result.length; i++) { + assertThat(result[i].getAgendaId()).isEqualTo(agendaTeamList.get(i).getAgenda().getId().toString()); + assertThat(result[i].getAgendaKey()).isEqualTo(agendaTeamList.get(i).getAgenda().getAgendaKey()); + assertThat(result[i].getAgendaTitle()).isEqualTo(agendaTeamList.get(i).getAgenda().getTitle()); + assertThat(result[i].getAgendaLocation()).isEqualTo( + agendaTeamList.get(i).getAgenda().getLocation().toString()); + assertThat(result[i].getTeamKey()).isEqualTo(agendaTeamList.get(i).getTeamKey()); + assertThat(result[i].getIsOfficial()).isEqualTo(agendaTeamList.get(i).getAgenda().getIsOfficial()); + assertThat(result[i].getTeamName()).isEqualTo(agendaTeamList.get(i).getName()); + } + } + + @Test + @DisplayName("200 내가 참여 중인 대회가 없을 때 조회 성공") + public void getCurrentAttendAgendaListSuccessNoAgenda() throws Exception { + //given + agendaProfile = agendaMockData.createAgendaProfile(user, SEOUL); + // 참여 중인 대회가 없는 상태 + + // when + String res = mockMvc.perform( + get("/agenda/profile/current/list") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + CurrentAttendAgendaListResDto[] result = objectMapper.readValue(res, CurrentAttendAgendaListResDto[].class); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("해당 로그인 유저의 아젠다 프로필이 없을 때") + void testAgendaProfileNotFound() throws Exception { + // given: 특정 유저와 관련된 AgendaProfile이 없음 + + // when & then: 예외가 발생해야 함 + mockMvc.perform(get("/agenda/profile/current/list") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("해당 로그인 유저의 AgendaTeam이 없을 때") + void testAgendaTeamNotFound() throws Exception { + // given: 특정 유저와 관련된 AgendaTeam이 없음 + + // when & then: 예외가 발생해야 함 + mockMvc.perform(get("/agenda/profile/current/list") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("내가 과거에 참여했던 대회 보기") + class GetAttendedAgendaList { + @BeforeEach + void beforeEach() { + user = testDataUtils.createNewUser(); + accessToken = testDataUtils.getLoginAccessTokenFromUser(user); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3}) + @DisplayName("200 내가 참여 했었던 대회 조회 성공") + void getAttendedAgendaListSuccess(int page) throws Exception { + // given + int size = 10; + int total = 25; + agendaProfile = agendaMockData.createAgendaProfile(user, Location.SEOUL); + List attendedAgendas = new ArrayList<>(); + for (int i = 0; i < total; i++) { + User agendaCreateUser = testDataUtils.createNewUser(); + User otherUser = testDataUtils.createNewUser(); + LocalDateTime startTime = LocalDateTime.now().minusDays(i + 1); + Agenda agenda = agendaMockData.createAgenda(agendaCreateUser.getIntraId(), startTime, + AgendaStatus.FINISH); + AgendaTeam agendaTeam = agendaMockData.createAgendaTeam(agenda, otherUser, Location.SEOUL); + attendedAgendas.add(agendaMockData.createAgendaTeamProfile(agendaTeam, agendaProfile)); + } + + PageRequestDto pageRequest = new PageRequestDto(page, size); + String request = objectMapper.writeValueAsString(pageRequest); + + // when + String response = mockMvc.perform(get("/agenda/profile/history/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result).hasSize(size * page < total ? size : total % size); + attendedAgendas.sort((o1, o2) -> Long.compare(o2.getAgenda().getId(), o1.getAgenda().getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getAgendaId()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgenda().getId().toString()); + assertThat(result.get(i).getAgendaKey()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgenda().getAgendaKey()); + assertThat(result.get(i).getAgendaTitle()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgenda().getTitle()); + assertThat(result.get(i).getAgendaLocation()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgenda().getLocation().toString()); + assertThat(result.get(i).getTeamKey()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgendaTeam().getTeamKey()); + assertThat(result.get(i).getIsOfficial()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgenda().getIsOfficial()); + assertThat(result.get(i).getTeamName()).isEqualTo( + attendedAgendas.get(i + (page - 1) * size).getAgendaTeam().getName()); + } + } + + @Test + @DisplayName("200 내가 참여 했었던 대회가 없을 때 조회 성공") + void getAttendedAgendaListSuccessNoAgenda() throws Exception { + // given + agendaProfile = agendaMockData.createAgendaProfile(user, Location.SEOUL); + PageRequestDto pageRequest = new PageRequestDto(1, 10); + String request = objectMapper.writeValueAsString(pageRequest); + + // when + String response = mockMvc.perform(get("/agenda/profile/history/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", "1") + .param("size", "10") + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + + PageResponseDto pageResponseDto = objectMapper + .readValue(response, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("400 잘못된 페이지 요청 시 실패") + void getAttendedAgendaListBadRequest() throws Exception { + // given + PageRequestDto pageRequest = new PageRequestDto(0, 10); + String request = objectMapper.writeValueAsString(pageRequest); + + // when & then + mockMvc.perform(get("/agenda/profile/history/list/" + user.getIntraId()) + .header("Authorization", "Bearer " + accessToken) + .param("page", "0") + .param("size", "10") + .contentType(MediaType.APPLICATION_JSON) + .content(request)) + .andExpect(status().isBadRequest()); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/agendateam/AgendaTeamControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendateam/AgendaTeamControllerTest.java new file mode 100644 index 000000000..ccc21e603 --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/agendateam/AgendaTeamControllerTest.java @@ -0,0 +1,1884 @@ +package gg.agenda.api.user.agendateam; + +import static gg.data.agenda.type.AgendaStatus.*; +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.agenda.api.AgendaMockData; +import gg.agenda.api.user.agendateam.controller.request.TeamCreateReqDto; +import gg.agenda.api.user.agendateam.controller.request.TeamKeyReqDto; +import gg.agenda.api.user.agendateam.controller.request.TeamUpdateReqDto; +import gg.agenda.api.user.agendateam.controller.response.ConfirmTeamResDto; +import gg.agenda.api.user.agendateam.controller.response.OpenTeamResDto; +import gg.agenda.api.user.agendateam.controller.response.TeamDetailsResDto; +import gg.agenda.api.user.agendateam.controller.response.TeamKeyResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.user.User; +import gg.repo.agenda.AgendaTeamProfileRepository; +import gg.repo.agenda.AgendaTeamRepository; +import gg.repo.agenda.TicketRepository; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaTeamFixture; +import gg.utils.fixture.agenda.AgendaTeamProfileFixture; +import gg.utils.fixture.agenda.TicketFixture; + +@IntegrationTest +@AutoConfigureMockMvc +@Transactional +public class AgendaTeamControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private TestDataUtils testDataUtils; + @Autowired + private AgendaMockData agendaMockData; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private AgendaTeamRepository agendaTeamRepository; + @Autowired + private AgendaTeamProfileRepository agendaTeamProfileRepository; + @Autowired + private AgendaFixture agendaFixture; + @Autowired + private AgendaTeamFixture agendaTeamFixture; + @Autowired + private AgendaTeamProfileFixture agendaTeamProfileFixture; + @Autowired + private TicketFixture ticketFixture; + User seoulUser; + User gyeongsanUser; + User anotherSeoulUser; + String seoulUserAccessToken; + String gyeongsanUserAccessToken; + String anotherSeoulUserAccessToken; + AgendaProfile seoulUserAgendaProfile; + AgendaProfile gyeongsanUserAgendaProfile; + AgendaProfile anotherSeoulUserAgendaProfile; + + @Nested + @DisplayName("팀 생성 테스트") + class AddTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("201 서울 agenda에 서울 user 팀 생성 성공") + public void addNewTeamStatusSeoul() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + TeamKeyResDto result = objectMapper.readValue(res, TeamKeyResDto.class); + // then + AgendaTeam createdTeam = agendaTeamRepository.findByTeamKey(UUID.fromString(result.getTeamKey())) + .orElse(null); + assertThat(createdTeam).isNotNull(); + assertThat(createdTeam.getName()).isEqualTo("teamName"); + assertThat(createdTeam.getLocation()).isEqualTo(SEOUL); + assertThat(createdTeam.getContent()).isEqualTo("teamContent"); + } + + @Test + @DisplayName("201 경산 agenda에 경산 user 팀 생성 성공") + public void addNewTeamStatusGyeongsan() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(GYEONGSAN); + agendaMockData.createTicket(gyeongsanUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "GYEONGSAN", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + TeamKeyResDto result = objectMapper.readValue(res, TeamKeyResDto.class); + // then + AgendaTeam createdTeam = agendaTeamRepository.findByTeamKey(UUID.fromString(result.getTeamKey())) + .orElse(null); + assertThat(createdTeam).isNotNull(); + assertThat(createdTeam.getName()).isEqualTo("teamName"); + assertThat(createdTeam.getLocation()).isEqualTo(GYEONGSAN); + assertThat(createdTeam.getContent()).isEqualTo("teamContent"); + } + + @Test + @DisplayName("201 mix agenda에 서울 user 팀 생성 성공") + public void addNewTeamStatusMixFromSeoul() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(MIX); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + TeamKeyResDto result = objectMapper.readValue(res, TeamKeyResDto.class); + // then + AgendaTeam createdTeam = agendaTeamRepository.findByTeamKey(UUID.fromString(result.getTeamKey())) + .orElse(null); + assertThat(createdTeam).isNotNull(); + assertThat(createdTeam.getName()).isEqualTo("teamName"); + assertThat(createdTeam.getLocation()).isEqualTo(SEOUL); + assertThat(createdTeam.getContent()).isEqualTo("teamContent"); + } + + @Test + @DisplayName("201 MIX agenda에 경산 user 팀 생성 성공") + public void addNewTeamStatusMixFromGyeongsan() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(MIX); + agendaMockData.createTicket(gyeongsanUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "GYEONGSAN", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + TeamKeyResDto result = objectMapper.readValue(res, TeamKeyResDto.class); + // then + AgendaTeam createdTeam = agendaTeamRepository.findByTeamKey(UUID.fromString(result.getTeamKey())) + .orElse(null); + assertThat(createdTeam).isNotNull(); + assertThat(createdTeam.getName()).isEqualTo("teamName"); + assertThat(createdTeam.getLocation()).isEqualTo(GYEONGSAN); + assertThat(createdTeam.getContent()).isEqualTo("teamContent"); + } + + @Test + @DisplayName("201 mix agenda에 서울 user가 mix 팀 생성 성공") + public void addNewTeamStatusMixFromMixToSeoul() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(MIX); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "MIX", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + TeamKeyResDto result = objectMapper.readValue(res, TeamKeyResDto.class); + // then + AgendaTeam createdTeam = agendaTeamRepository.findByTeamKey(UUID.fromString(result.getTeamKey())) + .orElse(null); + assertThat(createdTeam).isNotNull(); + assertThat(createdTeam.getName()).isEqualTo("teamName"); + assertThat(createdTeam.getLocation()).isEqualTo(MIX); + assertThat(createdTeam.getContent()).isEqualTo("teamContent"); + } + + @Test + @DisplayName("201 MIX agenda에 경산 user가 mix 팀 생성 성공") + public void addNewTeamStatusMixFromMixToGyeongsan() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(MIX); + agendaMockData.createTicket(gyeongsanUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "MIX", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andReturn().getResponse().getContentAsString(); + TeamKeyResDto result = objectMapper.readValue(res, TeamKeyResDto.class); + // then + AgendaTeam createdTeam = agendaTeamRepository.findByTeamKey(UUID.fromString(result.getTeamKey())) + .orElse(null); + assertThat(createdTeam).isNotNull(); + assertThat(createdTeam.getName()).isEqualTo("teamName"); + assertThat(createdTeam.getLocation()).isEqualTo(MIX); + assertThat(createdTeam.getContent()).isEqualTo("teamContent"); + } + + @Test + @DisplayName("404 아젠다 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("400 참여 불가능한 Agenda Location 으로 인한 실패") + public void notValidAgendaLocation() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + agendaMockData.createTicket(gyeongsanUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "GYEONGSAN", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 참여 불가능한 Agenda Status 으로 인한 실패") + public void notValidAgendaStatus() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(FINISH); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 참여 불가능한 Agenda Team 한도로 인한 실패") + public void notValidAgendaTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(5); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("400 참여 불가능한 Agenda 시간으로 인한 실패") + public void notValidAgendaDeadline() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + agendaMockData.createTicket(gyeongsanUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "GYEONGSAN", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 아젠다 호스트의 팀 생성으로 인한 실패") + public void agendaHostFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(seoulUser.getIntraId()); + agendaMockData.createTicket(seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("400 참여 불가능한 유저의 Location 으로 인한 실패") + public void notValidUserLocation() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + agendaMockData.createTicket(gyeongsanUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("teamName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("409 이미 있는 팀 이름으로 인한 실패") + public void alreadyTeamNameExist() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + agendaMockData.createTicket(seoulUserAgendaProfile); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda); + TeamCreateReqDto req = new TeamCreateReqDto(team.getName(), true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()); + } + + @Test + @DisplayName("409 한 agenda에 여러 팀 참가 및 생성 불가로 인한 실패") + public void alreadyTeamExistForAgenda() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + agendaMockData.createTicket(seoulUserAgendaProfile); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamCreateReqDto req = new TeamCreateReqDto("newName", true, "SEOUL", + "teamContent"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()); + } + } + + @Nested + @DisplayName("팀 상세 정보 조회 테스트") + class AgendaTeamDetails { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("200 팀 상세 정보 조회 성공") + public void teamDetailsGetSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(MIX); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, MIX); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + agendaMockData.createAgendaTeamProfile(team, gyeongsanUserAgendaProfile); + // when + String res = mockMvc.perform( + get("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + TeamDetailsResDto result = objectMapper.readValue(res, TeamDetailsResDto.class); + // then + assertThat(result.getTeamName()).isEqualTo(team.getName()); + assertThat(result.getTeamLeaderIntraId()).isEqualTo(seoulUser.getIntraId()); + assertThat(result.getTeamStatus()).isEqualTo(team.getStatus()); + assertThat(result.getTeamLocation()).isEqualTo(team.getLocation()); + assertThat(result.getTeamContent()).isEqualTo(team.getContent()); + assertThat(result.getTeamMates().get(0).getIntraId()).isEqualTo(seoulUser.getIntraId()); + assertThat(result.getTeamMates().get(0).getCoalition()).isEqualTo(seoulUserAgendaProfile.getCoalition()); + assertThat(result.getTeamMates().get(1).getIntraId()).isEqualTo(gyeongsanUser.getIntraId()); + assertThat(result.getTeamMates().get(1).getCoalition()).isEqualTo( + gyeongsanUserAgendaProfile.getCoalition()); + } + + @Test + @DisplayName("404 agenda가 없음으로 인한 팀 상세 정보 조회 실패") + public void teamDetailsGetFailByNoAgenda() throws Exception { + //given + // when && then + mockMvc.perform( + get("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", UUID.randomUUID().toString()) + .param("teamKey", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 team이 없음으로 인한 팀 상세 정보 조회 실패") + public void teamDetailsGetFailByNoTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(MIX); + TeamKeyReqDto req = new TeamKeyReqDto(UUID.randomUUID()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + get("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("403 조회 불가능한 team으로 인한 팀 상세 정보 조회 실패") + public void teamDetailsGetFailByConfirmTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(FINISH); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, MIX, AgendaTeamStatus.CONFIRM); + // when && then + mockMvc.perform( + get("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("404 조회 불가능한 team으로 인한 팀 상세 정보 조회 실패") + public void teamDetailsGetFailByCancelTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(FINISH); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, MIX, AgendaTeamStatus.CANCEL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when && then + mockMvc.perform( + get("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("내 팀 조회 테스트") + class MyTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("200 서울 agenda에 서울 user 팀 조회 성공") + public void myTeamSimpleDetailsStatusSeoul() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when + String res = mockMvc.perform( + get("/agenda/team/my") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + // then + assertThat(res).isNotNull(); + } + + @Test + @DisplayName("200 경산 agenda에 경산 user 팀 조회 성공") + public void myTeamSimpleDetailsStatusGyeongsan() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(GYEONGSAN); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, gyeongsanUser); + agendaMockData.createAgendaTeamProfile(team, gyeongsanUserAgendaProfile); + // when + String res = mockMvc.perform( + get("/agenda/team/my") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + // then + assertThat(res).isNotNull(); + } + + @Test + @DisplayName("204 my팀 없을때 조회 성공") + public void myTeamSimpleDetailsStatusNoTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + // when + String res = mockMvc.perform( + get("/agenda/team/my") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isNoContent()) + .andReturn().getResponse().getContentAsString(); + // then + assertThat(res).isNotNull(); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + // when && then + mockMvc.perform( + get("/agenda/team/my") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString())) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 agenda에 프로필 없음으로 인한 실패") + public void noAgendaProfileFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + User noProfileUser = testDataUtils.createNewUser(); + String noProfileUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(noProfileUser); + // when && then + mockMvc.perform( + get("/agenda/team/my") + .header("Authorization", "Bearer " + noProfileUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString())) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("팀 CONFIRM 테스트") + class ConfirmTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("200 팀 CONFIRM 성공") + public void confirmTeamSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + // then + AgendaTeam updatedTeam = agendaTeamRepository.findByTeamKey(team.getTeamKey()).orElse(null); + assertThat(updatedTeam.getStatus()).isEqualTo(AgendaTeamStatus.CONFIRM); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + UUID noTeamKey = UUID.randomUUID(); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .param("teamKey", noTeamKey.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 team 없음으로 인한 실패") + public void noTeamFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("403 호스트가 아님으로 인한 실패") + public void notValidTeamHostFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(seoulUser.getIntraId()); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CONFIRM); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + User notHostUser = testDataUtils.createNewUser(); + String notHostUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(notHostUser); + agendaMockData.createAgendaTeamProfile(team, agendaMockData.createAgendaProfile(notHostUser, SEOUL)); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + notHostUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("404 OPEN 상태가 아닌 팀으로 인한 실패 -> 서비스 로직에서 처리됨, 엔티티는 별개") + public void notValidTeamStatusFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CANCEL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("400 이미 CONFIRM된 팀으로 인한 실패") + public void alreadyConfirmTeamFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CONFIRM); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 참여 불가능한 Agenda Status 으로 인한 실패") + public void notValidAgendaStatus() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(FINISH); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 참여 불가능한 Agenda Location 으로 인한 실패") + public void notValidAgendaLocation() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(GYEONGSAN); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 참여 불가능한 Agenda Team 인원으로 인한 실패") + public void notValidAgendaTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createNeedMorePeopleAgenda(5); + AgendaTeam team = agendaMockData.createAgendaTeam(2, agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + User notHostUser = testDataUtils.createNewUser(); + agendaMockData.createAgendaTeamProfile(team, agendaMockData.createAgendaProfile(notHostUser, SEOUL)); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/confirm") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("팀장 Leave 테스트") + class LeaveTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + anotherSeoulUser = testDataUtils.createNewUser(); + anotherSeoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(anotherSeoulUser); + anotherSeoulUserAgendaProfile = agendaMockData.createAgendaProfile(anotherSeoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("403 팀원 팀 나가기 실패") + public void leaveTeamMateSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, 2); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + anotherSeoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + // then + } + + @Test + @DisplayName("200 팀리더 팀 나가기 성공") + public void leaveTeamLeaderSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, 2); + AgendaTeamProfile atpLeader = agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + AgendaTeamProfile atp = agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + // then + AgendaTeam updatedTeam = agendaTeamRepository.findByTeamKey(team.getTeamKey()).orElse(null); + assert updatedTeam != null; + assertThat(updatedTeam.getMateCount()).isEqualTo(0); + assertThat(agenda.getCurrentTeam()).isEqualTo(1); + AgendaTeamProfile updatedAtp = agendaTeamProfileRepository.findById(atp.getId()).orElse(null); + assert updatedAtp != null; + assertThat(updatedAtp.getIsExist()).isFalse(); + AgendaTeamProfile updatedAtpLeader = agendaTeamProfileRepository.findById(atpLeader.getId()).orElse(null); + assert updatedAtpLeader != null; + assertThat(updatedAtpLeader.getIsExist()).isFalse(); + ticketRepository.findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + updatedAtp.getProfile()) + .ifPresent(ticket -> { + assertThat(ticket.getUsedTo()).isNull(); + }); + ticketRepository.findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + updatedAtpLeader.getProfile()) + .ifPresent(ticket -> { + assertThat(ticket.getUsedTo()).isNull(); + }); + } + + @Test + @DisplayName("200 Confirm 팀 리더 팀 나가기 성공") + public void leaveTeamLeaderTeamStatusConfirmSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CONFIRM); + AgendaTeamProfile atpLeader = agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + AgendaTeamProfile atp = agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + // then + AgendaTeam updatedTeam = agendaTeamRepository.findByTeamKey(team.getTeamKey()).orElse(null); + assert updatedTeam != null; + assertThat(updatedTeam.getMateCount()).isEqualTo(0); + assertThat(agenda.getCurrentTeam()).isEqualTo(0); + AgendaTeamProfile updatedAtp = agendaTeamProfileRepository.findById(atp.getId()).orElse(null); + assert updatedAtp != null; + assertThat(updatedAtp.getIsExist()).isFalse(); + AgendaTeamProfile updatedAtpLeader = agendaTeamProfileRepository.findById(atpLeader.getId()).orElse(null); + assert updatedAtpLeader != null; + assertThat(updatedAtpLeader.getIsExist()).isFalse(); + ticketRepository.findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + updatedAtp.getProfile()) + .ifPresent(ticket -> { + assertThat(ticket.getUsedTo()).isNull(); + }); + ticketRepository.findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + updatedAtpLeader.getProfile()) + .ifPresent(ticket -> { + assertThat(ticket.getUsedTo()).isNull(); + }); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + UUID noTeamKey = UUID.randomUUID(); + // when && then + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .param("teamKey", noTeamKey.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 team 없음으로 인한 실패") + public void noTeamFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + // when && then + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("400 탈퇴 불가능한 AgendaTeam Status로 인한 팀장의 나가기 실패") + public void notValidAgendaTeamStatusWhenTeamLeader() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CANCEL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when && then + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 탈퇴 불가능한 Agenda 시간으로 인한 실패") + public void notValidAgendaDeadline() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(LocalDateTime.now().minusHours(1)); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 탈퇴 불가능한 Agenda Status 으로 인한 실패") + public void notValidAgendaStatus() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(FINISH); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 팀장이 아님으로 인한 실패") + public void notTeamMateFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, 2); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when && then + mockMvc.perform( + patch("/agenda/team/cancel") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + } + + @Nested + @DisplayName("팀원 Drop 테스트") + class DropTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + anotherSeoulUser = testDataUtils.createNewUser(); + anotherSeoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(anotherSeoulUser); + anotherSeoulUserAgendaProfile = agendaMockData.createAgendaProfile(anotherSeoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("200 팀원 팀 나가기 성공") + public void leaveTeamMateSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, 2); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + AgendaTeamProfile atp = agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + anotherSeoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + // then + AgendaTeam updatedTeam = agendaTeamRepository.findByTeamKey(team.getTeamKey()).orElse(null); + assert updatedTeam != null; + assertThat(updatedTeam.getMateCount()).isEqualTo(1); + assertThat(agenda.getCurrentTeam()).isEqualTo(1); + AgendaTeamProfile updatedAtp = agendaTeamProfileRepository.findById(atp.getId()).orElse(null); + assert updatedAtp != null; + assertThat(updatedAtp.getIsExist()).isFalse(); + ticketRepository.findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + updatedAtp.getProfile()) + .ifPresent(ticket -> { + assertThat(ticket.getUsedTo()).isNull(); + }); + } + + @Test + @DisplayName("403 팀리더 팀 나가기 실패") + public void leaveTeamLeaderSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, 2); + AgendaTeamProfile atpLeader = agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + AgendaTeamProfile atp = agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("403 Confirm 팀 리더 팀 나가기 실패") + public void leaveTeamLeaderTeamStatusConfirmSuccess() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CONFIRM); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + // when + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + UUID noTeamKey = UUID.randomUUID(); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .param("teamKey", noTeamKey.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 team 없음으로 인한 실패") + public void noTeamFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("400 탈퇴 불가능한 AgendaTeam Status로 인한 팀원의 나가기 실패") + public void notValidAgendaTeamStatusConfirmWhenTeamMate() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CONFIRM); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + AgendaTeamProfile atp = agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + anotherSeoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 탈퇴 불가능한 AgendaTeam Status로 인한 팀원의 나가기 실패") + public void notValidAgendaTeamStatusCoiWhenTeamMate() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL, AgendaTeamStatus.CONFIRM); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + AgendaTeamProfile atp = agendaMockData.createAgendaTeamProfile(team, anotherSeoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + anotherSeoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 탈퇴 불가능한 Agenda 시간으로 인한 실패") + public void notValidAgendaDeadline() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(LocalDateTime.now().minusHours(1)); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 탈퇴 불가능한 Agenda Status 으로 인한 실패") + public void notValidAgendaStatus() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(FINISH); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("403 팀원이 아님으로 인한 실패") + public void notTeamMateFail() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + AgendaTeam team = agendaMockData.createAgendaTeam(agenda, seoulUser, 2); + agendaMockData.createAgendaTeamProfile(team, seoulUserAgendaProfile); + // when && then + mockMvc.perform( + patch("/agenda/team/drop") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + } + + @Nested + @DisplayName("OPEN팀 조회 테스트") + class OpenTeamListTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("200 OPEN팀 조회 성공") + public void openTeamGetSuccess(int page) throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + List teams = agendaMockData.createAgendaTeamList(agenda, 23, AgendaTeamStatus.OPEN); + PageRequestDto req = new PageRequestDto(page, 5); + // when + String res = mockMvc.perform( + get("/agenda/team/open/list") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(((page - 1) * 5) < teams.size() + ? Math.min(5, teams.size() - (page - 1) * 5) : 0); + teams.sort((a, b) -> b.getId().compareTo(a.getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getTeamName()).isEqualTo(teams.get((page - 1) * 5 + i).getName()); + } + } + + @Test + @DisplayName("200 OPEN팀 없을때 조회 성공") + public void openTeamGetSuccessNoTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + PageRequestDto req = new PageRequestDto(1, 5); + // when + String res = mockMvc.perform( + get("/agenda/team/open/list") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(0); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + PageRequestDto req = new PageRequestDto(1, 5); + // when && then + mockMvc.perform( + get("/agenda/team/open/list") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("CONFIRM팀 조회 테스트") + class ConfirmTeamListTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("200 CONFIRM팀 조회 성공") + public void confirmTeamGetSuccess(int page) throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + List teams = agendaMockData.createAgendaTeamList(agenda, 23, AgendaTeamStatus.CONFIRM); + PageRequestDto req = new PageRequestDto(page, 5); + // when + String res = mockMvc.perform( + get("/agenda/team/confirm/list") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(((page - 1) * 5) < teams.size() + ? Math.min(5, teams.size() - (page - 1) * 5) : 0); + teams.sort((a, b) -> b.getId().compareTo(a.getId())); + for (int i = 0; i < result.size(); i++) { + assertThat(result.get(i).getTeamName()).isEqualTo(teams.get((page - 1) * 5 + i).getName()); + } + } + + @Test + @DisplayName("200 CONFIRM팀 없을때 조회 성공") + public void confirmTeamGetSuccessNoTeam() throws Exception { + //given + Agenda agenda = agendaMockData.createAgenda(SEOUL); + PageRequestDto req = new PageRequestDto(1, 5); + String content = objectMapper.writeValueAsString(req); + // when + String res = mockMvc.perform( + get("/agenda/team/confirm/list") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper + .readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + // then + assertThat(result.size()).isEqualTo(0); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + PageRequestDto req = new PageRequestDto(1, 5); + // when && then + mockMvc.perform( + get("/agenda/team/confirm/list") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("팀 참가 신청 테스트") + class ApplyTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("201 팀 참가 신청 성공") + public void applyTeamSuccess() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + // when + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + // then + AgendaTeam updatedTeam = agendaTeamRepository.findByTeamKey(team.getTeamKey()).orElse(null); + assert updatedTeam != null; + assertThat(updatedTeam.getMateCount()).isEqualTo(2); + AgendaTeamProfile updatedAtp = agendaTeamProfileRepository.findByAgendaAndProfileAndIsExistTrue(agenda, + seoulUserAgendaProfile).orElse(null); + assertThat(updatedAtp.getIsExist()).isTrue(); + } + + @Test + @DisplayName("404 agendaProfile 없음으로 인한 실패") + public void noAgendaProfileFail() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + User noProfileUser = testDataUtils.createNewUser(); + String noProfileUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(noProfileUser); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + noProfileUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + UUID noTeamKey = UUID.randomUUID(); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .param("teamKey", noTeamKey.toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 team 없음으로 인한 실패") + public void noTeamFail() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", UUID.randomUUID().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("400 참가 불가능한 지역으로 인한 실패") + public void notValidAgendaLocation() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + ticketFixture.createTicket(gyeongsanUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 참가 불가능한 인원으로 인한 실패") + public void notValidAgendaTeam() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda.getMaxPeople(), agenda, seoulUser, SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 참가 불가능한 Agenda 시간으로 인한 실패") + public void notValidAgendaStatus() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(LocalDateTime.now().minusHours(50)); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 참가 불가능한 Agenda status 으로 인한 실패") + public void notValidAgendaDeadline() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(FINISH); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + TeamKeyReqDto req = new TeamKeyReqDto(team.getTeamKey()); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("409 이미 같은 아젠다에 참가 신청으로 인한 실패") + public void alreadyApplyFail() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + ticketFixture.createTicket(seoulUserAgendaProfile); + agendaTeamProfileFixture.createAgendaTeamProfile(agenda, team, seoulUserAgendaProfile); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("403 티켓 없음으로 인한 실패") + public void noTicketFail() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("404 참가 불가능한 Team Status Cancel로 인한 실패") + public void notValidTeamStatus() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL, AgendaTeamStatus.CANCEL); + ticketFixture.createTicket(seoulUserAgendaProfile); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 참가 불가능한 Team Status Confirm으로 인한 실패") + public void notValidTeamStatusConfirm() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(SEOUL); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, SEOUL, AgendaTeamStatus.CONFIRM); + ticketFixture.createTicket(seoulUserAgendaProfile); + // when && then + mockMvc.perform( + post("/agenda/team/join") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .param("teamKey", team.getTeamKey().toString()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + } + + @Nested + @DisplayName("팀 수정 테스트") + class UpdateTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaMockData.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaMockData.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("204 팀장 팀 수정 성공") + public void updateTeamSuccess() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, SEOUL); + AgendaTeamProfile atp = agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()); + // then + AgendaTeam updatedTeam = agendaTeamRepository.findByTeamKey(team.getTeamKey()).orElse(null); + assert updatedTeam != null; + assertThat(updatedTeam.getName()).isEqualTo("newName"); + assertThat(updatedTeam.getContent()).isEqualTo("newDesc"); + assertThat(updatedTeam.getIsPrivate()).isTrue(); + assertThat(updatedTeam.getLocation()).isEqualTo(MIX); + } + + @Test + @DisplayName("404 agenda 없음으로 인한 실패") + public void noAgendaFail() throws Exception { + //given + UUID noAgendaKey = UUID.randomUUID(); + TeamUpdateReqDto req = new TeamUpdateReqDto(UUID.randomUUID(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", noAgendaKey.toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("404 team 없음으로 인한 실패") + public void noTeamFail() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + TeamUpdateReqDto req = new TeamUpdateReqDto(UUID.randomUUID(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("403 팀원 팀 수정 실패") + public void notTeamLeaderFail() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, MIX); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + agendaTeamProfileFixture.createAgendaTeamProfile(team, gyeongsanUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "SEOUL"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + gyeongsanUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + @DisplayName("400 수정 불가능한 지역으로 인한 실패") + public void notValidAgendaLocation() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, SEOUL); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "GYEONGSAN"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 수정 불가능한 인원으로 인한 실패") + public void notValidAgendaTeam() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda.getMaxPeople(), agenda, seoulUser, MIX); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + agendaTeamProfileFixture.createAgendaTeamProfile(team, gyeongsanUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "SEOUL"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 수정 불가능한 Agenda 시간으로 인한 실패") + public void notValidAgendaStatus() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(LocalDateTime.now().minusHours(50)); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, MIX); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 수정 불가능한 Agenda status 으로 인한 실패") + public void notValidAgendaDeadline() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(FINISH); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, MIX); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("404 수정 불가능한 Team Status Cancel로 인한 실패") + public void notValidTeamStatus() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, MIX, AgendaTeamStatus.CANCEL); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("400 수정 불가능한 Team Status Confirm으로 인한 실패") + public void notValidTeamStatusConfirm() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, MIX, AgendaTeamStatus.CONFIRM); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("400 수정 불가능한 Team Status Cancel로 인한 실패") + public void notValidTeamStatusCancel() throws Exception { + //given + Agenda agenda = agendaFixture.createAgenda(MIX); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, seoulUser, MIX, AgendaTeamStatus.CANCEL); + agendaTeamProfileFixture.createAgendaTeamProfile(team, seoulUserAgendaProfile); + TeamUpdateReqDto req = new TeamUpdateReqDto(team.getTeamKey(), "newName", "newDesc", true, "MIX"); + String content = objectMapper.writeValueAsString(req); + // when && then + mockMvc.perform( + patch("/agenda/team") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("agenda_key", agenda.getAgendaKey().toString()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + } +} diff --git a/gg-agenda-api/src/test/java/gg/agenda/api/user/ticket/TicketControllerTest.java b/gg-agenda-api/src/test/java/gg/agenda/api/user/ticket/TicketControllerTest.java new file mode 100644 index 000000000..d8839894e --- /dev/null +++ b/gg-agenda-api/src/test/java/gg/agenda/api/user/ticket/TicketControllerTest.java @@ -0,0 +1,424 @@ +package gg.agenda.api.user.ticket; + +import static gg.data.agenda.type.Location.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import gg.agenda.api.user.ticket.controller.response.TicketCountResDto; +import gg.agenda.api.user.ticket.controller.response.TicketHistoryResDto; +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; +import gg.data.user.User; +import gg.repo.agenda.TicketRepository; +import gg.utils.TestDataUtils; +import gg.utils.annotation.IntegrationTest; +import gg.utils.dto.PageRequestDto; +import gg.utils.dto.PageResponseDto; +import gg.utils.external.ApiUtil; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaProfileFixture; +import gg.utils.fixture.agenda.TicketFixture; + +@IntegrationTest +@AutoConfigureMockMvc +@Transactional +public class TicketControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private TestDataUtils testDataUtils; + @Autowired + private TicketRepository ticketRepository; + @Autowired + private AgendaFixture agendaFixture; + @Autowired + private AgendaProfileFixture agendaProfileFixture; + @Autowired + private TicketFixture ticketFixture; + @MockBean + private ApiUtil apiUtil; + User seoulUser; + User gyeongsanUser; + String seoulUserAccessToken; + String gyeongsanUserAccessToken; + AgendaProfile seoulUserAgendaProfile; + AgendaProfile gyeongsanUserAgendaProfile; + + @Nested + @DisplayName("Apporve되어 있지 않은 티켓 생성 테스트") + class AddTeamTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaProfileFixture.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("티켓 생성 성공") + void addTicketSetupSuccess() throws Exception { + //given && when + mockMvc.perform( + post("/agenda/ticket") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + // then + Ticket createdTicket = ticketRepository.findByAgendaProfileId(seoulUserAgendaProfile.getId()) + .orElseThrow(); + assertThat(createdTicket.getAgendaProfile().getId()).isEqualTo(seoulUserAgendaProfile.getId()); + assertThat(createdTicket.getIsApproved()).isFalse(); + assertThat(createdTicket.getIsUsed()).isFalse(); + } + + @Test + @DisplayName("404 티켓 생성 실패 - 프로필이 존재하지 않는 경우") + void addTicketSetupFailToNotFoundProfile() throws Exception { + //given + User notExistUser = testDataUtils.createNewUser(); + String notExistUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(notExistUser); + //when + mockMvc.perform( + post("/agenda/ticket") + .header("Authorization", "Bearer " + notExistUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("409 티켓 생성 실패 - 이미 티켓이 존재하는 경우") + void addTicketSetupFailToAnotherTicketSet() throws Exception { + //given + ticketFixture.createNotApporveTicket(seoulUserAgendaProfile); + //when + mockMvc.perform( + post("/agenda/ticket") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isConflict()); + } + } + + @Nested + @DisplayName("티켓 개수 확인 테스트") + class FindTicketCountTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaProfileFixture.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @Test + @DisplayName("200 티켓 개수 확인 성공 및 setupTicket 확인") + void findTicketCountSetupTrueSuccess() throws Exception { + //given + ticketFixture.createTicket(seoulUserAgendaProfile); + ticketFixture.createTicket(seoulUserAgendaProfile); + ticketFixture.createNotApporveTicket(seoulUserAgendaProfile); + + //when + String res = mockMvc.perform( + get("/agenda/ticket") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + TicketCountResDto result = objectMapper.readValue(res, TicketCountResDto.class); + //then + assertThat(result.getTicketCount()).isEqualTo(2); + assertThat(result.isSetupTicket()).isTrue(); + } + + @Test + @DisplayName("200 티켓 개수 확인 성공 및 setupTicket 확인") + void findTicketCountSetupFalseSuccess() throws Exception { + //given + ticketFixture.createTicket(seoulUserAgendaProfile); + ticketFixture.createTicket(seoulUserAgendaProfile); + + //when + String res = mockMvc.perform( + get("/agenda/ticket") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + TicketCountResDto result = objectMapper.readValue(res, TicketCountResDto.class); + //then + assertThat(result.getTicketCount()).isEqualTo(2); + assertThat(result.isSetupTicket()).isFalse(); + } + + @Test + @DisplayName("200 티켓 개수 확인 성공 - 티켓이 없는 경우") + void findTicketCountSuccessToEmptyTicket() throws Exception { + //when + String res = mockMvc.perform( + get("/agenda/ticket") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + TicketCountResDto result = objectMapper.readValue(res, TicketCountResDto.class); + //then + assertThat(result.getTicketCount()).isEqualTo(0); + } + + @Test + @DisplayName("404 티켓 개수 확인 실패 - 프로필이 존재하지 않는 경우") + void findTicketCountFailToNotFoundProfile() throws Exception { + //given + User notExistUser = testDataUtils.createNewUser(); + String notExistUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(notExistUser); + //when + mockMvc.perform( + get("/agenda/ticket") + .header("Authorization", "Bearer " + notExistUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + } + + @Nested + @DisplayName("티켓 히스토리 조회 테스트") + class FindTicketHistoryTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaProfileFixture.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3, 4, 5}) + @DisplayName("200 티켓 히스토리 조회 성공") + void findTicketHistorySuccess(int page) throws Exception { + //given + for (int i = 0; i < 23; i++) { + ticketFixture.createTicket(seoulUserAgendaProfile); + } + PageRequestDto req = new PageRequestDto(page, 5); + //when + String res = mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("page", String.valueOf(page)) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + //then + assertThat(result.size()).isEqualTo(((page - 1) * 5) < 23 + ? Math.min(5, 23 - (page - 1) * 5) : 0); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - approve 되어있지 않은 경우") + void findTicketHistorySuccessToNotApprove() throws Exception { + //given + ticketFixture.createTicket(seoulUserAgendaProfile, false, false, null, null); + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo("42Intra"); + assertThat(result.get(0).getUsedTo()).isEqualTo("NotApproved"); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - approve 되어있고 used 되어있는 경우") + void findTicketHistorySuccessToUsed() throws Exception { + //given + Agenda seoulAgenda = agendaFixture.createAgenda(SEOUL); + Ticket ticket = ticketFixture.createTicket(seoulUserAgendaProfile, true, true, null, + seoulAgenda.getAgendaKey()); + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo("42Intra"); + assertThat(result.get(0).getUsedTo()).isEqualTo(seoulAgenda.getTitle()); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - approve 되어있고 used 되어있지 않은 경우") + void findTicketHistorySuccessToNotUsed() throws Exception { + //given + Agenda seoulAgenda = agendaFixture.createAgenda(SEOUL); + Ticket ticket = ticketFixture.createTicket(seoulUserAgendaProfile, true, false, null, + null); + PageRequestDto req = new PageRequestDto(1, 5); + String content = objectMapper.writeValueAsString(req); + //when + String res = mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo("42Intra"); + assertThat(result.get(0).getUsedTo()).isEqualTo("NotUsed"); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - refund 되어있고 used 되어있지 않은 경우") + void findTicketHistorySuccessToRefund() throws Exception { + //given + Agenda seoulAgenda = agendaFixture.createAgenda(SEOUL); + Ticket ticket = ticketFixture.createTicket(seoulUserAgendaProfile, true, false, seoulAgenda.getAgendaKey(), + null); + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + //then + assertThat(result.size()).isEqualTo(1); + assertThat(result.get(0).getIssuedFrom()).isEqualTo(seoulAgenda.getTitle()); + assertThat(result.get(0).getUsedTo()).isEqualTo("NotUsed"); + } + + @Test + @DisplayName("200 티켓 히스토리 조회 성공 - 티켓이 없는 경우") + void findTicketHistorySuccessToEmptyTicket() throws Exception { + //given + PageRequestDto req = new PageRequestDto(1, 5); + //when + String res = mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); + PageResponseDto pageResponseDto = objectMapper.readValue(res, new TypeReference<>() { + }); + List result = pageResponseDto.getContent(); + //then + assertThat(result.size()).isEqualTo(0); + } + + @Test + @DisplayName("404 티켓 히스토리 조회 실패 - 프로필이 존재하지 않는 경우") + void findTicketHistoryFailToNotFoundProfile() throws Exception { + //given + User notExistUser = testDataUtils.createNewUser(); + String notExistUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(notExistUser); + PageRequestDto req = new PageRequestDto(1, 5); + //when + mockMvc.perform( + get("/agenda/ticket/history") + .header("Authorization", "Bearer " + notExistUserAccessToken) + .param("page", String.valueOf(req.getPage())) + .param("size", String.valueOf(req.getSize()))) + .andExpect(status().isNotFound()); + } + } + + @Nested + class ApproveTicketTest { + @BeforeEach + void beforeEach() { + seoulUser = testDataUtils.createNewUser(); + seoulUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(seoulUser); + seoulUserAgendaProfile = agendaProfileFixture.createAgendaProfile(seoulUser, SEOUL); + gyeongsanUser = testDataUtils.createNewUser(); + gyeongsanUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(gyeongsanUser); + gyeongsanUserAgendaProfile = agendaProfileFixture.createAgendaProfile(gyeongsanUser, GYEONGSAN); + } + + @DisplayName("setup 된 티켓 approve 실패 - 프로필이 없는 경우") + @Transactional + public void testTicketSetupAndRefundFailToNotFoundProfile() throws Exception { + User notExistUser = testDataUtils.createNewUser(); + String notExistUserAccessToken = testDataUtils.getLoginAccessTokenFromUser(notExistUser); + // when + mockMvc.perform(patch("/agenda/ticket") + .header("Authorization", "Bearer " + notExistUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("setup 된 티켓 approve 실패 - 티켓이 없는 경우") + @Transactional + public void testTicketSetupAndRefundFailToNotFoundTicket() throws Exception { + // when + mockMvc.perform(patch("/agenda/ticket") + .header("Authorization", "Bearer " + seoulUserAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + } +} diff --git a/gg-agenda-api/src/test/resources/application.yml b/gg-agenda-api/src/test/resources/application.yml new file mode 100644 index 000000000..6e703ec8a --- /dev/null +++ b/gg-agenda-api/src/test/resources/application.yml @@ -0,0 +1,204 @@ +spring: + application: + name: 42gg + + profiles: + active: testCode + + security: + oauth2.client: + authenticationScheme: header + registration: + 42: + redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + authorization-grant-type: authorization_code + scope: public + kakao: + redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" + authorization-grant-type: authorization_code + scope: profile_nickname, profile_image, account_email + provider: + 42: + authorization-uri: "https://api.intra.42.fr/oauth/authorize" + token-uri: "https://api.intra.42.fr/oauth/token" + user-info-uri: "https://api.intra.42.fr/v2/me" + user-name-attribute: id + kakao: + authorization-uri: "https://kauth.kakao.com/oauth/authorize" + token-uri: "https://kauth.kakao.com/oauth/token" + user-info-uri: "https://kapi.kakao.com/v2/user/me" + user-name-attribute: id + + mvc: + hiddenmethod: + filter: + enabled: true + data: + web: + pageable: + default-page-size: 20 + one-indexed-parameters: false + + mail: + host: smtp.gmail.com + port: 587 + username: dummy + password: dummy + properties: + mail: + smtp: + starttls: + enable: true + required: true + auth: true + + # Message 설정 + messages: + basename: 'messages/validation' + encoding: UTF-8 + +springdoc: + swagger-ui: + path: /api-docs + default-consumes-media-type: application/json + default-produces-media-type: application/json + +app: + auth: + tokenSecret: authdummydummydummydummydummydummydummydummydummy + refreshTokenSecret: refreshdummydummydummydummydummydummydummydummydummy + +info: + image: + defaultUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/small_default.jpeg' + itemNotFoundUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/not_found.svg' + web: + coalitionUrl: 'https://api.intra.42.fr/v2/users/{id}/coalitions' + pointHistoryUrl: 'https://api.intra.42.fr/v2/users/{id}/correction_point_historics?sort=-id' + +constant: + allowedMinimalStartDays: 2 + tournamentSchedule: "0 0 0 * * *" + +# -- actuator + +management: + server: + port: 8081 + + info: + java: + enabled: true + os: + enabled: true + env: + enabled: true + + health: + show-details: always + + endpoints: + jmx: + exposure: + exclude: "*" + + web: + exposure: + include: "prometheus" + +server: + tomcat: + mbeanregistry: + enabled: true + +--- +spring.config.activate.on-profile: testCode + +# =========================== LOCAL =========================== +spring: + flyway: + enabled: true + baselineOnMigrate: true + locations: classpath:db/migration + user: root + password: 1234 + + jpa: + database-platform: org.hibernate.dialect.MySQL8Dialect + hibernate: + ddl-auto: validate + properties: + hibernate: + show_sql: true + format_sql: true + use_sql_comments: false + + security: + oauth2.client: + registration: + 42: + client-id: "dummy" + client-secret: "dummy" + kakao: + client-id: "dummy" + client-secret: "dummy" + client-authentication-method: POST + + # Redis 설정 + cache: + type: redis + +# cors 설정 +cors: + allowed-origins: 'http://localhost:8080,http://127.0.0.1:8081' + allowed-methods: GET,POST,PUT,DELETE,OPTIONS,PATCH + allowed-headers: '*' + allowed-Credentials: false + max-age: 3600 + +logging-level: + org.hibernate.SQL: debug + org.hibernate.type: trace + +slack: + xoxbToken: "dummy" + +info: + web: + frontUrl: 'http://localhost:8080' + domain: "localhost" + +cloud: + aws: + credentials: + accessKey: dummy + secretKey: dummy + s3: + bucket: 42gg-public-test-image + dir: images/ + region: + static: ap-northeast-2 + stack: + auto: false + +app: + auth: + refreshTokenExpiry: 604800000 + tokenExpiry: 604800000 + + + +--- +spring.config.activate.on-profile: test-mvc + +spring: + security: + oauth2.client: + registration: + 42: + client-id: "dummy" + client-secret: "dummy" + kakao: + client-id: "dummy" + client-secret: "dummy" + client-authentication-method: POST diff --git a/gg-auth/build.gradle b/gg-auth/build.gradle index 1a7f1307b..ea88c4a13 100644 --- a/gg-auth/build.gradle +++ b/gg-auth/build.gradle @@ -15,6 +15,8 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" diff --git a/gg-auth/src/main/java/gg/auth/FortyTwoAuthUtil.java b/gg-auth/src/main/java/gg/auth/FortyTwoAuthUtil.java new file mode 100644 index 000000000..9a6b6299d --- /dev/null +++ b/gg-auth/src/main/java/gg/auth/FortyTwoAuthUtil.java @@ -0,0 +1,126 @@ +package gg.auth; + +import static gg.utils.exception.ErrorCode.*; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import gg.utils.exception.ErrorCode; +import gg.utils.exception.custom.NotExistException; +import gg.utils.exception.user.TokenNotValidException; +import gg.utils.external.ApiUtil; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class FortyTwoAuthUtil { + private final ApiUtil apiUtil; + private final OAuth2AuthorizedClientService authorizedClientService; + + public String getAccessToken() { + Authentication authentication = getAuthenticationFromContext(); + OAuth2AuthorizedClient client = getClientFromAuthentication(authentication); + if (Objects.isNull(client)) { + throw new TokenNotValidException(); + } + return client.getAccessToken().getTokenValue(); + } + + /** + * 토큰 갱신 + * @return 갱신된 OAuth2AuthorizedClient + */ + public String refreshAccessToken() { + Authentication authentication = getAuthenticationFromContext(); + OAuth2AuthorizedClient client = getClientFromAuthentication(authentication); + ClientRegistration registration = client.getClientRegistration(); + + OAuth2AuthorizedClient newClient = requestNewClient(client, registration); + + authorizedClientService.removeAuthorizedClient( + registration.getRegistrationId(), client.getPrincipalName()); + authorizedClientService.saveAuthorizedClient(newClient, authentication); + + return newClient.getAccessToken().getTokenValue(); + } + + private Authentication getAuthenticationFromContext() { + SecurityContext context = SecurityContextHolder.getContext(); + return context.getAuthentication(); + } + + private OAuth2AuthorizedClient getClientFromAuthentication(Authentication authentication) { + OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken)authentication; + String registrationId = oauthToken.getAuthorizedClientRegistrationId(); + return authorizedClientService.loadAuthorizedClient(registrationId, oauthToken.getName()); + } + + private OAuth2AuthorizedClient requestNewClient(OAuth2AuthorizedClient client, ClientRegistration registration) { + if (Objects.isNull(client.getRefreshToken())) { + throw new NotExistException(AUTH_NOT_FOUND); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "refresh_token"); + params.add("refresh_token", client.getRefreshToken().getTokenValue()); + params.add("client_id", registration.getClientId()); + params.add("client_secret", registration.getClientSecret()); + params.add("redirect_uri", registration.getRedirectUri()); + + List> responseBody = apiUtil.apiCall( + registration.getProviderDetails().getTokenUri(), + List.class, + headers, + params, + HttpMethod.POST + ); + if (Objects.isNull(responseBody) || responseBody.isEmpty()) { + throw new NotExistException(ErrorCode.AUTH_NOT_FOUND); + } + return createNewClientFromApiResponse(responseBody.get(0), client); + } + + private OAuth2AuthorizedClient createNewClientFromApiResponse( + Map response, OAuth2AuthorizedClient client) { + + OAuth2AccessToken newAccessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + (String)response.get("access_token"), + Instant.now(), + Instant.now().plusSeconds((Integer)response.get("expires_in")) + ); + + OAuth2RefreshToken newRefreshToken = new OAuth2RefreshToken( + (String)response.get("refresh_token"), + Instant.now() + ); + + return new OAuth2AuthorizedClient( + client.getClientRegistration(), + client.getPrincipalName(), + newAccessToken, + newRefreshToken + ); + } +} diff --git a/gg-auth/src/main/java/gg/auth/utils/RefreshTokenUtil.java b/gg-auth/src/main/java/gg/auth/utils/RefreshTokenUtil.java new file mode 100644 index 000000000..c812c22ee --- /dev/null +++ b/gg-auth/src/main/java/gg/auth/utils/RefreshTokenUtil.java @@ -0,0 +1,58 @@ +package gg.auth.utils; + +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import gg.data.agenda.Auth42Token; +import gg.utils.exception.ErrorCode; +import gg.utils.exception.custom.NotExistException; +import gg.utils.external.ApiUtil; +import lombok.RequiredArgsConstructor; + +@Deprecated +@Component +@RequiredArgsConstructor +public class RefreshTokenUtil { + private final ApiUtil apiUtil; + + @Value("${spring.security.oauth2.client.registration.42.client-id}") + private String clientId; + + @Value("${spring.security.oauth2.client.registration.42.client-secret}") + private String clientSecret; + + @Value("${spring.security.oauth2.client.registration.42.redirect-uri}") + private String url; + + public Auth42Token refreshAuth42Token(Auth42Token auth42Token) { + final HttpHeaders headers = new HttpHeaders(); + + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + MultiValueMap params = createTokenRefreshParam(auth42Token.getRefreshToken()); + + List> response = apiUtil.apiCall(url, List.class, headers, params, HttpMethod.POST); + if (response == null || response.isEmpty()) { + throw new NotExistException(ErrorCode.AUTH_NOT_FOUND); + } + Map map = response.get(0); + return new Auth42Token(auth42Token.getIntra42Id(), (String)map.get("access_token"), + (String)map.get("refresh_token")); + } + + private MultiValueMap createTokenRefreshParam(final String refreshToken) { + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "refresh_token"); + params.add("client_id", clientId); + params.add("client_secret", clientSecret); + params.add("refresh_token", refreshToken); + return params; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/Agenda.java b/gg-data/src/main/java/gg/data/agenda/Agenda.java new file mode 100644 index 000000000..11c11b025 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/Agenda.java @@ -0,0 +1,339 @@ +package gg.data.agenda; + +import static gg.utils.exception.ErrorCode.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.springframework.core.io.support.ResourcePatternUtils; + +import gg.data.BaseTimeEntity; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.utils.exception.custom.BusinessException; +import gg.utils.exception.custom.ForbiddenException; +import gg.utils.exception.custom.InvalidParameterException; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "agenda", uniqueConstraints = { + @UniqueConstraint(name = "uk_agenda_agenda_key", columnNames = "agenda_key") +}) +public class Agenda extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "agenda_key", nullable = false, unique = true, columnDefinition = "BINARY(16)") + private UUID agendaKey; + + @Column(name = "title", nullable = false, columnDefinition = "VARCHAR(50)") + private String title; + + @Column(name = "content", nullable = false, columnDefinition = "VARCHAR(500)") + private String content; + + @Column(name = "deadline", nullable = false, columnDefinition = "DATETIME") + private LocalDateTime deadline; + + @Column(name = "start_time", nullable = false, columnDefinition = "DATETIME") + private LocalDateTime startTime; + + @Column(name = "end_time", nullable = false, columnDefinition = "DATETIME") + private LocalDateTime endTime; + + @Column(name = "min_team", nullable = false, columnDefinition = "INT") + private int minTeam; + + @Column(name = "max_team", nullable = false, columnDefinition = "INT") + private int maxTeam; + + @Column(name = "current_team", nullable = false, columnDefinition = "INT") + private int currentTeam; + + @Column(name = "min_people", nullable = false, columnDefinition = "INT") + private int minPeople; + + @Column(name = "max_people", nullable = false, columnDefinition = "INT") + private int maxPeople; + + @Column(name = "poster_uri", columnDefinition = "VARCHAR(255)") + private String posterUri; + + @Column(name = "host_intra_id", nullable = false, columnDefinition = "VARCHAR(30)") + private String hostIntraId; + + @Column(name = "location", nullable = false, columnDefinition = "VARCHAR(30)") + @Enumerated(EnumType.STRING) + private Location location; + + @Column(name = "status", nullable = false, columnDefinition = "VARCHAR(10)") + @Enumerated(EnumType.STRING) + private AgendaStatus status; + + @Column(name = "is_official", nullable = false, columnDefinition = "BIT(1)") + private Boolean isOfficial; + + @Column(name = "is_ranking", nullable = false, columnDefinition = "BIT(1)") + private Boolean isRanking; + + @Builder + public Agenda(Long id, String title, String content, LocalDateTime deadline, + LocalDateTime startTime, LocalDateTime endTime, int minTeam, int maxTeam, int currentTeam, + int minPeople, int maxPeople, String posterUri, String hostIntraId, Location location, + AgendaStatus status, Boolean isOfficial, Boolean isRanking) { + this.id = id; + this.agendaKey = UUID.randomUUID(); + this.title = title; + this.content = content; + this.deadline = deadline; + this.startTime = startTime; + this.endTime = endTime; + this.minTeam = minTeam; + this.maxTeam = maxTeam; + this.currentTeam = currentTeam; + this.minPeople = minPeople; + this.maxPeople = maxPeople; + this.posterUri = posterUri; + this.hostIntraId = hostIntraId; + this.location = location; + this.status = status; + this.isOfficial = isOfficial; + this.isRanking = isRanking; + } + + public void confirmAgenda() { + this.agendaStatusMustBeOpen(); + this.status = AgendaStatus.CONFIRM; + } + + public void cancelAgenda() { + this.agendaStatusMustBeOpen(); + this.status = AgendaStatus.CANCEL; + } + + public void finishAgenda() { + this.agendaStatusMustBeConfirm(); + this.status = AgendaStatus.FINISH; + } + + public void updateInformation(String title, String content) { + if (Objects.nonNull(title) && !title.isBlank()) { + this.title = title; + } + if (Objects.nonNull(content) && !content.isBlank()) { + this.content = content; + } + } + + public void updatePosterUri(String posterUri) { + if (Objects.nonNull(posterUri) && ResourcePatternUtils.isUrl(posterUri)) { + this.posterUri = posterUri; + } + } + + public void updateIsOfficial(Boolean isOfficial) { + if (Objects.nonNull(isOfficial)) { + this.isOfficial = isOfficial; + } + } + + public void updateIsRanking(Boolean isRanking) { + if (Objects.nonNull(isRanking)) { + this.isRanking = isRanking; + } + } + + public void updateAgendaStatus(AgendaStatus agendaStatus) { + if (Objects.nonNull(agendaStatus)) { + this.status = agendaStatus; + } + } + + public void updateSchedule(LocalDateTime deadline, LocalDateTime startTime, LocalDateTime endTime) { + if (Objects.isNull(deadline) || Objects.isNull(startTime) || Objects.isNull(endTime)) { + return; + } + mustHaveValidSchedule(); + this.deadline = deadline; + this.startTime = startTime; + this.endTime = endTime; + } + + public void updateLocation(Location location, List teams) { + if (Objects.isNull(location)) { + return; + } + boolean conflictAgendaLocation = teams.stream() + .map(AgendaTeam::getLocation) + .anyMatch(teamLocation -> !Location.isUnderLocation(location, teamLocation)); + if (conflictAgendaLocation) { + throw new InvalidParameterException(UPDATE_LOCATION_NOT_VALID); + } + this.location = location; + } + + public void updateAgendaCapacity(int minTeam, int maxTeam, List teams) { + if (minTeam < 2 || maxTeam < 2) { + return; + } + if (minTeam > maxTeam || teams.size() > maxTeam) { + throw new InvalidParameterException(AGENDA_CAPACITY_CONFLICT); + } + if (this.status == AgendaStatus.FINISH && teams.size() < minTeam) { + throw new InvalidParameterException(AGENDA_CAPACITY_CONFLICT); + } + this.minTeam = minTeam; + this.maxTeam = maxTeam; + } + + public void updateAgendaTeamCapacity(int minPeople, int maxPeople, List teams) { + if (minPeople < 1 || maxPeople < 1) { + return; + } + if (minPeople > maxPeople) { + throw new InvalidParameterException(AGENDA_INVALID_PARAM); + } + boolean conflictAgendaTeamCapacity = teams.stream() + .anyMatch(team -> team.getMateCount() > maxPeople + || (team.getStatus() == AgendaTeamStatus.CONFIRM && team.getMateCount() < minPeople)); + if (conflictAgendaTeamCapacity) { + throw new InvalidParameterException(AGENDA_TEAM_CAPACITY_CONFLICT); + } + this.minPeople = minPeople; + this.maxPeople = maxPeople; + } + + public void addTeam(Location location, LocalDateTime now) { + mustBeWithinLocation(location); + mustStatusOpen(); + mustBeforeDeadline(now); + mustHaveCapacity(); + } + + public void confirmTeam(Location location, LocalDateTime now) { + mustBeWithinLocation(location); + mustStatusOpen(); + mustBeforeDeadline(now); + mustHaveCapacity(); + this.currentTeam++; + } + + public void attendTeam(Location location, LocalDateTime now) { + mustBeWithinLocation(location); + mustStatusOpen(); + mustBeforeDeadline(now); + } + + public void updateTeam(Location location, LocalDateTime now) { + mustBeWithinLocation(location); + mustStatusOpen(); + mustBeforeDeadline(now); + } + + public void leaveTeam(LocalDateTime now, AgendaTeamStatus status) { + mustStatusOpen(); + mustBeforeDeadline(now); + if (status == AgendaTeamStatus.CONFIRM) { + this.currentTeam--; + } + } + + private void mustBeWithinLocation(Location location) { + if (this.location != Location.MIX && this.location != location) { + throw new InvalidParameterException(LOCATION_NOT_VALID); + } + } + + private void mustStatusOpen() { + if (this.status != AgendaStatus.OPEN) { + throw new InvalidParameterException(AGENDA_NOT_OPEN); + } + } + + private void mustBeforeDeadline(LocalDateTime now) { + if (this.deadline.isBefore(now)) { + throw new InvalidParameterException(AGENDA_NOT_OPEN); + } + } + + private void mustHaveCapacity() { + if (this.currentTeam == this.maxTeam) { + throw new ForbiddenException(AGENDA_NO_CAPACITY); + } + } + + private void mustHaveValidSchedule() { + if (this.deadline.isAfter(this.startTime)) { + throw new InvalidParameterException(AGENDA_INVALID_PARAM); + } + if (this.startTime.isAfter(this.endTime)) { + throw new InvalidParameterException(AGENDA_INVALID_PARAM); + } + } + + public void mustModifiedByHost(String userIntraId) { + if (this.hostIntraId.equals(userIntraId)) { + return; + } + throw new ForbiddenException(AGENDA_MODIFICATION_FORBIDDEN); + } + + public void agendaStatusMustBeOpen() { + if (this.status == AgendaStatus.FINISH) { + throw new InvalidParameterException(AGENDA_ALREADY_FINISHED); + } + if (this.status == AgendaStatus.CANCEL) { + throw new InvalidParameterException(AGENDA_ALREADY_CANCELED); + } + if (this.status == AgendaStatus.CONFIRM) { + throw new InvalidParameterException(AGENDA_ALREADY_CONFIRMED); + } + } + + public void agendaStatusMustBeConfirm() { + if (this.status == AgendaStatus.OPEN) { + throw new InvalidParameterException(AGENDA_DOES_NOT_CONFIRM); + } + if (this.status == AgendaStatus.CANCEL) { + throw new InvalidParameterException(AGENDA_ALREADY_CANCELED); + } + if (this.status == AgendaStatus.FINISH) { + throw new InvalidParameterException(AGENDA_ALREADY_FINISHED); + } + } + + public void adminCancelTeam(AgendaTeamStatus status) { + if (status == AgendaTeamStatus.CONFIRM) { + this.currentTeam--; + } + } + + public void adminConfirmTeam(AgendaTeamStatus status) { + if (status == AgendaTeamStatus.CANCEL) { + throw new BusinessException(AGENDA_TEAM_CANCEL_FAIL); + } + if (status == AgendaTeamStatus.CONFIRM) { + this.currentTeam++; + } + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/AgendaAnnouncement.java b/gg-data/src/main/java/gg/data/agenda/AgendaAnnouncement.java new file mode 100644 index 000000000..bdf5bf5dd --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/AgendaAnnouncement.java @@ -0,0 +1,56 @@ +package gg.data.agenda; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import gg.data.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "agenda_announcement") +public class AgendaAnnouncement extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "title", nullable = false, columnDefinition = "VARCHAR(50)") + private String title; + + @Column(name = "content", nullable = false, columnDefinition = "VARCHAR(1000)") + private String content; + + @Column(name = "is_show", nullable = false, columnDefinition = "BIT(1)") + private Boolean isShow; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "agenda_id") + private Agenda agenda; + + @Builder + public AgendaAnnouncement(Long id, String title, String content, Boolean isShow, Agenda agenda) { + this.id = id; + this.title = title; + this.content = content; + this.isShow = isShow; + this.agenda = agenda; + } + + public void updateByAdmin(String title, String content, Boolean isShow) { + this.title = title; + this.content = content; + this.isShow = isShow; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/AgendaPosterImage.java b/gg-data/src/main/java/gg/data/agenda/AgendaPosterImage.java new file mode 100644 index 000000000..6a54bcd29 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/AgendaPosterImage.java @@ -0,0 +1,45 @@ +package gg.data.agenda; + +import java.time.LocalDateTime; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgendaPosterImage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + @Column(name = "agenda_id", nullable = false) + private Long agendaId; + @Column(name = "image_uri", nullable = false, length = 255) + private String imageUri; + @Column(name = "is_current", nullable = false) + private Boolean isCurrent; + @Column(name = "s3_deleted", nullable = false) + private Boolean s3Deleted; + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + public AgendaPosterImage(Long agendaID, String imageUri) { + this.agendaId = agendaID; + this.imageUri = imageUri; + this.isCurrent = true; + this.s3Deleted = false; + this.createdAt = LocalDateTime.now(); + } + + public void updateIsCurrentToFalse() { + this.isCurrent = false; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/AgendaProfile.java b/gg-data/src/main/java/gg/data/agenda/AgendaProfile.java new file mode 100644 index 000000000..2637324be --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/AgendaProfile.java @@ -0,0 +1,75 @@ +package gg.data.agenda; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import gg.data.BaseTimeEntity; +import gg.data.agenda.type.Coalition; +import gg.data.agenda.type.Location; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "agenda_profile") +public class AgendaProfile extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "forty_two_id", nullable = false) + private Long fortyTwoId; + + @Column(name = "content", length = 1000, nullable = false) + private String content; + + @Column(name = "github_url", length = 255, nullable = true) + private String githubUrl; + + @Column(name = "coalition", length = 30, nullable = false) + @Enumerated(EnumType.STRING) + private Coalition coalition; + + @Column(name = "location", length = 30, nullable = false) + @Enumerated(EnumType.STRING) + private Location location; + + @Column(name = "intra_id", length = 30, nullable = false) + private String intraId; + + @Column(name = "user_id", nullable = false, columnDefinition = "BIGINT") + private Long userId; + + @Builder + public AgendaProfile(String content, String githubUrl, Coalition coalition, Location location, String intraId, + Long userId, Long fortyTwoId) { + this.content = content; + this.githubUrl = githubUrl; + this.coalition = coalition; + this.location = location; + this.intraId = intraId; + this.userId = userId; + this.fortyTwoId = fortyTwoId; + } + + public void updateProfile(String content, String githubUrl) { + this.content = content; + this.githubUrl = githubUrl; + } + + public void updateProfileAdmin(String content, String githubUrl, Location location) { + this.content = content; + this.githubUrl = githubUrl; + this.location = location; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/AgendaTeam.java b/gg-data/src/main/java/gg/data/agenda/AgendaTeam.java new file mode 100644 index 000000000..1a69ac55a --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/AgendaTeam.java @@ -0,0 +1,198 @@ +package gg.data.agenda; + +import static gg.data.agenda.type.AgendaTeamStatus.*; +import static gg.utils.exception.ErrorCode.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import gg.data.BaseTimeEntity; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.utils.exception.custom.BusinessException; +import gg.utils.exception.custom.InvalidParameterException; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class AgendaTeam extends BaseTimeEntity { + public static final String DEFAULT_AWARD = "participant"; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "agenda_id", nullable = false) + private Agenda agenda; + + @Column(name = "`team_key`", nullable = false, unique = true, columnDefinition = "BINARY(16)") + private UUID teamKey; + + @Column(name = "name", nullable = false, length = 30) + private String name; + + @Column(name = "content", nullable = false, length = 500) + private String content; + + @Column(name = "leader_intra_id", nullable = false, length = 30) + private String leaderIntraId; + + @Column(name = "status", nullable = false, length = 10) + @Enumerated(EnumType.STRING) + private AgendaTeamStatus status; + + @Column(name = "location", nullable = false, length = 10) + @Enumerated(EnumType.STRING) + private Location location; + + @Column(name = "mate_count", nullable = false) + private int mateCount; + + @Column(name = "award", nullable = false, length = 30) + private String award; + + @Column(name = "award_priority", nullable = false) + private int awardPriority; + + @Column(name = "is_private", nullable = false, columnDefinition = "BIT(1)") + private Boolean isPrivate; + + @Builder + public AgendaTeam(Agenda agenda, UUID teamKey, String name, String content, String leaderIntraId, + AgendaTeamStatus status, Location location, int mateCount, int awardPriority, Boolean isPrivate) { + this.agenda = agenda; + this.teamKey = teamKey; + this.name = name; + this.content = content; + this.leaderIntraId = leaderIntraId; + this.status = status; + this.location = location; + this.mateCount = mateCount; + this.award = DEFAULT_AWARD; + this.awardPriority = awardPriority; + this.isPrivate = isPrivate; + } + + public void acceptAward(String award, Integer awardPriority) { + this.award = award; + if (Objects.nonNull(awardPriority)) { + this.awardPriority = awardPriority; + } + } + + public void confirm() { + if (this.status == CANCEL) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CANCEL); + } + if (this.status == CONFIRM) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CONFIRM); + } + this.status = CONFIRM; + } + + public void cancelTeam(AgendaTeamStatus status) { + this.agenda.leaveTeam(LocalDateTime.now(), status); + this.status = CANCEL; + this.mateCount = 0; + } + + public void adminCancelTeam() { + this.status = CANCEL; + this.mateCount = 0; + } + + public void leaveTeamMate() { + this.mateCount--; + } + + public void leaveTeamMateAdmin(String intraId) { + this.mateCount--; + } + + public void attendTeam(Agenda agenda) { + if (this.status == CANCEL) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CANCEL); + } + if (this.status == CONFIRM) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CONFIRM); + } + if (this.mateCount >= agenda.getMaxPeople()) { + throw new BusinessException(AGENDA_TEAM_FULL); + } + this.mateCount++; + } + + public void attendTeamAdmin(Agenda agenda) { + if (this.mateCount >= agenda.getMaxPeople()) { + throw new BusinessException(AGENDA_TEAM_FULL); + } + this.mateCount++; + } + + public void updateTeam(String name, String content, Boolean isPrivate, Location location, + List profiles) { + if (this.status == CANCEL) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CANCEL); + } + if (this.status == CONFIRM) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CONFIRM); + } + this.name = name; + this.content = content; + this.isPrivate = isPrivate; + updateLocation(location, profiles); + } + + public void updateTeamAdmin(String name, String content, Boolean isPrivate, AgendaTeamStatus status) { + this.name = name; + this.content = content; + this.isPrivate = isPrivate; + this.status = status; + } + + public void updateLocation(Location location, List profiles) { + if (Objects.isNull(location)) { + return; + } + boolean conflictAgendaLocation = profiles.stream() + .map(AgendaTeamProfile::getProfile) + .anyMatch(profile -> !Location.isUnderLocation(location, profile.getLocation())); + if (conflictAgendaLocation) { + throw new InvalidParameterException(UPDATE_LOCATION_NOT_VALID); + } + this.location = location; + } + + public void agendaTeamStatusMustBeOpenAndConfirm() { + if (this.status == CANCEL) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CANCEL); + } + } + + public void agendaTeamStatusMustBeOpen() { + if (this.status == CANCEL) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CANCEL); + } + if (this.status == CONFIRM) { + throw new BusinessException(AGENDA_TEAM_ALREADY_CONFIRM); + } + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/AgendaTeamProfile.java b/gg-data/src/main/java/gg/data/agenda/AgendaTeamProfile.java new file mode 100644 index 000000000..ea5183a63 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/AgendaTeamProfile.java @@ -0,0 +1,60 @@ +package gg.data.agenda; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +import gg.data.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class AgendaTeamProfile extends BaseTimeEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + private AgendaProfile profile; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "agenda_id", nullable = false) + private Agenda agenda; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "agenda_team_id", nullable = false) + private AgendaTeam agendaTeam; + + @Column(name = "is_exist", nullable = false, columnDefinition = "BIT(1)") + private Boolean isExist; + + @Builder + public AgendaTeamProfile(AgendaProfile profile, Agenda agenda, AgendaTeam agendaTeam, Boolean isExist) { + this.profile = profile; + this.agenda = agenda; + this.agendaTeam = agendaTeam; + this.isExist = isExist; + } + + public AgendaTeamProfile(AgendaTeam agendaTeam, Agenda agenda, AgendaProfile profile) { + this.agendaTeam = agendaTeam; + this.agenda = agenda; + this.profile = profile; + this.isExist = true; + } + + public void changeExistFalse() { + this.isExist = false; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/Auth42Token.java b/gg-data/src/main/java/gg/data/agenda/Auth42Token.java new file mode 100644 index 000000000..f1daa3cea --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/Auth42Token.java @@ -0,0 +1,22 @@ +package gg.data.agenda; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Deprecated +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Auth42Token { + private String intra42Id; + private String accessToken; + private String refreshToken; + + @Builder + public Auth42Token(String intra42Id, String accessToken, String refreshToken) { + this.intra42Id = intra42Id; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/Ticket.java b/gg-data/src/main/java/gg/data/agenda/Ticket.java new file mode 100644 index 000000000..7249c0ea2 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/Ticket.java @@ -0,0 +1,139 @@ +package gg.data.agenda; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import gg.data.BaseTimeEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "ticket") +public class Ticket extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "profile_id", nullable = false) + private AgendaProfile agendaProfile; + + @Column(name = "issued_from", columnDefinition = "BINARY(16)") + private UUID issuedFrom; + + @Column(name = "used_to", columnDefinition = "BINARY(16)") + private UUID usedTo; + + @Column(name = "is_approved", nullable = false, columnDefinition = "BIT(1)") + private Boolean isApproved; + + @Column(name = "approved_at", columnDefinition = "DATETIME") + private LocalDateTime approvedAt; + + @Column(name = "is_used", nullable = false, columnDefinition = "BIT(1)") + private Boolean isUsed; + + @Column(name = "used_at", columnDefinition = "DATETIME") + private LocalDateTime usedAt; + + @Builder + public Ticket(AgendaProfile agendaProfile, UUID issuedFrom, UUID usedTo, Boolean isApproved, + LocalDateTime approvedAt, Boolean isUsed, LocalDateTime usedAt) { + this.agendaProfile = agendaProfile; + this.issuedFrom = issuedFrom; + this.usedTo = usedTo; + this.isApproved = isApproved; + this.approvedAt = approvedAt; + this.isUsed = isUsed; + this.usedAt = usedAt; + } + + public static void createRefundedTicket(AgendaTeamProfile agendaTeamProfile) { + Ticket.builder() + .agendaProfile(agendaTeamProfile.getProfile()) + .issuedFrom(agendaTeamProfile.getAgenda().getAgendaKey()) + .usedTo(null) + .isApproved(true) + .approvedAt(LocalDateTime.now()) + .isUsed(false) + .usedAt(null) + .build(); + } + + public static Ticket createApproveTicket(AgendaProfile agendaProfile) { + return Ticket.builder() + .agendaProfile(agendaProfile) + .issuedFrom(null) + .usedTo(null) + .isApproved(true) + .approvedAt(LocalDateTime.now()) + .isUsed(false) + .usedAt(null) + .build(); + } + + public static Ticket createNotApporveTicket(AgendaProfile agendaProfile) { + return Ticket.builder() + .agendaProfile(agendaProfile) + .issuedFrom(null) + .usedTo(null) + .isApproved(false) + .approvedAt(null) + .isUsed(false) + .usedAt(null) + .build(); + } + + public static Ticket createAdminTicket(AgendaProfile agendaProfile, UUID issuedFromKey) { + return Ticket.builder() + .agendaProfile(agendaProfile) + .issuedFrom(issuedFromKey) + .usedTo(null) + .isApproved(true) + .approvedAt(LocalDateTime.now()) + .isUsed(false) + .usedAt(null) + .build(); + } + + public void useTicket(UUID usedTo) { + this.usedTo = usedTo; + this.usedAt = LocalDateTime.now(); + this.isUsed = true; + } + + public void changeIsApproved() { + this.isApproved = true; + this.approvedAt = LocalDateTime.now(); + } + + public void updateTicketAdmin(UUID issuedFrom, UUID usedTo, Boolean isApproved, LocalDateTime approvedAt, + Boolean isUsed, LocalDateTime usedAt) { + this.issuedFrom = issuedFrom; + this.usedTo = usedTo; + this.approvedAt = approvedAt; + this.usedAt = usedAt; + if (Objects.nonNull(isUsed)) { + this.isUsed = isUsed; + } + if (Objects.nonNull(isApproved)) { + this.isApproved = isApproved; + } + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/type/AgendaStatus.java b/gg-data/src/main/java/gg/data/agenda/type/AgendaStatus.java new file mode 100644 index 000000000..c29e207e9 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/type/AgendaStatus.java @@ -0,0 +1,15 @@ +package gg.data.agenda.type; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum AgendaStatus { + CANCEL("CANCEL"), + OPEN("OPEN"), + CONFIRM("CONFIRM"), + FINISH("FINISH"); + + private final String status; +} diff --git a/gg-data/src/main/java/gg/data/agenda/type/AgendaTeamStatus.java b/gg-data/src/main/java/gg/data/agenda/type/AgendaTeamStatus.java new file mode 100644 index 000000000..acc893cb0 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/type/AgendaTeamStatus.java @@ -0,0 +1,14 @@ +package gg.data.agenda.type; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum AgendaTeamStatus { + OPEN("OPEN"), + CANCEL("CANCEL"), + CONFIRM("CONFIRM"); + + private String status; +} diff --git a/gg-data/src/main/java/gg/data/agenda/type/Coalition.java b/gg-data/src/main/java/gg/data/agenda/type/Coalition.java new file mode 100644 index 000000000..17898e3c8 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/type/Coalition.java @@ -0,0 +1,30 @@ +package gg.data.agenda.type; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Coalition { + GUN("GUN"), + GON("GON"), + GAM("GAM"), + LEE("LEE"), + SPRING("SPRING"), + SUMMER("SUMMER"), + AUTUMN("AUTUMN"), + WINTER("WINTER"), + OTHER("OTHER"); + + private String coalition; + + public static Coalition valueOfCoalition(String coalition) { + String coalitionToUpper = coalition.toUpperCase(); + for (Coalition c : values()) { + if (c.coalition.equals(coalitionToUpper)) { + return c; + } + } + return OTHER; + } +} diff --git a/gg-data/src/main/java/gg/data/agenda/type/Location.java b/gg-data/src/main/java/gg/data/agenda/type/Location.java new file mode 100644 index 000000000..9f027f1e2 --- /dev/null +++ b/gg-data/src/main/java/gg/data/agenda/type/Location.java @@ -0,0 +1,31 @@ +package gg.data.agenda.type; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Location { + SEOUL("SEOUL"), + GYEONGSAN("GYEONGSAN"), + MIX("MIX"); + + private final String location; + + public static Location valueOfLocation(String location) { + String locationToUpper = location.toUpperCase(); + for (Location l : values()) { + if (l.location.equals(locationToUpper)) { + return l; + } + } + return MIX; + } + + public static boolean isUnderLocation(Location criteria, Location target) { + if (criteria == MIX) { + return true; + } + return criteria == target; + } +} diff --git a/gg-data/src/main/java/gg/data/recruit/application/Application.java b/gg-data/src/main/java/gg/data/recruit/application/Application.java index 8e8f4ad23..e75505c82 100644 --- a/gg-data/src/main/java/gg/data/recruit/application/Application.java +++ b/gg-data/src/main/java/gg/data/recruit/application/Application.java @@ -49,7 +49,7 @@ public class Application extends BaseTimeEntity { private Boolean isDeleted; @Enumerated(EnumType.STRING) - @Column(length = 15, nullable = false) + @Column(length = 30, nullable = false) private ApplicationStatus status; @OneToMany(mappedBy = "application", fetch = FetchType.LAZY) diff --git a/gg-pingpong-api/src/main/java/gg/PingpongApiApplication.java b/gg-pingpong-api/src/main/java/gg/PingpongApiApplication.java index b61ffb94e..e92fed3b4 100644 --- a/gg-pingpong-api/src/main/java/gg/PingpongApiApplication.java +++ b/gg-pingpong-api/src/main/java/gg/PingpongApiApplication.java @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication(scanBasePackages = {"gg.admin.repo", "gg.data", "gg.repo", - "gg.pingpong.api", "gg.utils", "gg.party.api", "gg.auth", "gg.recruit.api"}) + "gg.pingpong.api", "gg.utils", "gg.party.api", "gg.auth", "gg.recruit.api", "gg.agenda.api"}) public class PingpongApiApplication { public static void main(String[] args) { diff --git a/gg-pingpong-api/src/main/java/gg/party/api/admin/templates/service/TemplateAdminService.java b/gg-pingpong-api/src/main/java/gg/party/api/admin/templates/service/TemplateAdminService.java index b1c52050d..61939cdd0 100644 --- a/gg-pingpong-api/src/main/java/gg/party/api/admin/templates/service/TemplateAdminService.java +++ b/gg-pingpong-api/src/main/java/gg/party/api/admin/templates/service/TemplateAdminService.java @@ -12,6 +12,7 @@ import gg.utils.exception.ErrorCode; import gg.utils.exception.party.CategoryNotFoundException; import gg.utils.exception.party.RoomMinMaxPeople; +import gg.utils.exception.party.RoomMinMaxTime; import gg.utils.exception.party.TemplateNotFoundException; import lombok.RequiredArgsConstructor; @@ -23,9 +24,17 @@ public class TemplateAdminService { /** * 템플릿 추가 - * @exception CategoryNotFoundException 존재하지 않는 카테고리 입력 - 404 + * @throws RoomMinMaxPeople 최소인원이 최대인원보다 큰 경우 - 400 + * @throws gg.utils.exception.party.RoomMinMaxTime 최소시간이 최대시간보다 큰 경우 - 400 + * @throws CategoryNotFoundException 존재하지 않는 카테고리 입력 - 404 */ public void addTemplate(TemplateAdminCreateReqDto request) { + if (request.getMaxGamePeople() < request.getMinGamePeople()) { + throw new RoomMinMaxPeople(ErrorCode.ROOM_MIN_MAX_PEOPLE); + } + if (request.getMinGameTime() > request.getMaxGameTime()) { + throw new RoomMinMaxPeople(ErrorCode.ROOM_MIN_MAX_TIME); + } Category category = categoryRepository.findByName(request.getCategoryName()) .orElseThrow(CategoryNotFoundException::new); GameTemplate gameTemplate = TemplateAdminCreateReqDto.toEntity(request, category); @@ -36,6 +45,7 @@ public void addTemplate(TemplateAdminCreateReqDto request) { * 템플릿 수정 * @throws TemplateNotFoundException 존재하지 않는 템플릿 입력 - 404 * @throws RoomMinMaxPeople 최소인원이 최대인원보다 큰 경우 - 400 + * @throws RoomMinMaxTime 최소시간이 최대시간보다 큰 경우 - 400 * @throws CategoryNotFoundException 존재하지 않는 카테고리 입력 - 404 */ @Transactional @@ -45,6 +55,10 @@ public void modifyTemplate(Long templateId, TemplateAdminUpdateReqDto request) { if (request.getMaxGamePeople() < request.getMinGamePeople()) { throw new RoomMinMaxPeople(ErrorCode.ROOM_MIN_MAX_PEOPLE); } + if (request.getMinGameTime() > request.getMaxGameTime()) { + throw new RoomMinMaxPeople(ErrorCode.ROOM_MIN_MAX_TIME); + } + request.updateEntity(template); if (request.getCategoryName() != null) { diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/config/SwaggerConfig.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/config/SwaggerConfig.java index e120dcb56..998244642 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/config/SwaggerConfig.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/config/SwaggerConfig.java @@ -14,6 +14,24 @@ @Configuration public class SwaggerConfig { + @Bean + public GroupedOpenApi agendaGroup() { + return GroupedOpenApi.builder() + .group("agenda") + .pathsToMatch("/agenda/**") + .packagesToScan("gg.agenda.api.user") + .build(); + } + + @Bean + public GroupedOpenApi agendaAdminGroup() { + return GroupedOpenApi.builder() + .group("agenda admin") + .pathsToMatch("/agenda/admin/**") + .packagesToScan("gg.agenda.api.admin") + .build(); + } + @Bean public GroupedOpenApi partyGroup() { return GroupedOpenApi.builder() diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/exception/GlobalExceptionHandler.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/exception/GlobalExceptionHandler.java index 9e0365043..92d550dbb 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/exception/GlobalExceptionHandler.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/exception/GlobalExceptionHandler.java @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.MaxUploadSizeExceededException; import com.amazonaws.AmazonServiceException; import com.amazonaws.SdkClientException; @@ -141,4 +142,11 @@ protected ResponseEntity handleException(BusinessException exception) { ErrorResponse response = new ErrorResponse(exception.getErrorCode()); return new ResponseEntity<>(response, HttpStatus.valueOf(response.getStatus())); } + + @ExceptionHandler(MaxUploadSizeExceededException.class) + protected ResponseEntity handleException(MaxUploadSizeExceededException exception) { + String message = "File Size Exceeded: maximum permitted size of 1MB"; + log.error(message); + return new ResponseEntity<>(message, HttpStatus.BAD_REQUEST); + } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/utils/TokenAuthenticationFilter.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/jwt/utils/TokenAuthenticationFilter.java similarity index 98% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/utils/TokenAuthenticationFilter.java rename to gg-pingpong-api/src/main/java/gg/pingpong/api/global/jwt/utils/TokenAuthenticationFilter.java index 9758a9f54..7599f54e5 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/utils/TokenAuthenticationFilter.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/jwt/utils/TokenAuthenticationFilter.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.security.jwt.utils; +package gg.pingpong.api.global.jwt.utils; import static org.apache.commons.lang3.StringUtils.*; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/utils/TokenHeaders.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/jwt/utils/TokenHeaders.java similarity index 74% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/utils/TokenHeaders.java rename to gg-pingpong-api/src/main/java/gg/pingpong/api/global/jwt/utils/TokenHeaders.java index 139c30e09..855e2894f 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/utils/TokenHeaders.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/jwt/utils/TokenHeaders.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.security.jwt.utils; +package gg.pingpong.api.global.jwt.utils; public class TokenHeaders { public static final String REFRESH_TOKEN = "refresh_token"; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/config/SecurityConfig.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/config/SecurityConfig.java index aedb2ed41..8c869d3c9 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/config/SecurityConfig.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/config/SecurityConfig.java @@ -12,9 +12,10 @@ import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import gg.pingpong.api.global.jwt.utils.TokenAuthenticationFilter; import gg.pingpong.api.global.security.config.properties.CorsProperties; import gg.pingpong.api.global.security.handler.OAuthAuthenticationSuccessHandler; -import gg.pingpong.api.global.security.jwt.utils.TokenAuthenticationFilter; +import gg.pingpong.api.global.security.handler.OauthAuthenticationFailureHandler; import gg.pingpong.api.global.security.repository.OAuthAuthorizationRequestBasedOnCookieRepository; import lombok.RequiredArgsConstructor; @@ -23,6 +24,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { private final OAuthAuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler; + private final OauthAuthenticationFailureHandler oauthAuthenticationFailureHandler; private final CorsProperties corsProperties; private final TokenAuthenticationFilter tokenAuthenticationFilter; private final OAuthAuthorizationRequestBasedOnCookieRepository oAuth2AuthorizationRequestBasedOnCookieRepository; @@ -38,7 +40,9 @@ protected void configure(HttpSecurity http) throws Exception { .authorizeRequests() .antMatchers("/pingpong/admin/**").hasRole("ADMIN") .antMatchers("/party/admin/**").hasRole("ADMIN") + .antMatchers("/agenda/admin/**").hasRole("ADMIN") .antMatchers("/admin/recruitments/**").hasRole("ADMIN") + .antMatchers("/agenda/admin/**").hasRole("ADMIN") .antMatchers(HttpMethod.PUT, "/pingpong/users/{intraId}").hasAnyRole("USER", "ADMIN") .antMatchers(HttpMethod.POST, "/pingpong/match").hasAnyRole("USER", "ADMIN") .antMatchers(HttpMethod.POST, "/pingpong/tournaments/{tournamentId}/users").hasAnyRole("USER", "ADMIN") @@ -58,7 +62,8 @@ protected void configure(HttpSecurity http) throws Exception { .baseUri("/oauth2/authorization") .authorizationRequestRepository(oAuth2AuthorizationRequestBasedOnCookieRepository) .and() - .successHandler(oAuth2AuthenticationSuccessHandler); + .successHandler(oAuth2AuthenticationSuccessHandler) + .failureHandler(oauthAuthenticationFailureHandler); http.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OAuthAuthenticationSuccessHandler.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OAuthAuthenticationSuccessHandler.java index 5edce39f5..ee374d6e3 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OAuthAuthenticationSuccessHandler.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OAuthAuthenticationSuccessHandler.java @@ -16,12 +16,12 @@ import gg.auth.utils.AuthTokenProvider; import gg.data.user.User; import gg.data.user.type.RoleType; +import gg.pingpong.api.global.jwt.utils.TokenHeaders; import gg.pingpong.api.global.security.UserPrincipal; -import gg.pingpong.api.global.security.cookie.CookieUtil; -import gg.pingpong.api.global.security.jwt.repository.JwtRedisRepository; -import gg.pingpong.api.global.security.jwt.utils.TokenHeaders; -import gg.pingpong.api.global.utils.ApplicationYmlRead; +import gg.repo.user.JwtRedisRepository; import gg.repo.user.UserRepository; +import gg.utils.ApplicationYmlRead; +import gg.utils.cookie.CookieUtil; import gg.utils.exception.user.UserNotFoundException; import lombok.RequiredArgsConstructor; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OauthAuthenticationFailureHandler.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OauthAuthenticationFailureHandler.java new file mode 100644 index 000000000..98ae3e5fd --- /dev/null +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/handler/OauthAuthenticationFailureHandler.java @@ -0,0 +1,28 @@ +package gg.pingpong.api.global.security.handler; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import gg.utils.ApplicationYmlRead; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class OauthAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final ApplicationYmlRead applicationYmlRead; + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + exception.printStackTrace(); + response.sendRedirect(applicationYmlRead.getFrontUrl() + "/404"); + } +} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/OAuthUserInfo.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/OAuthUserInfo.java index b1719d5f8..2f6922f71 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/OAuthUserInfo.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/OAuthUserInfo.java @@ -2,6 +2,7 @@ import java.util.Map; +import gg.data.agenda.type.Location; import gg.data.user.type.RoleType; public abstract class OAuthUserInfo { @@ -24,4 +25,8 @@ public Map getAttributes() { public abstract RoleType getRoleType(); public abstract Long getKakaoId(); + + public abstract String getUserId(); + + public abstract Location getLocation(); } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/FortyTwoOAuthUserInfo.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/FortyTwoOAuthUserInfo.java index 1e59b3de1..76c9fe8d8 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/FortyTwoOAuthUserInfo.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/FortyTwoOAuthUserInfo.java @@ -1,9 +1,13 @@ package gg.pingpong.api.global.security.info.impl; +import static gg.data.agenda.type.Location.*; + +import java.util.List; import java.util.Map; import org.springframework.beans.factory.annotation.Value; +import gg.data.agenda.type.Location; import gg.data.user.type.RoleType; import gg.pingpong.api.global.security.info.OAuthUserInfo; @@ -45,4 +49,21 @@ public RoleType getRoleType() { public Long getKakaoId() { return null; } + + @Override + public String getUserId() { + return attributes.get("id").toString(); + } + + @Override + public Location getLocation() { + List> campuses = (List>)attributes.get("campus"); + if (campuses != null && !campuses.isEmpty()) { + Map campus = campuses.get(0); + String campusName = (String)campus.get("city"); + return Location.valueOfLocation(campusName); + } else { + return MIX; + } + } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/KakaoOAuthUserInfo.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/KakaoOAuthUserInfo.java index b28bea0cf..a36ee6af4 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/KakaoOAuthUserInfo.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/info/impl/KakaoOAuthUserInfo.java @@ -1,9 +1,12 @@ package gg.pingpong.api.global.security.info.impl; +import static gg.data.agenda.type.Location.*; + import java.util.Map; import org.springframework.beans.factory.annotation.Value; +import gg.data.agenda.type.Location; import gg.data.user.type.RoleType; import gg.pingpong.api.global.security.info.OAuthUserInfo; @@ -45,4 +48,14 @@ public RoleType getRoleType() { public Long getKakaoId() { return (Long)attributes.get("id"); } + + @Override + public String getUserId() { + return "OTHER"; + } + + @Override + public Location getLocation() { + return MIX; + } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/repository/OAuthAuthorizationRequestBasedOnCookieRepository.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/repository/OAuthAuthorizationRequestBasedOnCookieRepository.java index 574f0474c..e7c87dd9d 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/repository/OAuthAuthorizationRequestBasedOnCookieRepository.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/repository/OAuthAuthorizationRequestBasedOnCookieRepository.java @@ -9,7 +9,7 @@ import com.nimbusds.oauth2.sdk.util.StringUtils; -import gg.pingpong.api.global.security.cookie.CookieUtil; +import gg.utils.cookie.CookieUtil; import lombok.RequiredArgsConstructor; @Repository diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/service/CustomOAuth2UserService.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/service/CustomOAuth2UserService.java index 288d049d4..f183934de 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/service/CustomOAuth2UserService.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/service/CustomOAuth2UserService.java @@ -1,8 +1,17 @@ package gg.pingpong.api.global.security.service; +import static gg.data.agenda.type.Coalition.*; + +import java.io.IOException; import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; @@ -12,6 +21,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Coalition; import gg.data.pingpong.rank.Rank; import gg.data.pingpong.rank.Tier; import gg.data.pingpong.rank.redis.RankRedis; @@ -23,6 +34,7 @@ import gg.pingpong.api.global.security.info.OAuthUserInfoFactory; import gg.pingpong.api.global.security.info.ProviderType; import gg.pingpong.api.global.utils.aws.AsyncNewUserImageUploader; +import gg.repo.agenda.AgendaProfileRepository; import gg.repo.rank.RankRepository; import gg.repo.rank.TierRepository; import gg.repo.rank.redis.RankRedisRepository; @@ -30,21 +42,26 @@ import gg.repo.user.UserRepository; import gg.utils.RedisKeyManager; import gg.utils.exception.tier.TierNotFoundException; +import gg.utils.external.ApiUtil; import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor @Transactional public class CustomOAuth2UserService extends DefaultOAuth2UserService { + private final ApiUtil apiUtil; private final UserRepository userRepository; private final AsyncNewUserImageUploader asyncNewUserImageUploader; private final RankRepository rankRepository; private final SeasonRepository seasonRepository; private final RankRedisRepository rankRedisRepository; private final TierRepository tierRepository; + private final AgendaProfileRepository agendaProfileRepository; @Value("${info.image.defaultUrl}") private String defaultImageUrl; + @Value("https://api.intra.42.fr/v2/users/{id}/coalitions") + private String coalitionUrl; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -60,7 +77,7 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic } } - private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) { + private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) throws IOException { ProviderType providerType = ProviderType.keyOf( userRequest.getClientRegistration().getRegistrationId().toUpperCase()); User savedUser; @@ -81,6 +98,11 @@ private OAuth2User process(OAuth2UserRequest userRequest, OAuth2User user) { asyncNewUserImageUploader.upload(userInfo.getIntraId(), userInfo.getImageUrl()); } } + if (agendaProfileRepository.findByUserId(savedUser.getId()).isEmpty()) { + String token = userRequest.getAccessToken().getTokenValue(); + createProfile(userInfo, savedUser, token); + } + return UserPrincipal.create(savedUser, user.getAttributes()); } @@ -110,4 +132,36 @@ private User createUser(OAuthUserInfo userInfo) { .build(); return userRepository.saveAndFlush(user); } + + private void createProfile(OAuthUserInfo userInfo, User user, String accessToken) { + AgendaProfile agendaProfile = AgendaProfile.builder() + .userId(user.getId()) + .intraId(userInfo.getIntraId()) + .content("안녕하세요! " + userInfo.getIntraId() + "입니다.") + .githubUrl(null) + .coalition(findCoalition(userInfo.getUserId(), accessToken)) + .fortyTwoId(Long.valueOf(userInfo.getUserId())) + .location(userInfo.getLocation()) + .build(); + agendaProfileRepository.save(agendaProfile); + } + + private Coalition findCoalition(String id, String accessToken) { + String url = coalitionUrl.replace("{id}", id); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", "Bearer " + accessToken); + headers.setContentType(MediaType.APPLICATION_JSON); + + ParameterizedTypeReference>> responseType = new ParameterizedTypeReference<>() { + }; + List> response = apiUtil.apiCall(url, responseType, headers, HttpMethod.GET); + + if (response != null && !response.isEmpty()) { + Map coalition = response.get(0); + String coalitionName = (String)coalition.get("name"); + return Coalition.valueOfCoalition(coalitionName); + } else { + return OTHER; + } + } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewItemImageUploader.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewItemImageUploader.java index 341643928..ab920cb7f 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewItemImageUploader.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewItemImageUploader.java @@ -1,6 +1,7 @@ package gg.pingpong.api.global.utils.aws; import java.io.IOException; +import java.net.URL; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -8,28 +9,21 @@ import org.springframework.web.multipart.MultipartFile; import gg.data.pingpong.store.Item; -import gg.pingpong.api.global.utils.ItemImageHandler; +import gg.utils.file.handler.ImageHandler; +import lombok.RequiredArgsConstructor; @Component +@RequiredArgsConstructor public class AsyncNewItemImageUploader { - private final ItemImageHandler itemImageHandler; + private final ImageHandler imageHandler; @Value("${info.image.itemNotFoundUrl}") - private String defaultImageUrl; - - public AsyncNewItemImageUploader(ItemImageHandler itemImageHandler) { - this.itemImageHandler = itemImageHandler; - } + private String defaultUrl; @Transactional - public void upload(Item item, - MultipartFile multipartFile) throws IOException { - String s3ImageUrl = itemImageHandler.updateAndGetS3ImageUri(multipartFile, item); - if (s3ImageUrl == null) { - item.imageUpdate(defaultImageUrl); - } else { - item.imageUpdate(s3ImageUrl); - } + public void upload(Item item, MultipartFile multipartFile) throws IOException { + URL s3ImageUrl = imageHandler.uploadImageOrDefault(multipartFile, item.getName(), defaultUrl); + item.imageUpdate(s3ImageUrl.toString()); } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewUserImageUploader.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewUserImageUploader.java index 5f8673d33..74f0ddfc3 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewUserImageUploader.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/AsyncNewUserImageUploader.java @@ -1,11 +1,11 @@ package gg.pingpong.api.global.utils.aws; +import static gg.utils.exception.ErrorCode.*; + import java.io.IOException; +import java.net.URL; import java.time.LocalDateTime; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; - import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; @@ -14,39 +14,37 @@ import gg.data.user.User; import gg.data.user.UserImage; -import gg.pingpong.api.global.utils.UserImageHandler; import gg.repo.user.UserImageRepository; import gg.repo.user.UserRepository; +import gg.utils.exception.custom.NotExistException; +import gg.utils.file.handler.ImageHandler; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Component +@RequiredArgsConstructor public class AsyncNewUserImageUploader { - private final UserImageHandler userImageHandler; + + private final ImageHandler imageHandler; + private final UserRepository userRepository; - @PersistenceContext - private EntityManager entityManager; - @Value("${info.image.defaultUrl}") - private String defaultImageUrl; private final UserImageRepository userImageRepository; - public AsyncNewUserImageUploader(UserImageHandler userImageHandler, UserRepository userRepository, - UserImageRepository userImageRepository) { - this.userImageHandler = userImageHandler; - this.userRepository = userRepository; - this.userImageRepository = userImageRepository; - } + @Value("${info.image.defaultUrl}") + private String defaultImageUrl; @Async("asyncExecutor") @Transactional - public void upload(String intraId, String imageUrl) { - String s3ImageUrl = userImageHandler.uploadAndGetS3ImageUri(intraId, imageUrl); - if (defaultImageUrl.equals(s3ImageUrl)) { + public void upload(String intraId, String imageUrl) throws IOException { + URL s3ImageUrl = imageHandler.uploadImageFromUrlOrDefault(imageUrl, intraId, defaultImageUrl); + if (defaultImageUrl.equals(s3ImageUrl.toString())) { return; } userRepository.findByIntraId(intraId).ifPresent(user -> { - UserImage userImage = new UserImage(user, (s3ImageUrl != null) ? s3ImageUrl : defaultImageUrl, + UserImage userImage = new UserImage(user, + (s3ImageUrl.toString() != null) ? s3ImageUrl.toString() : defaultImageUrl, LocalDateTime.now(), null, true); userImageRepository.save(userImage); userRepository.updateUserImage(user.getId(), userImage.getImageUri()); @@ -55,9 +53,10 @@ public void upload(String intraId, String imageUrl) { @Transactional public void update(String intraId, MultipartFile multipartFile) throws IOException { - User user = userRepository.findByIntraId(intraId).get(); - String s3ImageUrl = userImageHandler.updateAndGetS3ImageUri(multipartFile, user); - s3ImageUrl = s3ImageUrl == null ? defaultImageUrl : s3ImageUrl; + User user = userRepository.findByIntraId(intraId) + .orElseThrow(() -> new NotExistException(USER_NOT_FOUND)); + URL storedUrl = imageHandler.uploadImageOrDefault(multipartFile, user.getIntraId(), defaultImageUrl); + String s3ImageUrl = storedUrl.toString(); UserImage userImage = new UserImage(user, s3ImageUrl, LocalDateTime.now(), null, true); userImageRepository.saveAndFlush(userImage); user.updateImageUri(s3ImageUrl); diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ItemImageHandler.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/ItemImageHandler.java similarity index 92% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ItemImageHandler.java rename to gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/ItemImageHandler.java index cd61914f0..aa4d6df7f 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ItemImageHandler.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/ItemImageHandler.java @@ -1,11 +1,10 @@ -package gg.pingpong.api.global.utils; +package gg.pingpong.api.global.utils.aws; import java.io.IOException; import java.io.InputStream; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import com.amazonaws.services.s3.AmazonS3; @@ -15,7 +14,10 @@ import gg.data.pingpong.store.Item; -@Component +/** + * This Module has been replaced by gg.utils.file.handler.AwsImageHandler + */ +@Deprecated public class ItemImageHandler { private final AmazonS3 amazonS3; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/UserImageHandler.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/UserImageHandler.java similarity index 91% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/UserImageHandler.java rename to gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/UserImageHandler.java index 21d46a36e..bf7627418 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/UserImageHandler.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/aws/UserImageHandler.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.utils; +package gg.pingpong.api.global.utils.aws; import java.io.IOException; import java.io.InputStream; @@ -15,8 +15,14 @@ import gg.data.user.User; import gg.repo.user.UserImageRepository; +import gg.utils.file.FileDownloader; +import gg.utils.file.ImageResizingUtil; +import gg.utils.file.JpegMultipartFile; -@Component +/** + * This Module has been replaced by gg.utils.file.handler.AwsImageHandler + */ +@Deprecated public class UserImageHandler { private final AmazonS3 amazonS3; private final FileDownloader fileDownloader; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/PartyNotiService.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/PartyNotiService.java index efbfe9deb..82fa06e0d 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/PartyNotiService.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/PartyNotiService.java @@ -1,25 +1,30 @@ package gg.pingpong.api.user.noti.service; import java.util.List; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import gg.data.user.User; -import gg.pingpong.api.user.noti.service.sns.SlackPartybotService; +import gg.utils.sns.MessageSender; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class PartyNotiService { - private final SlackPartybotService slackPartybotService; - public PartyNotiService(SlackPartybotService slackPartybotService) { - this.slackPartybotService = slackPartybotService; - } + private static final String PARTY_MESSAGE = "파티요정🧚으로부터 편지가 도착했습니다.\n" + + "장소 및 시간을 상호 협의해서 진행해주세요.\n" + + "파티원이 연락두절이라면 $$마지 못해 신고$$ ----> https://42gg.kr"; + + private final MessageSender messageSender; @Transactional(readOnly = true) public void sendPartyNotifications(List users) { - slackPartybotService.partySend(users); + List intraUserNames = users.stream().map(User::getIntraId).collect(Collectors.toList()); + messageSender.sendGroup(intraUserNames, PARTY_MESSAGE); } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/SnsNotiService.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/SnsNotiService.java index 3287ee3e1..7fb65b9c9 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/SnsNotiService.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/SnsNotiService.java @@ -8,19 +8,18 @@ import gg.data.user.type.SnsType; import gg.pingpong.api.user.noti.dto.UserNotiDto; import gg.pingpong.api.user.noti.service.sns.NotiMailSender; -import gg.pingpong.api.user.noti.service.sns.SlackbotService; +import gg.pingpong.api.user.noti.service.sns.NotiSlackMessageSender; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Service @Slf4j +@Service +@RequiredArgsConstructor public class SnsNotiService { + private final NotiMailSender notiMailSender; - private final SlackbotService slackbotService; - public SnsNotiService(NotiMailSender notiMailSender, SlackbotService slackbotService) { - this.notiMailSender = notiMailSender; - this.slackbotService = slackbotService; - } + private final NotiSlackMessageSender notiSlackMessageSender; /** * 유저가 설정해둔 알림 옵션(email, slack, both, none)에 따라 알림을 전송합니다. @@ -38,10 +37,10 @@ public void sendSnsNotification(Noti noti, UserNotiDto user) { if (userSnsNotiOpt == SnsType.EMAIL) { notiMailSender.send(user, noti); } else if (userSnsNotiOpt == SnsType.SLACK) { - slackbotService.send(user, noti); + notiSlackMessageSender.send(user, noti); } else if (userSnsNotiOpt == SnsType.BOTH) { notiMailSender.send(user, noti); - slackbotService.send(user, noti); + notiSlackMessageSender.send(user, noti); } } @@ -61,10 +60,10 @@ public void sendSnsNotification(Noti noti, UserDto user) { if (userSnsNotiOpt == SnsType.EMAIL) { notiMailSender.send(user, noti); } else if (userSnsNotiOpt == SnsType.SLACK) { - slackbotService.send(user, noti); + notiSlackMessageSender.send(user, noti); } else if (userSnsNotiOpt == SnsType.BOTH) { notiMailSender.send(user, noti); - slackbotService.send(user, noti); + notiSlackMessageSender.send(user, noti); } } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiMailSender.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiMailSender.java index 3cbc55c50..944b60ac3 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiMailSender.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiMailSender.java @@ -1,28 +1,26 @@ package gg.pingpong.api.user.noti.service.sns; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; - -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Component; import gg.auth.UserDto; import gg.data.noti.Noti; -import gg.pingpong.api.global.utils.AsyncMailSender; import gg.pingpong.api.user.noti.dto.UserNotiDto; import gg.pingpong.api.user.noti.service.NotiService; +import gg.utils.sns.MailSender; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +@Slf4j @Component @RequiredArgsConstructor -@Slf4j public class NotiMailSender { - private final JavaMailSender javaMailSender; - private final AsyncMailSender asyncMailSender; + private final NotiService notiService; + private final MailSender mailSender; + + private static final String SUBJECT = "핑퐁요정🧚으로부터 도착한 편지"; + /** * 알림을 전송합니다. * UserNotiDto 이용 @@ -30,18 +28,8 @@ public class NotiMailSender { * @param noti 알림 */ public void send(UserNotiDto user, Noti noti) { - MimeMessage message = javaMailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message); - try { - helper.setSubject("핑퐁요정🧚으로부터 도착한 편지"); - log.info(user.getEmail()); - helper.setTo(user.getEmail()); - helper.setText(notiService.getMessage(noti)); - } catch (MessagingException e) { - log.error("MessagingException message = {}", e.getMessage()); - } - log.info("Email send {}", user.getUserId()); - asyncMailSender.send(message); + String message = notiService.getMessage(noti); + mailSender.send(SUBJECT, user.getEmail(), message); } /** @@ -51,17 +39,7 @@ public void send(UserNotiDto user, Noti noti) { * @param noti 알림 */ public void send(UserDto user, Noti noti) { - MimeMessage message = javaMailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message); - try { - helper.setSubject("핑퐁요정🧚으로부터 도착한 편지"); - log.info(user.getEMail()); - helper.setTo(user.getEMail()); - helper.setText(notiService.getMessage(noti)); - } catch (MessagingException e) { - log.error("MessagingException message = {}", e.getMessage()); - } - log.info("Email send {}", user.getId()); - asyncMailSender.send(message); + String message = notiService.getMessage(noti); + mailSender.send(SUBJECT, user.getEMail(), message); } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiSlackMessageSender.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiSlackMessageSender.java new file mode 100644 index 000000000..7bcb5233f --- /dev/null +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/NotiSlackMessageSender.java @@ -0,0 +1,31 @@ +package gg.pingpong.api.user.noti.service.sns; + +import org.springframework.stereotype.Component; + +import gg.auth.UserDto; +import gg.data.noti.Noti; +import gg.pingpong.api.user.noti.dto.UserNotiDto; +import gg.pingpong.api.user.noti.service.NotiService; +import gg.utils.sns.MessageSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class NotiSlackMessageSender { + + private final NotiService notiService; + + private final MessageSender messageSender; + + public void send(UserDto user, Noti noti) { + String message = notiService.getMessage(noti); + messageSender.send(user.getIntraId(), message); + } + + public void send(UserNotiDto user, Noti noti) { + String message = notiService.getMessage(noti); + messageSender.send(user.getIntraId(), message); + } +} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackPartybotService.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackPartybotService.java index 16cb97331..2e3dbe8ee 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackPartybotService.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackPartybotService.java @@ -1,7 +1,5 @@ package gg.pingpong.api.user.noti.service.sns; -import static gg.pingpong.api.user.noti.service.sns.SlackbotUtils.*; - import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,13 +15,15 @@ import org.springframework.util.MultiValueMap; import gg.data.user.User; -import gg.pingpong.api.global.utils.external.ApiUtil; +import gg.utils.external.ApiUtil; +import gg.utils.sns.slack.constant.SlackConstant; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -@Component @Slf4j +@Deprecated +@Component @RequiredArgsConstructor public class SlackPartybotService { @Value("${slack.xoxbToken}") @@ -32,15 +32,17 @@ public class SlackPartybotService { private final ApiUtil apiUtil; private String getSlackUserId(String intraId) { - String userEmail = intraId + intraEmailSuffix; + String userEmail = intraId + SlackConstant.INTRA_EMAIL_SUFFIX; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.add(HttpHeaders.AUTHORIZATION, authenticationPrefix + authenticationToken); + headers.setBearerAuth(authenticationToken); MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("email", userEmail); - SlackUserInfoRes res = apiUtil.apiCall(userIdGetUrl, SlackUserInfoRes.class, + SlackUserInfoRes res = apiUtil.apiCall( + SlackConstant.GET_USER_ID_URL.getValue(), + SlackUserInfoRes.class, headers, parameters, HttpMethod.POST); if (res == null || res.getUser() == null) { @@ -52,13 +54,15 @@ private String getSlackUserId(String intraId) { private String createGroupChannelId(List slackUserIds) { HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(HttpHeaders.AUTHORIZATION, authenticationPrefix + authenticationToken); + httpHeaders.setBearerAuth(authenticationToken); httpHeaders.setContentType(MediaType.APPLICATION_JSON); Map bodyMap = new HashMap<>(); bodyMap.put("users", String.join(",", slackUserIds)); - ConversationRes res = apiUtil.apiCall(conversationsUrl, ConversationRes.class, + ConversationRes res = apiUtil.apiCall( + SlackConstant.CONVERSATION_URL.getValue(), + ConversationRes.class, httpHeaders, bodyMap, HttpMethod.POST); return res.channel.id; @@ -79,14 +83,16 @@ public void partySend(List users) { private void sendGroupMessage(String channelId, String message) { HttpHeaders headers = new HttpHeaders(); - headers.add(HttpHeaders.AUTHORIZATION, authenticationPrefix + authenticationToken); + headers.setBearerAuth(authenticationToken); headers.setContentType(MediaType.APPLICATION_JSON); Map bodyMap = new HashMap<>(); bodyMap.put("channel", channelId); bodyMap.put("text", message); - apiUtil.apiCall(sendMessageUrl, String.class, headers, bodyMap, HttpMethod.POST); + apiUtil.apiCall( + SlackConstant.SEND_MESSAGE_URL.getValue(), + String.class, headers, bodyMap, HttpMethod.POST); } @Getter diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotService.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotService.java index c6660b8bc..683514502 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotService.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotService.java @@ -1,7 +1,5 @@ package gg.pingpong.api.user.noti.service.sns; -import static gg.pingpong.api.user.noti.service.sns.SlackbotUtils.*; - import java.util.HashMap; import java.util.Map; @@ -16,14 +14,16 @@ import gg.auth.UserDto; import gg.data.noti.Noti; -import gg.pingpong.api.global.utils.external.ApiUtil; import gg.pingpong.api.user.noti.dto.UserNotiDto; import gg.pingpong.api.user.noti.service.NotiService; import gg.utils.exception.noti.SlackSendException; +import gg.utils.external.ApiUtil; +import gg.utils.sns.slack.constant.SlackConstant; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +@Deprecated @Component @Slf4j @RequiredArgsConstructor @@ -35,15 +35,17 @@ public class SlackbotService { private final ApiUtil apiUtil; private String getSlackUserId(String intraId) { - String userEmail = intraId + intraEmailSuffix; + String userEmail = intraId + SlackConstant.INTRA_EMAIL_SUFFIX; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.add(HttpHeaders.AUTHORIZATION, authenticationPrefix + authenticationToken); + headers.setBearerAuth(authenticationToken); MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.add("email", userEmail); - SlackUserInfoResponse res = apiUtil.apiCall(userIdGetUrl, SlackUserInfoResponse.class, + SlackUserInfoResponse res = apiUtil.apiCall( + SlackConstant.GET_USER_ID_URL.getValue(), + SlackUserInfoResponse.class, headers, parameters, HttpMethod.POST); if (res == null || res.getUser() == null) { @@ -55,14 +57,15 @@ private String getSlackUserId(String intraId) { private String getDmChannelId(String slackUserId) { HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(HttpHeaders.AUTHORIZATION, - authenticationPrefix + authenticationToken); + httpHeaders.setBearerAuth(authenticationToken); httpHeaders.setContentType(MediaType.APPLICATION_JSON); Map bodyMap = new HashMap<>(); bodyMap.put("users", slackUserId); - ConversationResponse res = apiUtil.apiCall(conversationsUrl, ConversationResponse.class, + ConversationResponse res = apiUtil.apiCall( + SlackConstant.CONVERSATION_URL.getValue(), + ConversationResponse.class, httpHeaders, bodyMap, HttpMethod.POST); return res.channel.id; @@ -94,14 +97,15 @@ private void startSendNoti(String intraId, Noti noti) { String message = notiService.getMessage(noti); HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(HttpHeaders.AUTHORIZATION, - authenticationPrefix + authenticationToken); + httpHeaders.setBearerAuth(authenticationToken); httpHeaders.setContentType(MediaType.APPLICATION_JSON); Map map = new HashMap<>(); map.put("channel", slackChannelId); map.put("text", message); - apiUtil.apiCall(sendMessageUrl, String.class, httpHeaders, map, HttpMethod.POST); + apiUtil.apiCall( + SlackConstant.SEND_MESSAGE_URL.getValue(), + String.class, httpHeaders, map, HttpMethod.POST); } @Getter diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotUtils.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotUtils.java deleted file mode 100644 index 2a271eac5..000000000 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/noti/service/sns/SlackbotUtils.java +++ /dev/null @@ -1,9 +0,0 @@ -package gg.pingpong.api.user.noti.service.sns; - -public class SlackbotUtils { - public static String conversationsUrl = "https://slack.com/api/conversations.open"; - public static String sendMessageUrl = "https://slack.com/api/chat.postMessage"; - public static String userIdGetUrl = "https://slack.com/api/users.lookupByEmail"; - public static String authenticationPrefix = "Bearer "; - public static String intraEmailSuffix = "@student.42seoul.kr"; -} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/controller/UserController.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/controller/UserController.java index a8f277cc6..ee4bca164 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/controller/UserController.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/controller/UserController.java @@ -31,8 +31,7 @@ import gg.data.pingpong.game.type.Mode; import gg.data.user.type.OauthType; import gg.data.user.type.RoleType; -import gg.pingpong.api.global.security.cookie.CookieUtil; -import gg.pingpong.api.global.security.jwt.utils.TokenHeaders; +import gg.pingpong.api.global.jwt.utils.TokenHeaders; import gg.pingpong.api.user.user.controller.request.UserModifyRequestDto; import gg.pingpong.api.user.user.controller.request.UserProfileImageRequestDto; import gg.pingpong.api.user.user.controller.response.UserAttendanceResponseDto; @@ -53,6 +52,7 @@ import gg.pingpong.api.user.user.service.UserAuthenticationService; import gg.pingpong.api.user.user.service.UserCoinService; import gg.pingpong.api.user.user.service.UserService; +import gg.utils.cookie.CookieUtil; import gg.utils.dto.PageRequestDto; import gg.utils.exception.user.KakaoOauth2AlreadyExistException; import io.swagger.v3.oas.annotations.Parameter; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/service/UserAuthenticationService.java b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/service/UserAuthenticationService.java index 71456b835..2c3eaf94d 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/service/UserAuthenticationService.java +++ b/gg-pingpong-api/src/main/java/gg/pingpong/api/user/user/service/UserAuthenticationService.java @@ -3,7 +3,7 @@ import org.springframework.stereotype.Service; import gg.auth.utils.AuthTokenProvider; -import gg.pingpong.api.global.security.jwt.repository.JwtRedisRepository; +import gg.repo.user.JwtRedisRepository; import gg.utils.exception.user.TokenNotValidException; import lombok.RequiredArgsConstructor; diff --git a/gg-pingpong-api/src/main/resources/db/migration/V2.1__recruit_update_status_length.sql b/gg-pingpong-api/src/main/resources/db/migration/V2.1__recruit_update_status_length.sql new file mode 100644 index 000000000..0299d3b18 --- /dev/null +++ b/gg-pingpong-api/src/main/resources/db/migration/V2.1__recruit_update_status_length.sql @@ -0,0 +1,2 @@ +ALTER TABLE `application` + MODIFY `status` varchar(30) NOT NULL; diff --git a/gg-pingpong-api/src/main/resources/db/migration/V3__agenda.sql b/gg-pingpong-api/src/main/resources/db/migration/V3__agenda.sql new file mode 100644 index 000000000..8f75c8208 --- /dev/null +++ b/gg-pingpong-api/src/main/resources/db/migration/V3__agenda.sql @@ -0,0 +1,123 @@ +CREATE TABLE `agenda` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `agenda_key` BINARY(16) NOT NULL, + `title` VARCHAR(50) NOT NULL, + `content` VARCHAR(500) NOT NULL, + `deadline` DATETIME NOT NULL, + `start_time` DATETIME NOT NULL, + `end_time` DATETIME NOT NULL, + `min_team` INT NOT NULL, + `max_team` INT NOT NULL, + `current_team` INT NOT NULL, + `min_people` INT NOT NULL, + `max_people` INT NOT NULL, + `poster_uri` VARCHAR(255) NULL, + `host_intra_id` VARCHAR(30) NOT NULL, + `location` VARCHAR(30) NOT NULL, + `status` VARCHAR(10) NOT NULL, + `is_official` BIT(1) NOT NULL, + `is_ranking` BIT(1) NOT NULL, + `created_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uk_agenda_agenda_key` (`agenda_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `agenda_team` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `agenda_id` BIGINT NOT NULL, + `team_key` BINARY(16) NOT NULL, + `name` VARCHAR(30) NOT NULL, + `content` VARCHAR(500) NOT NULL, + `leader_intra_id` VARCHAR(30) NOT NULL, + `status` VARCHAR(10) NOT NULL, + `location` VARCHAR(10) NOT NULL, + `mate_count` INT NOT NULL, + `award` VARCHAR(30) NOT NULL, + `award_priority` INT NOT NULL, + `is_private` BIT(1) NOT NULL, + `created_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_agenda_team_agenda_agenda_id` (`agenda_id`), + CONSTRAINT `fk_agenda_team_agenda_agenda_id` FOREIGN KEY (`agenda_id`) REFERENCES `agenda` (`id`), + UNIQUE KEY `uk_agenda_team_team_key` (`team_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `agenda_announcement` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `agenda_id` BIGINT NOT NULL, + `title` VARCHAR(50) NOT NULL, + `content` VARCHAR(1000) NOT NULL, + `is_show` BIT(1) NOT NULL, + `created_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_agenda_announcement_agenda_agenda_id` (`agenda_id`), + CONSTRAINT `fk_agenda_announcement_agenda_agenda_id` FOREIGN KEY (`agenda_id`) REFERENCES `agenda` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `agenda_profile` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL, + `forty_two_id`BIGINT NOT NULL, + `intra_id` VARCHAR(30) NOT NULL, + `content` VARCHAR(1000) NOT NULL, + `github_url` VARCHAR(255) NULL, + `coalition` VARCHAR(30) NOT NULL, + `location` VARCHAR(30) NOT NULL, + `created_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_agenda_profile_user_user_id` (`user_id`), + CONSTRAINT `fk_agenda_profile_user_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `agenda_team_profile` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `profile_id` BIGINT NOT NULL, + `agenda_id` BIGINT NOT NULL, + `agenda_team_id` BIGINT NOT NULL, + `is_exist` BIT(1) NOT NULL, + `created_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_agenda_team_profile_profile_profile_id` (`profile_id`), + KEY `fk_agenda_team_profile_agenda_agenda_id` (`agenda_id`), + KEY `fk_agenda_team_profile_agenda_team_agenda_team_id` (`agenda_team_id`), + CONSTRAINT `fk_agenda_team_profile_profile_profile_id` FOREIGN KEY (`profile_id`) REFERENCES `agenda_profile` (`id`), + CONSTRAINT `fk_agenda_team_profile_agenda_team_agenda_team_id` FOREIGN KEY (`agenda_team_id`) REFERENCES `agenda_team` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `ticket` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `profile_id` BIGINT NOT NULL, + `issued_from` BINARY(16) NULL, + `used_to` BINARY(16) NULL, + `is_approved` BOOLEAN NOT NULL, + `approved_at` DATETIME NULL, + `is_used` BOOLEAN NOT NULL, + `used_at` DATETIME NULL, + `created_at` DATETIME NOT NULL, + `modified_at` DATETIME NOT NULL, + PRIMARY KEY (`id`), + KEY `fk_ticket_profile_profile_id` (`profile_id`), + CONSTRAINT `fk_ticket_profile_profile_id` FOREIGN KEY (`profile_id`) REFERENCES `agenda_profile` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE `agenda_poster_image` +( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `agenda_id` BIGINT NOT NULL, + `image_uri` VARCHAR(255) NOT NULL, + `is_current` BOOLEAN NOT NULL, + `s3_deleted` BOOLEAN NOT NULL, + `created_at` DATETIME NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/gg-pingpong-api/src/test/java/gg/party/api/admin/template/TemplateAdminControllerTest.java b/gg-pingpong-api/src/test/java/gg/party/api/admin/template/TemplateAdminControllerTest.java index 65b66931b..08bb666fc 100644 --- a/gg-pingpong-api/src/test/java/gg/party/api/admin/template/TemplateAdminControllerTest.java +++ b/gg-pingpong-api/src/test/java/gg/party/api/admin/template/TemplateAdminControllerTest.java @@ -103,6 +103,40 @@ public void fail() throws Exception { .header(HttpHeaders.AUTHORIZATION, "Bearer " + userAccessToken)) .andExpect(status().isNotFound()); } + + @Test + @DisplayName("최소인원이 최대인원보다 큰 오류 400") + public void notValidMinMaxPeopleFail() throws Exception { + //given + String url = "/party/admin/templates"; + TemplateAdminCreateReqDto templateAdminCreateReqDto = new TemplateAdminCreateReqDto( + "category", "gameName", 2, 4, + 180, 180, "genre", "hard", "summary"); + String jsonRequest = objectMapper.writeValueAsString(templateAdminCreateReqDto); + //when && then + mockMvc.perform(post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + userAccessToken)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("최소시간이 최대시간보다 큰 오류 400") + public void notValidMinMaxTimeFail() throws Exception { + //given + String url = "/party/admin/templates"; + TemplateAdminCreateReqDto templateAdminCreateReqDto = new TemplateAdminCreateReqDto( + "category", "gameName", 4, 2, + 100, 180, "genre", "hard", "summary"); + String jsonRequest = objectMapper.writeValueAsString(templateAdminCreateReqDto); + //when && then + mockMvc.perform(post(url) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + userAccessToken)) + .andExpect(status().isBadRequest()); + } } @Nested @@ -172,7 +206,7 @@ public void noCategoryFail() throws Exception { @Test @DisplayName("최소인원이 최대인원보다 큰 오류 400") - public void notValidMinMaxFail() throws Exception { + public void notValidMinMaxPeopleFail() throws Exception { //given String templateId = testTemplate.getId().toString(); String url = "/party/admin/templates/" + templateId; @@ -188,6 +222,24 @@ public void notValidMinMaxFail() throws Exception { .andExpect(status().isBadRequest()); } + @Test + @DisplayName("최소시간이 최대시간보다 큰 오류 400") + public void notValidMinMaxTimeFail() throws Exception { + //given + String templateId = testTemplate.getId().toString(); + String url = "/party/admin/templates/" + templateId; + TemplateAdminUpdateReqDto templateAdminUpdateReqDto = new TemplateAdminUpdateReqDto( + testCategory.getName(), "newGameName", 8, 4, + 90, 120, "newGenre", "easy", "newSummary"); + String jsonRequest = objectMapper.writeValueAsString(templateAdminUpdateReqDto); + //when && then + mockMvc.perform(patch(url) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonRequest) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + userAccessToken)) + .andExpect(status().isBadRequest()); + } + @Test @DisplayName("템플릿 없음으로 인한 수정 실패 404") public void noTemplateFail() throws Exception { diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/admin/item/controller/ItemAdminControllerTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/admin/item/controller/ItemAdminControllerTest.java index eaefbf250..651d167da 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/admin/item/controller/ItemAdminControllerTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/admin/item/controller/ItemAdminControllerTest.java @@ -4,6 +4,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.net.URL; import java.util.List; import javax.transaction.Transactional; @@ -14,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; @@ -31,11 +33,11 @@ import gg.pingpong.api.admin.store.controller.request.ItemUpdateRequestDto; import gg.pingpong.api.admin.store.controller.response.ItemListResponseDto; import gg.pingpong.api.admin.store.service.ItemAdminService; -import gg.pingpong.api.global.utils.ItemImageHandler; import gg.repo.user.UserRepository; import gg.utils.ItemTestUtils; import gg.utils.TestDataUtils; import gg.utils.annotation.IntegrationTest; +import gg.utils.file.handler.AwsImageHandler; @IntegrationTest @AutoConfigureMockMvc @@ -58,7 +60,10 @@ class ItemAdminControllerTest { @Autowired ItemTestUtils itemTestUtils; @MockBean - ItemImageHandler itemImageHandler; + AwsImageHandler imageHandler; + + @Value("${info.image.defaultUrl}") + private String defaultImageUrl; Item item; @@ -99,8 +104,9 @@ public void getAllItemHistoryTest() throws Exception { @Test @DisplayName("POST /pingpong/admin/items/history/{itemId}") public void updateItemTest() throws Exception { - Mockito.when(itemImageHandler.uploadToS3(Mockito.any(), Mockito.anyString())) - .thenAnswer(invocation -> invocation.getArgument(1, String.class)); + URL mockUrl = new URL(defaultImageUrl); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.anyString(), Mockito.anyString())) + .thenReturn(mockUrl); String accessToken = testDataUtils.getAdminLoginAccessToken(); Long userId = tokenProvider.getUserIdFromAccessToken(accessToken); diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/SnsNotiServiceUnitTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/SnsNotiServiceUnitTest.java index b06af7597..84d8ddfc8 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/SnsNotiServiceUnitTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/SnsNotiServiceUnitTest.java @@ -15,7 +15,7 @@ import gg.data.user.type.SnsType; import gg.pingpong.api.user.noti.dto.UserNotiDto; import gg.pingpong.api.user.noti.service.sns.NotiMailSender; -import gg.pingpong.api.user.noti.service.sns.SlackbotService; +import gg.pingpong.api.user.noti.service.sns.NotiSlackMessageSender; import gg.repo.game.out.GameUser; import gg.utils.annotation.UnitTest; @@ -23,10 +23,13 @@ @ExtendWith(MockitoExtension.class) @DisplayName("SnsNotiServiceTest") class SnsNotiServiceUnitTest { + @Mock NotiMailSender notiMailSender; + @Mock - SlackbotService slackbotService; + NotiSlackMessageSender notiSlackMessageSender; + @InjectMocks SnsNotiService snsNotiService; @@ -43,7 +46,7 @@ void sendSnsNotificationWithEmail() { //when snsNotiService.sendSnsNotification(noti, userDto); //then - verify(slackbotService, never()).send(any(UserDto.class), any(Noti.class)); + verify(notiSlackMessageSender, never()).send(any(UserDto.class), any(Noti.class)); verify(notiMailSender, times(1)).send(any(UserDto.class), any(Noti.class)); } @@ -58,7 +61,7 @@ void sendSnsNotificationWithSlack() { snsNotiService.sendSnsNotification(noti, userDto); //then verify(notiMailSender, never()).send(any(UserDto.class), any(Noti.class)); - verify(slackbotService, times(1)).send(any(UserDto.class), any(Noti.class)); + verify(notiSlackMessageSender, times(1)).send(any(UserDto.class), any(Noti.class)); } @Test @@ -72,7 +75,7 @@ void sendSnsNotificationWithBoth() { snsNotiService.sendSnsNotification(noti, userDto); //then verify(notiMailSender, times(1)).send(any(UserDto.class), any(Noti.class)); - verify(slackbotService, times(1)).send(any(UserDto.class), any(Noti.class)); + verify(notiSlackMessageSender, times(1)).send(any(UserDto.class), any(Noti.class)); } @Test @@ -86,7 +89,7 @@ void sendSnsNotificationWithNone() { snsNotiService.sendSnsNotification(noti, userDto); //then verify(notiMailSender, never()).send(any(UserDto.class), any(Noti.class)); - verify(slackbotService, never()).send(any(UserDto.class), any(Noti.class)); + verify(notiSlackMessageSender, never()).send(any(UserDto.class), any(Noti.class)); } @Test @@ -101,7 +104,7 @@ void sendSnsNotificationWithUserNotiDto() { snsNotiService.sendSnsNotification(noti, user); //then verify(notiMailSender, times(1)).send(user, noti); - verify(slackbotService, never()).send(any(UserNotiDto.class), any(Noti.class)); + verify(notiSlackMessageSender, never()).send(any(UserNotiDto.class), any(Noti.class)); } } } diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/sns/NotiMailSenderUnitTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/sns/NotiMailSenderUnitTest.java index 8df09d2fc..3f55cc074 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/sns/NotiMailSenderUnitTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/noti/service/sns/NotiMailSenderUnitTest.java @@ -10,26 +10,26 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mail.javamail.JavaMailSender; import gg.auth.UserDto; import gg.data.noti.Noti; -import gg.pingpong.api.global.utils.AsyncMailSender; import gg.pingpong.api.user.noti.dto.UserNotiDto; import gg.pingpong.api.user.noti.service.NotiService; import gg.repo.game.out.GameUser; import gg.utils.annotation.UnitTest; +import gg.utils.sns.MailSender; @UnitTest @ExtendWith(MockitoExtension.class) @DisplayName("NotiMailSenderUnitTest") class NotiMailSenderUnitTest { + @Mock - JavaMailSender javaMailSender; - @Mock - AsyncMailSender asyncMailSender; + MailSender mailSender; + @Mock NotiService notiService; + @InjectMocks NotiMailSender notiMailSender; @@ -40,13 +40,15 @@ void sendToUserEmailByUserNotiDto() { GameUser gameUser = mock(GameUser.class); MimeMessage mimeMessage = mock(MimeMessage.class); when(gameUser.getEmail()).thenReturn("testEmail"); - when(javaMailSender.createMimeMessage()).thenReturn(mimeMessage); + doNothing().when(mailSender).send(anyString(), anyString(), anyString()); when(notiService.getMessage(any(Noti.class))).thenReturn("Test message"); + // when notiMailSender.send(new UserNotiDto(gameUser), new Noti()); + // then - verify(javaMailSender).createMimeMessage(); - verify(asyncMailSender).send(mimeMessage); + verify(notiService).getMessage(any(Noti.class)); + verify(mailSender).send(anyString(), anyString(), anyString()); } @Test @@ -56,12 +58,13 @@ void sendToUserEmailByUserDto() { UserDto userDto = mock(UserDto.class); MimeMessage mimeMessage = mock(MimeMessage.class); when(userDto.getEMail()).thenReturn("testEmail"); - when(javaMailSender.createMimeMessage()).thenReturn(mimeMessage); + doNothing().when(mailSender).send(anyString(), anyString(), anyString()); when(notiService.getMessage(any(Noti.class))).thenReturn("Test message"); // when notiMailSender.send(userDto, new Noti()); + // then - verify(javaMailSender).createMimeMessage(); - verify(asyncMailSender).send(mimeMessage); + verify(notiService).getMessage(any(Noti.class)); + verify(mailSender).send(anyString(), anyString(), anyString()); } } diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/rank/controller/RankV2ControllerTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/rank/controller/RankV2ControllerTest.java index 114f4030d..f8e0398a4 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/rank/controller/RankV2ControllerTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/rank/controller/RankV2ControllerTest.java @@ -23,8 +23,8 @@ import gg.auth.utils.AuthTokenProvider; import gg.data.user.User; import gg.pingpong.api.global.config.WebConfig; +import gg.pingpong.api.global.jwt.utils.TokenAuthenticationFilter; import gg.pingpong.api.global.security.config.SecurityConfig; -import gg.pingpong.api.global.security.jwt.utils.TokenAuthenticationFilter; import gg.pingpong.api.global.utils.querytracker.LoggingInterceptor; import gg.pingpong.api.user.rank.controller.response.ExpRankPageResponseDto; import gg.pingpong.api.user.rank.controller.response.RankPageResponseDto; diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/tournament/controller/TournamentControllerMvcTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/tournament/controller/TournamentControllerMvcTest.java index 511c34b33..6685d3bfd 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/tournament/controller/TournamentControllerMvcTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/tournament/controller/TournamentControllerMvcTest.java @@ -23,8 +23,8 @@ import gg.auth.UserDto; import gg.auth.config.AuthWebConfig; import gg.pingpong.api.global.config.WebConfig; +import gg.pingpong.api.global.jwt.utils.TokenAuthenticationFilter; import gg.pingpong.api.global.security.config.SecurityConfig; -import gg.pingpong.api.global.security.jwt.utils.TokenAuthenticationFilter; import gg.pingpong.api.global.utils.querytracker.LoggingInterceptor; import gg.pingpong.api.user.tournament.controller.request.TournamentFilterRequestDto; import gg.pingpong.api.user.tournament.controller.response.TournamentGameListResponseDto; diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/controller/UserControllerTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/controller/UserControllerTest.java index 38d9abf55..28c671779 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/controller/UserControllerTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/controller/UserControllerTest.java @@ -4,6 +4,7 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.net.URL; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Optional; @@ -17,13 +18,13 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import com.fasterxml.jackson.databind.ObjectMapper; @@ -46,7 +47,6 @@ import gg.data.user.type.RoleType; import gg.data.user.type.SnsType; import gg.pingpong.api.admin.store.controller.request.ItemUpdateRequestDto; -import gg.pingpong.api.global.utils.UserImageHandler; import gg.pingpong.api.user.game.controller.request.RankResultReqDto; import gg.pingpong.api.user.game.service.GameService; import gg.pingpong.api.user.store.service.CoinHistoryService; @@ -77,6 +77,7 @@ import gg.utils.annotation.IntegrationTest; import gg.utils.dto.GameInfoDto; import gg.utils.exception.user.UserNotFoundException; +import gg.utils.file.handler.AwsImageHandler; import lombok.extern.slf4j.Slf4j; @IntegrationTest @@ -116,11 +117,14 @@ class UserControllerTest { @Autowired ItemTestUtils itemTestUtils; @MockBean - UserImageHandler userImageHandler; + AwsImageHandler imageHandler; User admin; @Autowired private MockMvc mockMvc; + @Value("${info.image.defaultUrl}") + private String defaultUrl; + @BeforeEach public void setUp() { testDataUtils.createTierSystem("pingpong"); @@ -570,9 +574,8 @@ public void getUserCoinHistory() throws Exception { @Test @DisplayName("[post]/pingpong/users/profile-image") public void getUserImage() throws Exception { - String mockS3Path = "mockS3Path"; - Mockito.when(userImageHandler - .uploadToS3(Mockito.any(MultipartFile.class), Mockito.any(String.class))) + URL mockS3Path = new URL(defaultUrl); + Mockito.when(imageHandler.uploadImageOrDefault(Mockito.any(), Mockito.any(String.class), Mockito.anyString())) .thenReturn(mockS3Path); // String accessToken = testDataUtils.getLoginAccessToken(); ItemUpdateRequestDto dto = new ItemUpdateRequestDto("name", "mainContent", diff --git a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/service/UserAuthenticationServiceUnitTest.java b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/service/UserAuthenticationServiceUnitTest.java index 713c49a25..330615aa0 100644 --- a/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/service/UserAuthenticationServiceUnitTest.java +++ b/gg-pingpong-api/src/test/java/gg/pingpong/api/user/user/service/UserAuthenticationServiceUnitTest.java @@ -12,7 +12,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import gg.auth.utils.AuthTokenProvider; -import gg.pingpong.api.global.security.jwt.repository.JwtRedisRepository; +import gg.repo.user.JwtRedisRepository; import gg.utils.annotation.UnitTest; import gg.utils.exception.user.TokenNotValidException; diff --git a/gg-pingpong-api/src/test/resources/application.yml b/gg-pingpong-api/src/test/resources/application.yml index f28374ce9..eddd3339a 100644 --- a/gg-pingpong-api/src/test/resources/application.yml +++ b/gg-pingpong-api/src/test/resources/application.yml @@ -72,7 +72,10 @@ info: image: defaultUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/small_default.jpeg' itemNotFoundUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/not_found.svg' - + web: + coalitionUrl: 'https://api.intra.42.fr/v2/users/{id}/coalitions' + pointHistoryUrl: 'https://api.intra.42.fr/v2/users/{id}/correction_point_historics?sort=-id' + constant: allowedMinimalStartDays: 2 tournamentSchedule: "0 0 0 * * *" diff --git a/gg-recruit-api/src/main/java/gg/recruit/api/admin/controller/request/InterviewRequestDto.java b/gg-recruit-api/src/main/java/gg/recruit/api/admin/controller/request/InterviewRequestDto.java index ab0845af1..3dc0f88f4 100644 --- a/gg-recruit-api/src/main/java/gg/recruit/api/admin/controller/request/InterviewRequestDto.java +++ b/gg-recruit-api/src/main/java/gg/recruit/api/admin/controller/request/InterviewRequestDto.java @@ -13,7 +13,7 @@ @NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) @Getter public class InterviewRequestDto { - public static final String MUST_DOCS_RESULT_STATUS = "PROGRESS_INTERVIEW or FAIL 중 하나를 선택해주세요."; + public static final String MUST_DOCS_RESULT_STATUS = "PROGRESS_INTERVIEW or INTERVIEW_FAIL 중 하나를 선택해주세요."; @NotNull(message = MUST_DOCS_RESULT_STATUS) private ApplicationStatus status; diff --git a/gg-recruit-api/src/test/resources/application.yml b/gg-recruit-api/src/test/resources/application.yml index e01be0052..6e703ec8a 100644 --- a/gg-recruit-api/src/test/resources/application.yml +++ b/gg-recruit-api/src/test/resources/application.yml @@ -72,6 +72,9 @@ info: image: defaultUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/small_default.jpeg' itemNotFoundUrl: 'https://42gg-public-test-image.s3.ap-northeast-2.amazonaws.com/images/not_found.svg' + web: + coalitionUrl: 'https://api.intra.42.fr/v2/users/{id}/coalitions' + pointHistoryUrl: 'https://api.intra.42.fr/v2/users/{id}/correction_point_historics?sort=-id' constant: allowedMinimalStartDays: 2 diff --git a/gg-repo/src/main/java/gg/repo/agenda/AgendaAnnouncementRepository.java b/gg-repo/src/main/java/gg/repo/agenda/AgendaAnnouncementRepository.java new file mode 100644 index 000000000..2ad1c5eed --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/AgendaAnnouncementRepository.java @@ -0,0 +1,27 @@ +package gg.repo.agenda; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; + +public interface AgendaAnnouncementRepository extends JpaRepository { + + Optional findFirstByAgendaAndIsShowIsTrueOrderByIdDesc(Agenda agenda); + + Page findAllByAgendaAndIsShowIsTrueOrderByIdDesc(Pageable pageable, Agenda agenda); + + default Optional findLatestByAgenda(Agenda agenda) { + return findFirstByAgendaAndIsShowIsTrueOrderByIdDesc(agenda); + } + + default Page findListByAgenda(Pageable pageable, Agenda agenda) { + return findAllByAgendaAndIsShowIsTrueOrderByIdDesc(pageable, agenda); + } +} diff --git a/gg-repo/src/main/java/gg/repo/agenda/AgendaPosterImageRepository.java b/gg-repo/src/main/java/gg/repo/agenda/AgendaPosterImageRepository.java new file mode 100644 index 000000000..f8d747347 --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/AgendaPosterImageRepository.java @@ -0,0 +1,14 @@ +package gg.repo.agenda; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import gg.data.agenda.AgendaPosterImage; + +public interface AgendaPosterImageRepository extends JpaRepository { + + Optional findByAgendaIdAndIsCurrentFalse(Long agendaId); + + Optional findByAgendaIdAndIsCurrentTrue(Long agendaId); +} diff --git a/gg-repo/src/main/java/gg/repo/agenda/AgendaProfileRepository.java b/gg-repo/src/main/java/gg/repo/agenda/AgendaProfileRepository.java new file mode 100644 index 000000000..5d110342d --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/AgendaProfileRepository.java @@ -0,0 +1,13 @@ +package gg.repo.agenda; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import gg.data.agenda.AgendaProfile; + +public interface AgendaProfileRepository extends JpaRepository { + Optional findByUserId(Long userId); + + Optional findByIntraId(String intraId); +} diff --git a/gg-repo/src/main/java/gg/repo/agenda/AgendaRepository.java b/gg-repo/src/main/java/gg/repo/agenda/AgendaRepository.java new file mode 100644 index 000000000..f8f3b030a --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/AgendaRepository.java @@ -0,0 +1,31 @@ +package gg.repo.agenda; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; + +public interface AgendaRepository extends JpaRepository { + @Query("SELECT a FROM Agenda a WHERE a.agendaKey = :agendaKey") + Optional findByAgendaKey(UUID agendaKey); + + @Query("SELECT a FROM Agenda a WHERE a.status = :status") + List findAllByStatusIs(AgendaStatus status); + + @Query("SELECT a FROM Agenda a WHERE a.status = :status1 OR a.status = :status2") + List findAllByStatusIs(AgendaStatus status1, AgendaStatus status2); + + @Query("SELECT a FROM Agenda a WHERE a.status = :status") + Page findAllByStatusIs(AgendaStatus status, Pageable pageable); + + @Query("SELECT a FROM Agenda a WHERE a.hostIntraId = :intraId AND (a.status = :status1 OR a.status = :status2)") + Page findAllByHostIntraIdAndStatus( + String intraId, AgendaStatus status1, AgendaStatus status2, Pageable pageable); +} diff --git a/gg-repo/src/main/java/gg/repo/agenda/AgendaTeamProfileRepository.java b/gg-repo/src/main/java/gg/repo/agenda/AgendaTeamProfileRepository.java new file mode 100644 index 000000000..4a35b75bd --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/AgendaTeamProfileRepository.java @@ -0,0 +1,54 @@ +package gg.repo.agenda; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.data.agenda.type.AgendaStatus; + +public interface AgendaTeamProfileRepository extends JpaRepository { + @Query("SELECT atp FROM AgendaTeamProfile atp " + + "WHERE atp.agendaTeam.agenda = :agenda AND atp.profile = :agendaProfile AND atp.isExist = true") + Optional findByAgendaAndAgendaProfileAndIsExistTrue(Agenda agenda, AgendaProfile agendaProfile); + + @Query("SELECT atp FROM AgendaTeamProfile atp " + + "WHERE atp.agendaTeam = :agendaTeam AND atp.isExist = true") + List findByAgendaTeamAndIsExistTrue(AgendaTeam agendaTeam); + + @Query("SELECT atp FROM AgendaTeamProfile atp " + + "WHERE atp.agenda = :agenda AND atp.profile = :agendaProfile AND atp.isExist = true") + Optional findByAgendaAndProfileAndIsExistTrue(Agenda agenda, AgendaProfile agendaProfile); + + @Query("SELECT atp FROM AgendaTeamProfile atp JOIN FETCH atp.profile " + + "WHERE atp.agendaTeam = :agendaTeam AND atp.isExist = true") + List findAllByAgendaTeamAndIsExistTrue(AgendaTeam agendaTeam); + + @Query("SELECT atp FROM AgendaTeamProfile atp JOIN FETCH atp.agenda " + + "WHERE atp.profile = :agendaProfile AND atp.isExist = true") + List findByProfileAndIsExistTrue(@Param("agendaProfile") AgendaProfile agendaProfile); + + @Query( + value = "SELECT atp FROM AgendaTeamProfile atp JOIN FETCH atp.agendaTeam at " + + "WHERE atp.profile = :agendaProfile AND atp.isExist = true AND atp.agenda.status = :status", + countQuery = "SELECT count(atp) FROM AgendaTeamProfile atp " + + "WHERE atp.profile = :agendaProfile AND atp.isExist = true AND atp.agenda.status = :status") + Page findByProfileAndIsExistTrueAndAgendaStatus( + AgendaProfile agendaProfile, AgendaStatus status, Pageable pageable); + + @Query("SELECT atp FROM AgendaTeamProfile atp JOIN FETCH atp.profile " + + "WHERE atp.agenda = :agenda AND atp.isExist = true") + List findAllByAgendaAndIsExistTrue(Agenda agenda); + + @Query("SELECT atp FROM AgendaTeamProfile atp JOIN FETCH atp.profile " + + "WHERE atp.agendaTeam IN :agendaTeams AND atp.isExist = true") + List findByAgendaTeamInAndIsExistTrue(List agendaTeams); +} diff --git a/gg-repo/src/main/java/gg/repo/agenda/AgendaTeamRepository.java b/gg-repo/src/main/java/gg/repo/agenda/AgendaTeamRepository.java new file mode 100644 index 000000000..213a2f484 --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/AgendaTeamRepository.java @@ -0,0 +1,38 @@ +package gg.repo.agenda; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaTeamStatus; + +public interface AgendaTeamRepository extends JpaRepository { + @Query("SELECT a FROM AgendaTeam a WHERE a.agenda = :agenda AND a.name = :teamName AND" + + " (a.status = :status1 OR a.status = :status2)") + Optional findByAgendaAndTeamNameAndStatus(Agenda agenda, String teamName, AgendaTeamStatus status1, + AgendaTeamStatus status2); + + @Query("SELECT a FROM AgendaTeam a WHERE a.agenda = :agenda AND a.teamKey = :teamKey AND" + + " (a.status = :status1 OR a.status = :status2)") + Optional findByAgendaAndTeamKeyAndStatus(Agenda agenda, UUID teamKey, AgendaTeamStatus status1, + AgendaTeamStatus status2); + + @Query("SELECT a FROM AgendaTeam a JOIN FETCH a.agenda WHERE a.teamKey = :teamKey") + Optional findByTeamKey(UUID teamKey); + + @Query("SELECT a FROM AgendaTeam a WHERE a.agenda = :agenda AND a.status = :status") + List findAllByAgendaAndStatus(Agenda agenda, AgendaTeamStatus status); + + @Query("SELECT a FROM AgendaTeam a WHERE a.agenda = :agenda AND (a.status = :status1 OR a.status = :status2)") + List findAllByAgendaAndStatus(Agenda agenda, AgendaTeamStatus status1, AgendaTeamStatus status2); + + @Query("SELECT a FROM AgendaTeam a WHERE a.agenda = :agenda AND a.status = :status AND a.isPrivate = false") + Page findByAgendaAndStatus(Agenda agenda, AgendaTeamStatus status, Pageable pageable); +} diff --git a/gg-repo/src/main/java/gg/repo/agenda/TicketRepository.java b/gg-repo/src/main/java/gg/repo/agenda/TicketRepository.java new file mode 100644 index 000000000..9037062ed --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/agenda/TicketRepository.java @@ -0,0 +1,26 @@ +package gg.repo.agenda; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; + +public interface TicketRepository extends JpaRepository { + Optional findFirstByAgendaProfileAndIsApprovedTrueAndIsUsedFalseOrderByCreatedAtAsc( + AgendaProfile agendaProfile); + + Optional findByAgendaProfileAndIsApprovedFalse(AgendaProfile agendaProfile); + + Optional findByAgendaProfileId(Long agendaProfileId); + + Page findByAgendaProfileId(Long agendaProfileId, Pageable pageable); + + List findByAgendaProfileAndIsUsedFalseAndIsApprovedTrue(AgendaProfile agendaProfile); + + List findByAgendaProfileAndIsUsedFalse(AgendaProfile agendaProfile); +} diff --git a/gg-repo/src/main/java/gg/repo/user/Auth42TokenRedisRepository.java b/gg-repo/src/main/java/gg/repo/user/Auth42TokenRedisRepository.java new file mode 100644 index 000000000..17b31cc1f --- /dev/null +++ b/gg-repo/src/main/java/gg/repo/user/Auth42TokenRedisRepository.java @@ -0,0 +1,40 @@ +package gg.repo.user; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import gg.data.agenda.Auth42Token; +import gg.utils.exception.custom.NotExistException; +import lombok.RequiredArgsConstructor; + +@Deprecated +@Repository +@RequiredArgsConstructor +public class Auth42TokenRedisRepository { + private final RedisTemplate redisTemplate; + private static final long TOKEN_EXPIRATION_TIME = 7 * 24 * 60 * 60; // 7일 + + public void save42Token(String intraId, Auth42Token token) { + redisTemplate.opsForValue().set(intraId, token, TOKEN_EXPIRATION_TIME, TimeUnit.SECONDS); + } + + public Optional findByIntraId(String intraId) { + return Optional.ofNullable(redisTemplate.opsForValue().get(intraId)); + } + + public void expire42Token(String intraId) { + redisTemplate.expire(intraId, 0, TimeUnit.SECONDS); + } + + public void update42Token(String intraId, Auth42Token token) { + Long currentTtl = redisTemplate.getExpire(intraId, TimeUnit.SECONDS); + + if (currentTtl == null) { + throw new NotExistException("토큰이 존재하지 않습니다."); + } + redisTemplate.opsForValue().set(intraId, token, currentTtl, TimeUnit.SECONDS); + } +} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/repository/JwtRedisRepository.java b/gg-repo/src/main/java/gg/repo/user/JwtRedisRepository.java similarity index 93% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/repository/JwtRedisRepository.java rename to gg-repo/src/main/java/gg/repo/user/JwtRedisRepository.java index f98b6141b..e410f79fd 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/jwt/repository/JwtRedisRepository.java +++ b/gg-repo/src/main/java/gg/repo/user/JwtRedisRepository.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.security.jwt.repository; +package gg.repo.user; import java.util.concurrent.TimeUnit; diff --git a/gg-utils/build.gradle b/gg-utils/build.gradle index 3c6d27f6b..5cd09ebf3 100644 --- a/gg-utils/build.gradle +++ b/gg-utils/build.gradle @@ -73,6 +73,10 @@ unitTestCoverageReport { dependencies { + implementation "com.amazonaws:aws-java-sdk-s3:1.12.281" + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-mail' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' @@ -84,6 +88,7 @@ dependencies { testFixturesImplementation("org.testcontainers:junit-jupiter:1.19.3") testFixturesImplementation("org.testcontainers:mysql:1.19.3") testFixturesImplementation("com.redis:testcontainers-redis:2.0.1") + testFixturesImplementation("com.fasterxml.jackson.core:jackson-databind") testFixturesImplementation("org.projectlombok:lombok:1.18.26") testFixturesCompileOnly("org.projectlombok:lombok") diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ApplicationYmlRead.java b/gg-utils/src/main/java/gg/utils/ApplicationYmlRead.java similarity index 93% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ApplicationYmlRead.java rename to gg-utils/src/main/java/gg/utils/ApplicationYmlRead.java index b675bbc22..99290db35 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ApplicationYmlRead.java +++ b/gg-utils/src/main/java/gg/utils/ApplicationYmlRead.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.utils; +package gg.utils; import java.util.Map; diff --git a/gg-utils/src/main/java/gg/utils/DateTimeUtil.java b/gg-utils/src/main/java/gg/utils/DateTimeUtil.java new file mode 100644 index 000000000..e4641ff2a --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/DateTimeUtil.java @@ -0,0 +1,18 @@ +package gg.utils; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class DateTimeUtil { + private static final ZoneId SEOUL_ZONE_ID = ZoneId.of("Asia/Seoul"); + private static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); + + public static LocalDateTime convertToSeoulDateTime(String dateTimeString) { + return ZonedDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME) + .withZoneSameInstant(UTC_ZONE_ID) + .withZoneSameInstant(SEOUL_ZONE_ID) + .toLocalDateTime(); + } +} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/cookie/CookieUtil.java b/gg-utils/src/main/java/gg/utils/cookie/CookieUtil.java similarity index 95% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/cookie/CookieUtil.java rename to gg-utils/src/main/java/gg/utils/cookie/CookieUtil.java index a7e4663ff..a4e49fe63 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/security/cookie/CookieUtil.java +++ b/gg-utils/src/main/java/gg/utils/cookie/CookieUtil.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.security.cookie; +package gg.utils.cookie; import java.util.Base64; import java.util.Optional; @@ -12,7 +12,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.SerializationUtils; -import gg.pingpong.api.global.utils.ApplicationYmlRead; +import gg.utils.ApplicationYmlRead; import lombok.RequiredArgsConstructor; @Component diff --git a/gg-utils/src/main/java/gg/utils/dto/PageResponseDto.java b/gg-utils/src/main/java/gg/utils/dto/PageResponseDto.java new file mode 100644 index 000000000..c8395b339 --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/dto/PageResponseDto.java @@ -0,0 +1,31 @@ +package gg.utils.dto; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PageResponseDto { + + private Long totalSize; + + private List content; + + @Builder + public PageResponseDto(Long totalSize, List content) { + this.totalSize = totalSize; + this.content = content; + } + + public static PageResponseDto of(Long totalSize, List content) { + return PageResponseDto.builder() + .totalSize(totalSize) + .content(content) + .build(); + } +} diff --git a/gg-utils/src/main/java/gg/utils/exception/ErrorCode.java b/gg-utils/src/main/java/gg/utils/exception/ErrorCode.java index 5e10f5628..5526b53d0 100644 --- a/gg-utils/src/main/java/gg/utils/exception/ErrorCode.java +++ b/gg-utils/src/main/java/gg/utils/exception/ErrorCode.java @@ -129,6 +129,7 @@ public enum ErrorCode { UNREADABLE_HTTP_MESSAGE(400, "CM008", "유효하지 않은 HTTP 메시지입니다."), CONFLICT(409, "CM009", "CONFLICT"), FORBIDDEN(403, "CM010", "접근이 금지된 요청입니다."), + REFRESH_TOKEN_EXPIRED(401, "CM011", "토큰이 만료되었습니다. 다시 로그인해주세요."), //Feedback FEEDBACK_NOT_FOUND(404, "FB100", "FB NOT FOUND"), @@ -180,7 +181,8 @@ public enum ErrorCode { ROOM_NOT_OPEN(400, "PT204", "모집중인 방이 아닙니다."), ROOM_NOT_PARTICIPANT(400, "PT205", "참여하지 않은 방 입니다."), ROOM_MIN_MAX_PEOPLE(400, "PT206", "최소인원이 최대인원보다 큽니다."), - SELF_REPORT(400, "PT207", "자신을 신고할 수 없습니다."), + ROOM_MIN_MAX_TIME(400, "PT207", "최소시간이 최대시간보다 큽니다."), + SELF_REPORT(400, "PT208", "자신을 신고할 수 없습니다."), USER_ALREADY_IN_ROOM(409, "PT301", "이미 참여한 방 입니다."), ALREADY_REPORTED(409, "PT302", "이미 신고한 요청입니다."), CATEGORY_DUPLICATE(409, "PT304", "중복된 카테고리 입니다."), @@ -188,7 +190,51 @@ public enum ErrorCode { ON_PENALTY(403, "PT501", "패널티 상태입니다."), // recruitment - INVALID_CHECKLIST(400, "RE001", "잘못된 요청 데이터입니다."); + INVALID_CHECKLIST(400, "RE001", "잘못된 요청 데이터입니다."), + + // agenda + AUTH_NOT_VALID(401, "AG001", "인증이 유효하지 않습니다."), + AGENDA_TEAM_FULL(400, "AG101", "팀이 꽉 찼습니다."), + LOCATION_NOT_VALID(400, "AG102", "유효하지 않은 지역입니다."), + AGENDA_AWARD_EMPTY(400, "AG103", "시상 정보가 없습니다."), + AGENDA_INVALID_PARAM(400, "AG104", "유효하지 않은 파라미터입니다."), + AGENDA_NOT_OPEN(400, "AG105", "마감된 일정에는 팀을 생성할 수 없습니다."), + AGENDA_CREATE_FAILED(400, "AG106", "일정 생성에 실패했습니다."), + AGENDA_UPDATE_FAILED(400, "AG107", "일정 수정에 실패했습니다."), + AGENDA_INVALID_SCHEDULE(400, "AG108", "유효하지 않은 일정입니다."), + NOT_ENOUGH_TEAM_MEMBER(400, "AG109", "팀원이 부족합니다."), + UPDATE_LOCATION_NOT_VALID(400, "AG110", "지역을 변경할 수 없습니다."), + AGENDA_TEAM_ALREADY_CANCEL(400, "AG111", "이미 취소된 팀입니다."), + AGENDA_TEAM_ALREADY_CONFIRM(400, "AG112", "이미 확정된 팀입니다."), + AGENDA_POSTER_SIZE_TOO_LARGE(400, "AG113", "포스터 사이즈가 너무 큽니다."), + AGENDA_AWARD_PRIORITY_DUPLICATE(400, "AG114", "시상 우선순위가 중복됩니다."), + AGENDA_TEAM_CANCEL_FAIL(400, "AG115", "팀 취소를 이용해주세요."), + AGENDA_NO_CAPACITY(403, "AG201", "해당 일정에 참여할 수 있는 팀이 꽉 찼습니다."), + HOST_FORBIDDEN(403, "AG202", "개최자는 팀을 생성할 수 없습니다."), + TICKET_NOT_EXIST(403, "AG203", "보유한 티켓이 부족합니다."), + NOT_TEAM_MATE(403, "AG204", "팀원이 아닙니다."), + CONFIRM_FORBIDDEN(403, "AG205", "개최자만 일정을 종료할 수 있습니다."), + TICKET_FORBIDDEN(403, "AG206", "티켓 신청은 1분의 대기시간이 있습니다."), + TEAM_LEADER_FORBIDDEN(403, "AG207", "팀장이 아닙니다."), + AGENDA_TEAM_FORBIDDEN(403, "AG208", "일정에 참여한 팀이 있습니다."), + AGENDA_MODIFICATION_FORBIDDEN(403, "AG209", "개최자만 일정을 수정할 수 있습니다."), + AUTH_NOT_FOUND(404, "AG301", "42 정보를 찾을 수 없습니다."), + TICKET_NOT_FOUND(404, "AG302", "해당 티켓이 존재하지 않습니다."), + AGENDA_NOT_FOUND(404, "AG303", "해당 일정이 존재하지 않습니다."), + NOT_SETUP_TICKET(404, "AG304", "티켓 신청이 되어있지 않습니다."), + AGENDA_TEAM_NOT_FOUND(404, "AG305", "팀이 존재하지 않습니다."), + AGENDA_PROFILE_NOT_FOUND(404, "AG306", "프로필이 존재하지 않습니다."), + POINT_HISTORY_NOT_FOUND(404, "AG307", "기부 내역이 존재하지 않습니다."), + AGENDA_ANNOUNCEMENT_NOT_FOUND(404, "AG308", "공지사항이 존재하지 않습니다."), + TEAM_LEADER_NOT_FOUND(404, "AG309", "팀장이 존재하지 않습니다."), + TEAM_NAME_EXIST(409, "AG401", "이미 존재하는 팀 이름입니다."), + ALREADY_TICKET_SETUP(409, "AG402", "이미 티켓 신청이 되어있습니다."), + AGENDA_DOES_NOT_CONFIRM(409, "AG403", "확정되지 않은 일정입니다."), + AGENDA_ALREADY_FINISHED(409, "AG404", "이미 종료된 일정입니다."), + AGENDA_ALREADY_CANCELED(409, "AG405", "이미 취소된 일정입니다."), + AGENDA_ALREADY_CONFIRMED(409, "AG406", "이미 확정된 일정입니다."), + AGENDA_CAPACITY_CONFLICT(409, "AG407", "팀 제한을 변경할 수 없습니다."), + AGENDA_TEAM_CAPACITY_CONFLICT(409, "AG408", "팀 인원 제한을 변경할 수 없습니다."); private final int status; private final String errCode; diff --git a/gg-utils/src/main/java/gg/utils/exception/custom/AuthenticationException.java b/gg-utils/src/main/java/gg/utils/exception/custom/AuthenticationException.java index 6fb9ac499..107d3b307 100644 --- a/gg-utils/src/main/java/gg/utils/exception/custom/AuthenticationException.java +++ b/gg-utils/src/main/java/gg/utils/exception/custom/AuthenticationException.java @@ -8,4 +8,8 @@ public class AuthenticationException extends CustomRuntimeException { public AuthenticationException(String message, ErrorCode errorCode) { super(message, errorCode); } + + public AuthenticationException(ErrorCode errorCode) { + super(errorCode.getMessage(), errorCode); + } } diff --git a/gg-utils/src/main/java/gg/utils/exception/custom/BusinessException.java b/gg-utils/src/main/java/gg/utils/exception/custom/BusinessException.java index eafb5fa95..8909e97d9 100644 --- a/gg-utils/src/main/java/gg/utils/exception/custom/BusinessException.java +++ b/gg-utils/src/main/java/gg/utils/exception/custom/BusinessException.java @@ -10,4 +10,8 @@ public BusinessException(String message, ErrorCode errorCode) { public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage(), errorCode); } + + public BusinessException(String message) { + super(message, ErrorCode.BAD_REQUEST); + } } diff --git a/gg-utils/src/main/java/gg/utils/exception/custom/DuplicationException.java b/gg-utils/src/main/java/gg/utils/exception/custom/DuplicationException.java index 0fc4c5861..dc82f298a 100644 --- a/gg-utils/src/main/java/gg/utils/exception/custom/DuplicationException.java +++ b/gg-utils/src/main/java/gg/utils/exception/custom/DuplicationException.java @@ -10,4 +10,8 @@ public DuplicationException(String message, ErrorCode errorCode) { public DuplicationException(String message) { super(message, ErrorCode.CONFLICT); } + + public DuplicationException(ErrorCode errorCode) { + super(errorCode.getMessage(), errorCode); + } } diff --git a/gg-utils/src/main/java/gg/utils/exception/custom/ForbiddenException.java b/gg-utils/src/main/java/gg/utils/exception/custom/ForbiddenException.java index f31592bd4..c01922872 100644 --- a/gg-utils/src/main/java/gg/utils/exception/custom/ForbiddenException.java +++ b/gg-utils/src/main/java/gg/utils/exception/custom/ForbiddenException.java @@ -10,4 +10,8 @@ public ForbiddenException(String message, ErrorCode errorCode) { public ForbiddenException(String message) { super(message, ErrorCode.FORBIDDEN); } + + public ForbiddenException(ErrorCode errorCode) { + super(errorCode); + } } diff --git a/gg-utils/src/main/java/gg/utils/exception/custom/InvalidParameterException.java b/gg-utils/src/main/java/gg/utils/exception/custom/InvalidParameterException.java index d2f182ec5..69a6bc6ee 100644 --- a/gg-utils/src/main/java/gg/utils/exception/custom/InvalidParameterException.java +++ b/gg-utils/src/main/java/gg/utils/exception/custom/InvalidParameterException.java @@ -6,4 +6,8 @@ public class InvalidParameterException extends CustomRuntimeException { public InvalidParameterException(String message, ErrorCode errorCode) { super(message, errorCode); } + + public InvalidParameterException(ErrorCode errorCode) { + super(errorCode); + } } diff --git a/gg-utils/src/main/java/gg/utils/exception/custom/NotExistException.java b/gg-utils/src/main/java/gg/utils/exception/custom/NotExistException.java index 8255e9873..60516b34d 100644 --- a/gg-utils/src/main/java/gg/utils/exception/custom/NotExistException.java +++ b/gg-utils/src/main/java/gg/utils/exception/custom/NotExistException.java @@ -12,4 +12,8 @@ public NotExistException(String message, ErrorCode errorCode) { public NotExistException(String message) { super(message, ErrorCode.NOT_FOUND); } + + public NotExistException(ErrorCode errorCode) { + super(errorCode); + } } diff --git a/gg-utils/src/main/java/gg/utils/exception/party/RoomMinMaxTime.java b/gg-utils/src/main/java/gg/utils/exception/party/RoomMinMaxTime.java new file mode 100644 index 000000000..819312b07 --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/exception/party/RoomMinMaxTime.java @@ -0,0 +1,10 @@ +package gg.utils.exception.party; + +import gg.utils.exception.ErrorCode; +import gg.utils.exception.custom.InvalidParameterException; + +public class RoomMinMaxTime extends InvalidParameterException { + public RoomMinMaxTime(ErrorCode errorCode) { + super(ErrorCode.ROOM_MIN_MAX_TIME.getMessage(), ErrorCode.ROOM_MIN_MAX_TIME); + } +} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/external/ApiUtil.java b/gg-utils/src/main/java/gg/utils/external/ApiUtil.java similarity index 55% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/external/ApiUtil.java rename to gg-utils/src/main/java/gg/utils/external/ApiUtil.java index ef30fb58d..e8c1df77b 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/external/ApiUtil.java +++ b/gg-utils/src/main/java/gg/utils/external/ApiUtil.java @@ -1,11 +1,14 @@ -package gg.pingpong.api.global.utils.external; +package gg.utils.external; +import java.util.List; import java.util.Map; import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; @@ -14,6 +17,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import gg.utils.exception.user.TokenNotValidException; + @Component public class ApiUtil { private final RestTemplate restTemplate; @@ -49,4 +54,40 @@ public T apiCall(String url, Class responseType, HttpHeaders headers, } return res.getBody(); } + + public T apiCall(String url, Class responseType, HttpHeaders headers, HttpMethod method) { + HttpEntity request = new HttpEntity<>(headers); + ResponseEntity res = restTemplate.exchange(url, method, request, responseType); + if (!res.getStatusCode().is2xxSuccessful()) { + throw new TokenNotValidException(); + } + return res.getBody(); + } + + public T apiCall(String url, ParameterizedTypeReference responseType, HttpHeaders headers, + HttpMethod method) { + HttpEntity request = new HttpEntity<>(headers); + ResponseEntity res = restTemplate.exchange(url, method, request, responseType); + if (!res.getStatusCode().is2xxSuccessful()) { + throw new TokenNotValidException(); + } + return res.getBody(); + } + + /** + * API 호출 + * @param url 호출할 URL + * @param accessToken 액세스 토큰 + * @param responseType 응답 타입 + * @return 응답 + */ + public List> callApiWithAccessToken(String url, String accessToken, + ParameterizedTypeReference>> responseType) { + + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + headers.setContentType(MediaType.APPLICATION_JSON); + + return apiCall(url, responseType, headers, HttpMethod.GET); + } } diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/FileDownloader.java b/gg-utils/src/main/java/gg/utils/file/FileDownloader.java similarity index 94% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/FileDownloader.java rename to gg-utils/src/main/java/gg/utils/file/FileDownloader.java index b7a080041..fd6306c11 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/FileDownloader.java +++ b/gg-utils/src/main/java/gg/utils/file/FileDownloader.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.utils; +package gg.utils.file; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; @@ -10,7 +10,8 @@ @Component public class FileDownloader { - private RestTemplate restTemplate; + + private final RestTemplate restTemplate; public FileDownloader() { HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(); diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ImageResizingUtil.java b/gg-utils/src/main/java/gg/utils/file/ImageResizingUtil.java similarity index 96% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ImageResizingUtil.java rename to gg-utils/src/main/java/gg/utils/file/ImageResizingUtil.java index f6e586987..eedb9ed19 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/ImageResizingUtil.java +++ b/gg-utils/src/main/java/gg/utils/file/ImageResizingUtil.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.utils; +package gg.utils.file; import java.awt.*; import java.awt.image.BufferedImage; diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/JpegMultipartFile.java b/gg-utils/src/main/java/gg/utils/file/JpegMultipartFile.java similarity index 96% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/JpegMultipartFile.java rename to gg-utils/src/main/java/gg/utils/file/JpegMultipartFile.java index 3426c5906..03c5edd6b 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/JpegMultipartFile.java +++ b/gg-utils/src/main/java/gg/utils/file/JpegMultipartFile.java @@ -1,4 +1,4 @@ -package gg.pingpong.api.global.utils; +package gg.utils.file; import java.io.ByteArrayInputStream; import java.io.File; diff --git a/gg-utils/src/main/java/gg/utils/file/handler/AwsImageHandler.java b/gg-utils/src/main/java/gg/utils/file/handler/AwsImageHandler.java new file mode 100644 index 000000000..96cc9859a --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/file/handler/AwsImageHandler.java @@ -0,0 +1,112 @@ +package gg.utils.file.handler; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Objects; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.support.ResourcePatternUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; + +import gg.utils.file.FileDownloader; +import gg.utils.file.ImageResizingUtil; +import gg.utils.file.JpegMultipartFile; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AwsImageHandler implements ImageHandler { + + private final AmazonS3 amazonS3; + + private final FileDownloader fileDownloader; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + @Value("${cloud.aws.s3.dir}") + private String dir; + + @Override + public URL uploadImageOrDefault(MultipartFile multipartFile, + String filename, String defaultUrl) throws IOException { + if (filename.isBlank() || isDefaultImage(multipartFile)) { + return new URL(defaultUrl); + } + String originalFilename = multipartFile.getOriginalFilename(); + String storeFileName = createStoredFileName(originalFilename, filename); + URL storedUrl = uploadToS3(multipartFile, storeFileName); + if (Objects.isNull(storedUrl)) { + return new URL(defaultUrl); + } + return storedUrl; + } + + private static boolean isDefaultImage(MultipartFile multipartFile) { + if (Objects.isNull(multipartFile)) { + return true; + } + if (Objects.isNull(multipartFile.getOriginalFilename())) { + return true; + } + return multipartFile.getOriginalFilename().equals("small_default.jpeg"); + } + + @Override + public URL uploadImageFromUrlOrDefault(String imageUrl, String filename, String defaultUrl) throws IOException { + if (filename.isBlank() || ResourcePatternUtils.isUrl(imageUrl)) { + return new URL(defaultUrl); + } + byte[] downloadedImageBytes = fileDownloader.downloadFromUrl(imageUrl); + byte[] resizedImageBytes = ImageResizingUtil.resizeImageBytes(downloadedImageBytes, 0.5); + MultipartFile multipartFile = new JpegMultipartFile(resizedImageBytes, filename); + URL storedUrl = uploadToS3(multipartFile, multipartFile.getOriginalFilename()); + if (Objects.isNull(storedUrl)) { + return new URL(defaultUrl); + } + return storedUrl; + } + + private String createStoredFileName(String originalFilename, String filename) { + String ext = extractExtensionOrDefault(originalFilename, "jpeg"); + return filename + "-" + UUID.randomUUID() + "." + ext; + } + + private String extractExtensionOrDefault(String uploadFileName, String defaultExtension) { + if (uploadFileName == null) { + return defaultExtension; + } + + int pos = uploadFileName.lastIndexOf("."); + if (pos == -1) { + return defaultExtension; + } + + return uploadFileName.substring(pos + 1); + } + + public URL uploadToS3(MultipartFile multipartFile, String fileName) throws IOException { + String s3FileName = this.dir + fileName; + InputStream inputStream = multipartFile.getInputStream(); + + ObjectMetadata objMeta = new ObjectMetadata(); + objMeta.setContentLength(multipartFile.getSize()); + + PutObjectRequest putObjectRequest = new PutObjectRequest( + bucketName, + s3FileName, + inputStream, + objMeta + ).withCannedAcl(CannedAccessControlList.PublicRead); + amazonS3.putObject(putObjectRequest); + return amazonS3.getUrl(bucketName, s3FileName); + } +} diff --git a/gg-utils/src/main/java/gg/utils/file/handler/ImageHandler.java b/gg-utils/src/main/java/gg/utils/file/handler/ImageHandler.java new file mode 100644 index 000000000..d6ee34f4a --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/file/handler/ImageHandler.java @@ -0,0 +1,12 @@ +package gg.utils.file.handler; + +import java.io.IOException; +import java.net.URL; + +import org.springframework.web.multipart.MultipartFile; +public interface ImageHandler { + + URL uploadImageOrDefault(MultipartFile multipartFile, String filename, String defaultUrl) throws IOException; + + URL uploadImageFromUrlOrDefault(String imageUrl, String filename, String defaultUrl) throws IOException; +} diff --git a/gg-utils/src/main/java/gg/utils/sns/MailSender.java b/gg-utils/src/main/java/gg/utils/sns/MailSender.java new file mode 100644 index 000000000..6171c78c2 --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/MailSender.java @@ -0,0 +1,6 @@ +package gg.utils.sns; + +public interface MailSender { + + void send(String emailTo, String subject, String text); +} diff --git a/gg-utils/src/main/java/gg/utils/sns/MessageSender.java b/gg-utils/src/main/java/gg/utils/sns/MessageSender.java new file mode 100644 index 000000000..089f3f465 --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/MessageSender.java @@ -0,0 +1,10 @@ +package gg.utils.sns; + +import java.util.List; + +public interface MessageSender { + + void send(String intraUsername, String message); + + void sendGroup(List intraUsernames, String message); +} diff --git a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/AsyncMailSender.java b/gg-utils/src/main/java/gg/utils/sns/mail/AsyncMailSender.java similarity index 50% rename from gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/AsyncMailSender.java rename to gg-utils/src/main/java/gg/utils/sns/mail/AsyncMailSender.java index 0eba18d45..bbe275d67 100644 --- a/gg-pingpong-api/src/main/java/gg/pingpong/api/global/utils/AsyncMailSender.java +++ b/gg-utils/src/main/java/gg/utils/sns/mail/AsyncMailSender.java @@ -1,23 +1,32 @@ -package gg.pingpong.api.global.utils; +package gg.utils.sns.mail; import javax.mail.internet.MimeMessage; import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; +import gg.utils.sns.MailSender; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +@Slf4j @Component @AllArgsConstructor -@Slf4j -public class AsyncMailSender { +public class AsyncMailSender implements MailSender { + private final JavaMailSender javaMailSender; @Async("asyncExecutor") - public void send(MimeMessage message) { + public void send(String emailTo, String subject, String text) { + MimeMessage message = javaMailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message); try { + helper.setTo(emailTo); + helper.setSubject(subject); + helper.setText(text); + log.info("Send email to {}", emailTo); javaMailSender.send(message); } catch (Exception ex) { log.error(ex.getMessage()); diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/AsyncSlackMessageSender.java b/gg-utils/src/main/java/gg/utils/sns/slack/AsyncSlackMessageSender.java new file mode 100644 index 000000000..a0cb0916e --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/AsyncSlackMessageSender.java @@ -0,0 +1,46 @@ +package gg.utils.sns.slack; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import gg.utils.exception.noti.SlackSendException; +import gg.utils.sns.MessageSender; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AsyncSlackMessageSender implements MessageSender { + + private final SlackbotApiUtils slackbotApiUtils; + + @Async("asyncExecutor") + public void send(String intraUsername, String message) { + try { + String slackUsername = slackbotApiUtils.findSlackUserIdByIntraId(intraUsername); + String slackChannelName = slackbotApiUtils.createChannel(slackUsername); + log.info("slack alarm send to {}:{}", slackChannelName, slackUsername); + slackbotApiUtils.sendSlackMessage(message, slackChannelName); + } catch (SlackSendException e) { + log.error("SlackSendException message = {}", e.getMessage()); + } + } + + @Async("asyncExecutor") + public void sendGroup(List intraUsernames, String message) { + try { + List slackUsernames = intraUsernames.stream() + .map(slackbotApiUtils::findSlackUserIdByIntraId) + .collect(Collectors.toList()); + String channelName = slackbotApiUtils.createGroupChannel(slackUsernames); + log.info("slack alarm send to {}", channelName); + slackbotApiUtils.sendSlackMessage(message, channelName); + } catch (SlackSendException e) { + log.error("SlackSendException message = {}", e.getMessage()); + } + } +} diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/SlackbotApiUtils.java b/gg-utils/src/main/java/gg/utils/sns/slack/SlackbotApiUtils.java new file mode 100644 index 000000000..2a954a23b --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/SlackbotApiUtils.java @@ -0,0 +1,118 @@ +package gg.utils.sns.slack; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import gg.utils.external.ApiUtil; +import gg.utils.sns.slack.constant.SlackConstant; +import gg.utils.sns.slack.response.ConversationResponse; +import gg.utils.sns.slack.response.SlackUserInfoResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class SlackbotApiUtils { + + @Value("${slack.xoxbToken}") + private String authenticationToken; + + private final ApiUtil apiUtil; + + public String findSlackUserIdByIntraId(String intraId) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + httpHeaders.setBearerAuth(authenticationToken); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("email", convertToIntraEmail(intraId)); + + SlackUserInfoResponse res = apiUtil.apiCall( + SlackConstant.GET_USER_ID_URL.getValue(), + SlackUserInfoResponse.class, + httpHeaders, + params, + HttpMethod.POST + ); + + if (Objects.isNull(res) || Objects.isNull(res.getUser())) { + throw new RuntimeException("슬랙 API 고장으로 인한 NULL 참조" + intraId); + } + return res.getUser().getId(); + } + + private String convertToIntraEmail(String intraId) { + return intraId + SlackConstant.INTRA_EMAIL_SUFFIX.getValue(); + } + + public String createChannel(String slackUser) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(authenticationToken); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + + Map params = new HashMap<>(); + params.put("users", slackUser); + + ConversationResponse res = apiUtil.apiCall( + SlackConstant.CONVERSATION_URL.getValue(), + ConversationResponse.class, + httpHeaders, + params, + HttpMethod.POST + ); + + if (Objects.isNull(res) || Objects.isNull(res.getChannel())) { + throw new RuntimeException("슬랙 API 고장으로 인한 NULL 참조" + slackUser); + } + return res.getChannel().getId(); + } + + public String createGroupChannel(List slackUserNames) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(authenticationToken); + httpHeaders.setContentType(MediaType.APPLICATION_JSON); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("users", String.join(",", slackUserNames)); + + ConversationResponse res = apiUtil.apiCall( + SlackConstant.CONVERSATION_URL.getValue(), + ConversationResponse.class, + httpHeaders, + params, + HttpMethod.POST + ); + + if (Objects.isNull(res) || Objects.isNull(res.getChannel())) { + throw new RuntimeException("슬랙 API 고장으로 인한 NULL 참조" + slackUserNames); + } + return res.getChannel().getId(); + } + + public void sendSlackMessage(String message, String channelId) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(authenticationToken); + headers.setContentType(MediaType.APPLICATION_JSON); + + Map params = new HashMap<>(); + params.put("channel", channelId); + params.put("text", message); + + apiUtil.apiCall( + SlackConstant.SEND_MESSAGE_URL.getValue(), + String.class, + headers, + params, + HttpMethod.POST + ); + } +} diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/constant/SlackConstant.java b/gg-utils/src/main/java/gg/utils/sns/slack/constant/SlackConstant.java new file mode 100644 index 000000000..28a1750e1 --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/constant/SlackConstant.java @@ -0,0 +1,16 @@ +package gg.utils.sns.slack.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum SlackConstant { + + CONVERSATION_URL("https://slack.com/api/conversations.open"), + SEND_MESSAGE_URL("https://slack.com/api/chat.postMessage"), + GET_USER_ID_URL("https://slack.com/api/users.lookupByEmail"), + INTRA_EMAIL_SUFFIX("@student.42seoul.kr"); + + private final String value; +} diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/response/Channel.java b/gg-utils/src/main/java/gg/utils/sns/slack/response/Channel.java new file mode 100644 index 000000000..7829e5f13 --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/response/Channel.java @@ -0,0 +1,9 @@ +package gg.utils.sns.slack.response; + +import lombok.Getter; + +@Getter +public class Channel { + + private String id; +} diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/response/ConversationResponse.java b/gg-utils/src/main/java/gg/utils/sns/slack/response/ConversationResponse.java new file mode 100644 index 000000000..0468c2b6d --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/response/ConversationResponse.java @@ -0,0 +1,11 @@ +package gg.utils.sns.slack.response; + +import lombok.Getter; + +@Getter +public class ConversationResponse { + + private Boolean ok; + + private Channel channel; +} diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/response/SlackUser.java b/gg-utils/src/main/java/gg/utils/sns/slack/response/SlackUser.java new file mode 100644 index 000000000..ecb317f2c --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/response/SlackUser.java @@ -0,0 +1,9 @@ +package gg.utils.sns.slack.response; + +import lombok.Getter; + +@Getter +public class SlackUser { + + private String id; +} diff --git a/gg-utils/src/main/java/gg/utils/sns/slack/response/SlackUserInfoResponse.java b/gg-utils/src/main/java/gg/utils/sns/slack/response/SlackUserInfoResponse.java new file mode 100644 index 000000000..110ce4f2f --- /dev/null +++ b/gg-utils/src/main/java/gg/utils/sns/slack/response/SlackUserInfoResponse.java @@ -0,0 +1,11 @@ +package gg.utils.sns.slack.response; + +import lombok.Getter; + +@Getter +public class SlackUserInfoResponse { + + private Boolean ok; + + private SlackUser user; +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/AgendaTestDataUtils.java b/gg-utils/src/testFixtures/java/gg/utils/AgendaTestDataUtils.java new file mode 100644 index 000000000..6f81ff646 --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/AgendaTestDataUtils.java @@ -0,0 +1,93 @@ +package gg.utils; + +import java.util.ArrayList; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.utils.fixture.agenda.AgendaAnnouncementFixture; +import gg.utils.fixture.agenda.AgendaFixture; +import gg.utils.fixture.agenda.AgendaProfileFixture; +import gg.utils.fixture.agenda.AgendaTeamFixture; +import gg.utils.fixture.agenda.AgendaTeamProfileFixture; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaTestDataUtils { + + private final AgendaFixture agendaFixture; + + private final AgendaAnnouncementFixture agendaAnnouncementFixture; + + private final AgendaTeamFixture agendaTeamFixture; + + private final AgendaProfileFixture agendaProfileFixture; + + private final AgendaTeamProfileFixture agendaTeamProfileFixture; + + @PersistenceContext + private final EntityManager em; + + public Agenda createAgendaAndAnnouncements(int size) { + Agenda agenda = agendaFixture.createAgenda(); + agendaAnnouncementFixture.createAgendaAnnouncementList(agenda, size / 2, true); + agendaAnnouncementFixture.createAgendaAnnouncementList(agenda, size - size / 2, false); + return agenda; + } + + public Agenda createAgendaAndAgendaTeams(String intraId, int size, AgendaStatus status) { + Agenda agenda = agendaFixture.createAgenda(intraId, status); + agendaTeamFixture.createAgendaTeamList(agenda, AgendaTeamStatus.CONFIRM, size); + return agenda; + } + + public Agenda createAgendaTeamProfiles(User user, AgendaStatus status) { + AgendaProfile host = agendaProfileFixture.createAgendaProfile(user, Location.SEOUL); + Agenda agenda = agendaFixture.createAgenda(host.getIntraId(), status); + for (int i = 0; i < 3; i++) { + AgendaProfile leader = agendaProfileFixture.createAgendaProfile(); + List mates = agendaProfileFixture.createAgendaProfileList(3); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, leader, AgendaTeamStatus.OPEN); + agendaTeamProfileFixture.createAgendaTeamProfileList(agenda, team, mates); + } + for (int i = 0; i < 3; i++) { + AgendaProfile leader = agendaProfileFixture.createAgendaProfile(); + List mates = agendaProfileFixture.createAgendaProfileList(3); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, leader, AgendaTeamStatus.OPEN); + agendaTeamProfileFixture.createAgendaTeamProfileList(agenda, team, mates); + team.confirm(); + } + for (int i = 0; i < 3; i++) { + AgendaProfile leader = agendaProfileFixture.createAgendaProfile(); + List mates = agendaProfileFixture.createAgendaProfileList(3); + AgendaTeam team = agendaTeamFixture.createAgendaTeam(agenda, leader, AgendaTeamStatus.OPEN); + agendaTeamProfileFixture.createAgendaTeamProfileList(agenda, team, mates); + team.cancelTeam(team.getStatus()); + } + return agenda; + } + + public List createAgendasWithAllStatus(User user, int size) { + List agendas = new ArrayList<>(); + for (int i = 0; i < size; i++) { + agendas.add(agendaFixture.createAgenda(user.getIntraId(), AgendaStatus.OPEN)); + agendas.add(agendaFixture.createAgenda(user.getIntraId(), AgendaStatus.CONFIRM)); + agendas.add(agendaFixture.createAgenda(user.getIntraId(), AgendaStatus.FINISH)); + agendas.add(agendaFixture.createAgenda(user.getIntraId(), AgendaStatus.CANCEL)); + } + em.flush(); + em.clear(); + return agendas; + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/TestDataUtils.java b/gg-utils/src/testFixtures/java/gg/utils/TestDataUtils.java index cc6ae463a..07592f5d3 100644 --- a/gg-utils/src/testFixtures/java/gg/utils/TestDataUtils.java +++ b/gg-utils/src/testFixtures/java/gg/utils/TestDataUtils.java @@ -205,6 +205,20 @@ public User createNewUser() { return user; } + public User createNewAdminUser(RoleType roleType) { + String randomId = UUID.randomUUID().toString().substring(0, 30); + User user = User.builder() + .eMail("email") + .intraId(randomId) + .racketType(RacketType.PENHOLDER) + .snsNotiOpt(SnsType.NONE) + .roleType(roleType) + .totalExp(1000) + .build(); + userRepository.save(user); + return user; + } + public User createNewUser(String intraId) { User user = User.builder() .eMail("email") diff --git a/gg-utils/src/testFixtures/java/gg/utils/converter/MultiValueMapConverter.java b/gg-utils/src/testFixtures/java/gg/utils/converter/MultiValueMapConverter.java new file mode 100644 index 000000000..d2b561eff --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/converter/MultiValueMapConverter.java @@ -0,0 +1,33 @@ +package gg.utils.converter; + +import java.util.Map; + +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class MultiValueMapConverter { + + public static MultiValueMap convert(ObjectMapper objectMapper, Object dto) { + try { + MultiValueMap params = new LinkedMultiValueMap<>(); + Map map = objectMapper.convertValue(dto, new TypeReference<>() { + }); + params.setAll(map); + return params; + } catch (Exception e) { + log.error("Url Parameter 변환중 오류가 발생했습니다. requestDto={}", dto, e); + throw new IllegalStateException("Url Parameter 변환중 오류가 발생했습니다."); + } + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaAnnouncementFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaAnnouncementFixture.java new file mode 100644 index 000000000..f6ce93d8f --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaAnnouncementFixture.java @@ -0,0 +1,62 @@ +package gg.utils.fixture.agenda; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.persistence.EntityManager; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaAnnouncement; +import gg.repo.agenda.AgendaAnnouncementRepository; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaAnnouncementFixture { + + private final AgendaAnnouncementRepository agendaAnnouncementRepository; + + private final EntityManager em; + + public AgendaAnnouncement createAgendaAnnouncement(Agenda agenda) { + AgendaAnnouncement announcement = AgendaAnnouncement.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .isShow(true) + .agenda(agenda) + .build(); + em.persist(announcement); + em.flush(); + em.clear(); + return announcement; + } + + public List createAgendaAnnouncementList(Agenda agenda, int size) { + List announcements = new ArrayList<>(); + for (int i = 0; i < size; i++) { + announcements.add(AgendaAnnouncement.builder() + .title("title " + i) + .content("content " + i) + .isShow(true) + .agenda(agenda) + .build()); + } + return agendaAnnouncementRepository.saveAll(announcements); + } + + public List createAgendaAnnouncementList(Agenda agenda, int size, boolean isShow) { + List announcements = new ArrayList<>(); + for (int i = 0; i < size; i++) { + announcements.add(AgendaAnnouncement.builder() + .title("title " + i) + .content("content " + i) + .isShow(isShow) + .agenda(agenda) + .build()); + } + return agendaAnnouncementRepository.saveAll(announcements); + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaFixture.java new file mode 100644 index 000000000..23affa18b --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaFixture.java @@ -0,0 +1,153 @@ +package gg.utils.fixture.agenda; + +import static gg.data.agenda.type.AgendaStatus.*; + +import java.time.LocalDateTime; +import java.util.UUID; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.type.AgendaStatus; +import gg.data.agenda.type.Location; +import gg.repo.agenda.AgendaRepository; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaFixture { + + private final AgendaRepository agendaRepository; + + public Agenda createAgenda() { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(1) + .minPeople(1) + .maxPeople(6) + .status(OPEN) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(int minTeam, int maxTeam, int minPeople, int maxPeople) { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(minTeam) + .maxTeam(maxTeam) + .currentTeam(0) + .minPeople(minPeople) + .maxPeople(maxPeople) + .status(OPEN) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(String intraId, AgendaStatus status) { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(2) + .maxTeam(20) + .currentTeam(0) + .minPeople(1) + .maxPeople(10) + .status(status) + .posterUri("posterUri") + .hostIntraId(intraId) + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(Location location) { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(6) + .status(OPEN) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(location) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(LocalDateTime localDateTime) { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(localDateTime) + .startTime(localDateTime.plusDays(2)) + .endTime(localDateTime.plusDays(3)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(OPEN) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } + + public Agenda createAgenda(AgendaStatus agendaStatus) { + Agenda agenda = Agenda.builder() + .title("title " + UUID.randomUUID()) + .content("content " + UUID.randomUUID()) + .deadline(LocalDateTime.now().plusDays(3)) + .startTime(LocalDateTime.now().plusDays(5)) + .endTime(LocalDateTime.now().plusDays(6)) + .minTeam(2) + .maxTeam(5) + .currentTeam(0) + .minPeople(1) + .maxPeople(5) + .status(agendaStatus) + .posterUri("posterUri") + .hostIntraId("hostIntraId") + .location(Location.MIX) + .isOfficial(true) + .isRanking(true) + .build(); + return agendaRepository.save(agenda); + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaPosterImageFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaPosterImageFixture.java new file mode 100644 index 000000000..a4a9a8b84 --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaPosterImageFixture.java @@ -0,0 +1,23 @@ +package gg.utils.fixture.agenda; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaPosterImage; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaPosterImageFixture { + @PersistenceContext + private final EntityManager em; + + public AgendaPosterImage createAgendaPosterImage(Agenda agenda, String posterUri) { + AgendaPosterImage posterImage = new AgendaPosterImage(agenda.getId(), "posterUri"); + em.persist(posterImage); + return posterImage; + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaProfileFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaProfileFixture.java new file mode 100644 index 000000000..0f09648a5 --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaProfileFixture.java @@ -0,0 +1,69 @@ +package gg.utils.fixture.agenda; + +import static gg.data.agenda.type.Coalition.*; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.repo.agenda.AgendaProfileRepository; +import gg.utils.TestDataUtils; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaProfileFixture { + + private final AgendaProfileRepository agendaProfileRepository; + + private final TestDataUtils testDataUtils; + + public AgendaProfile createAgendaProfile(User user, Location location) { + AgendaProfile agendaProfile = AgendaProfile.builder() + .content("content") + .githubUrl("githubUrl") + .coalition(LEE) + .location(location) + .intraId(user.getIntraId()) + .userId(user.getId()) + .fortyTwoId(user.getId()) + .build(); + return agendaProfileRepository.save(agendaProfile); + } + + public AgendaProfile createAgendaProfile() { + User user = testDataUtils.createNewUser(); + AgendaProfile agendaProfile = AgendaProfile.builder() + .content("content") + .githubUrl("githubUrl") + .coalition(LEE) + .location(Location.SEOUL) + .intraId(user.getIntraId()) + .userId(user.getId()) + .fortyTwoId(user.getId()) + .build(); + return agendaProfileRepository.save(agendaProfile); + } + + public List createAgendaProfileList(int size) { + List agendaProfileList = new ArrayList<>(); + for (int i = 0; i < size; i++) { + User user = testDataUtils.createNewUser(); + AgendaProfile agendaProfile = AgendaProfile.builder() + .content("content") + .githubUrl("githubUrl") + .coalition(LEE) + .location(Location.SEOUL) + .intraId(user.getIntraId()) + .userId(user.getId()) + .fortyTwoId(user.getId()) + .build(); + agendaProfileList.add(agendaProfile); + } + return agendaProfileRepository.saveAll(agendaProfileList); + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaTeamFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaTeamFixture.java new file mode 100644 index 000000000..21064c62f --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaTeamFixture.java @@ -0,0 +1,210 @@ +package gg.utils.fixture.agenda; + +import static gg.data.agenda.type.AgendaTeamStatus.*; +import static gg.data.agenda.type.Location.*; +import static java.util.UUID.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.type.AgendaTeamStatus; +import gg.data.agenda.type.Location; +import gg.data.user.User; +import gg.repo.agenda.AgendaTeamRepository; +import gg.utils.TestDataUtils; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaTeamFixture { + + private final TestDataUtils testDataUtils; + + private final AgendaProfileFixture agendaProfileFixture; + + private final AgendaTeamProfileFixture agendaTeamProfileFixture; + + private final AgendaTeamRepository agendaTeamRepository; + + @PersistenceContext + private final EntityManager em; + + public AgendaTeam createAgendaTeam(Agenda agenda) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId("leaderIntraId") + .status(OPEN) + .location(MIX) + .mateCount(1) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, AgendaProfile profile) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId(profile.getIntraId()) + .status(OPEN) + .location(MIX) + .mateCount(1) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, AgendaProfile profile, AgendaTeamStatus status) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId(profile.getIntraId()) + .status(status) + .location(MIX) + .mateCount(1) + .awardPriority(1) + .isPrivate(false) + .build(); + AgendaTeam savedTeam = agendaTeamRepository.save(agendaTeam); + agendaTeamProfileFixture.createAgendaTeamProfile(agenda, savedTeam, profile); + return savedTeam; + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(OPEN) + .location(MIX) + .mateCount(1) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, Location location) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId("leaderIntraId") + .status(OPEN) + .location(location) + .mateCount(1) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(int mateCount, Agenda agenda, User seoulUser, Location location) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId(seoulUser.getIntraId()) + .status(OPEN) + .location(location) + .mateCount(mateCount) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, Location location, AgendaTeamStatus agendaTeamStatus) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(UUID.randomUUID()) + .name("name") + .content("content") + .leaderIntraId("leaderIntraId") + .status(agendaTeamStatus) + .location(location) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user, Location location) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(OPEN) + .location(location) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public AgendaTeam createAgendaTeam(Agenda agenda, User user, Location location, AgendaTeamStatus status) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId(user.getIntraId()) + .status(status) + .location(location) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + return agendaTeamRepository.save(agendaTeam); + } + + public List createAgendaTeamList(Agenda agenda, AgendaTeamStatus status, int size) { + List teams = new ArrayList<>(); + for (int i = 0; i < size; i++) { + AgendaTeam agendaTeam = AgendaTeam.builder() + .agenda(agenda) + .teamKey(randomUUID()) + .name("name") + .content("content") + .leaderIntraId("intraId" + i) + .status(status) + .location(SEOUL) + .mateCount(3) + .awardPriority(1) + .isPrivate(false) + .build(); + teams.add(agendaTeam); + if (status == CONFIRM) { + agenda.confirmTeam(agendaTeam.getLocation(), LocalDateTime.now()); + } + } + return agendaTeamRepository.saveAll(teams); + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaTeamProfileFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaTeamProfileFixture.java new file mode 100644 index 000000000..d5af5054d --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/AgendaTeamProfileFixture.java @@ -0,0 +1,48 @@ +package gg.utils.fixture.agenda; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.Agenda; +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.AgendaTeam; +import gg.data.agenda.AgendaTeamProfile; +import gg.repo.agenda.AgendaTeamProfileRepository; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class AgendaTeamProfileFixture { + private final AgendaTeamProfileRepository agendaTeamProfileRepository; + + public AgendaTeamProfile createAgendaTeamProfile(Agenda agenda, AgendaTeam agendaTeam, + AgendaProfile agendaProfile) { + AgendaTeamProfile agendaTeamProfile = AgendaTeamProfile.builder() + .agenda(agenda) + .agendaTeam(agendaTeam) + .profile(agendaProfile) + .isExist(true) + .build(); + agendaTeam.attendTeam(agenda); + return agendaTeamProfileRepository.save(agendaTeamProfile); + } + + public AgendaTeamProfile createAgendaTeamProfile(AgendaTeam team, AgendaProfile seoulUserAgendaProfile) { + AgendaTeamProfile agendaTeamProfile = AgendaTeamProfile.builder() + .agenda(team.getAgenda()) + .agendaTeam(team) + .profile(seoulUserAgendaProfile) + .isExist(true) + .build(); + return agendaTeamProfileRepository.save(agendaTeamProfile); + } + + public List createAgendaTeamProfileList(Agenda agenda, + AgendaTeam team, List mates) { + return mates.stream() + .map(mate -> createAgendaTeamProfile(agenda, team, mate)) + .collect(Collectors.toList()); + } +} diff --git a/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/TicketFixture.java b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/TicketFixture.java new file mode 100644 index 000000000..09ca8a4fa --- /dev/null +++ b/gg-utils/src/testFixtures/java/gg/utils/fixture/agenda/TicketFixture.java @@ -0,0 +1,58 @@ +package gg.utils.fixture.agenda; + +import java.time.LocalDateTime; +import java.util.UUID; + +import org.springframework.stereotype.Component; + +import gg.data.agenda.AgendaProfile; +import gg.data.agenda.Ticket; +import gg.repo.agenda.TicketRepository; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class TicketFixture { + private final TicketRepository ticketRepository; + + public Ticket createTicket(AgendaProfile agendaProfile) { + Ticket ticket = Ticket.builder() + .agendaProfile(agendaProfile) + .issuedFrom(null) + .usedTo(null) + .isApproved(true) + .approvedAt(LocalDateTime.now().minusDays(1)) + .isUsed(false) + .usedAt(null) + .build(); + return ticketRepository.save(ticket); + } + + public Ticket createNotApporveTicket(AgendaProfile seoulUserAgendaProfile) { + Ticket ticket = Ticket.builder() + .agendaProfile(seoulUserAgendaProfile) + .issuedFrom(null) + .usedTo(null) + .isApproved(false) + .approvedAt(null) + .isUsed(false) + .usedAt(null) + .build(); + return ticketRepository.save(ticket); + } + + public Ticket createTicket(AgendaProfile agendaProfile, boolean isApproved, boolean isUsed, UUID issuedFrom, + UUID usedTo) { + Ticket ticket = Ticket.builder() + .agendaProfile(agendaProfile) + .issuedFrom(issuedFrom) + .usedTo(usedTo) + .isApproved(isApproved) + .approvedAt(LocalDateTime.now().minusDays(1)) + .isUsed(isUsed) + .usedAt(null) + .build(); + ticketRepository.save(ticket); + return ticket; + } +} diff --git a/settings.gradle b/settings.gradle index 9e311f424..6b5cabcf4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,4 +6,4 @@ include 'gg-pingpong-api' include 'gg-utils' include 'gg-auth' include 'gg-recruit-api' - +include 'gg-agenda-api'