### ⚡️⚡ 로그인 연동 추가
-- v1에서 지원하지 않던 카카오계정 연동 기능 추가(좌 : v1, 우: v2)
-
-
+- v1에서 지원하지 않던 카카오계정 연동 기능 추가(좌 : v1, 우: v2)
+
+
+
+
### ⚡️⚡ DB table 구조 변경
+
- v1에서 확장을 위해 열어둔 구조나 테이블마다 여러 곳에 있던 중복된 속성 제거
- v1 -> v2 테이블 수 감소 : 14 -> 12
-
+
+
-
+
+
### ⚡️⚡ 게임추가 기능
+
- v1에서 1개의 예약만 되던 것에서 최대 3개까지 예약을 잡을 수 있도록 변경
@@ -72,15 +113,19 @@ https://42gg.kr/
### ⚡️⚡ 도커 도입
+
- v2에서 도커 도입을 통해 컨테이너를 통한 서버 관리 도입
-
+
+
### ⚡️⚡ 모니터링 도입
+
- grafana를 통한 서버 모니터링 도입
-
+
+
@@ -116,33 +161,37 @@ https://42gg.kr/
-
-
### 4기
+
4기 진행 사항
### ⚡️⚡ DB table 구조 변경
+
- 상점, 티어 등 서비스 확장을 위한 DB 재설계
-
+
### ⚡️⚡ 재화 시스템 추가
+
- 출석, 게임 승패에 연관해 재화 시스템 추가
-
+
### ⚡️⚡ 상점, 아이템 서비스 추가
+
- 유저 요구사항을 반영한 기능 확장
-
-
+
+
### ⚡️⚡ 티어 시스템 추가
+
- 랭킹전 활성화를 위한 티어 시스템 추가
-
+
### ⚡️⚡ 관리자 페이지 구현
+
- 원활한 운영을 위한 관리자 기능 추가
-
+
@@ -175,28 +224,38 @@ https://42gg.kr/
### 5기
+
5기 진행 사항
### ⚡️⚡ 토너먼트 개발
+
### ⚡️⚡ 테스트 커버리지 개선 (2024-03-19 기준)
+
### 전체 68% -> 74%
+
### 단위 테스트 0% -> 30%
-
+
### ⚡️⚡ 아키텍처 변경
+
### BEFORE
-
+
+
+
+
### 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기 진행 사항
+
+
+### ⚡️⚡ 파티 서비스 개발
+
+
+
+### ⚡️⚡ 테스트 커버리지 개선 (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)
+
+
+
+
+
+
+
+### 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)
+
+
+
+
+
+
+
## ⚡️ 필요 파일
+
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 extends Payload>[] 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 extends Payload>[] 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