From 211e9be8e445a04b186e4fe02a8d8c2e5372dce0 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Sat, 28 Sep 2024 21:15:58 +0900 Subject: [PATCH 01/10] =?UTF-8?q?refactor(AdvertisementGetDto):=20isSponso?= =?UTF-8?q?redContent=20=ED=95=84=EB=93=9C=20DTO=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/AdvertisementsGetResponseDto.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/advertisement/dto/AdvertisementsGetResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/advertisement/dto/AdvertisementsGetResponseDto.java index 0638b89e..1c37d4d0 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/advertisement/dto/AdvertisementsGetResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/advertisement/dto/AdvertisementsGetResponseDto.java @@ -35,12 +35,7 @@ public record AdvertisementGetDto( @Schema(description = "광고 게시 시작일", example = "2024-07-31T00:00:00") @NotNull - LocalDateTime advertisementStartDate, - - @Schema(description = "광고성 컨텐츠 여부", example = "true") - @NotNull - boolean isSponsoredContent - + LocalDateTime advertisementStartDate ) { public static AdvertisementGetDto of(Advertisement advertisement) { return new AdvertisementGetDto( @@ -48,8 +43,7 @@ public static AdvertisementGetDto of(Advertisement advertisement) { advertisement.getAdvertisementDesktopImageUrl(), advertisement.getAdvertisementMobileImageUrl(), advertisement.getAdvertisementLink(), - advertisement.getAdvertisementStartDate(), - advertisement.isSponsoredContent() + advertisement.getAdvertisementStartDate() ); } } From 2f94ddf80f03fbb6ee7674a3ca760f6a38d6c097 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Sun, 13 Oct 2024 15:42:42 +0900 Subject: [PATCH 02/10] =?UTF-8?q?hotfix:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20d?= =?UTF-8?q?ev=20=EC=84=9C=EB=B2=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sopt/makers/crew/main/global/config/SecurityConfig.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java b/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java index c4c46c51..f810a2af 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/config/SecurityConfig.java @@ -3,8 +3,6 @@ import java.util.Arrays; import java.util.stream.Stream; -import lombok.RequiredArgsConstructor; - import org.sopt.makers.crew.main.global.jwt.JwtAuthenticationEntryPoint; import org.sopt.makers.crew.main.global.jwt.JwtAuthenticationFilter; import org.sopt.makers.crew.main.global.jwt.JwtTokenProvider; @@ -23,6 +21,8 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import lombok.RequiredArgsConstructor; + @Configuration @RequiredArgsConstructor @EnableWebSecurity @@ -91,7 +91,7 @@ CorsConfigurationSource corsConfigurationSource() { "https://playground.sopt.org/", "http://localhost:3000/", "https://sopt-internal-dev.pages.dev/", - "https://crew.api.dev.sopt.org", + "https://crew.api.develop.sopt.org", "https://crew.api.prod.sopt.org" )); configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE", "PUT", "OPTIONS")); From ce20f79a010c0d6543927f3c3de91f514e3ebe32 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sun, 13 Oct 2024 19:08:53 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EC=A0=84=EC=B2=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#448)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 임시 API 삭제 * feat: 전체 사용자 조회 API 구현 * chore: 기존 멘션 사용자 조회 API deprecated 처리 --- .../makers/crew/main/user/v2/UserV2Api.java | 29 +++----------- .../crew/main/user/v2/UserV2Controller.java | 40 ++++++------------- .../v2/dto/response/UserV2GetAllUserDto.java | 36 +++++++++++++++++ .../main/user/v2/service/UserV2Service.java | 3 ++ .../user/v2/service/UserV2ServiceImpl.java | 12 ++++++ 5 files changed, 69 insertions(+), 51 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllUserDto.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Api.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Api.java index 932fee5c..05fe3f7e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Api.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Api.java @@ -12,6 +12,7 @@ import org.sopt.makers.crew.main.global.dto.TempResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllUserDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAppliedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetCreatedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetUserOwnProfileResponseDto; @@ -34,21 +35,18 @@ public interface UserV2Api { @ApiResponse(responseCode = "200", description = "성공")}) ResponseEntity> getAllMentionUser(Principal principal); - @Operation(summary = "유저 본인 프로필 조회") - @ResponseStatus(HttpStatus.OK) + @Operation(summary = "전체 사용자 조회") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공"), - @ApiResponse(responseCode = "400", description = "해당 유저가 없는 경우", content = @Content), - }) - ResponseEntity getUserOwnProfile(Principal principal); + @ApiResponse(responseCode = "200", description = "성공")}) + ResponseEntity> getAllUser(Principal principal); - @Operation(summary = "[TEMP] 유저 본인 프로필 조회") + @Operation(summary = "유저 본인 프로필 조회") @ResponseStatus(HttpStatus.OK) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "성공"), @ApiResponse(responseCode = "400", description = "해당 유저가 없는 경우", content = @Content), }) - ResponseEntity> getUserOwnProfileTemp(Principal principal); + ResponseEntity getUserOwnProfile(Principal principal); @Operation(summary = "내가 신청한 모임 조회") @ResponseStatus(HttpStatus.OK) @@ -58,14 +56,6 @@ public interface UserV2Api { ResponseEntity getAppliedMeetingByUser( Principal principal); - @Operation(summary = "[TEMP] 내가 신청한 모임 조회") - @ResponseStatus(HttpStatus.OK) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공") - }) - ResponseEntity> getAppliedMeetingByUserTemp( - Principal principal); - @Operation(summary = "내가 만든 모임 조회") @ResponseStatus(HttpStatus.OK) @ApiResponses(value = { @@ -74,12 +64,5 @@ ResponseEntity> getApp ResponseEntity getCreatedMeetingByUser( Principal principal); - @Operation(summary = "[TEMP] 내가 만든 모임 조회") - @ResponseStatus(HttpStatus.OK) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "성공") - }) - ResponseEntity> getCreatedMeetingByUserTemp( - Principal principal); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java index ff280f01..73773362 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/UserV2Controller.java @@ -5,10 +5,10 @@ import lombok.RequiredArgsConstructor; -import org.sopt.makers.crew.main.global.dto.TempResponseDto; import org.sopt.makers.crew.main.global.util.UserUtil; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllUserDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAppliedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetCreatedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetUserOwnProfileResponseDto; @@ -30,13 +30,14 @@ public class UserV2Controller implements UserV2Api { @GetMapping("/meeting/all") @ResponseStatus(HttpStatus.OK) public ResponseEntity> getAllMeetingByUser( - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok(userV2Service.getAllMeetingByUser(userId)); + Principal principal) { + Integer userId = UserUtil.getUserId(principal); + return ResponseEntity.ok(userV2Service.getAllMeetingByUser(userId)); } @Override @GetMapping("/mention") + @Deprecated public ResponseEntity> getAllMentionUser( Principal principal) { @@ -45,19 +46,18 @@ public ResponseEntity> getAllMentionUser( } @Override - @GetMapping("/profile/me") - public ResponseEntity getUserOwnProfile(Principal principal) { + @GetMapping + public ResponseEntity> getAllUser(Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok().body(userV2Service.getUserOwnProfile(userId)); + return ResponseEntity.ok(userV2Service.getAllUser()); } @Override - @GetMapping("/profile/me/temp") - public ResponseEntity> getUserOwnProfileTemp( - Principal principal) { + @GetMapping("/profile/me") + public ResponseEntity getUserOwnProfile(Principal principal) { + Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok().body(TempResponseDto.of(userV2Service.getUserOwnProfile(userId))); + return ResponseEntity.ok().body(userV2Service.getUserOwnProfile(userId)); } @Override @@ -68,14 +68,6 @@ public ResponseEntity getAppliedMeetin return ResponseEntity.ok().body(userV2Service.getAppliedMeetingByUser(userId)); } - @Override - @GetMapping("/apply/temp") - public ResponseEntity> getAppliedMeetingByUserTemp( - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok().body(TempResponseDto.of(userV2Service.getAppliedMeetingByUser(userId))); - } - @Override @GetMapping("/meeting") public ResponseEntity getCreatedMeetingByUser(Principal principal) { @@ -84,12 +76,4 @@ public ResponseEntity getCreatedMeetin return ResponseEntity.ok().body(userV2Service.getCreatedMeetingByUser(userId)); } - @Override - @GetMapping("/meeting/temp") - public ResponseEntity> getCreatedMeetingByUserTemp( - Principal principal) { - Integer userId = UserUtil.getUserId(principal); - return ResponseEntity.ok().body(TempResponseDto.of(userV2Service.getCreatedMeetingByUser(userId))); - } - } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllUserDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllUserDto.java new file mode 100644 index 00000000..27be48ca --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/UserV2GetAllUserDto.java @@ -0,0 +1,36 @@ +package org.sopt.makers.crew.main.user.v2.dto.response; + +import org.sopt.makers.crew.main.entity.user.User; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(name = "UserV2GetAllUserDto", description = "전체 사용자 조회 응답 Dto") +public record UserV2GetAllUserDto( + @Schema(description = "크루에서 사용하는 userId", example = "103") + @NotNull + Integer userId, + @Schema(description = "메이커스 프로덕트에서 범용적으로 사용하는 userId", example = "403") + @NotNull + Integer orgId, + + @Schema(description = "유저 이름", example = "홍길동") + @NotNull + String userName, + + @Schema(description = "최근 파트", example = "서버") + @NotNull + String recentPart, + + @Schema(description = "최근 기수", example = "33") + @NotNull + int recentGeneration, + + @Schema(description = "유저 프로필 사진", example = "[url] 형식") + String profileImageUrl +) { + public static UserV2GetAllUserDto of(User user) { + return new UserV2GetAllUserDto(user.getId(), user.getOrgId(), user.getName(), + user.getRecentActivityVO().getPart(), user.getRecentActivityVO().getGeneration(), user.getProfileImage()); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java index deb1fb9b..9b644bdc 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2Service.java @@ -5,6 +5,7 @@ import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllUserDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAppliedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetCreatedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetUserOwnProfileResponseDto; @@ -15,6 +16,8 @@ public interface UserV2Service { List getAllMentionUser(); + List getAllUser(); + UserV2GetUserOwnProfileResponseDto getUserOwnProfile(Integer userId); UserV2GetAppliedMeetingByUserResponseDto getAppliedMeetingByUser(Integer userId); diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index 9787ba61..694d64b1 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -19,6 +19,7 @@ import org.sopt.makers.crew.main.user.v2.dto.response.MeetingV2GetCreatedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMeetingByUserMeetingDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllUserDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAppliedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetCreatedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetUserOwnProfileResponseDto; @@ -73,6 +74,17 @@ public List getAllMentionUser() { .toList(); } + @Override + public List getAllUser() { + + List users = userRepository.findAll(); + + return users.stream() + .filter(user -> user.getActivities() != null) + .map(UserV2GetAllUserDto::of) + .toList(); + } + @Override public UserV2GetUserOwnProfileResponseDto getUserOwnProfile(Integer userId) { User user = userRepository.findByIdOrThrow(userId); From 01dbc3a28c171e215f29484062fe028c52fdbc01 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:45:17 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EA=B3=B5=EB=8F=99=20=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=EC=9E=A5=20=EA=B5=AC=ED=98=84=20(#449)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: 공동 모임장 관련 엔티티 및 레포지토리 추가 * chore: 모임 생성 API에 공동 모임장 관련 로직 추가 * chore: 모임 수정 API에 공동 모임장 관련 로직 추가 * chore: 모임 삭제 API에 공동 모임장 관련 로직 추가 * chore: id -> ManyToOne 연관관계 생성 * chore: 연관관계 설정으로 인한 수정 * chore: join 컬럼 이름 변경 * chore: 모임 상세 조회 API에 공동 모임장 관련 로직 추가 * fix: 기존 검증로직 원상 복구 및 공동모임장 설정하지 않은 경우 분기처리 * add: 공동 모임장 관련 테이블 추가 * test: 공동 모임장 관련 테스트 코드 추가 * chore: 메서드 이름 변경 * feat: JointLeaders 일급컬렉션 구현 및 공동모임장 여부 확인 로직 구현 * feat: 모임 상세 조회 API 에 공동모임장 여부 구현 * chore: JointLeader 자료형 Map 으로 변경 및 hasNotJointLeader 추가 * chore: nullable 하게 변경 * feat: 내 모임 조회 API 에 공동모임장 여부 추가 * fix: isJointLeader 호환을 위해 getter 메서드 직접 구현 * chore: JointLeader -> CoLeader로 수정 * chore: CoLeaders 조건 반전 * chore: @Getter(AccessLevel.NONE) 추가 * chore: update 메서드 수정 * chore: 필드 설명 추가 * chore: 모임 생성 코드 리뷰 반영 * feat: 공동 모임장이 존재하지 않는 경우의 예외 처리 --- .../crew/main/entity/meeting/CoLeader.java | 43 +++++ .../entity/meeting/CoLeaderRepository.java | 15 ++ .../crew/main/entity/meeting/CoLeaders.java | 45 +++++ .../crew/main/entity/user/UserRepository.java | 17 ++ .../main/global/exception/ErrorStatus.java | 1 + .../MeetingV2CreateMeetingBodyDto.java | 6 +- .../MeetingV2CoLeaderResponseDto.java | 27 +++ .../MeetingV2GetMeetingByIdResponseDto.java | 31 +++- .../v2/service/MeetingV2ServiceImpl.java | 45 ++++- ...gV2GetCreatedMeetingByUserResponseDto.java | 10 +- .../user/v2/service/UserV2ServiceImpl.java | 9 +- main/src/main/resources/schema.sql | 15 ++ .../v2/service/MeetingV2ServiceTest.java | 160 +++++++++++++++++- 13 files changed, 403 insertions(+), 21 deletions(-) create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java create mode 100644 main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2CoLeaderResponseDto.java diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java new file mode 100644 index 00000000..729b7e68 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java @@ -0,0 +1,43 @@ +package org.sopt.makers.crew.main.entity.meeting; + +import org.sopt.makers.crew.main.entity.user.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "co_leader") +public class CoLeader { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "meetingId") + @NotNull + private Meeting meeting; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId") + @NotNull + private User user; + + @Builder + private CoLeader(Meeting meeting, User user) { + this.meeting = meeting; + this.user = user; + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java new file mode 100644 index 00000000..717365f8 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java @@ -0,0 +1,15 @@ +package org.sopt.makers.crew.main.entity.meeting; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CoLeaderRepository extends JpaRepository { + + void deleteAllByMeetingId(Integer meetingId); + + List findAllByMeetingId(Integer meetingId); + + List findAllByMeetingIdIn(List meetingId); + +} \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java new file mode 100644 index 00000000..d88afb15 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java @@ -0,0 +1,45 @@ +package org.sopt.makers.crew.main.entity.meeting; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class CoLeaders { + /** + * Key : MeetingId + * Value : 해당 모임의 공동 모임장 목록 + * + * @implNote : List 내에 있는 CoLeader 객체는 fetch join 으로 다른 객체를 불러오지 않은 상태 + * @implNote : 해당 자료형을 사용할 때는 'hasCoLeader' 메서드 사용 적극 권장 + * + * */ + private final Map> coLeadersMap; + + public CoLeaders(List coLeaders) { + this.coLeadersMap = coLeaders.stream() + .collect(Collectors.groupingBy(coLeader -> coLeader.getMeeting().getId())); + } + + public boolean isCoLeader(Integer meetingId, Integer requestUserId) { + if (!isCoLeaderPresent(meetingId)) { + return false; + } + + return coLeadersMap.get(meetingId).stream() + .anyMatch(coLeader -> coLeader.getUser().getId().equals(requestUserId)); + } + + public List getCoLeaders(Integer meetingId) { + if (!isCoLeaderPresent(meetingId)) { + return Collections.emptyList(); + } + + return Collections.unmodifiableList(coLeadersMap.get(meetingId)); + } + + private boolean isCoLeaderPresent(Integer meetingId) { + return coLeadersMap.containsKey(meetingId); + } + +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java index 34123452..53ff48c9 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/user/UserRepository.java @@ -2,8 +2,10 @@ import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; +import java.util.List; import java.util.Optional; +import org.sopt.makers.crew.main.global.exception.NotFoundException; import org.sopt.makers.crew.main.global.exception.UnAuthorizedException; import org.springframework.data.jpa.repository.JpaRepository; @@ -16,4 +18,19 @@ default User findByIdOrThrow(Integer userId) { .orElseThrow(() -> new UnAuthorizedException(UNAUTHORIZED_USER.getErrorCode())); } + List findAllByIdIn(List userIds); + + default List findAllByIdInOrThrow(List userIds) { + List users = findAllByIdIn(userIds); + List foundUserIds = users.stream() + .map(User::getId) + .toList(); + + if (!foundUserIds.containsAll(userIds)) { + throw new NotFoundException(NOT_FOUND_USER.getErrorCode()); + } + + return users; + } + } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java index 47d68653..0bd9ddd6 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java @@ -21,6 +21,7 @@ public enum ErrorStatus { INVALID_INPUT_VALUE_FILTER("요청값 또는 토큰이 올바르지 않습니다."), NOT_FOUND_MEETING("모임이 없습니다."), NOT_FOUND_POST("존재하지 않는 게시글입니다."), + NOT_FOUND_USER("존재하지 않는 유저입니다."), NOT_FOUND_COMMENT("존재하지 않는 댓글입니다."), FULL_MEETING_CAPACITY("정원이 꽉 찼습니다."), ALREADY_APPLIED_MEETING("이미 지원한 모임입니다."), diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/request/MeetingV2CreateMeetingBodyDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/request/MeetingV2CreateMeetingBodyDto.java index 05e1abcc..b095a45e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/request/MeetingV2CreateMeetingBodyDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/request/MeetingV2CreateMeetingBodyDto.java @@ -25,7 +25,7 @@ public class MeetingV2CreateMeetingBodyDto { + " \"https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2023/04/12/7bd87736-b557-4b26-a0d5-9b09f1f1d7df\"\n" + " ]", description = "모임 이미지 리스트, 최대 6개") @NotEmpty - @Size(max = 6) + @Size(min = 1, max = 6) private List files; @Schema(example = "스터디", description = "모임 카테고리") @@ -79,8 +79,12 @@ public class MeetingV2CreateMeetingBodyDto { + " \"IOS\"\n" + " ]", description = "대상 파트 목록") @NotNull + @Size(min = 1, max = 6) private MeetingJoinablePart[] joinableParts; + @Schema(example = "\"[251, 942]\"", description = "공동 모임장 userId (크루에서 사용하는 userId)") + private List coLeaderUserIds; + public String getmStartDate() { return mStartDate; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2CoLeaderResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2CoLeaderResponseDto.java new file mode 100644 index 00000000..31a62399 --- /dev/null +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2CoLeaderResponseDto.java @@ -0,0 +1,27 @@ +package org.sopt.makers.crew.main.meeting.v2.dto.response; + +import org.sopt.makers.crew.main.entity.user.User; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@Schema(description = "공동 모임장 조회 dto") +public record MeetingV2CoLeaderResponseDto( + @Schema(description = "공동 모임장 id, 크루에서 사용하는 userId", example = "203") + @NotNull + Integer userId, + @Schema(description = "공동 모임장 org id, 메이커스 프로덕트에서 범용적으로 사용하는 userId", example = "789") + @NotNull + Integer orgId, + @Schema(description = "공동 모임장 이름", example = "공동 모임장 이름") + @NotNull + String userName, + @Schema(description = "공동 모임장 프로필 이미지 url", example = "[url 형식]") + @NotNull + String userprofileImage +) { + public static MeetingV2CoLeaderResponseDto of(User user) { + return new MeetingV2CoLeaderResponseDto(user.getId(), user.getOrgId(), user.getName(), + user.getProfileImage()); + } +} diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java index 0e97c3ab..f58cd236 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/dto/response/MeetingV2GetMeetingByIdResponseDto.java @@ -3,6 +3,7 @@ import java.time.LocalDateTime; import java.util.List; +import org.sopt.makers.crew.main.entity.meeting.CoLeader; import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto; import org.sopt.makers.crew.main.entity.meeting.Meeting; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; @@ -11,6 +12,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -59,11 +61,11 @@ public class MeetingV2GetMeetingByIdResponseDto { @NotNull private final String processDesc; - @Schema(description = "모임 활동 시작 시간", example = "2024-08-13T15:30:00", name = "mStartDate") + @Schema(description = "모임 활동 시작 시간", example = "2024-08-13T15:30:00", name = "mStartDate") @NotNull private final LocalDateTime mStartDate; - @Schema(description = "모임 활동 종료 시간", example = "2024-10-13T23:59:59", name = "mEndDate") + @Schema(description = "모임 활동 종료 시간", example = "2024-10-13T23:59:59", name = "mEndDate") @NotNull private final LocalDateTime mEndDate; @@ -101,7 +103,16 @@ public class MeetingV2GetMeetingByIdResponseDto { @NotNull private final MeetingJoinablePart[] joinableParts; - @Schema(description = "모임 상태, 0: 모집전, 1: 모집중, 2: 모집종료", example = "1", type = "integer", allowableValues = {"0", "1", "2"}) + @Schema(description = "공동 모임장 목록", example = "") + private final List coMeetingLeaders; + + @Schema(description = "공동 모임장 여부", example = "false") + @NotNull + @Getter(AccessLevel.NONE) + private final boolean isCoLeader; + + @Schema(description = "모임 상태, 0: 모집전, 1: 모집중, 2: 모집종료", example = "1", type = "integer", allowableValues = {"0", + "1", "2"}) @NotNull private final int status; @@ -129,7 +140,8 @@ public class MeetingV2GetMeetingByIdResponseDto { @NotNull private final List appliedInfo; - public static MeetingV2GetMeetingByIdResponseDto of(Meeting meeting, long approvedCount, Boolean isHost, Boolean isApply, + public static MeetingV2GetMeetingByIdResponseDto of(Meeting meeting, List coLeaders, + boolean isCoLeader, long approvedCount, Boolean isHost, Boolean isApply, Boolean isApproved, User meetingCreator, List appliedInfo, LocalDateTime now) { @@ -137,12 +149,17 @@ public static MeetingV2GetMeetingByIdResponseDto of(Meeting meeting, long approv Integer meetingStatus = meeting.getMeetingStatus(now); + List coLeaderResponseDtos = coLeaders.stream() + .map(coLeader -> MeetingV2CoLeaderResponseDto.of(coLeader.getUser())) + .toList(); + return new MeetingV2GetMeetingByIdResponseDto(meeting.getId(), meeting.getUserId(), meeting.getTitle(), meeting.getCategory().getValue(), meeting.getImageURL(), meeting.getStartDate(), meeting.getEndDate(), meeting.getCapacity(), meeting.getDesc(), meeting.getProcessDesc(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getLeaderDesc(), meeting.getNote(), meeting.getIsMentorNeeded(), meeting.getCanJoinOnlyActiveGeneration(), meeting.getCreatedGeneration(), - meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meetingStatus, + meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), coLeaderResponseDtos, isCoLeader, + meetingStatus, approvedCount, isHost, isApply, isApproved, meetingCreatorDto, appliedInfo); } @@ -153,4 +170,8 @@ public LocalDateTime getmStartDate() { public LocalDateTime getmEndDate() { return mEndDate; } + + public boolean getIsCoLeader() { + return this.isCoLeader; + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index 96af0b6a..ae3451c4 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -24,6 +24,9 @@ import lombok.RequiredArgsConstructor; +import org.sopt.makers.crew.main.entity.meeting.CoLeader; +import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; +import org.sopt.makers.crew.main.entity.meeting.CoLeaders; import org.sopt.makers.crew.main.global.dto.MeetingResponseDto; import org.sopt.makers.crew.main.global.exception.BadRequestException; import org.sopt.makers.crew.main.global.exception.ServerException; @@ -91,6 +94,7 @@ public class MeetingV2ServiceImpl implements MeetingV2Service { private final PostRepository postRepository; private final CommentRepository commentRepository; private final LikeRepository likeRepository; + private final CoLeaderRepository coLeaderRepository; private final S3Service s3Service; @@ -198,6 +202,14 @@ public MeetingV2CreateMeetingResponseDto createMeeting(MeetingV2CreateMeetingBod user.getId()); Meeting savedMeeting = meetingRepository.save(meeting); + + List coLeaderUserIds = requestBody.getCoLeaderUserIds(); + if (coLeaderUserIds != null && !coLeaderUserIds.isEmpty()) { + List users = userRepository.findAllByIdInOrThrow(coLeaderUserIds); + List coLeaders = createCoLeaders(users, savedMeeting); + coLeaderRepository.saveAll(coLeaders); + } + return MeetingV2CreateMeetingResponseDto.of(savedMeeting.getId()); } @@ -292,6 +304,7 @@ public void deleteMeeting(Integer meetingId, Integer userId) { commentRepository.deleteAllByPostIdsInQuery(postIds); postRepository.deleteAllByMeetingIdQuery(meetingId); applyRepository.deleteAllByMeetingIdQuery(meetingId); + coLeaderRepository.deleteAllByMeetingId(meetingId); meetingRepository.delete(meeting); } @@ -308,9 +321,23 @@ public void updateMeeting(Integer meetingId, MeetingV2CreateMeetingBodyDto reque createTargetActiveGeneration(requestBody.getCanJoinOnlyActiveGeneration()), ACTIVE_GENERATION, user, user.getId()); + updateCoLeaders(requestBody.getCoLeaderUserIds(), updatedMeeting); + meeting.updateMeeting(updatedMeeting); } + private void updateCoLeaders(List coLeaderUserIds, Meeting updatedMeeting) { + coLeaderRepository.deleteAllByMeetingId(updatedMeeting.getId()); + + if (coLeaderUserIds == null || coLeaderUserIds.isEmpty()) { + return; + } + + List users = userRepository.findAllById(coLeaderUserIds); + List coLeaders = createCoLeaders(users, updatedMeeting); + coLeaderRepository.saveAll(coLeaders); + } + @Override @Transactional public void updateApplyStatus(Integer meetingId, ApplyV2UpdateStatusBodyDto requestBody, Integer userId) { @@ -353,7 +380,8 @@ public MeetingV2GetMeetingByIdResponseDto getMeetingById(Integer meetingId, Inte User user = userRepository.findByIdOrThrow(userId); Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); - User meetingCreator = userRepository.findByIdOrThrow(meeting.getUserId()); + User meetingLeader = userRepository.findByIdOrThrow(meeting.getUserId()); + CoLeaders coLeaders = new CoLeaders(coLeaderRepository.findAllByMeetingId(meetingId)); Applies applies = new Applies( applyRepository.findAllByMeetingIdWithUser(meetingId, List.of(WAITING, APPROVE, REJECT), ORDER_ASC)); @@ -361,6 +389,7 @@ public MeetingV2GetMeetingByIdResponseDto getMeetingById(Integer meetingId, Inte Boolean isHost = meeting.checkMeetingLeader(user.getId()); Boolean isApply = applies.isApply(meetingId, user.getId()); Boolean isApproved = applies.isApproved(meetingId, user.getId()); + boolean isCoLeader = coLeaders.isCoLeader(meetingId, userId); long approvedCount = applies.getApprovedCount(meetingId); List applyWholeInfoDtos = new ArrayList<>(); @@ -370,8 +399,9 @@ public MeetingV2GetMeetingByIdResponseDto getMeetingById(Integer meetingId, Inte .toList(); } - return MeetingV2GetMeetingByIdResponseDto.of(meeting, approvedCount, isHost, isApply, isApproved, - meetingCreator, applyWholeInfoDtos, time.now()); + return MeetingV2GetMeetingByIdResponseDto.of(meeting, coLeaders.getCoLeaders(meetingId), isCoLeader, + approvedCount, isHost, isApply, isApproved, + meetingLeader, applyWholeInfoDtos, time.now()); } private void deleteCsvFile(String filePath) { @@ -502,4 +532,13 @@ private void validateUserJoinableParts(User user, Meeting meeting) { throw new BadRequestException(NOT_TARGET_PART.getErrorCode()); } } + + private List createCoLeaders(List coLeaders, Meeting savedMeeting) { + return coLeaders.stream() + .map(coLeader -> CoLeader.builder() + .meeting(savedMeeting) + .user(coLeader) + .build()) + .toList(); + } } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java index 6a4b8e6a..5832c10c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java @@ -12,6 +12,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; @Schema(name = "MeetingV2GetCreatedMeetingByUserResponseDto", description = "모임 Dto") public record MeetingV2GetCreatedMeetingByUserResponseDto( @@ -39,6 +41,10 @@ public record MeetingV2GetCreatedMeetingByUserResponseDto( @Schema(example = "2", description = "모임 활동 상태", type = "integer", allowableValues = {"0", "1", "2"}) @NotNull int status, + @Schema(example = "false", description = "공동 모임장 여부") + @NotNull + @Getter(AccessLevel.NONE) + boolean isCoLeader, /** * 썸네일 이미지 * @@ -66,7 +72,7 @@ public record MeetingV2GetCreatedMeetingByUserResponseDto( @NotNull int appliedCount ) { - public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, User meetingCreator, int appliedCount, + public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, boolean isCoLeader, User meetingCreator, int appliedCount, LocalDateTime now) { MeetingCreatorDto creatorDto = MeetingCreatorDto.of(meetingCreator); boolean canJoinOnlyActiveGeneration = meeting.getTargetActiveGeneration() == CrewConst.ACTIVE_GENERATION @@ -74,7 +80,7 @@ public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, Us return new MeetingV2GetCreatedMeetingByUserResponseDto(meeting.getId(), meeting.getTitle(), meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meeting.getCategory().getValue(), - canJoinOnlyActiveGeneration, meeting.getMeetingStatus(now), meeting.getImageURL(), + canJoinOnlyActiveGeneration, meeting.getMeetingStatus(now), isCoLeader, meeting.getImageURL(), meeting.getIsMentorNeeded(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getCapacity(), creatorDto, appliedCount); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index 694d64b1..3a54c78c 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -5,6 +5,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; +import org.sopt.makers.crew.main.entity.meeting.CoLeaders; import org.sopt.makers.crew.main.global.exception.BaseException; import org.sopt.makers.crew.main.global.util.Time; import org.sopt.makers.crew.main.entity.apply.Applies; @@ -37,6 +39,7 @@ public class UserV2ServiceImpl implements UserV2Service { private final UserRepository userRepository; private final ApplyRepository applyRepository; private final MeetingRepository meetingRepository; + private final CoLeaderRepository coLeaderRepository; private final Time time; @@ -98,9 +101,11 @@ public UserV2GetCreatedMeetingByUserResponseDto getCreatedMeetingByUser(Integer List meetings = meetingRepository.findAllByUser(meetingCreator); List meetingIds = meetings.stream().map(Meeting::getId).toList(); Applies applies = new Applies(applyRepository.findAllByMeetingIdIn(meetingIds)); + CoLeaders coLeaders = new CoLeaders(coLeaderRepository.findAllByMeetingIdIn(meetingIds)); List meetingByUserDtos = meetings.stream() - .map(meeting -> MeetingV2GetCreatedMeetingByUserResponseDto.of(meeting, meetingCreator, + .map(meeting -> MeetingV2GetCreatedMeetingByUserResponseDto.of(meeting, + coLeaders.isCoLeader(meeting.getId(), userId), meetingCreator, applies.getApprovedCount(meeting.getId()), time.now())) .toList(); @@ -116,7 +121,7 @@ public UserV2GetAppliedMeetingByUserResponseDto getAppliedMeetingByUser(Integer List appliedMeetingByUserDtos = myApplies.stream() .map(apply -> ApplyV2GetAppliedMeetingByUserResponseDto.of(apply.getId(), apply.getStatus().getValue(), - MeetingV2GetCreatedMeetingByUserResponseDto.of(apply.getMeeting(), apply.getMeeting().getUser(), + MeetingV2GetCreatedMeetingByUserResponseDto.of(apply.getMeeting(), false, apply.getMeeting().getUser(), allApplies.getApprovedCount(apply.getMeetingId()), time.now()))) .toList(); diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql index 534b5ae2..c80c5d2f 100644 --- a/main/src/main/resources/schema.sql +++ b/main/src/main/resources/schema.sql @@ -7,6 +7,7 @@ drop table if exists "notice" cascade; drop table if exists "post" cascade; drop table if exists "report" cascade; drop table if exists "user" cascade; +drop table if exists "joint_leader" cascade; DROP TYPE IF EXISTS meeting_joinableparts_enum; @@ -60,6 +61,20 @@ create table if not exists meeting "modifiedTimestamp" timestamp default CURRENT_TIMESTAMP ); +create table if not exists co_leader +( + id serial + primary key, + "meetingId" integer not null + constraint fk_meeting + references meeting + on delete cascade, + "userId" integer not null + constraint fk_user + references "user" + on delete cascade +); + create table if not exists apply ( id serial diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index 41b600ed..0e55f470 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -19,6 +19,8 @@ import org.sopt.makers.crew.main.entity.apply.ApplyRepository; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; import org.sopt.makers.crew.main.entity.apply.enums.EnApplyType; +import org.sopt.makers.crew.main.entity.meeting.CoLeader; +import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; import org.sopt.makers.crew.main.entity.meeting.enums.EnMeetingStatus; import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; import org.sopt.makers.crew.main.global.annotation.IntegratedTest; @@ -31,6 +33,7 @@ import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto; import org.sopt.makers.crew.main.global.dto.MeetingResponseDto; +import org.sopt.makers.crew.main.global.exception.NotFoundException; import org.sopt.makers.crew.main.global.exception.BadRequestException; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; @@ -58,6 +61,9 @@ public class MeetingV2ServiceTest { @Autowired private MeetingRepository meetingRepository; + @Autowired + private CoLeaderRepository coLeaderRepository; + @Autowired private UserRepository userRepository; @@ -110,7 +116,8 @@ void normal_createMeeting_meetingId(boolean canJoinOnlyActiveGeneration) { "준비물은 노트북과 열정입니다.", // note (유의할 사항) false, // isMentorNeeded (멘토 필요 여부) canJoinOnlyActiveGeneration, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) - joinableParts // joinableParts (대상 파트 목록) + joinableParts, // joinableParts (대상 파트 목록) + null ); // when @@ -157,6 +164,140 @@ void normal_createMeeting_meetingId(boolean canJoinOnlyActiveGeneration) { ); } + @Test + @DisplayName("공동 모임장을 포함하여 모임 생성 성공 시, 생성된 모임 번호가 반환되며 공동 모임장으로 저장된다.") + void coLeader_createMeeting_meetingId() { + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User savedUser = userRepository.save(user); + + User coLeader1 = User.builder() + .name("공동모임장1") + .orgId(2) + .activities(List.of(new UserActivityVO("서버", 34), new UserActivityVO("iOS", 34))) + .profileImage("image-url11") + .phone("010-1234-5678") + .build(); + User savedJointLeader1 = userRepository.save(coLeader1); + + User coLeader2 = User.builder() + .name("공동모임장2") + .orgId(3) + .activities(List.of(new UserActivityVO("서버", 34), new UserActivityVO("iOS", 34))) + .profileImage("image-url12") + .phone("010-1234-5678") + .build(); + User savedJointLeader2 = userRepository.save(coLeader2); + + // 모임 이미지 리스트 + List files = Arrays.asList( + "https://example.com/image1.jpg" + ); + + // 대상 파트 목록 + MeetingJoinablePart[] joinableParts = { + MeetingJoinablePart.SERVER, + MeetingJoinablePart.IOS + }; + + // DTO 생성 + MeetingV2CreateMeetingBodyDto meetingDto = new MeetingV2CreateMeetingBodyDto( + "알고보면 쓸데있는 개발 프로세스", // title + files, // files (모임 이미지 리스트) + "스터디", // category + "2024.10.01", // startDate (모집 시작 날짜) + "2024.10.15", // endDate (모집 끝 날짜) + 10, // capacity (모집 인원) + "백엔드 개발에 관심 있는 사람들을 위한 스터디입니다.", // desc (모집 정보) + "매주 온라인으로 진행되며, 발표와 토론이 포함됩니다.", // processDesc (진행 방식 소개) + "2024.10.16", // mStartDate (모임 활동 시작 날짜) + "2024.12.30", // mEndDate (모임 활동 종료 날짜) + "5년차 백엔드 개발자입니다.", // leaderDesc (개설자 소개) + "준비물은 노트북과 열정입니다.", // note (유의할 사항) + false, // isMentorNeeded (멘토 필요 여부) + true, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) + joinableParts, // joinableParts (대상 파트 목록) + List.of(savedJointLeader1.getId(), savedJointLeader2.getId()) + ); + + // when + MeetingV2CreateMeetingResponseDto responseDto = meetingV2Service.createMeeting(meetingDto, + savedUser.getId()); + + // then + Meeting foundMeeting = meetingRepository.findByIdOrThrow(responseDto.getMeetingId()); + List coLeaders = coLeaderRepository.findAllByMeetingId(foundMeeting.getId()); + + Assertions.assertThat(foundMeeting.getId()).isEqualTo(responseDto.getMeetingId()); + + Assertions.assertThat(foundMeeting).isNotNull(); + Assertions.assertThat(coLeaders) + .hasSize(2) + .extracting("user.orgId", "user.name", "meeting.id") + .containsExactly( + tuple(2, "공동모임장1", foundMeeting.getId()), + tuple(3, "공동모임장2", foundMeeting.getId()) + ); + } + + @Test + @DisplayName("존재하지 않는 공동 모임장 id를 요청할 경우, 예외가 발생한다.") + void notPresentCoLeaderId_createMeeting_throwException() { + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User savedUser = userRepository.save(user); + + // 모임 이미지 리스트 + List files = Arrays.asList( + "https://example.com/image1.jpg" + ); + + // 대상 파트 목록 + MeetingJoinablePart[] joinableParts = { + MeetingJoinablePart.SERVER, + MeetingJoinablePart.IOS + }; + + // DTO 생성 + MeetingV2CreateMeetingBodyDto meetingDto = new MeetingV2CreateMeetingBodyDto( + "알고보면 쓸데있는 개발 프로세스", // title + files, // files (모임 이미지 리스트) + "스터디", // category + "2024.10.01", // startDate (모집 시작 날짜) + "2024.10.15", // endDate (모집 끝 날짜) + 10, // capacity (모집 인원) + "백엔드 개발에 관심 있는 사람들을 위한 스터디입니다.", // desc (모집 정보) + "매주 온라인으로 진행되며, 발표와 토론이 포함됩니다.", // processDesc (진행 방식 소개) + "2024.10.16", // mStartDate (모임 활동 시작 날짜) + "2024.12.30", // mEndDate (모임 활동 종료 날짜) + "5년차 백엔드 개발자입니다.", // leaderDesc (개설자 소개) + "준비물은 노트북과 열정입니다.", // note (유의할 사항) + false, // isMentorNeeded (멘토 필요 여부) + true, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) + joinableParts, // joinableParts (대상 파트 목록) + List.of(0, Integer.MAX_VALUE) + ); + + // when, then + Assertions.assertThatThrownBy(() -> meetingV2Service.createMeeting((meetingDto), + savedUser.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessageContaining(NOT_FOUND_USER.getErrorCode()); + + } + @Test @DisplayName("모임 개설자의 활동기수 정보가 없을 경우, 예외가 발생한다.") void userHasNotActivities_createMeeting_exception() { @@ -197,7 +338,8 @@ void userHasNotActivities_createMeeting_exception() { "유의할 사항", // note (유의할 사항) false, // isMentorNeeded (멘토 필요 여부) false, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) - joinableParts // joinableParts (대상 파트 목록) + joinableParts, // joinableParts (대상 파트 목록) + null ); // when, then @@ -242,7 +384,8 @@ void isJoinalbePartsEmpty_createMeeting_exception() { "유의할 사항", // note (유의할 사항) false, // isMentorNeeded (멘토 필요 여부) false, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) - joinableParts // joinableParts (대상 파트 목록) + joinableParts, // joinableParts (대상 파트 목록) + null ); // when, then @@ -288,7 +431,8 @@ void isImageFileEmpty_createMeeting_exception() { "유의할 사항", // note (유의할 사항) false, // isMentorNeeded (멘토 필요 여부) false, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) - joinableParts // joinableParts (대상 파트 목록) + joinableParts, // joinableParts (대상 파트 목록) + null ); // when, then @@ -559,7 +703,7 @@ void pageIs1_getMeetings_11meetings() { // given User user = userRepository.findByIdOrThrow(5); - for(int i=0; i<30; i++){ + for (int i = 0; i < 30; i++) { Meeting meeting = createMeetingFixture(i, user); meetingRepository.save(meeting); } @@ -582,7 +726,7 @@ void pageGreaterThen2_getMeetings_12meetings() { // given User user = userRepository.findByIdOrThrow(5); - for(int i=0; i<30; i++){ + for (int i = 0; i < 30; i++) { Meeting meeting = createMeetingFixture(i, user); meetingRepository.save(meeting); } @@ -1403,7 +1547,7 @@ private Meeting createMeetingFixture(Integer index, User user) { new ImageUrlVO(2, "https://example.com/image2.png") ); - return Meeting.builder() + return Meeting.builder() .userId(user.getId()) .user(user) .title("Weekly Coding Meetup" + index) @@ -1421,7 +1565,7 @@ private Meeting createMeetingFixture(Integer index, User user) { .isMentorNeeded(true) .canJoinOnlyActiveGeneration(false) .createdGeneration(34) - .joinableParts(new MeetingJoinablePart[]{WEB, SERVER}) + .joinableParts(new MeetingJoinablePart[] {WEB, SERVER}) .build(); } } From 3d5c260d619506dd706b3b3fa3c63bd7509ba9ab Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:35:35 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EA=B3=B5=EB=8F=99=20=EB=AA=A8?= =?UTF-8?q?=EC=9E=84=EC=9E=A5=20=EC=B6=94=EA=B0=80=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#463)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add: BaseTimeEntity 추가 * feat: 공동모임장인 모임도 조회할 수 있도록 구현 * fix: co_leader로 수정 * test: test data 추가 * refactor: 내가 신청한 모임 정렬 조건 추가 * test: Mock 테스트 삭제 - 이제 해당 케이스들이 통합테스트로 커버됨. * test: 내가 만든 모임 조회, 내가 신청한 모임 조회 테스트 코드 작성 * refactor: 1. 모임장, 공동모임장의 경우 검증 로직 추가 2. LocalDateTime.now 부분 수정 * feat: 모임장, 공동모임장의 경우 검증 로직 구현 * test: 1. 모임장, 공동 모임장인 경우 모임 신청 테스트 코드 작성 2. 시간 수정으로 인한 일부 테스트 코드 수정 * test: 시간 수정으로 인한 일부 테스트 코드 수정 * fix: clearAutomatically = true 추가 * fix: User -> userId 변경 * test: '모임 수정', '모임 삭제', '모임 지원자 상태 변경'에 공동모임장 관련 테스트 코드 작성 * test: 테스트 시퀀스 및 데이터 일부 수정 --- .../main/entity/apply/ApplyRepository.java | 78 ++-- .../crew/main/entity/meeting/CoLeader.java | 3 +- .../entity/meeting/CoLeaderRepository.java | 3 + .../crew/main/entity/meeting/CoLeaders.java | 12 +- .../crew/main/entity/meeting/Meeting.java | 6 + .../entity/meeting/MeetingRepository.java | 12 +- .../main/global/exception/ErrorStatus.java | 2 + .../v2/service/MeetingV2ServiceImpl.java | 10 +- ...gV2GetCreatedMeetingByUserResponseDto.java | 4 +- .../user/v2/service/UserV2ServiceImpl.java | 30 +- main/src/main/resources/schema.sql | 6 +- .../v2/service/MeetingV2MockServiceTest.java | 222 ----------- .../v2/service/MeetingV2ServiceTest.java | 225 ++++++++++- .../crew/main/user/v2/UserServiceTest.java | 348 +++++++++++++----- .../test/resources/sql/delete-all-data.sql | 20 +- .../sql/meeting-service-sequence-restart.sql | 1 + .../sql/meeting-service-test-data.sql | 12 +- .../resources/sql/user-service-test-data.sql | 58 +++ 18 files changed, 650 insertions(+), 402 deletions(-) delete mode 100644 main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2MockServiceTest.java create mode 100644 main/src/test/resources/sql/meeting-service-sequence-restart.sql create mode 100644 main/src/test/resources/sql/user-service-test-data.sql diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java index a480a5bd..06350b91 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/apply/ApplyRepository.java @@ -14,41 +14,47 @@ public interface ApplyRepository extends JpaRepository, ApplySearchRepository { - @Query("select a from Apply a join fetch a.meeting m where a.userId = :userId and a.status = :statusValue") - List findAllByUserIdAndStatus(@Param("userId") Integer userId, - @Param("statusValue") EnApplyStatus statusValue); - - @Query("select a from Apply a join fetch a.meeting m join fetch m.user u where a.userId = :userId") - List findAllByUserId(@Param("userId") Integer userId); - - @Query("select a " - + "from Apply a " - + "join fetch a.user u " - + "where a.meetingId = :meetingId " - + "and a.status in :statuses order by :order") - List findAllByMeetingIdWithUser(@Param("meetingId") Integer meetingId, @Param("statuses") List statuses, @Param("order") String order); - - List findAllByMeetingIdAndStatus(Integer meetingId, EnApplyStatus statusValue); - - List findAllByMeetingId(Integer meetingId); - - List findAllByMeetingIdIn(List meetingIds); - - boolean existsByMeetingIdAndUserId(Integer meetingId, Integer userId); - - @Transactional - @Modifying - @Query("delete from Apply a where a.meeting.id = :meetingId and a.userId = :userId") - void deleteByMeetingIdAndUserId(@Param("meetingId") Integer meetingId, @Param("userId") Integer userId); - - default Apply findByIdOrThrow(Integer applyId) { - return findById(applyId) - .orElseThrow(() -> new BadRequestException(NOT_FOUND_APPLY.getErrorCode())); - } - - @Modifying(clearAutomatically = true) - @Transactional - @Query("DELETE FROM Apply a WHERE a.meetingId = :meetingId") - void deleteAllByMeetingIdQuery(Integer meetingId); + @Query("select a from Apply a join fetch a.meeting m where a.userId = :userId and a.status = :statusValue") + List findAllByUserIdAndStatus(@Param("userId") Integer userId, + @Param("statusValue") EnApplyStatus statusValue); + + @Query("select a " + + "from Apply a " + + "join fetch a.meeting m " + + "join fetch m.user u " + + "where a.userId = :userId " + + "ORDER BY a.id DESC ") + List findAllByUserIdOrderByIdDesc(@Param("userId") Integer userId); + + @Query("select a " + + "from Apply a " + + "join fetch a.user u " + + "where a.meetingId = :meetingId " + + "and a.status in :statuses order by :order") + List findAllByMeetingIdWithUser(@Param("meetingId") Integer meetingId, + @Param("statuses") List statuses, @Param("order") String order); + + List findAllByMeetingIdAndStatus(Integer meetingId, EnApplyStatus statusValue); + + List findAllByMeetingId(Integer meetingId); + + List findAllByMeetingIdIn(List meetingIds); + + boolean existsByMeetingIdAndUserId(Integer meetingId, Integer userId); + + @Transactional + @Modifying(clearAutomatically = true) + @Query("delete from Apply a where a.meeting.id = :meetingId and a.userId = :userId") + void deleteByMeetingIdAndUserId(@Param("meetingId") Integer meetingId, @Param("userId") Integer userId); + + default Apply findByIdOrThrow(Integer applyId) { + return findById(applyId) + .orElseThrow(() -> new BadRequestException(NOT_FOUND_APPLY.getErrorCode())); + } + + @Modifying(clearAutomatically = true) + @Transactional + @Query("DELETE FROM Apply a WHERE a.meetingId = :meetingId") + void deleteAllByMeetingIdQuery(Integer meetingId); } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java index 729b7e68..d8836a65 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java @@ -1,5 +1,6 @@ package org.sopt.makers.crew.main.entity.meeting; +import org.sopt.makers.crew.main.entity.common.BaseTimeEntity; import org.sopt.makers.crew.main.entity.user.User; import jakarta.persistence.Entity; @@ -20,7 +21,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "co_leader") -public class CoLeader { +public class CoLeader extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java index 717365f8..734591b0 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaderRepository.java @@ -2,6 +2,7 @@ import java.util.List; +import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.jpa.repository.JpaRepository; public interface CoLeaderRepository extends JpaRepository { @@ -12,4 +13,6 @@ public interface CoLeaderRepository extends JpaRepository { List findAllByMeetingIdIn(List meetingId); + List findAllByUserId(Integer userId); + } \ No newline at end of file diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java index d88afb15..ee77312a 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeaders.java @@ -1,17 +1,21 @@ package org.sopt.makers.crew.main.entity.meeting; +import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; + import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.sopt.makers.crew.main.global.exception.BadRequestException; + public class CoLeaders { /** * Key : MeetingId * Value : 해당 모임의 공동 모임장 목록 * * @implNote : List 내에 있는 CoLeader 객체는 fetch join 으로 다른 객체를 불러오지 않은 상태 - * @implNote : 해당 자료형을 사용할 때는 'hasCoLeader' 메서드 사용 적극 권장 + * @implNote : 해당 자료형을 사용할 때는 'isCoLeaderPresent' 메서드 사용 적극 권장 * * */ private final Map> coLeadersMap; @@ -21,6 +25,12 @@ public CoLeaders(List coLeaders) { .collect(Collectors.groupingBy(coLeader -> coLeader.getMeeting().getId())); } + public void validateCoLeader(Integer meetingId, Integer requestUserId) { + if (isCoLeader(meetingId, requestUserId)) { + throw new BadRequestException(CO_LEADER_CANNOT_APPLY.getErrorCode()); + } + } + public boolean isCoLeader(Integer meetingId, Integer requestUserId) { if (!isCoLeaderPresent(meetingId)) { return false; diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java index 8d815dda..7ecb0b5e 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/Meeting.java @@ -218,6 +218,12 @@ public void validateMeetingCreator(Integer requestUserId) { } } + public void validateIsNotMeetingLeader(Integer requestUserId) { + if (checkMeetingLeader(requestUserId)) { + throw new BadRequestException(LEADER_CANNOT_APPLY.getErrorCode()); + } + } + public Boolean checkMeetingLeader(Integer userId) { return this.userId.equals(userId); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingRepository.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingRepository.java index 6e1fe392..25173c0f 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingRepository.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/MeetingRepository.java @@ -5,7 +5,6 @@ import java.util.List; import org.sopt.makers.crew.main.global.exception.BadRequestException; -import org.sopt.makers.crew.main.entity.user.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -13,7 +12,16 @@ public interface MeetingRepository extends JpaRepository, Meet List findAllByUserId(Integer userId); - List findAllByUser(User user); + /** + * @implSpec : 특정 유저가 모임장이거나 공동모임장인 모임을 최근에 만들어진 순으로 조회한다. + * **/ + @Query("SELECT m " + + "FROM Meeting m " + + "JOIN fetch m.user " + + "WHERE m.user.id =:userId " + + "OR m.id IN (:coLeaderMeetingIds)" + + "ORDER BY m.id DESC ") + List findAllByUserIdOrIdInWithUser(Integer userId, List coLeaderMeetingIds); default Meeting findByIdOrThrow(Integer meetingId) { return findById(meetingId) diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java index 0bd9ddd6..645bee08 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java @@ -34,6 +34,8 @@ public enum ErrorStatus { NOT_FOUND_APPLY("신청상태가 아닌 모임입니다."), ALREADY_PROCESSED_APPLY("이미 해당 상태로 처리된 신청 정보입니다."), MAX_IMAGE_UPLOAD_EXCEEDED("이미지는 최대 10개까지만 업로드 가능합니다."), + LEADER_CANNOT_APPLY("모임장은 신청할 수 없습니다."), + CO_LEADER_CANNOT_APPLY("공동 모임장은 신청할 수 없습니다."), /** * 401 UNAUTHORIZED diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index ae3451c4..1ea3af6f 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -218,6 +218,7 @@ public MeetingV2CreateMeetingResponseDto createMeeting(MeetingV2CreateMeetingBod public MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto requestBody, Integer userId) { Meeting meeting = meetingRepository.findByIdOrThrow(requestBody.getMeetingId()); User user = userRepository.findByIdOrThrow(userId); + CoLeaders coLeaders = new CoLeaders(coLeaderRepository.findAllByMeetingId(meeting.getId())); List applies = applyRepository.findAllByMeetingId(meeting.getId()); @@ -226,9 +227,10 @@ public MeetingV2ApplyMeetingResponseDto applyMeeting(MeetingV2ApplyMeetingDto re validateApplyPeriod(meeting); validateUserActivities(user); validateUserJoinableParts(user, meeting); + coLeaders.validateCoLeader(meeting.getId(), user.getId()); + meeting.validateIsNotMeetingLeader(userId); - Apply apply = applyMapper.toApplyEntity(requestBody, EnApplyType.APPLY, meeting, user, - userId); + Apply apply = applyMapper.toApplyEntity(requestBody, EnApplyType.APPLY, meeting, user, userId); Apply savedApply = applyRepository.save(apply); return MeetingV2ApplyMeetingResponseDto.of(savedApply.getId()); } @@ -449,7 +451,7 @@ private String createCsvFile(List applies) { } private Boolean checkActivityStatus(Meeting meeting) { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = time.now(); LocalDateTime mStartDate = meeting.getMStartDate(); LocalDateTime mEndDate = meeting.getMEndDate(); return now.isEqual(mStartDate) || (now.isAfter(mStartDate) && now.isBefore(mEndDate)); @@ -494,7 +496,7 @@ private void validateUserAlreadyApplied(Integer userId, List applies) { } private void validateApplyPeriod(Meeting meeting) { - LocalDateTime now = LocalDateTime.now(); + LocalDateTime now = time.now(); if (now.isAfter(meeting.getEndDate()) || now.isBefore(meeting.getStartDate())) { throw new BadRequestException(NOT_IN_APPLY_PERIOD.getErrorCode()); } diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java index 5832c10c..c4c39067 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java @@ -72,9 +72,9 @@ public record MeetingV2GetCreatedMeetingByUserResponseDto( @NotNull int appliedCount ) { - public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, boolean isCoLeader, User meetingCreator, int appliedCount, + public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, boolean isCoLeader, int appliedCount, LocalDateTime now) { - MeetingCreatorDto creatorDto = MeetingCreatorDto.of(meetingCreator); + MeetingCreatorDto creatorDto = MeetingCreatorDto.of(meeting.getUser()); boolean canJoinOnlyActiveGeneration = meeting.getTargetActiveGeneration() == CrewConst.ACTIVE_GENERATION && meeting.getCanJoinOnlyActiveGeneration(); diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java index 3a54c78c..879880ae 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/service/UserV2ServiceImpl.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.sopt.makers.crew.main.entity.meeting.CoLeader; import org.sopt.makers.crew.main.entity.meeting.CoLeaderRepository; import org.sopt.makers.crew.main.entity.meeting.CoLeaders; import org.sopt.makers.crew.main.global.exception.BaseException; @@ -94,34 +95,43 @@ public UserV2GetUserOwnProfileResponseDto getUserOwnProfile(Integer userId) { return UserV2GetUserOwnProfileResponseDto.of(user); } + /** + * @implSpec : 유저가 모임장이거나 공동모임장인 모임을 모두 조회한다. + * @implNote : my 의미 == 내가 모임장이거나 공동모임장인 경우 + * + * **/ @Override public UserV2GetCreatedMeetingByUserResponseDto getCreatedMeetingByUser(Integer userId) { - User meetingCreator = userRepository.findByIdOrThrow(userId); + List coLeaderMeetingIds = getCoLeaderMeetingIds(coLeaderRepository.findAllByUserId(userId)); - List meetings = meetingRepository.findAllByUser(meetingCreator); - List meetingIds = meetings.stream().map(Meeting::getId).toList(); - Applies applies = new Applies(applyRepository.findAllByMeetingIdIn(meetingIds)); - CoLeaders coLeaders = new CoLeaders(coLeaderRepository.findAllByMeetingIdIn(meetingIds)); + List myMeetings = meetingRepository.findAllByUserIdOrIdInWithUser(userId, coLeaderMeetingIds); + List myMeetingIds = myMeetings.stream().map(Meeting::getId).toList(); + Applies applies = new Applies(applyRepository.findAllByMeetingIdIn(myMeetingIds)); + CoLeaders coLeaders = new CoLeaders(coLeaderRepository.findAllByMeetingIdIn(myMeetingIds)); - List meetingByUserDtos = meetings.stream() + List meetingByUserDtos = myMeetings.stream() .map(meeting -> MeetingV2GetCreatedMeetingByUserResponseDto.of(meeting, - coLeaders.isCoLeader(meeting.getId(), userId), meetingCreator, - applies.getApprovedCount(meeting.getId()), time.now())) + coLeaders.isCoLeader(meeting.getId(), userId), applies.getApprovedCount(meeting.getId()), time.now())) .toList(); return UserV2GetCreatedMeetingByUserResponseDto.of(meetingByUserDtos); } + private List getCoLeaderMeetingIds(List coLeaders) { + return coLeaders.stream() + .map(coLeader -> coLeader.getMeeting().getId()).toList(); + } + @Override public UserV2GetAppliedMeetingByUserResponseDto getAppliedMeetingByUser(Integer userId) { - List myApplies = applyRepository.findAllByUserId(userId); + List myApplies = applyRepository.findAllByUserIdOrderByIdDesc(userId); List meetingIds = myApplies.stream().map(Apply::getMeetingId).toList(); Applies allApplies = new Applies(applyRepository.findAllByMeetingIdIn(meetingIds)); List appliedMeetingByUserDtos = myApplies.stream() .map(apply -> ApplyV2GetAppliedMeetingByUserResponseDto.of(apply.getId(), apply.getStatus().getValue(), - MeetingV2GetCreatedMeetingByUserResponseDto.of(apply.getMeeting(), false, apply.getMeeting().getUser(), + MeetingV2GetCreatedMeetingByUserResponseDto.of(apply.getMeeting(), false, allApplies.getApprovedCount(apply.getMeetingId()), time.now()))) .toList(); diff --git a/main/src/main/resources/schema.sql b/main/src/main/resources/schema.sql index c80c5d2f..1d9f3d15 100644 --- a/main/src/main/resources/schema.sql +++ b/main/src/main/resources/schema.sql @@ -7,7 +7,7 @@ drop table if exists "notice" cascade; drop table if exists "post" cascade; drop table if exists "report" cascade; drop table if exists "user" cascade; -drop table if exists "joint_leader" cascade; +drop table if exists "co_leader" cascade; DROP TYPE IF EXISTS meeting_joinableparts_enum; @@ -72,7 +72,9 @@ create table if not exists co_leader "userId" integer not null constraint fk_user references "user" - on delete cascade + on delete cascade, + "createdTimestamp" timestamp default CURRENT_TIMESTAMP not null, + "modifiedTimestamp" timestamp default CURRENT_TIMESTAMP not null ); create table if not exists apply diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2MockServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2MockServiceTest.java deleted file mode 100644 index cccd033b..00000000 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2MockServiceTest.java +++ /dev/null @@ -1,222 +0,0 @@ -package org.sopt.makers.crew.main.meeting.v2.service; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.sopt.makers.crew.main.global.exception.ErrorStatus.FULL_MEETING_CAPACITY; -import static org.sopt.makers.crew.main.global.exception.ErrorStatus.NOT_IN_APPLY_PERIOD; - -import java.time.LocalDateTime; -import java.time.Month; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.sopt.makers.crew.main.global.exception.BadRequestException; -import org.sopt.makers.crew.main.entity.apply.Apply; -import org.sopt.makers.crew.main.entity.apply.ApplyRepository; -import org.sopt.makers.crew.main.entity.apply.enums.EnApplyStatus; -import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.meeting.MeetingRepository; -import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; -import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; -import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; -import org.sopt.makers.crew.main.entity.user.User; -import org.sopt.makers.crew.main.entity.user.UserFixture; -import org.sopt.makers.crew.main.entity.user.UserRepository; -import org.sopt.makers.crew.main.entity.user.enums.UserPart; -import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; -import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; -import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingByOrgUserQueryDto; -import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; -import org.sopt.makers.crew.main.meeting.v2.dto.response.MeetingV2GetAllMeetingByOrgUserDto; - -@ExtendWith(MockitoExtension.class) -public class MeetingV2MockServiceTest { - @InjectMocks - private MeetingV2ServiceImpl meetingV2Service; - @Mock - private UserRepository userRepository; - @Mock - private ApplyRepository applyRepository; - @Mock - private MeetingRepository meetingRepository; - @Mock - private ApplyMapper applyMapper; - - - private List applies; - - private User ownerUser; - - private User applyUser; - - private Meeting meeting; - - @BeforeEach - void init() { - List activityVOS = new ArrayList<>(); - activityVOS.add(new UserActivityVO(UserPart.SERVER.getValue(), 33)); - - ownerUser = User.builder() - .name("송민규") - .orgId(1) - .activities(activityVOS) - .phone("010-9472-6796") - .build(); - ownerUser.setUserIdForTest(1); - - applyUser = User.builder() - .name("홍길동") - .orgId(2) - .activities(activityVOS) - .phone("010-1234-5678") - .build(); - applyUser.setUserIdForTest(2); - - ImageUrlVO imageUrlVO = new ImageUrlVO(1, "www.~~"); - - meeting = Meeting.builder() - .user(ownerUser) - .userId(ownerUser.getId()) - .title("사람 구해요") - .category(MeetingCategory.STUDY) - .imageURL(List.of(imageUrlVO)) - .startDate(LocalDateTime.of(2024, Month.MARCH, 17, 0, 0)) - .endDate(LocalDateTime.of(2024, Month.MARCH, 20, 23, 59)) - .capacity(10) - .desc("열정 많은 사람 구해요") - .processDesc("이렇게 할거에여") - .mStartDate(LocalDateTime.of(2024, Month.APRIL, 1, 0, 0)) - .mEndDate(LocalDateTime.of(2030, Month.APRIL, 20, 0, 0)) - .leaderDesc("저는 이런 사람이에요.") - .targetDesc("이런 사람이 왔으면 좋겠어요") - .note("유의사항은 이거에요") - .isMentorNeeded(true) - .canJoinOnlyActiveGeneration(true) - .createdGeneration(33) - .targetActiveGeneration(33) - .joinableParts(MeetingJoinablePart.values()) - .build(); - - Apply apply = Apply.builder() - .meeting(meeting) - .meetingId(1) - .user(applyUser) - .userId(2) - .content("제 지원동기는요") - .build(); - - //meeting.addApply(apply); - apply.updateApplyStatus(EnApplyStatus.APPROVE); - - applies = new ArrayList<>(); - applies.add(apply); - } - - @Test - void 내모임조회_성공() { - // given - User user = UserFixture.createStaticUser(); - user.setUserIdForTest(3); - doReturn(Optional.of(user)).when(userRepository).findByOrgId(any()); - doReturn(applies).when(applyRepository).findAllByUserIdAndStatus(any(), any()); - - MeetingV2GetAllMeetingByOrgUserQueryDto dto = new MeetingV2GetAllMeetingByOrgUserQueryDto( - applyUser.getOrgId(), 1, 12); - - // when - MeetingV2GetAllMeetingByOrgUserDto myMeetings = meetingV2Service.getAllMeetingByOrgUser(dto); - - // then - Assertions.assertThat(myMeetings.getMeetings().get(0)) - .extracting("isMeetingLeader", "title", "category", "mStartDate", "mEndDate", "isActiveMeeting") - .containsExactly(false, meeting.getTitle(), meeting.getCategory().getValue(), meeting.getMStartDate(), - meeting.getMEndDate(), true); - } - - @Test - public void 모임신청시_정원이찼을때_예외발생() { - // given - applies = new ArrayList<>(); - for (int i = 0; i < meeting.getCapacity(); i++) { - User tempUser = User.builder() - .name("user" + i) - .orgId(2 + i) - .phone("010-0000-000" + i) - .build(); - tempUser.setUserIdForTest(3 + i); // 3부터 시작하는 userId 설정 - - Apply apply = Apply.builder() - .meeting(meeting) - .meetingId(meeting.getId()) - .user(tempUser) - .userId(tempUser.getId()) - .content("꼭 하고 싶어요" + i) - .build(); - - apply.updateApplyStatus(EnApplyStatus.APPROVE); // 승인 상태로 변경 - applies.add(apply); - } - - MeetingV2ApplyMeetingDto requestBody = new MeetingV2ApplyMeetingDto(meeting.getId(), "열심히 하겠습니다."); - - doReturn(meeting).when(meetingRepository).findByIdOrThrow(requestBody.getMeetingId()); - doReturn(applyUser).when(userRepository).findByIdOrThrow(applyUser.getId()); - doReturn(applies).when(applyRepository).findAllByMeetingId(any()); - - // when & then - BadRequestException exception = assertThrows(BadRequestException.class, () -> { - meetingV2Service.applyMeeting(requestBody, applyUser.getId()); - }); - - assertEquals(FULL_MEETING_CAPACITY.getErrorCode(), exception.getMessage()); - } - - @Test - public void 모집시작시간이전_모임지원시_예외발생() { - // given - LocalDateTime oneMinuteAfterNow = LocalDateTime.now().plusMinutes(1); - - meeting = Meeting.builder() - .user(ownerUser) - .userId(ownerUser.getId()) - .title("사람 구해요") - .category(MeetingCategory.STUDY) - .startDate(oneMinuteAfterNow) // 모집 시작 시간을 현재 시간으로부터 1분 후로 설정 - .endDate(oneMinuteAfterNow.plusDays(1)) - .capacity(10) - .desc("열정 많은 사람 구해요") - .processDesc("이렇게 할거에여") - .mStartDate(LocalDateTime.of(2028, Month.APRIL, 20, 0, 0)) - .mEndDate(LocalDateTime.of(2030, Month.APRIL, 20, 0, 0)) - .leaderDesc("저는 이런 사람이에요.") - .targetDesc("이런 사람이 왔으면 좋겠어요") - .note("유의사항은 이거에요") - .isMentorNeeded(true) - .canJoinOnlyActiveGeneration(true) - .createdGeneration(33) - .targetActiveGeneration(33) - .joinableParts(MeetingJoinablePart.values()) - .build(); - - MeetingV2ApplyMeetingDto requestBody = new MeetingV2ApplyMeetingDto(meeting.getId(), "열심히 하겠습니다."); - - doReturn(meeting).when(meetingRepository).findByIdOrThrow(requestBody.getMeetingId()); - doReturn(applyUser).when(userRepository).findByIdOrThrow(applyUser.getId()); - - // when & then - BadRequestException exception = assertThrows(BadRequestException.class, () -> { - meetingV2Service.applyMeeting(requestBody, applyUser.getId()); - }); - - assertEquals(NOT_IN_APPLY_PERIOD.getErrorCode(), exception.getMessage()); - } -} diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index 0e55f470..78ffd8a6 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -33,11 +33,13 @@ import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; import org.sopt.makers.crew.main.global.dto.MeetingCreatorDto; import org.sopt.makers.crew.main.global.dto.MeetingResponseDto; +import org.sopt.makers.crew.main.global.exception.ForbiddenException; import org.sopt.makers.crew.main.global.exception.NotFoundException; import org.sopt.makers.crew.main.global.exception.BadRequestException; import org.sopt.makers.crew.main.meeting.v2.dto.ApplyMapper; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingGetAppliesQueryDto; import org.sopt.makers.crew.main.meeting.v2.dto.query.MeetingV2GetAllMeetingQueryDto; +import org.sopt.makers.crew.main.meeting.v2.dto.request.ApplyV2UpdateStatusBodyDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2ApplyMeetingDto; import org.sopt.makers.crew.main.meeting.v2.dto.request.MeetingV2CreateMeetingBodyDto; import org.sopt.makers.crew.main.meeting.v2.dto.response.ApplyInfoDto; @@ -444,8 +446,8 @@ void isImageFileEmpty_createMeeting_exception() { @Nested @SqlGroup({ @Sql(value = "/sql/meeting-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/meeting-service-sequence-restart.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) - }) class 모임_전체_조회 { @Test @@ -1310,8 +1312,8 @@ void applyMeeting_ShouldSaveSuccessfully() { .title("모임 지원 테스트") .category(MeetingCategory.STUDY) .imageURL(List.of(new ImageUrlVO(0, "testImage.jpg"))) - .startDate(LocalDateTime.of(2024, 10, 6, 0, 0, 0)) - .endDate(LocalDateTime.of(2029, 10, 7, 23, 59, 59)) + .startDate(LocalDateTime.of(2024, 4, 23, 0, 0, 0)) + .endDate(LocalDateTime.of(2029, 4, 27, 23, 59, 59)) .capacity(20) .desc("모임 지원 테스트입니다.") .processDesc("테스트 진행 방식입니다.") @@ -1368,7 +1370,7 @@ void applyMeeting_ShouldFailWhenCapacityExceeded() { .title("모임 지원 테스트") .category(MeetingCategory.STUDY) .imageURL(List.of(new ImageUrlVO(0, "testImage.jpg"))) - .startDate(LocalDateTime.of(2024, 10, 6, 0, 0, 0)) + .startDate(LocalDateTime.of(2024, 4, 23, 0, 0, 0)) .endDate(LocalDateTime.of(2029, 10, 7, 23, 59, 59)) .capacity(1) .desc("모임 지원 테스트입니다.") @@ -1440,7 +1442,7 @@ void applyMeeting_Fail_WhenAlreadyApplied() { .title("모임 지원 테스트") .category(MeetingCategory.STUDY) .imageURL(List.of(new ImageUrlVO(0, "testImage.jpg"))) - .startDate(LocalDateTime.of(2024, 10, 6, 0, 0, 0)) + .startDate(LocalDateTime.of(2024, 4, 23, 0, 0, 0)) .endDate(LocalDateTime.of(2029, 10, 7, 23, 59, 59)) .capacity(50) .desc("모임 지원 테스트입니다.") @@ -1505,7 +1507,7 @@ void applyMeeting_Fail_WhenOutsideApplicationPeriod() { .title("모임 지원 테스트") .category(MeetingCategory.STUDY) .imageURL(List.of(new ImageUrlVO(0, "testImage.jpg"))) - .startDate(LocalDateTime.of(2030, 10, 7, 0, 0, 0)) + .startDate(LocalDateTime.of(2030, 4, 7, 0, 0, 0)) .endDate(LocalDateTime.of(2030, 10, 7, 23, 59, 59)) .capacity(50) .desc("모임 지원 테스트입니다.") @@ -1537,8 +1539,219 @@ void applyMeeting_Fail_WhenOutsideApplicationPeriod() { .isInstanceOf(BadRequestException.class) .hasMessageContaining("지원 기간이 아닙니다."); } + + @Test + @DisplayName("공동 모임장은 지원할 수 없다.") + void applyMeeting_Fail_WhenIsCoLeader() { + // given + User leader = User.builder() + .name("모임장") + .orgId(1) + .activities(List.of(new UserActivityVO("iOS", 35))) + .profileImage("testProfileImage.jpg") + .phone("010-1234-5678") + .build(); + + User coLeaderUser1 = User.builder() + .name("공동 모임장1") + .orgId(2) + .activities(List.of(new UserActivityVO("서버", 35))) + .profileImage("testProfileImage.jpg") + .phone("010-2222-2222") + .build(); + + User coLeaderUser2 = User.builder() + .name("공동 모임장2") + .orgId(3) + .activities(List.of(new UserActivityVO("기획", 33))) + .profileImage("testProfileImage.jpg") + .phone("010-3333-3333") + .build(); + User savedLeader = userRepository.save(leader); + User savedCoLeader1 = userRepository.save(coLeaderUser1); + User savedCoLeader2 = userRepository.save(coLeaderUser2); + + Meeting meeting = Meeting.builder() + .user(savedLeader) + .userId(savedLeader.getId()) + .title("모임 지원 테스트") + .category(MeetingCategory.STUDY) + .imageURL(List.of(new ImageUrlVO(0, "testImage.jpg"))) + .startDate(LocalDateTime.of(2024, 4, 23, 0, 0, 0)) + .endDate(LocalDateTime.of(2029, 4, 27, 23, 59, 59)) + .capacity(20) + .desc("모임 지원 테스트입니다.") + .processDesc("테스트 진행 방식입니다.") + .mStartDate(LocalDateTime.of(2024, 5, 24, 0, 0, 0)) + .mEndDate(LocalDateTime.of(2024, 5, 30, 23, 59, 59)) + .leaderDesc("모임 리더 설명입니다.") + .note("유의사항입니다.") + .isMentorNeeded(false) + .canJoinOnlyActiveGeneration(false) + .createdGeneration(35) + .targetActiveGeneration(null) + .joinableParts(MeetingJoinablePart.values()) + .build(); + + meetingRepository.save(meeting); + + CoLeader coLeader1 = CoLeader.builder() + .meeting(meeting) + .user(savedCoLeader1) + .build(); + + CoLeader coLeader2 = CoLeader.builder() + .meeting(meeting) + .user(savedCoLeader2) + .build(); + coLeaderRepository.saveAll(List.of(coLeader1, coLeader2)); + + // when & then + MeetingV2ApplyMeetingDto applyDto = new MeetingV2ApplyMeetingDto(meeting.getId(), "지원 동기"); + Assertions.assertThatThrownBy(() -> meetingV2Service.applyMeeting(applyDto, savedCoLeader1.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage("공동 모임장은 신청할 수 없습니다."); + } + + @Test + @DisplayName("모임장은 지원할 수 없다.") + void applyMeeting_Fail_WhenIsLeader() { + // given + User leader = User.builder() + .name("모임장") + .orgId(1) + .activities(List.of(new UserActivityVO("iOS", 35))) + .profileImage("testProfileImage.jpg") + .phone("010-1234-5678") + .build(); + + userRepository.save(leader); + + Meeting meeting = Meeting.builder() + .user(leader) + .userId(leader.getId()) + .title("모임 지원 테스트") + .category(MeetingCategory.STUDY) + .imageURL(List.of(new ImageUrlVO(0, "testImage.jpg"))) + .startDate(LocalDateTime.of(2024, 4, 23, 0, 0, 0)) + .endDate(LocalDateTime.of(2029, 4, 27, 23, 59, 59)) + .capacity(20) + .desc("모임 지원 테스트입니다.") + .processDesc("테스트 진행 방식입니다.") + .mStartDate(LocalDateTime.of(2024, 5, 24, 0, 0, 0)) + .mEndDate(LocalDateTime.of(2024, 5, 30, 23, 59, 59)) + .leaderDesc("모임 리더 설명입니다.") + .note("유의사항입니다.") + .isMentorNeeded(false) + .canJoinOnlyActiveGeneration(false) + .createdGeneration(35) + .targetActiveGeneration(null) + .joinableParts(MeetingJoinablePart.values()) + .build(); + + meetingRepository.save(meeting); + + // when & then + MeetingV2ApplyMeetingDto applyDto = new MeetingV2ApplyMeetingDto(meeting.getId(), "지원 동기"); + Assertions.assertThatThrownBy(() -> meetingV2Service.applyMeeting(applyDto, leader.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessage("모임장은 신청할 수 없습니다."); + } + } + + @Nested + @SqlGroup({ + @Sql(value = "/sql/meeting-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + + }) + class 모임_지원자_상태_변경 { + @Test + @DisplayName("공동 모임장은 모임 지원자 상태 변경을 할 수 없다.") + void updateApplyStatus_Fail_isCoLeader() { + // given + Integer coLeaderId = 5; + ApplyV2UpdateStatusBodyDto dto = new ApplyV2UpdateStatusBodyDto(1, 1); + + // when, then + Assertions.assertThatThrownBy( + () -> meetingV2Service.updateApplyStatus(1, dto, coLeaderId)) + .isInstanceOf(ForbiddenException.class) + .hasMessage(FORBIDDEN_EXCEPTION.getErrorCode()); + } + } + + @Nested + @SqlGroup({ + @Sql(value = "/sql/meeting-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + + }) + class 모임_수정 { + @Test + @DisplayName("공동 모임장은 모임을 수정할 수 없다.") + void modifyMeeting_Fail_isCoLeader() { + // given + Integer coLeaderId = 5; + + // 모임 이미지 리스트 + List files = Arrays.asList( + "https://example.com/image1.jpg" + ); + + // 대상 파트 목록 + MeetingJoinablePart[] joinableParts = { + MeetingJoinablePart.SERVER, + MeetingJoinablePart.IOS + }; + + // DTO 생성 + MeetingV2CreateMeetingBodyDto dto = new MeetingV2CreateMeetingBodyDto( + "알고보면 쓸데있는 개발 프로세스", // title + files, // files (모임 이미지 리스트) + "스터디", // category + "2024.10.01", // startDate (모집 시작 날짜) + "2024.10.15", // endDate (모집 끝 날짜) + 10, // capacity (모집 인원) + "백엔드 개발에 관심 있는 사람들을 위한 스터디입니다.", // desc (모집 정보) + "매주 온라인으로 진행되며, 발표와 토론이 포함됩니다.", // processDesc (진행 방식 소개) + "2024.10.16", // mStartDate (모임 활동 시작 날짜) + "2024.12.30", // mEndDate (모임 활동 종료 날짜) + "5년차 백엔드 개발자입니다.", // leaderDesc (개설자 소개) + "준비물은 노트북과 열정입니다.", // note (유의할 사항) + false, // isMentorNeeded (멘토 필요 여부) + true, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) + joinableParts, // joinableParts (대상 파트 목록) + null + ); + // when, then + Assertions.assertThatThrownBy( + () -> meetingV2Service.updateMeeting(1, dto, coLeaderId)) + .isInstanceOf(ForbiddenException.class) + .hasMessage(FORBIDDEN_EXCEPTION.getErrorCode()); + } } + @Nested + @SqlGroup({ + @Sql(value = "/sql/meeting-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + + }) + class 모임_삭제 { + @Test + @DisplayName("공동 모임장은 모임을 삭제할 수 없다.") + void deleteMeeting_Fail_isCoLeader() { + // given + Integer coLeaderId = 5; + + // when, then + Assertions.assertThatThrownBy( + () -> meetingV2Service.deleteMeeting(1, coLeaderId)) + .isInstanceOf(ForbiddenException.class) + .hasMessage(FORBIDDEN_EXCEPTION.getErrorCode()); + } + } private Meeting createMeetingFixture(Integer index, User user) { diff --git a/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java index f621c756..c9a9596e 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/user/v2/UserServiceTest.java @@ -1,118 +1,264 @@ package org.sopt.makers.crew.main.user.v2; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.*; +import static org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart.*; +import java.time.LocalDateTime; import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.global.annotation.IntegratedTest; import org.sopt.makers.crew.main.entity.user.User; import org.sopt.makers.crew.main.entity.user.UserRepository; import org.sopt.makers.crew.main.entity.user.vo.UserActivityVO; -import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllMentionUserDto; +import org.sopt.makers.crew.main.user.v2.dto.response.ApplyV2GetAppliedMeetingByUserResponseDto; +import org.sopt.makers.crew.main.user.v2.dto.response.MeetingV2GetCreatedMeetingByUserResponseDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAllUserDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetAppliedMeetingByUserResponseDto; +import org.sopt.makers.crew.main.user.v2.dto.response.UserV2GetCreatedMeetingByUserResponseDto; import org.sopt.makers.crew.main.user.v2.service.UserV2Service; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; @IntegratedTest public class UserServiceTest { - @Autowired - private UserV2Service userV2Service; - - @Autowired - private UserRepository userRepository; - - - @Test - void 멘션_사용자_조회(){ - // given - User user1 = User.builder() - .name("홍길동") - .orgId(1) - .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) - .profileImage("image-url1") - .phone("010-1234-5678") - .build(); - User user2 = User.builder() - .name("김철수") - .orgId(2) - .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) - .profileImage("image-url2") - .phone("010-1111-2222") - .build(); - userRepository.saveAll(List.of(user1, user2)); - - // when - List allMentionUsers = userV2Service.getAllMentionUser(); - - // then - assertThat(allMentionUsers).hasSize(2); - assertThat(allMentionUsers.get(0)) - .extracting( "userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly( "홍길동", "iOS", 34, "image-url1"); - assertThat(allMentionUsers.get(1)) - .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly("김철수", "안드로이드", 33, "image-url2"); - - } - - @Test - void 멘션_사용자_조회시_db에_null_저장된_경우(){ - // given - User user1 = User.builder() - .name("홍길동") - .orgId(1) - .activities(null) - .profileImage("image-url1") - .phone("010-1234-5678") - .build(); - User user2 = User.builder() - .name("김철수") - .orgId(2) - .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) - .profileImage("image-url2") - .phone("010-1111-2222") - .build(); - userRepository.saveAll(List.of(user1, user2)); - - // when - List allMentionUsers = userV2Service.getAllMentionUser(); - - // then - assertThat(allMentionUsers).hasSize(1); - assertThat(allMentionUsers.get(0)) - .extracting( "userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly("김철수", "안드로이드", 33, "image-url2"); - } - - @Test - void 멘션_사용자_조회시_db에_올바르지_않은_데이터_저장된_경우(){ - // given - User user1 = User.builder() - .name("홍길동") - .orgId(1) - .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) - .profileImage("image-url1") - .phone("010-1234-5678") - .build(); - User user2 = User.builder() - .name("김철수") - .orgId(2) - .activities(List.of(new UserActivityVO(null, 30), new UserActivityVO("", 34))) - .profileImage("image-url2") - .phone("010-1111-2222") - .build(); - userRepository.saveAll(List.of(user1, user2)); - - // when - List allMentionUsers = userV2Service.getAllMentionUser(); - - // then - assertThat(allMentionUsers).hasSize(2); - assertThat(allMentionUsers.get(0)) - .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly("홍길동", "iOS", 34, "image-url1"); - assertThat(allMentionUsers.get(1)) - .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") - .containsExactly("김철수", "", 34, "image-url2"); - } + @Autowired + private UserV2Service userV2Service; + + @Autowired + private UserRepository userRepository; + + @Nested + class 전체_사용자_조회 { + @Test + void 멘션_사용자_조회() { + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllUser(); + + // then + assertThat(allMentionUsers).hasSize(2); + assertThat(allMentionUsers.get(0)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("홍길동", "iOS", 34, "image-url1"); + assertThat(allMentionUsers.get(1)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "안드로이드", 33, "image-url2"); + + } + + @Test + void 멘션_사용자_조회시_db에_null_저장된_경우() { + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(null) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO("iOS", 30), new UserActivityVO("안드로이드", 33))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllUser(); + + // then + assertThat(allMentionUsers).hasSize(1); + assertThat(allMentionUsers.get(0)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "안드로이드", 33, "image-url2"); + } + + @Test + void 멘션_사용자_조회시_db에_올바르지_않은_데이터_저장된_경우() { + // given + User user1 = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User user2 = User.builder() + .name("김철수") + .orgId(2) + .activities(List.of(new UserActivityVO(null, 30), new UserActivityVO("", 34))) + .profileImage("image-url2") + .phone("010-1111-2222") + .build(); + userRepository.saveAll(List.of(user1, user2)); + + // when + List allMentionUsers = userV2Service.getAllUser(); + + // then + assertThat(allMentionUsers).hasSize(2); + assertThat(allMentionUsers.get(0)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("홍길동", "iOS", 34, "image-url1"); + assertThat(allMentionUsers.get(1)) + .extracting("userName", "recentPart", "recentGeneration", "profileImageUrl") + .containsExactly("김철수", "", 34, "image-url2"); + } + } + + @Nested + @SqlGroup({ + @Sql(value = "/sql/user-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + + }) + class 내가_신청한_모임_조회 { + + @Test + @DisplayName("내가 신청한 모임 목록을 가장 최근에 신청한 순으로 조회할 수 있다.") + void getAppliedMeetingByUser_userApplyMeetings() { + // given + Integer userId = 2; + + // when + UserV2GetAppliedMeetingByUserResponseDto responseDto = userV2Service.getAppliedMeetingByUser( + userId); + + // then + Assertions.assertThat(responseDto.apply()).hasSize(4); + + List applyResponseDto = responseDto.apply(); + + Assertions.assertThat(applyResponseDto) + .extracting("status", "meeting.title", "meeting.status") + .containsExactly( + tuple(1, "세미나 구합니다 - 신청후", 2), + tuple(0, "스터디 구합니다 - 신청후", 2), + tuple(0, "스터디 구합니다 - 신청전", 0), + tuple(0, "스터디 구합니다1", 1) + ); + } + + @Test + @DisplayName("내가 신청한 모임 목록에서 모임장이나 공동모임장이 될 수 없다.") + void getAppliedMeetingByUser_notLeaderOrCoLeader() { + // given + Integer userId = 2; + + // when + UserV2GetAppliedMeetingByUserResponseDto responseDto = userV2Service.getAppliedMeetingByUser( + userId); + + // then + Assertions.assertThat(responseDto.apply()).hasSize(4); + + List applyResponseDto = responseDto.apply(); + + Assertions.assertThat(applyResponseDto) + .extracting("status", "meeting.isCoLeader", "meeting.title", "meeting.status") + .containsExactly( + tuple(1, false, "세미나 구합니다 - 신청후", 2), + tuple(0, false, "스터디 구합니다 - 신청후", 2), + tuple(0, false, "스터디 구합니다 - 신청전", 0), + tuple(0, false, "스터디 구합니다1", 1) + ); + } + } + + @Nested + @SqlGroup({ + @Sql(value = "/sql/user-service-test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + + }) + class 내가_만든_모임_조회 { + + @Test + @DisplayName("내가 만든 모임 목록을 조회할 수 있다.") + void getCreatedMeetingByUser_meetingMadeByUser() { + // given + Integer userId = 4; + + // when + UserV2GetCreatedMeetingByUserResponseDto responseDto = userV2Service.getCreatedMeetingByUser(userId); + + // then + Assertions.assertThat(responseDto.meetings()).hasSize(3); + + List meetings = responseDto.meetings(); + + Assertions.assertThat(meetings) + .extracting("title", "targetActiveGeneration", "joinableParts", "category", + "canJoinOnlyActiveGeneration", "status", "isCoLeader", "isMentorNeeded", + "mStartDate", "mEndDate", "capacity", + "appliedCount", "user.orgId") + .containsExactly( + tuple("세미나 구합니다 - 신청후", null, new MeetingJoinablePart[]{WEB, IOS}, "세미나", + false, 2, false, false, + LocalDateTime.of(2024, 5, 29, 0, 0, 0), LocalDateTime.of(2024, 5, 31, 23, 59, 59), + 13, 1, 1004), + tuple("스터디 구합니다 - 신청후", null, new MeetingJoinablePart[]{PM, SERVER}, "스터디", + false, 2, false, false, + LocalDateTime.of(2024, 5, 29, 0, 0, 0), LocalDateTime.of(2024, 5, 31, 23, 59, 59), + 10, 0, 1004), + tuple("스터디 구합니다 - 신청전", null, new MeetingJoinablePart[]{PM, SERVER}, "스터디", + false, 0, false, false, + LocalDateTime.of(2024, 5, 29, 0, 0, 0), LocalDateTime.of(2024, 5, 31, 23, 59, 59), + 10, 0, 1004) + ); + } + + @Test + @DisplayName("공동 모임장인 모임도 내가 만든 모임 목록에 조회된다.") + void getCreatedMeetingByUser_coLeaderMeeting() { + // given + Integer userId = 5; + + // when + UserV2GetCreatedMeetingByUserResponseDto responseDto = userV2Service.getCreatedMeetingByUser(userId); + + // then + Assertions.assertThat(responseDto.meetings()).hasSize(1); + + List meetings = responseDto.meetings(); + + Assertions.assertThat(meetings) + .extracting("title", "targetActiveGeneration", "joinableParts", "category", + "canJoinOnlyActiveGeneration", "status", "isCoLeader", "isMentorNeeded", + "mStartDate", "mEndDate", "capacity", + "appliedCount", "user.orgId") + .containsExactly( + tuple("세미나 구합니다 - 신청후", null, new MeetingJoinablePart[]{WEB, IOS}, "세미나", + false, 2, true, false, + LocalDateTime.of(2024, 5, 29, 0, 0, 0), LocalDateTime.of(2024, 5, 31, 23, 59, 59), + 13, 1, 1004) + ); + } + } } diff --git a/main/src/test/resources/sql/delete-all-data.sql b/main/src/test/resources/sql/delete-all-data.sql index 493bd583..c685999e 100644 --- a/main/src/test/resources/sql/delete-all-data.sql +++ b/main/src/test/resources/sql/delete-all-data.sql @@ -1,12 +1,3 @@ --- 테이블의 데이터 삭제 -DELETE FROM "apply"; -DELETE FROM "comment"; -DELETE FROM "like"; -DELETE FROM "post"; -DELETE FROM "meeting"; -DELETE FROM "notice"; -DELETE FROM "user"; - -- 시퀀스 초기화 -- apply 테이블의 시퀀스 초기화 ALTER SEQUENCE "apply_id_seq" RESTART WITH 1; @@ -27,4 +18,13 @@ ALTER SEQUENCE "notice_id_seq" RESTART WITH 1; ALTER SEQUENCE "post_id_seq" RESTART WITH 1; -- user 테이블의 시퀀스 초기화 -ALTER SEQUENCE "user_id_seq" RESTART WITH 1; \ No newline at end of file +ALTER SEQUENCE "user_id_seq" RESTART WITH 1; + +-- 테이블의 데이터 삭제 +DELETE FROM "apply"; +DELETE FROM "comment"; +DELETE FROM "like"; +DELETE FROM "post"; +DELETE FROM "meeting"; +DELETE FROM "notice"; +DELETE FROM "user"; \ No newline at end of file diff --git a/main/src/test/resources/sql/meeting-service-sequence-restart.sql b/main/src/test/resources/sql/meeting-service-sequence-restart.sql new file mode 100644 index 00000000..9e42d6d7 --- /dev/null +++ b/main/src/test/resources/sql/meeting-service-sequence-restart.sql @@ -0,0 +1 @@ +ALTER SEQUENCE "meeting_id_seq" RESTART WITH 6; \ No newline at end of file diff --git a/main/src/test/resources/sql/meeting-service-test-data.sql b/main/src/test/resources/sql/meeting-service-test-data.sql index a7c5548a..283ea460 100644 --- a/main/src/test/resources/sql/meeting-service-test-data.sql +++ b/main/src/test/resources/sql/meeting-service-test-data.sql @@ -15,32 +15,32 @@ VALUES (1, '모임개설자', 1001, '[{"part": "iOS", "generation": 35}, {"part": "안드로이드", "generation": 34}]', 'profile5.jpg', '010-6666-6666'); -INSERT INTO meeting ("userId", title, category, "imageURL", "startDate", "endDate", capacity, +INSERT INTO meeting ("id", "userId", title, category, "imageURL", "startDate", "endDate", capacity, "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", "targetActiveGeneration", "joinableParts") -VALUES (1, '스터디 구합니다1', '행사', +VALUES (1, 1, '스터디 구합니다1', '행사', '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', '2024-04-24 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, '스터디 설명입니다.', '스터디 진행방식입니다.', '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', '스터디장 설명입니다.', '시간지키세요.', true, true, 35, 35, '{PM,SERVER}'), - (5, '스터디 구합니다 - 신청전', '스터디', + (2, 5, '스터디 구합니다 - 신청전', '스터디', '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', '2024-04-25 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, '스터디 설명입니다.', '스터디 진행방식입니다.', '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', null, null, false, false, 34, null, '{PM,SERVER}'), - (5, '스터디 구합니다 - 신청후', '스터디', + (3, 5, '스터디 구합니다 - 신청후', '스터디', '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', '2024-04-22 00:00:00.000000', '2024-04-22 23:59:59.000000', 10, '스터디 설명입니다.', '스터디 진행방식입니다.', '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', null, null, false, false, 34, null, '{PM,SERVER}'), - (5, '세미나 구합니다 - 신청후', '세미나', + (4, 5, '세미나 구합니다 - 신청후', '세미나', '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', '2024-04-22 00:00:00.000000', '2024-04-22 23:59:59.000000', 13, '세미나 설명입니다.', '세미나 진행방식입니다.', @@ -52,3 +52,5 @@ VALUES (0, 1, 2, '2024-05-19 00:00:00.913489', 1), (0, 1, 3, '2024-05-19 00:00:02.413489', 1), (0, 1, 4, '2024-05-19 00:00:03.413489', 0); +INSERT INTO co_leader("meetingId", "userId") +VALUES (1, 5); diff --git a/main/src/test/resources/sql/user-service-test-data.sql b/main/src/test/resources/sql/user-service-test-data.sql new file mode 100644 index 00000000..43b79d80 --- /dev/null +++ b/main/src/test/resources/sql/user-service-test-data.sql @@ -0,0 +1,58 @@ +INSERT INTO "user" (id, name, "orgId", activities, "profileImage", phone) +VALUES (1, '김삼순', 1001, + '[{"part": "서버", "generation": 33}, {"part": "iOS", "generation": 32}]', + 'profile1.jpg', '010-1234-5678'), + (2, '홍길동', 1002, + '[{"part": "기획", "generation": 32}, {"part": "기획", "generation": 29}, {"part": "기획", "generation": 33}, {"part": "기획", "generation": 30}]', + 'profile2.jpg', '010-1111-2222'), + (3, '김철수', 1003, + '[{"part": "웹", "generation": 34}]', + 'profile3.jpg', '010-3333-4444'), + (4, '이영지', 1004, + '[{"part": "iOS", "generation": 32}, {"part": "안드로이드", "generation": 29}]', + 'profile4.jpg', '010-5555-5555'), + (5, '김솝트', 1005, + '[{"part": "iOS", "generation": 32}, {"part": "안드로이드", "generation": 29}]', + 'profile4.jpg', '010-5555-5555'); + +INSERT INTO meeting ("userId", title, category, "imageURL", "startDate", "endDate", capacity, + "desc", "processDesc", "mStartDate", "mEndDate", "leaderDesc", + note, "isMentorNeeded", "canJoinOnlyActiveGeneration", "createdGeneration", + "targetActiveGeneration", "joinableParts") +VALUES (1, '스터디 구합니다1', '행사', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-04-24 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', '스터디장 설명입니다.', + '시간지키세요.', true, true, 35, 35, '{PM,SERVER}'), + + (4, '스터디 구합니다 - 신청전', '스터디', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-04-25 00:00:00.000000', '2024-05-24 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', null, + null, false, false, 34, null, '{PM,SERVER}'), + + (4, '스터디 구합니다 - 신청후', '스터디', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-04-22 00:00:00.000000', '2024-04-22 23:59:59.000000', 10, + '스터디 설명입니다.', '스터디 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', null, + null, false, false, 34, null, '{PM,SERVER}'), + + (4, '세미나 구합니다 - 신청후', '세미나', + '[{"id": 0, "url": "https://makers-web-img.s3.ap-northeast-2.amazonaws.com/meeting/2024/05/19/79ba8312-0ebf-48a2-9a5e-b372fb8a9e64.png"}]', + '2024-04-22 00:00:00.000000', '2024-04-22 23:59:59.000000', 13, + '세미나 설명입니다.', '세미나 진행방식입니다.', + '2024-05-29 00:00:00.000000', '2024-05-31 23:59:59.000000', null, + null, false, false, 34, null, '{WEB, IOS}'); + +INSERT INTO apply (type, "meetingId", "userId", "appliedDate", status) +VALUES (0, 1, 2, '2024-05-19 00:00:00.913489', 0), + (0, 2, 2, '2024-05-19 00:00:02.413489', 0), + (0, 3, 2, '2024-05-19 00:00:03.413489', 0), + (0, 4, 2, '2024-05-19 00:00:03.413489', 1); + +INSERT INTO co_leader ("meetingId", "userId") +VALUES (4, 3), + (4, 5); \ No newline at end of file From 82c999e3e3f71204635961225c141f7e8d2758f4 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:46:58 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=EC=9A=A9=20AOP=20=EA=B5=AC=ED=98=84=20(#430)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(aop): 로그 출력용 AOP 구현 * fix(aop): 테스트 코드 실행시 실행되지 않도록 설정 * chore(aop): 필드까지 출력되도록 수정 --- .../main/global/aop/ExecutionLoggingAop.java | 106 +++++++++++------- 1 file changed, 67 insertions(+), 39 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java b/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java index c3f50f6d..ad8d6de0 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/aop/ExecutionLoggingAop.java @@ -1,12 +1,12 @@ package org.sopt.makers.crew.main.global.aop; -import jakarta.servlet.http.HttpServletRequest; - +import java.lang.reflect.Field; import java.util.Objects; -import lombok.extern.log4j.Log4j2; + import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; +import org.springframework.context.annotation.Profile; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -15,56 +15,84 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.log4j.Log4j2; + @Aspect @Component @Log4j2 +@Profile("!test") public class ExecutionLoggingAop { - // 모든 패키지 내의 controller package에 존재하는 클래스 - @Around("execution(* org.sopt.makers.crew..*Controller.*(..))") - public Object logExecutionTrace(ProceedingJoinPoint pjp) throws Throwable { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); - RequestMethod httpMethod = RequestMethod.valueOf(request.getMethod()); + /** + * @implNote : 일부 클래스를 제외하고, 모든 클래스의 메서드의 시작과 끝을 로깅한다. + * @implNote : 제외 클래스 - global 패키지, config 관련 패키지, Test 클래스 + * */ + @Around("execution(* org.sopt.makers.crew.main..*(..)) " + + "&& !within(org.sopt.makers.crew.main.global..*) " + + "&& !within(org.sopt.makers.crew.main.external.s3.config..*)" + ) + public Object logExecutionTrace(ProceedingJoinPoint pjp) throws Throwable { + HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); + RequestMethod httpMethod = RequestMethod.valueOf(request.getMethod()); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + Object userId = authentication.getPrincipal().toString(); + + String className = pjp.getSignature().getDeclaringType().getSimpleName(); + String methodName = pjp.getSignature().getName(); + String task = className + "." + methodName; - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - Object userId = authentication.getPrincipal().toString(); + log.info(""); + log.info("🚨 {} Start", className); + log.info("[Call Method] " + httpMethod + ": " + task + " | Request userId=" + userId); - String className = pjp.getSignature().getDeclaringType().getSimpleName(); - String methodName = pjp.getSignature().getName(); - String task = className + "." + methodName; + Object[] paramArgs = pjp.getArgs(); + for (Object object : paramArgs) { + if (Objects.nonNull(object)) { + log.info("[Parameter] {} {}", object.getClass().getSimpleName(), object); - log.info("[Call Method] " + httpMethod.toString() + ": " + task + " | Request userId=" + userId); + String packageName = object.getClass().getPackage().getName(); + if (packageName.contains("java")) { + break; + } - Object[] paramArgs = pjp.getArgs(); - for (Object object : paramArgs) { - if (Objects.nonNull(object)) { - log.info("[parameter type] {}", object.getClass().getSimpleName()); - log.info("[parameter value] {}", object); - } - } + Field[] fields = object.getClass().getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); // private 필드에도 접근 가능하도록 설정 + try { + Object value = field.get(object); // 필드값 가져오기 + log.info("[Field] {} = {}", field.getName(), value); + } catch (IllegalAccessException e) { + log.warn("[Field Access Error] Cannot access field: " + field.getName()); + } + } + } + } - // 해당 클래스 처리 전의 시간 - StopWatch sw = new StopWatch(); - sw.start(); + // 해당 클래스 처리 전의 시간 + StopWatch sw = new StopWatch(); + sw.start(); - Object result = null; + Object result = null; - // 해당 클래스의 메소드 실행 - try{ - result = pjp.proceed(); - } - catch (Exception e){ - log.warn("[ERROR] " + task + " 메서드 예외 발생 : " + e.getMessage()); - throw e; - } + // 해당 클래스의 메소드 실행 + try { + result = pjp.proceed(); + } catch (Exception e) { + log.warn("[ERROR] " + task + " 메서드 예외 발생 : " + e.getMessage()); + throw e; + } - // 해당 클래스 처리 후의 시간 - sw.stop(); - long executionTime = sw.getTotalTimeMillis(); + // 해당 클래스 처리 후의 시간 + sw.stop(); + long executionTime = sw.getTotalTimeMillis(); - log.info("[ExecutionTime] " + task + " --> " + executionTime + " (ms)"); + log.info("[ExecutionTime] " + task + " --> " + executionTime + " (ms)"); + log.info("🚨 {} End", className); + log.info(""); - return result; - } + return result; + } } From 0534d138004e33c401c90ef7d49be29bfe75cbd3 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Sun, 27 Oct 2024 12:26:45 +0900 Subject: [PATCH 07/10] =?UTF-8?q?refactor:=20=EC=8A=B9=EC=9D=B8=EB=90=9C?= =?UTF-8?q?=20=EC=8B=A0=EC=B2=AD=EC=9E=90=20=EC=88=98=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#464)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 승인된 신청자 수 필드 추가 * refactor: 사용하지 않는 QueryProjection 삭제 + 모임 제목에 private final 추가 * test: appliedCount -> approvedCount 로 변경 --- .../main/global/dto/MeetingResponseDto.java | 47 +++++-------------- .../v2/service/MeetingV2ServiceTest.java | 6 +-- 2 files changed, 14 insertions(+), 39 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java index 07022937..d8a676a7 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/dto/MeetingResponseDto.java @@ -3,16 +3,14 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Objects; import org.sopt.makers.crew.main.global.constant.CrewConst; import org.sopt.makers.crew.main.entity.meeting.Meeting; -import org.sopt.makers.crew.main.entity.meeting.enums.MeetingCategory; import org.sopt.makers.crew.main.entity.meeting.enums.MeetingJoinablePart; import org.sopt.makers.crew.main.entity.meeting.vo.ImageUrlVO; import org.sopt.makers.crew.main.entity.user.User; -import com.querydsl.core.annotations.QueryProjection; - import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Getter; @@ -28,7 +26,7 @@ public class MeetingResponseDto { private final Integer id; @Schema(description = "모임 제목", example = "모임 제목입니다") @NotNull - String title; + private final String title; @Schema(description = "대상 기수", example = "33") @NotNull private final Integer targetActiveGeneration; @@ -70,36 +68,12 @@ public class MeetingResponseDto { @Schema(description = "모임장 정보", example = "") @NotNull private final MeetingCreatorDto user; - @Schema(description = "신청 승인 수", example = "7") + @Schema(description = "TODO: FE에서 수정 완료 후 삭제 ", example = "[DEPRECATED]") @NotNull private final int appliedCount; - - @QueryProjection - public MeetingResponseDto(Integer id, String title, Integer targetActiveGeneration, - @NotNull MeetingJoinablePart[] joinableParts, MeetingCategory category, Boolean canJoinOnlyActiveGeneration, - Integer status, - List imageURL, Boolean isMentorNeeded, LocalDateTime mStartDate, LocalDateTime mEndDate, - int capacity, - MeetingCreatorDto user, int appliedCount) { - - boolean processedCanJoinOnlyActiveGeneration = canJoinOnlyActiveGeneration - && (CrewConst.ACTIVE_GENERATION.equals(targetActiveGeneration)); - - this.id = id; - this.title = title; - this.targetActiveGeneration = targetActiveGeneration; - this.joinableParts = joinableParts; - this.category = category.getValue(); - this.canJoinOnlyActiveGeneration = processedCanJoinOnlyActiveGeneration; - this.status = status; - this.imageURL = imageURL; - this.isMentorNeeded = isMentorNeeded; - this.mStartDate = mStartDate; - this.mEndDate = mEndDate; - this.capacity = capacity; - this.user = user; - this.appliedCount = appliedCount; - } + @Schema(description = "승인된 신청자 수", example = "3") + @NotNull + private final int approvedCount; public LocalDateTime getmStartDate() { return mStartDate; @@ -109,15 +83,16 @@ public LocalDateTime getmEndDate() { return mEndDate; } - public static MeetingResponseDto of(Meeting meeting, User meetingCreator, int appliedCount, LocalDateTime now) { + public static MeetingResponseDto of(Meeting meeting, User meetingCreator, int approvedCount, LocalDateTime now) { MeetingCreatorDto creatorDto = MeetingCreatorDto.of(meetingCreator); - boolean canJoinOnlyActiveGeneration = meeting.getTargetActiveGeneration() == CrewConst.ACTIVE_GENERATION + boolean canJoinOnlyActiveGeneration = + Objects.equals(meeting.getTargetActiveGeneration(), CrewConst.ACTIVE_GENERATION) && meeting.getCanJoinOnlyActiveGeneration(); return new MeetingResponseDto(meeting.getId(), meeting.getTitle(), - meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meeting.getCategory(), + meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meeting.getCategory().getValue(), canJoinOnlyActiveGeneration, meeting.getMeetingStatus(now), meeting.getImageURL(), meeting.getIsMentorNeeded(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getCapacity(), - creatorDto, appliedCount); + creatorDto, approvedCount, approvedCount); } } diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index 78ffd8a6..21b8a8ba 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -472,7 +472,7 @@ void normal_getMeetings_success() { "title", "category", "canJoinOnlyActiveGeneration", "mStartDate", "mEndDate", "capacity", "isMentorNeeded", "targetActiveGeneration", - "joinableParts", "status", "appliedCount" + "joinableParts", "status", "approvedCount" ).containsExactly( tuple("세미나 구합니다 - 신청후", "세미나", false, LocalDateTime.of(2024, 5, 29, 0, 0), @@ -542,7 +542,7 @@ void getOnlyActiveGenerationMeeting_getMeetings_meetings() { "title", "category", "canJoinOnlyActiveGeneration", "mStartDate", "mEndDate", "capacity", "isMentorNeeded", "targetActiveGeneration", - "joinableParts", "status", "appliedCount" + "joinableParts", "status", "approvedCount" ).containsExactly( tuple("스터디 구합니다1", "행사", true, LocalDateTime.of(2024, 5, 29, 0, 0), @@ -588,7 +588,7 @@ void getByCategory_getMeetings_meetings() { "title", "category", "canJoinOnlyActiveGeneration", "mStartDate", "mEndDate", "capacity", "isMentorNeeded", "targetActiveGeneration", - "joinableParts", "status", "appliedCount" + "joinableParts", "status", "approvedCount" ).containsExactly( tuple("세미나 구합니다 - 신청후", "세미나", false, LocalDateTime.of(2024, 5, 29, 0, 0), From e15f8d825d14655afde09fc70f81aefcbd469b9e Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:48:28 +0900 Subject: [PATCH 08/10] =?UTF-8?q?chore:=20approvedCount=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#466)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MeetingV2GetCreatedMeetingByUserResponseDto.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java index c4c39067..83b4ce47 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java +++ b/main/src/main/java/org/sopt/makers/crew/main/user/v2/dto/response/MeetingV2GetCreatedMeetingByUserResponseDto.java @@ -68,11 +68,14 @@ public record MeetingV2GetCreatedMeetingByUserResponseDto( @Schema(description = "모임장 정보", example = "") @NotNull MeetingCreatorDto user, - @Schema(description = "신청 수", example = "7") + @Schema(description = "[DEPRECATED] TODO: FE에서 수정 완료 후 삭제 ", example = "7") @NotNull - int appliedCount + int appliedCount, + @Schema(description = "신청자 수", example = "7") + @NotNull + int approvedCount ) { - public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, boolean isCoLeader, int appliedCount, + public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, boolean isCoLeader, int approvedCount, LocalDateTime now) { MeetingCreatorDto creatorDto = MeetingCreatorDto.of(meeting.getUser()); boolean canJoinOnlyActiveGeneration = meeting.getTargetActiveGeneration() == CrewConst.ACTIVE_GENERATION @@ -82,7 +85,7 @@ public static MeetingV2GetCreatedMeetingByUserResponseDto of(Meeting meeting, bo meeting.getTargetActiveGeneration(), meeting.getJoinableParts(), meeting.getCategory().getValue(), canJoinOnlyActiveGeneration, meeting.getMeetingStatus(now), isCoLeader, meeting.getImageURL(), meeting.getIsMentorNeeded(), meeting.getMStartDate(), meeting.getMEndDate(), meeting.getCapacity(), - creatorDto, appliedCount); + creatorDto, approvedCount, approvedCount); } } From 00a6ba83f7166e17c31104fd351505629dc7fc75 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:13:46 +0900 Subject: [PATCH 09/10] =?UTF-8?q?=08fix:=20=EB=AA=A8=EC=9E=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0=20(#470)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 공동모임장이 실제 모임 객체 참조하도록 수정 * test: 모임 수정 성공 케이스 테스트 작성 --- .../v2/service/MeetingV2ServiceImpl.java | 5 +- .../v2/service/MeetingV2ServiceTest.java | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java index 1ea3af6f..38a01284 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java +++ b/main/src/main/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceImpl.java @@ -319,12 +319,11 @@ public void updateMeeting(Integer meetingId, MeetingV2CreateMeetingBodyDto reque Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); meeting.validateMeetingCreator(userId); + updateCoLeaders(requestBody.getCoLeaderUserIds(), meeting); + Meeting updatedMeeting = meetingMapper.toMeetingEntity(requestBody, createTargetActiveGeneration(requestBody.getCanJoinOnlyActiveGeneration()), ACTIVE_GENERATION, user, user.getId()); - - updateCoLeaders(requestBody.getCoLeaderUserIds(), updatedMeeting); - meeting.updateMeeting(updatedMeeting); } diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index 21b8a8ba..c55b2925 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -1730,6 +1730,65 @@ void modifyMeeting_Fail_isCoLeader() { .isInstanceOf(ForbiddenException.class) .hasMessage(FORBIDDEN_EXCEPTION.getErrorCode()); } + + @Test + @DisplayName("모임 수정을 할 수 있다.") + void modifyMeeting_Success() { + // given + Integer userId = 1; + Integer meetingId = 1; + + // 모임 이미지 리스트 + List files = Arrays.asList( + "https://example.com/image1.jpg" + ); + + // 대상 파트 목록 + MeetingJoinablePart[] joinableParts = { + MeetingJoinablePart.SERVER, + MeetingJoinablePart.IOS + }; + + // DTO 생성 + MeetingV2CreateMeetingBodyDto dto = new MeetingV2CreateMeetingBodyDto( + "수정1", // title + files, // files (모임 이미지 리스트) + "행사", // category + "2024.12.12", // startDate (모집 시작 날짜) + "2024.12.25", // endDate (모집 끝 날짜) + 10, // capacity (모집 인원) + "백엔드 개발에 관심 있는 사람들을 위한 스터디입니다.", // desc (모집 정보) + "매주 온라인으로 진행되며, 발표와 토론이 포함됩니다.", // processDesc (진행 방식 소개) + "2024.10.16", // mStartDate (모임 활동 시작 날짜) + "2024.12.30", // mEndDate (모임 활동 종료 날짜) + "5년차 백엔드 개발자입니다.", // leaderDesc (개설자 소개) + "준비물은 노트북과 열정입니다.", // note (유의할 사항) + false, // isMentorNeeded (멘토 필요 여부) + true, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) + joinableParts, // joinableParts (대상 파트 목록) + List.of(3, 4) + ); + + // when + meetingV2Service.updateMeeting(meetingId, dto, userId); + + // then + Meeting meeting = meetingRepository.findByIdOrThrow(meetingId); + Assertions.assertThat(meeting) + .extracting("title", "category", "startDate", "endDate") + .containsExactly("수정1", MeetingCategory.EVENT, + LocalDateTime.of(2024, 12, 12, 0, 0, 0), + LocalDateTime.of(2024, 12, 25, 23, 59, 59)); + + List coLeaders = coLeaderRepository.findAllByMeetingId(meetingId); + Assertions.assertThat(coLeaders).hasSize(2); + Assertions.assertThat(coLeaders) + .extracting("user.name", "user.orgId") + .containsExactly( + tuple("승인신청자", 1003), + tuple("대기신청자", 1004) + ); + } } @Nested From 91f2139a11faf1464ea1d75b41252f3868ae1408 Mon Sep 17 00:00:00 2001 From: Mingyu Song <100754581+mikekks@users.noreply.github.com> Date: Fri, 8 Nov 2024 10:29:33 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix:=20=EB=AA=A8=EC=9E=84=EC=9E=A5?= =?UTF-8?q?=EC=9D=80=20=EA=B3=B5=EB=8F=99=EB=AA=A8=EC=9E=84=EC=9E=A5?= =?UTF-8?q?=EC=9D=B4=20=EB=90=A0=20=EC=88=98=20=EC=97=86=EB=8B=A4.=20(#476?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 모임장은 공동모임장이 될 수 없도록 구현 * test: 모임장은 공동모임장이 될 수 없도록 구현 내용 테스트 --- .../crew/main/entity/meeting/CoLeader.java | 8 +++ .../main/global/exception/ErrorStatus.java | 1 + .../v2/service/MeetingV2ServiceTest.java | 52 +++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java index d8836a65..0dee399b 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java +++ b/main/src/main/java/org/sopt/makers/crew/main/entity/meeting/CoLeader.java @@ -1,7 +1,12 @@ package org.sopt.makers.crew.main.entity.meeting; +import static org.sopt.makers.crew.main.global.exception.ErrorStatus.*; + +import java.util.Objects; + import org.sopt.makers.crew.main.entity.common.BaseTimeEntity; import org.sopt.makers.crew.main.entity.user.User; +import org.sopt.makers.crew.main.global.exception.BadRequestException; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -38,6 +43,9 @@ public class CoLeader extends BaseTimeEntity { @Builder private CoLeader(Meeting meeting, User user) { + if (Objects.equals(meeting.getUserId(), user.getId())) { + throw new BadRequestException(LEADER_CANNOT_BE_CO_LEADER_APPLY.getErrorCode()); + } this.meeting = meeting; this.user = user; } diff --git a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java index 645bee08..4026b7f2 100644 --- a/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java +++ b/main/src/main/java/org/sopt/makers/crew/main/global/exception/ErrorStatus.java @@ -36,6 +36,7 @@ public enum ErrorStatus { MAX_IMAGE_UPLOAD_EXCEEDED("이미지는 최대 10개까지만 업로드 가능합니다."), LEADER_CANNOT_APPLY("모임장은 신청할 수 없습니다."), CO_LEADER_CANNOT_APPLY("공동 모임장은 신청할 수 없습니다."), + LEADER_CANNOT_BE_CO_LEADER_APPLY("모임장은 공동 모임장이 될 수 없습니다."), /** * 401 UNAUTHORIZED diff --git a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java index c55b2925..7ee97255 100644 --- a/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java +++ b/main/src/test/java/org/sopt/makers/crew/main/meeting/v2/service/MeetingV2ServiceTest.java @@ -300,6 +300,58 @@ void notPresentCoLeaderId_createMeeting_throwException() { } + @Test + @DisplayName("모임장은 공동모임장이 될 수 없다.") + void setLeaderCoLeader_createMeeting_throwException() { + // given + User user = User.builder() + .name("홍길동") + .orgId(1) + .activities(List.of(new UserActivityVO("서버", 33), new UserActivityVO("iOS", 34))) + .profileImage("image-url1") + .phone("010-1234-5678") + .build(); + User savedUser = userRepository.save(user); + + // 모임 이미지 리스트 + List files = Arrays.asList( + "https://example.com/image1.jpg" + ); + + // 대상 파트 목록 + MeetingJoinablePart[] joinableParts = { + MeetingJoinablePart.SERVER, + MeetingJoinablePart.IOS + }; + + // DTO 생성 + MeetingV2CreateMeetingBodyDto meetingDto = new MeetingV2CreateMeetingBodyDto( + "알고보면 쓸데있는 개발 프로세스", // title + files, // files (모임 이미지 리스트) + "스터디", // category + "2024.10.01", // startDate (모집 시작 날짜) + "2024.10.15", // endDate (모집 끝 날짜) + 10, // capacity (모집 인원) + "백엔드 개발에 관심 있는 사람들을 위한 스터디입니다.", // desc (모집 정보) + "매주 온라인으로 진행되며, 발표와 토론이 포함됩니다.", // processDesc (진행 방식 소개) + "2024.10.16", // mStartDate (모임 활동 시작 날짜) + "2024.12.30", // mEndDate (모임 활동 종료 날짜) + "5년차 백엔드 개발자입니다.", // leaderDesc (개설자 소개) + "준비물은 노트북과 열정입니다.", // note (유의할 사항) + false, // isMentorNeeded (멘토 필요 여부) + true, // canJoinOnlyActiveGeneration (활동기수만 지원 가능 여부) + joinableParts, // joinableParts (대상 파트 목록) + List.of(savedUser.getId()) + ); + + // when, then + Assertions.assertThatThrownBy(() -> meetingV2Service.createMeeting((meetingDto), + savedUser.getId())) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining(LEADER_CANNOT_BE_CO_LEADER_APPLY.getErrorCode()); + + } + @Test @DisplayName("모임 개설자의 활동기수 정보가 없을 경우, 예외가 발생한다.") void userHasNotActivities_createMeeting_exception() {