diff --git a/src/main/java/com/example/eatmate/app/domain/chatRoom/dto/response/ChatRoomResponseDto.java b/src/main/java/com/example/eatmate/app/domain/chatRoom/dto/response/ChatRoomResponseDto.java index 2f09e0aa..c476de56 100644 --- a/src/main/java/com/example/eatmate/app/domain/chatRoom/dto/response/ChatRoomResponseDto.java +++ b/src/main/java/com/example/eatmate/app/domain/chatRoom/dto/response/ChatRoomResponseDto.java @@ -17,6 +17,7 @@ @NoArgsConstructor public class ChatRoomResponseDto { private List chats; + private String meetingName; private List participants; private ChatRoomDeliveryNoticeDto deliveryNotice; private ChatRoomOfflineNoticeDto offlineNotice; @@ -24,9 +25,11 @@ public class ChatRoomResponseDto { private boolean isLast; @Builder - private ChatRoomResponseDto(Slice chats, List participants, + private ChatRoomResponseDto(Slice chats, String meetingName, + List participants, ChatRoomDeliveryNoticeDto deliveryNotice, ChatRoomOfflineNoticeDto offlineNotice) { this.chats = chats.getContent(); + this.meetingName = meetingName; this.participants = participants; this.deliveryNotice = deliveryNotice; this.offlineNotice = offlineNotice; @@ -34,18 +37,22 @@ private ChatRoomResponseDto(Slice chats, List participants, Slice chatPage, ChatRoomDeliveryNoticeDto deliveryNotice) { + public static ChatRoomResponseDto ofWithDelivery(String meetingName, List participants, + Slice chatPage, ChatRoomDeliveryNoticeDto deliveryNotice) { return ChatRoomResponseDto.builder() .chats(chatPage) + .meetingName(meetingName) .participants(participants) .offlineNotice(null) .deliveryNotice(deliveryNotice) .build(); } - public static ChatRoomResponseDto ofWithOffline(List participants, Slice chatPage, ChatRoomOfflineNoticeDto offlineNotice) { + public static ChatRoomResponseDto ofWithOffline(String meetingName, List participants, + Slice chatPage, ChatRoomOfflineNoticeDto offlineNotice) { return ChatRoomResponseDto.builder() .chats(chatPage) + .meetingName(meetingName) .participants(participants) .offlineNotice(offlineNotice) .deliveryNotice(null) @@ -62,7 +69,8 @@ public static class ChatMemberResponseDto { private Boolean isMine; @Builder - private ChatMemberResponseDto(Long memberId, String nickname, Mbti mbti, String profileImageUrl, ParticipantRole role, Boolean isMine) { + private ChatMemberResponseDto(Long memberId, String nickname, Mbti mbti, String profileImageUrl, + ParticipantRole role, Boolean isMine) { this.memberId = memberId; this.nickname = nickname; this.mbti = mbti; @@ -76,7 +84,8 @@ public static ChatMemberResponseDto of(MeetingParticipant participant, Boolean i .memberId(participant.getMember().getMemberId()) .nickname(participant.getMember().getNickname()) .mbti(participant.getMember().getMbti()) - .profileImageUrl(participant.getMember().getProfileImage() != null ? participant.getMember().getProfileImage().getImageUrl() : null) + .profileImageUrl(participant.getMember().getProfileImage() != null ? + participant.getMember().getProfileImage().getImageUrl() : null) .role(participant.getRole()) .isMine(isMine) .build(); diff --git a/src/main/java/com/example/eatmate/app/domain/chatRoom/service/ChatRoomService.java b/src/main/java/com/example/eatmate/app/domain/chatRoom/service/ChatRoomService.java index a40cbc1c..3303bf58 100644 --- a/src/main/java/com/example/eatmate/app/domain/chatRoom/service/ChatRoomService.java +++ b/src/main/java/com/example/eatmate/app/domain/chatRoom/service/ChatRoomService.java @@ -79,7 +79,7 @@ public void joinChatRoom(Long meetingId, UserDetails userDetails) { ChatMessageResponseDto enterMessage = ChatMessageResponseDto.of( participant.getMemberId(), chatRoom.getId(), - participant.getNickname()+"님이 입장하셨습니다.", + participant.getNickname() + "님이 입장하셨습니다.", LocalDateTime.now()); List participants = meetingParticipantRepository.findByMeeting(chatRoom.getMeeting()); @@ -89,21 +89,22 @@ public void joinChatRoom(Long meetingId, UserDetails userDetails) { .map(member -> ChatMemberListDto.of(member.getId(), member.getMember().getNickname())) .collect(Collectors.toList()); - //실시간 유저 입장 메세지 - messagingTemplate.convertAndSend("/topic/chat."+chatRoom.getId(), enterMessage); + messagingTemplate.convertAndSend("/topic/chat." + chatRoom.getId(), enterMessage); //실시간 유저 업데이트 메세지 messagingTemplate.convertAndSend("/topic/chat." + chatRoom.getId() + ".members", participantDTOs); } //채팅방 입장(지난 로딩 위치는 클라이언트에서 조절) - public ChatRoomResponseDto enterChatRoomAndLoadMessage(Long chatRoomId, UserDetails userDetails, Pageable pageable) { + public ChatRoomResponseDto enterChatRoomAndLoadMessage(Long chatRoomId, UserDetails userDetails, + Pageable pageable) { Member mine = securityUtils.getMember(userDetails); ChatRoom chatRoom = chatRoomRepository.findByIdAndDeletedStatus(chatRoomId, DeletedStatus.NOT_DELETED) .orElseThrow(() -> new CommonException(ErrorCode.CHATROOM_NOT_FOUND)); Meeting meeting = chatRoom.getMeeting(); - List participants = Optional.ofNullable(meetingParticipantRepository.findByMeeting(meeting)) + List participants = Optional.ofNullable( + meetingParticipantRepository.findByMeeting(meeting)) .orElseThrow(() -> new CommonException(ErrorCode.USER_NOT_FOUND)) .stream() .map(meetingParticipant -> { @@ -116,18 +117,20 @@ public ChatRoomResponseDto enterChatRoomAndLoadMessage(Long chatRoomId, UserDeta //채팅방 공지 처리 if (meeting instanceof OfflineMeeting) { - OfflineMeeting offlineMeeting = (OfflineMeeting) meeting; - ChatRoomOfflineNoticeDto notice = ChatRoomOfflineNoticeDto.of(offlineMeeting.getMeetingPlace(), offlineMeeting.getMeetingDate()); + OfflineMeeting offlineMeeting = (OfflineMeeting)meeting; + ChatRoomOfflineNoticeDto notice = ChatRoomOfflineNoticeDto.of(offlineMeeting.getMeetingPlace(), + offlineMeeting.getMeetingDate()); - return ChatRoomResponseDto.ofWithOffline(participants, chatList, notice); + return ChatRoomResponseDto.ofWithOffline(offlineMeeting.getMeetingName(), participants, chatList, notice); } if (meeting instanceof DeliveryMeeting) { - DeliveryMeeting deliveryMeeting = (DeliveryMeeting) meeting; + DeliveryMeeting deliveryMeeting = (DeliveryMeeting)meeting; ChatRoomDeliveryNoticeDto notice = ChatRoomDeliveryNoticeDto - .of(deliveryMeeting.getStoreName(), deliveryMeeting.getAccountNumber(), deliveryMeeting.getBankName().toString(), deliveryMeeting.getPickupLocation()); + .of(deliveryMeeting.getStoreName(), deliveryMeeting.getAccountNumber(), + deliveryMeeting.getBankName().toString(), deliveryMeeting.getPickupLocation()); - return ChatRoomResponseDto.ofWithDelivery(participants, chatList, notice); + return ChatRoomResponseDto.ofWithDelivery(deliveryMeeting.getMeetingName(), participants, chatList, notice); } throw new CommonException(ErrorCode.INVALID_MEETING_TYPE); @@ -142,7 +145,7 @@ public Void leaveChatRoom(Long chatRoomId, UserDetails userDetails) { MemberChatRoom target = memberChatRoomRepository.findByMember_MemberId(member.getMemberId()) .orElseThrow(() -> new CommonException(ErrorCode.MEMBER_CHATROOM_NOT_FOUND)); - if(chatRoom.getOwnerId().equals(member.getMemberId())) { + if (chatRoom.getOwnerId().equals(member.getMemberId())) { chatRoom.deleteChatRoom(); chatRoom.getParticipant().forEach(memberChatRoomRepository::delete); chatService.deleteChat(chatRoom); @@ -153,13 +156,12 @@ public Void leaveChatRoom(Long chatRoomId, UserDetails userDetails) { ChatMessageResponseDto leaveMessage = ChatMessageResponseDto.of( member.getMemberId(), chatRoom.getId(), - member.getNickname()+"님(호스트)이 퇴장하셨습니다. 모임이 종료되었습니다.", + member.getNickname() + "님(호스트)이 퇴장하셨습니다. 모임이 종료되었습니다.", LocalDateTime.now()); //실시간 호스트 퇴장 메세지 - messagingTemplate.convertAndSend("/topic/chat."+chatRoom.getId(), leaveMessage); - } - else { + messagingTemplate.convertAndSend("/topic/chat." + chatRoom.getId(), leaveMessage); + } else { chatRoom.removeParticipant(target); memberChatRoomRepository.delete(target); eventPublisher.publishEvent(new ParticipantChatRoomLeftEvent(chatRoom.getMeeting().getId(), userDetails)); @@ -172,7 +174,7 @@ public void sendLeveMessage(ChatRoom chatRoom, Member member) { ChatMessageResponseDto leaveMessage = ChatMessageResponseDto.of( member.getMemberId(), chatRoom.getId(), - member.getNickname()+"님이 퇴장하셨습니다.", + member.getNickname() + "님이 퇴장하셨습니다.", LocalDateTime.now()); List participants = meetingParticipantRepository.findByMeeting(chatRoom.getMeeting()); @@ -183,7 +185,7 @@ public void sendLeveMessage(ChatRoom chatRoom, Member member) { .collect(Collectors.toList()); //실시간 유저 퇴장 메세지 - messagingTemplate.convertAndSend("/topic/chat."+chatRoom.getId(), leaveMessage); + messagingTemplate.convertAndSend("/topic/chat." + chatRoom.getId(), leaveMessage); //실시간 유저 업데이트 메세지 messagingTemplate.convertAndSend("/topic/chat." + chatRoom.getId() + ".members", participantDTOs); } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/controller/MeetingController.java b/src/main/java/com/example/eatmate/app/domain/meeting/controller/MeetingController.java index f0980ec0..e2331ada 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/controller/MeetingController.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/controller/MeetingController.java @@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -226,8 +226,11 @@ public ResponseEntity> deleteMeeting( .body(GlobalResponseDto.success()); } - @PatchMapping("/{meetingId}/offline") + @PutMapping("/{meetingId}/offline") @Operation(summary = "오프라인 모임 수정", description = "오프라인 모임을 수정합니다.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + schema = @Schema(implementation = UpdateOfflineMeetingRequestDto.class))) public ResponseEntity> updateOfflineMeeting( @PathVariable Long meetingId, @ModelAttribute @Valid UpdateOfflineMeetingRequestDto updateOfflineMeetingRequestDto, @@ -238,11 +241,14 @@ public ResponseEntity> updateOfflineMeeting( .body(GlobalResponseDto.success()); } - @PatchMapping("/{meetingId}/delivery") + @PutMapping("/{meetingId}/delivery") @Operation(summary = "배달 모임 수정", description = "배달 모임을 수정합니다.") + @io.swagger.v3.oas.annotations.parameters.RequestBody(required = true, content = @Content( + mediaType = MediaType.MULTIPART_FORM_DATA_VALUE, + schema = @Schema(implementation = UpdateDeliveryMeetingRequestDto.class))) public ResponseEntity> updateDeliveryMeeting( @PathVariable Long meetingId, - @RequestBody @Valid UpdateDeliveryMeetingRequestDto UpdateDeliveryMeetingRequestDto, + @ModelAttribute @Valid UpdateDeliveryMeetingRequestDto UpdateDeliveryMeetingRequestDto, @AuthenticationPrincipal UserDetails userDetails ) { meetingService.updateDeliveryMeeting(meetingId, UpdateDeliveryMeetingRequestDto, userDetails); diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/domain/DeliveryMeeting.java b/src/main/java/com/example/eatmate/app/domain/meeting/domain/DeliveryMeeting.java index 75140d92..7036ef25 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/domain/DeliveryMeeting.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/domain/DeliveryMeeting.java @@ -49,9 +49,10 @@ public void updateDeliveryMeeting( String pickupLocation, String accountNumber, BankName bankName, + MeetingBackgroundType BackgroundType, Image backgroundImage ) { - super.updateMeeting(meetingName, description, backgroundImage); + super.updateMeeting(meetingName, description, backgroundImage, BackgroundType); this.foodCategory = foodCategory; this.storeName = storeName; this.pickupLocation = pickupLocation; diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/domain/Meeting.java b/src/main/java/com/example/eatmate/app/domain/meeting/domain/Meeting.java index eeb844ac..c00a6839 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/domain/Meeting.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/domain/Meeting.java @@ -63,6 +63,10 @@ public abstract class Meeting { @Enumerated(EnumType.STRING) private MeetingStatus meetingStatus; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private MeetingBackgroundType backgroundType; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "background_image_id") private Image backgroundImage; @@ -80,10 +84,12 @@ public abstract class Meeting { private LocalDateTime updatedAt; // 모임 수정 - public void updateMeeting(String meetingName, String meetingDescription, Image backgroundImage) { + public void updateMeeting(String meetingName, String meetingDescription, Image backgroundImage, + MeetingBackgroundType backgroundImageType) { this.meetingName = meetingName; this.meetingDescription = meetingDescription; this.backgroundImage = backgroundImage; + this.backgroundType = backgroundImageType; } // 모임 삭제 diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingBackgroundType.java b/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingBackgroundType.java new file mode 100644 index 00000000..20520c39 --- /dev/null +++ b/src/main/java/com/example/eatmate/app/domain/meeting/domain/MeetingBackgroundType.java @@ -0,0 +1,17 @@ +package com.example.eatmate.app.domain.meeting.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MeetingBackgroundType { + DEFAULT_IMAGE_1("기본 이미지 1", + "https://eatmatebucket.s3.ap-northeast-2.amazonaws.com/a2c4c1b796de95f92ed6b1c8430075a2.jpg"), + DEFAULT_IMAGE_2("기본 이미지 2", + "https://eatmatebucket.s3.ap-northeast-2.amazonaws.com/ee36549fbadb7d88712f2b18cdc72eb1.jpg"), + CUSTOM("사용자 지정", null); + + private final String description; + private final String imageUrl; +} diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/domain/OfflineMeeting.java b/src/main/java/com/example/eatmate/app/domain/meeting/domain/OfflineMeeting.java index 26aa9e06..7b1af476 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/domain/OfflineMeeting.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/domain/OfflineMeeting.java @@ -36,10 +36,10 @@ public void updateOfflineMeeting( String meetingPlace, LocalDateTime meetingDate, OfflineMeetingCategory offlineMeetingCategory, + MeetingBackgroundType backgroundType, Image backgroundImage) { - super.updateMeeting(meetingName, description, backgroundImage); - + super.updateMeeting(meetingName, description, backgroundImage, backgroundType); this.meetingPlace = meetingPlace; this.meetingDate = meetingDate; this.offlineMeetingCategory = offlineMeetingCategory; diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/domain/repository/MeetingCustomRepositoryImpl.java b/src/main/java/com/example/eatmate/app/domain/meeting/domain/repository/MeetingCustomRepositoryImpl.java index 025db289..090d2c02 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/domain/repository/MeetingCustomRepositoryImpl.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/domain/repository/MeetingCustomRepositoryImpl.java @@ -38,35 +38,50 @@ public class MeetingCustomRepositoryImpl implements MeetingCustomRepository { private final JPAQueryFactory queryFactory; + /** + * 사용자의 모임 목록을 조회하는 메서드 + * + * @param memberId 조회할 회원 ID + * @param role 조회할 역할 (HOST/PARTICIPANT) + * @param meetingStatus 모임 상태 (ACTIVE/INACTIVE) + * @param lastMeetingId 마지막으로 조회한 모임 ID (페이징) + * @param lastDateTime 마지막으로 조회한 시간 (페이징) + * @param pageSize 페이지 크기 + * @return 모임 목록 + */ @Override public List findMyMeetingList(Long memberId, ParticipantRole role, MeetingStatus meetingStatus, Long lastMeetingId, LocalDateTime lastDateTime, int pageSize) { + // 배달 모임인지 여부를 확인하는 조건 BooleanExpression isDelivery = meeting.type.eq("DELIVERY"); - // 상태와 역할에 대한 조건 + // 모임 상태와 역할에 대한 필터링 조건 BooleanExpression statusCondition = meetingStatus != null ? meeting.meetingStatus.eq(meetingStatus) : null; - BooleanExpression roleCondition = role != null ? meetingParticipant.role.eq(role) : null; + // 모임 시간을 가져오는 표현식 + // 배달 모임이면 주문 마감시간, 아니면 모임 시간을 가져옴 DateTimeExpression meetingTimeExpr = new CaseBuilder() .when(meeting.type.eq("DELIVERY")) .then(deliveryMeeting.orderDeadline) .otherwise(offlineMeeting.meetingDate); - // No-Offset 페이징을 위한 동적 조건 + // No-Offset 페이징을 위한 동적 조건 생성 BooleanExpression cursorCondition = getCursorCondition(lastMeetingId, lastDateTime, meetingTimeExpr); return queryFactory .select(Projections.constructor(MyMeetingListResponseDto.class, - meeting.type, - meeting.id, - meeting.meetingName, - meeting.meetingStatus, - meeting.meetingDescription, - meeting.participantLimit.maxParticipants, + meeting.type, // 모임 타입 (DELIVERY/OFFLINE) + meeting.id, // 모임 ID + meeting.meetingName, // 모임 이름 + meeting.meetingStatus, // 모임 상태 + meeting.meetingDescription, // 모임 설명 + meeting.participantLimit.maxParticipants, // 최대 참여자 수 + + // 오프라인 모임 카테고리 (배달 모임인 경우 null) ExpressionUtils.as( new CaseBuilder() .when(isDelivery) @@ -77,7 +92,10 @@ public List findMyMeetingList(Long memberId, Participa .where(offlineMeeting.id.eq(meeting.id))), "offlineMeetingCategory" ), - meeting.createdAt, + + meeting.createdAt, // 생성 시간 + + // 모임 장소 (배달 모임: 가게 이름, 오프라인 모임: 모임 장소) ExpressionUtils.as( new CaseBuilder() .when(isDelivery) @@ -90,6 +108,8 @@ public List findMyMeetingList(Long memberId, Participa .from(offlineMeeting) .where(offlineMeeting.id.eq(meeting.id))), "location"), + + // 마감 시간 (배달 모임: 주문 마감 시간, 오프라인 모임: 모임 시간) ExpressionUtils.as( new CaseBuilder() .when(isDelivery) @@ -102,6 +122,8 @@ public List findMyMeetingList(Long memberId, Participa .from(offlineMeeting) .where(offlineMeeting.id.eq(meeting.id))), "dueDateTime"), + + // 현재 참여자 수 계산 ExpressionUtils.as( JPAExpressions .select(meetingParticipant.count()) @@ -110,13 +132,20 @@ public List findMyMeetingList(Long memberId, Participa "participantCount") )) .from(meeting) + // 배달/오프라인 모임 테이블과 left join .leftJoin(deliveryMeeting).on(deliveryMeeting.id.eq(meeting.id)) .leftJoin(offlineMeeting).on(offlineMeeting.id.eq(meeting.id)) + // 참여자 정보와 join .join(meetingParticipant).on( meetingParticipant.meeting.id.eq(meeting.id), meetingParticipant.member.memberId.eq(memberId) ) + // 조건절 적용 .where(statusCondition, roleCondition, cursorCondition) + // 정렬 조건: + // 1. 모임 상태 오름차순 (활성 모임 우선) + // 2. 모임 시간 기준 오름차순 + // 3. 모임 ID 내림차순 .orderBy( meeting.meetingStatus.asc(), new OrderSpecifier<>(Order.ASC, @@ -133,10 +162,14 @@ public List findMyMeetingList(Long memberId, Participa ), meeting.id.desc() ) - .limit(pageSize + 1) + .limit(pageSize + 1) // 다음 페이지 존재 여부 확인을 위해 +1 .fetch(); } + /** + * No-Offset 페이징을 위한 커서 조건 생성 + * 마지막으로 조회한 모임 이후의 데이터만 조회하도록 함 + */ private BooleanExpression getCursorCondition(Long lastMeetingId, LocalDateTime lastDateTime, DateTimeExpression meetingTimeExpr) { @@ -144,6 +177,9 @@ private BooleanExpression getCursorCondition(Long lastMeetingId, LocalDateTime l return null; } + // 활성 상태인 모임만 필터링하고 + // 마지막으로 조회한 시간 이후의 모임 또는 + // 같은 시간대의 모임 중 ID가 더 작은 모임을 조회 return new CaseBuilder() .when(meeting.meetingStatus.ne(MeetingStatus.INACTIVE)) .then(1) @@ -157,6 +193,9 @@ private BooleanExpression getCursorCondition(Long lastMeetingId, LocalDateTime l .and(meeting.id.lt(lastMeetingId)))); } + /** + * 사용자의 다가오는 가장 가까운 모임 조회 + */ @Override public UpcomingMeetingResponseDto findUpcomingMeeting(Long memberId) { BooleanExpression isDelivery = meeting.type.eq("DELIVERY"); @@ -164,6 +203,7 @@ public UpcomingMeetingResponseDto findUpcomingMeeting(Long memberId) { return queryFactory .select(Projections.constructor(UpcomingMeetingResponseDto.class, meetingParticipant.member.nickname, + // 모임 시간 (배달/오프라인 구분하여 조회) ExpressionUtils.as( new CaseBuilder() .when(isDelivery) @@ -176,6 +216,7 @@ public UpcomingMeetingResponseDto findUpcomingMeeting(Long memberId) { .from(offlineMeeting) .where(offlineMeeting.id.eq(meeting.id))), "meetingTime"), + // 모임 장소 (배달: 가게이름, 오프라인: 모임장소) ExpressionUtils.as( new CaseBuilder() .when(isDelivery) @@ -194,7 +235,9 @@ public UpcomingMeetingResponseDto findUpcomingMeeting(Long memberId) { meetingParticipant.meeting.id.eq(meeting.id), meetingParticipant.member.memberId.eq(memberId) ) + // 활성 상태인 모임만 조회 .where(meeting.meetingStatus.eq(MeetingStatus.ACTIVE)) + // 가장 가까운 시간 순으로 정렬 .orderBy( new OrderSpecifier<>(Order.ASC, new CaseBuilder() @@ -209,37 +252,54 @@ public UpcomingMeetingResponseDto findUpcomingMeeting(Long memberId) { .where(offlineMeeting.id.eq(meeting.id))) ) ) - .fetchFirst(); + .fetchFirst(); // 첫 번째 결과만 조회 } + /** + * 배달 모임 목록 조회 + * + * @param category 음식 카테고리 필터 + * @param genderRestriction 성별 제한 필터 + * @param maxParticipant 최대 참여자 수 상한 필터 + * @param minParticipant 최대 참여자 수 하한 필터 + * @param sortType 정렬 방식 (생성일/모임시간/참여자수) + * @param pageSize 페이지 크기 + * @param lastMeetingId 마지막 조회 모임 ID (페이징) + * @param lastDateTime 마지막 조회 시간 (페이징) + */ @Override public List findDeliveryMeetingList(FoodCategory category, GenderRestriction genderRestriction, Long maxParticipant, Long minParticipant, MeetingSortType sortType, int pageSize, Long lastMeetingId, LocalDateTime lastDateTime) { - // 카테고리 제한 조건 + // 음식 카테고리 필터링 조건 BooleanExpression isCategory = category != null ? deliveryMeeting.foodCategory.eq(category) : null; - // 모임 시간 관련 expression + // 모임 시간 표현식 (배달 모임은 주문 마감시간 기준) DateTimeExpression meetingTimeExpr = deliveryMeeting.orderDeadline; + // 공통 모임 목록 조회 메서드 호출 return findMeetingList( - isCategory, - genderRestriction, - maxParticipant, - minParticipant, - sortType, - pageSize, - lastMeetingId, - lastDateTime, - meetingTimeExpr, - createDeliveryMeetingProjection(), - deliveryMeeting, - deliveryMeeting.id + isCategory, // 카테고리 조건 + genderRestriction, // 성별 제한 조건 + maxParticipant, // 최대 참여자 수 상한 + minParticipant, // 최대 참여자 수 하한 + sortType, // 정렬 방식 + pageSize, // 페이지 크기 + lastMeetingId, // 마지막 모임 ID + lastDateTime, // 마지막 조회 시간 + meetingTimeExpr, // 모임 시간 표현식 + createDeliveryMeetingProjection(), // 배달 모임용 프로젝션 + deliveryMeeting, // 조인할 테이블 + deliveryMeeting.id // 조인 키 ); } + /** + * 오프라인 모임 목록 조회 + * (매개변수 설명은 배달 모임과 동일) + */ @Override public List findOfflineMeetingList( OfflineMeetingCategory category, @@ -251,13 +311,14 @@ public List findOfflineMeetingList( Long lastMeetingId, LocalDateTime lastDateTime) { - // 카테고리 제한 조건 + // 오프라인 모임 카테고리 필터링 조건 BooleanExpression isCategory = category != null ? offlineMeeting.offlineMeetingCategory.eq(category) : null; - // 모임 시간 관련 expression + // 모임 시간 표현식 (오프라인 모임은 모임 시간 기준) DateTimeExpression meetingTimeExpr = offlineMeeting.meetingDate; + // 공통 모임 목록 조회 메서드 호출 return findMeetingList( isCategory, genderRestriction, @@ -268,12 +329,30 @@ public List findOfflineMeetingList( lastMeetingId, lastDateTime, meetingTimeExpr, - createOfflineMeetingProjection(), + createOfflineMeetingProjection(), // 오프라인 모임용 프로젝션 offlineMeeting, offlineMeeting.id ); } + /** + * 모임 목록 조회를 위한 공통 메서드 + * 배달 모임과 오프라인 모임 조회에 공통으로 사용되는 로직 + * + * @param 조회할 모임의 타입 (DeliveryMeeting 또는 OfflineMeeting) + * @param categoryCondition 카테고리 필터링 조건 (음식/모임 카테고리) + * @param genderRestriction 성별 제한 필터 (남자만/여자만/제한없음) + * @param maxParticipant 최대 참여자 수 상한값 + * @param minParticipant 최대 참여자 수 하한값 + * @param sortType 정렬 기준 (생성일/모임시간/참여자수) + * @param pageSize 페이지 크기 + * @param lastMeetingId 마지막으로 조회된 모임 ID (페이징) + * @param lastDateTime 마지막으로 조회된 시간 (페이징) + * @param meetingTimeExpr 모임 시간 표현식 (배달: 주문마감시간, 오프라인: 모임시간) + * @param projection 조회할 필드 정의 + * @param joinTable 조인할 테이블 (배달/오프라인 모임 테이블) + * @param joinTableId 조인 키 + */ private List findMeetingList( BooleanExpression categoryCondition, GenderRestriction genderRestriction, @@ -302,63 +381,90 @@ private List findMeetingList( meetingTimeExpr); return queryFactory - .select(projection) - .from(meeting) - .join(joinTable).on(joinTableId.eq(meeting.id)) + .select(projection) // DTO 변환을 위한 필드 선택 + .from(meeting) // 기본 모임 테이블 + .join(joinTable) // 배달/오프라인 모임 테이블과 조인 + .on(joinTableId.eq(meeting.id)) .where( - meeting.meetingStatus.eq(MeetingStatus.ACTIVE), - categoryCondition, - genderCondition, - participantCondition, - cursorCondition + meeting.meetingStatus.eq(MeetingStatus.ACTIVE), // 활성화된 모임만 조회 + categoryCondition, // 카테고리 필터 적용 + genderCondition, // 성별 제한 필터 적용 + participantCondition, // 참여자 수 제한 필터 적용 + cursorCondition // 페이징 조건 적용 ) - .orderBy(createOrderSpecifier(sortType, participantCount, meetingTimeExpr)) - .limit(pageSize + 1) - .fetch(); + .orderBy(createOrderSpecifier(sortType, participantCount, meetingTimeExpr)) // 정렬 조건 적용 + .limit(pageSize + 1) // No-Offset 페이징을 위해 limit + 1 + .fetch(); // 결과 조회 } + /** + * 배달 모임 조회를 위한 프로젝션 생성 + * MeetingListResponseDto에 매핑될 필드들을 정의 + * 배달 모임의 특성에 맞는 필드 매핑(예: 가게이름, 주문마감시간 등) + */ private ConstructorExpression createDeliveryMeetingProjection() { return Projections.constructor(MeetingListResponseDto.class, - meeting.id, - meeting.meetingName, - meeting.meetingDescription, - calculateParticipantCount(), - meeting.participantLimit.maxParticipants, - deliveryMeeting.storeName.as("location"), - meeting.createdAt, - deliveryMeeting.orderDeadline.as("dueDateTime"), - meeting.chatRoom.lastChatAt + meeting.id, // 모임 ID + meeting.meetingName, // 모임 이름 + meeting.meetingDescription, // 모임 설명 + calculateParticipantCount(), // 현재 참여자 수 계산 + meeting.participantLimit.maxParticipants, // 최대 참여 가능 인원 + deliveryMeeting.storeName.as("location"), // 가게 이름을 location으로 매핑 + meeting.createdAt, // 모임 생성 시간 + deliveryMeeting.orderDeadline.as("dueDateTime"), // 주문 마감시간을 dueDateTime으로 매핑 + meeting.chatRoom.lastChatAt // 마지막 채팅 시간 ); } + /** + * 오프라인 모임 조회를 위한 프로젝션 생성 + * MeetingListResponseDto에 매핑될 필드들을 정의 + * 오프라인 모임의 특성에 맞는 필드 매핑(예: 모임장소, 모임시간 등) + */ private ConstructorExpression createOfflineMeetingProjection() { return Projections.constructor(MeetingListResponseDto.class, - meeting.id, - meeting.meetingName, - meeting.meetingDescription, - calculateParticipantCount(), - meeting.participantLimit.maxParticipants, - offlineMeeting.meetingPlace.as("location"), - meeting.createdAt, - offlineMeeting.meetingDate.as("dueDateTime"), - meeting.chatRoom.lastChatAt + meeting.id, // 모임 ID + meeting.meetingName, // 모임 이름 + meeting.meetingDescription, // 모임 설명 + calculateParticipantCount(), // 현재 참여자 수 계산 + meeting.participantLimit.maxParticipants, // 최대 참여 가능 인원 + offlineMeeting.meetingPlace.as("location"), // 모임 장소를 location으로 매핑 + meeting.createdAt, // 모임 생성 시간 + offlineMeeting.meetingDate.as("dueDateTime"), // 모임 시간을 dueDateTime으로 매핑 + meeting.chatRoom.lastChatAt // 마지막 채팅 시간 ); } + /** + * 모임의 현재 참여자 수를 계산하는 서브쿼리 표현식 생성 + * meetingParticipant 테이블에서 해당 모임의 참여자 수를 카운트 + */ private NumberExpression calculateParticipantCount() { return Expressions.asNumber( JPAExpressions - .select(meetingParticipant.count()) - .from(meetingParticipant) - .where(meetingParticipant.meeting.eq(meeting)) + .select(meetingParticipant.count()) // 참여자 수 카운트 + .from(meetingParticipant) // 참여자 테이블 + .where(meetingParticipant.meeting.eq(meeting)) // 현재 모임의 참여자만 필터링 ).castToNum(Long.class); } + /** + * 성별 제한 조건 생성 + * 성별 제한이 있는 경우 해당 조건을 반환하고, 없으면 null 반환 + */ private BooleanExpression createGenderCondition(GenderRestriction genderRestriction) { return genderRestriction != null ? meeting.genderRestriction.eq(genderRestriction) : null; } + /** + * 참여자 수 제한 조건 생성 + * 최대 참여자 수의 상한과 하한을 기준으로 필터링 조건 생성 + * + * @param maxParticipant 최대 참여자 수 상한값 + * @param minParticipant 최대 참여자 수 하한값 + * @return 참여자 수 필터링 조건 + */ private BooleanExpression createParticipantCondition(Long maxParticipant, Long minParticipant) { if (maxParticipant == null && minParticipant == null) { return null; @@ -380,6 +486,10 @@ private BooleanExpression createParticipantCondition(Long maxParticipant, Long m return condition; } + /** + * 정렬 조건에 따른 커서 기반 페이징 조건 생성 + * No-Offset 페이징을 위한 동적 조건 생성 + */ private BooleanExpression createCursorCondition(Long lastMeetingId, LocalDateTime lastDateTime, MeetingSortType sortType, DateTimeExpression meetingTimeExpr) { if (lastMeetingId == null || lastDateTime == null) { @@ -388,17 +498,17 @@ private BooleanExpression createCursorCondition(Long lastMeetingId, LocalDateTim // 정렬 타입에 따른 커서 조건 생성 switch (sortType) { - case CREATED_AT: + case CREATED_AT: // 생성일 기준 정렬 return meeting.createdAt.lt(lastDateTime) .or(meeting.createdAt.eq(lastDateTime) .and(meeting.id.lt(lastMeetingId))); - case MEETING_TIME: + case MEETING_TIME: // 모임 시간 기준 정렬 return meetingTimeExpr.gt(lastDateTime) .or(meetingTimeExpr.eq(lastDateTime) .and(meeting.id.gt(lastMeetingId))); - case PARTICIPANT_COUNT: + case PARTICIPANT_COUNT: // 참여자 수 기준 정렬 NumberExpression participantCount = calculateParticipantCount(); NumberExpression lastParticipantCount = Expressions.asNumber( JPAExpressions @@ -417,24 +527,33 @@ private BooleanExpression createCursorCondition(Long lastMeetingId, LocalDateTim } } + /** + * 정렬 조건을 생성하는 메서드 + * 정렬 타입에 따라 적절한 정렬 조건을 생성 + * + * @param sortType 정렬 기준 (생성일/모임시간/참여자수) + * @param participantCount 참여자 수 표현식 + * @param meetingTimeExpr 모임 시간 표현식 + * @return 정렬 조건 + */ private OrderSpecifier createOrderSpecifier( MeetingSortType sortType, NumberExpression participantCount, Expression meetingTimeExpr) { if (sortType == null) { - return participantCount.desc(); + return participantCount.desc(); // 기본값: 참여자 수 내림차순 } switch (sortType) { - case CREATED_AT: + case CREATED_AT: // 생성일 기준 내림차순 return meeting.createdAt.desc(); - case MEETING_TIME: + case MEETING_TIME: // 모임 시간 기준 오름차순 return new OrderSpecifier<>(Order.ASC, meetingTimeExpr); - case PARTICIPANT_COUNT: + case PARTICIPANT_COUNT: // 참여자 수 기준 내림차순 return participantCount.desc(); default: - return participantCount.desc(); + return participantCount.desc(); // 기본값: 참여자 수 내림차순 } } } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateDeliveryMeetingRequestDto.java b/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateDeliveryMeetingRequestDto.java index cc89519a..e17cb782 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateDeliveryMeetingRequestDto.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateDeliveryMeetingRequestDto.java @@ -5,6 +5,7 @@ import com.example.eatmate.app.domain.meeting.domain.BankName; import com.example.eatmate.app.domain.meeting.domain.FoodCategory; import com.example.eatmate.app.domain.meeting.domain.GenderRestriction; +import com.example.eatmate.app.domain.meeting.domain.MeetingBackgroundType; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; @@ -57,5 +58,8 @@ public class CreateDeliveryMeetingRequestDto { @NotNull(message = "은행명은 필수입니다") private BankName bankName; + @NotNull(message = "배경 이미지 타입은 필수입니다") + private MeetingBackgroundType backgroundImageType; + private MultipartFile backgroundImage; } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateOfflineMeetingRequestDto.java b/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateOfflineMeetingRequestDto.java index cdf21986..54ebd1c9 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateOfflineMeetingRequestDto.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/dto/CreateOfflineMeetingRequestDto.java @@ -5,6 +5,7 @@ import org.springframework.web.multipart.MultipartFile; import com.example.eatmate.app.domain.meeting.domain.GenderRestriction; +import com.example.eatmate.app.domain.meeting.domain.MeetingBackgroundType; import com.example.eatmate.app.domain.meeting.domain.OfflineMeetingCategory; import jakarta.validation.constraints.Future; @@ -46,5 +47,8 @@ public class CreateOfflineMeetingRequestDto { @NotNull(message = "오프라인 모임 종류는 필수입니다") private OfflineMeetingCategory offlineMeetingCategory; + @NotNull(message = "배경 이미지 타입은 필수입니다") + private MeetingBackgroundType backgroundImageType; + private MultipartFile backgroundImage; } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/dto/MeetingDetailResponseDto.java b/src/main/java/com/example/eatmate/app/domain/meeting/dto/MeetingDetailResponseDto.java index bddd6c0f..ca3ebd55 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/dto/MeetingDetailResponseDto.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/dto/MeetingDetailResponseDto.java @@ -4,6 +4,7 @@ import java.util.List; import com.example.eatmate.app.domain.meeting.domain.GenderRestriction; +import com.example.eatmate.app.domain.meeting.domain.OfflineMeetingCategory; import lombok.Builder; import lombok.Getter; @@ -20,12 +21,14 @@ public class MeetingDetailResponseDto { private Boolean isOwner; private Boolean isCurrentUser; private List participants; + private OfflineMeetingCategory offlineMeetingCategory; private Long chatRoomId; @Builder private MeetingDetailResponseDto(String meetingType, String meetingName, String meetingDescription, GenderRestriction genderRestriction, String location, LocalDateTime dueDateTime, - String backgroundImage, Boolean isOwner, Boolean isCurrentUser, List participants, Long chatRoomId) { + String backgroundImage, Boolean isOwner, Boolean isCurrentUser, List participants, + OfflineMeetingCategory offlineMeetingCategory, Long chatRoomId) { this.meetingType = meetingType; this.meetingName = meetingName; this.meetingDescription = meetingDescription; @@ -36,6 +39,7 @@ private MeetingDetailResponseDto(String meetingType, String meetingName, String this.isOwner = isOwner; this.isCurrentUser = isCurrentUser; this.participants = participants; + this.offlineMeetingCategory = offlineMeetingCategory; this.chatRoomId = chatRoomId; } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateDeliveryMeetingRequestDto.java b/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateDeliveryMeetingRequestDto.java index aca34472..b18a802e 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateDeliveryMeetingRequestDto.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateDeliveryMeetingRequestDto.java @@ -6,6 +6,7 @@ import com.example.eatmate.app.domain.meeting.domain.BankName; import com.example.eatmate.app.domain.meeting.domain.FoodCategory; +import com.example.eatmate.app.domain.meeting.domain.MeetingBackgroundType; import jakarta.validation.constraints.Future; import jakarta.validation.constraints.Pattern; @@ -37,5 +38,7 @@ public class UpdateDeliveryMeetingRequestDto { private BankName bankName; + private MeetingBackgroundType backgroundImageType; + private MultipartFile backgroundImage; } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateOfflineMeetingRequestDto.java b/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateOfflineMeetingRequestDto.java index bd43f096..ae47bed5 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateOfflineMeetingRequestDto.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/dto/UpdateOfflineMeetingRequestDto.java @@ -4,6 +4,7 @@ import org.springframework.web.multipart.MultipartFile; +import com.example.eatmate.app.domain.meeting.domain.MeetingBackgroundType; import com.example.eatmate.app.domain.meeting.domain.OfflineMeetingCategory; import jakarta.validation.constraints.Future; @@ -27,5 +28,7 @@ public class UpdateOfflineMeetingRequestDto { private OfflineMeetingCategory offlineMeetingCategory; + private MeetingBackgroundType backgroundImageType; + private MultipartFile backgroundImage; } diff --git a/src/main/java/com/example/eatmate/app/domain/meeting/service/MeetingService.java b/src/main/java/com/example/eatmate/app/domain/meeting/service/MeetingService.java index 978f0164..275584bf 100644 --- a/src/main/java/com/example/eatmate/app/domain/meeting/service/MeetingService.java +++ b/src/main/java/com/example/eatmate/app/domain/meeting/service/MeetingService.java @@ -24,6 +24,7 @@ import com.example.eatmate.app.domain.meeting.domain.FoodCategory; import com.example.eatmate.app.domain.meeting.domain.GenderRestriction; import com.example.eatmate.app.domain.meeting.domain.Meeting; +import com.example.eatmate.app.domain.meeting.domain.MeetingBackgroundType; import com.example.eatmate.app.domain.meeting.domain.MeetingParticipant; import com.example.eatmate.app.domain.meeting.domain.MeetingStatus; import com.example.eatmate.app.domain.meeting.domain.OfflineMeeting; @@ -98,10 +99,13 @@ public CreateDeliveryMeetingResponseDto createDeliveryMeeting( createDeliveryMeetingRequestDto.getIsLimited() ); - Image backgroundImage = Optional.ofNullable(createDeliveryMeetingRequestDto.getBackgroundImage()) - .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) - .orElse(null); + Image backgroundImage = null; + if (createDeliveryMeetingRequestDto.getBackgroundImageType() == MeetingBackgroundType.CUSTOM) { + backgroundImage = Optional.ofNullable(createDeliveryMeetingRequestDto.getBackgroundImage()) + .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) + .orElseThrow(() -> new CommonException(ErrorCode.IMAGE_REQUIRED_FOR_CUSTOM)); + } DeliveryMeeting deliveryMeeting = DeliveryMeeting.builder() .meetingName(createDeliveryMeetingRequestDto.getMeetingName()) .meetingDescription(createDeliveryMeetingRequestDto.getMeetingDescription()) @@ -118,6 +122,7 @@ public CreateDeliveryMeetingResponseDto createDeliveryMeeting( .accountNumber(createDeliveryMeetingRequestDto.getAccountNumber()) .bankName(createDeliveryMeetingRequestDto.getBankName()) .backgroundImage(backgroundImage) + .backgroundType(createDeliveryMeetingRequestDto.getBackgroundImageType()) .build(); deliveryMeeting = deliveryMeetingRepository.save(deliveryMeeting); @@ -144,10 +149,13 @@ public CreateOfflineMeetingResponseDto createOfflineMeeting( createOfflineMeetingRequestDto.getIsLimited() ); - Image backgroundImage = Optional.ofNullable(createOfflineMeetingRequestDto.getBackgroundImage()) - .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) - .orElse(null); + Image backgroundImage = null; + if (createOfflineMeetingRequestDto.getBackgroundImageType() == MeetingBackgroundType.CUSTOM) { + backgroundImage = Optional.ofNullable(createOfflineMeetingRequestDto.getBackgroundImage()) + .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) + .orElseThrow(() -> new CommonException(ErrorCode.IMAGE_REQUIRED_FOR_CUSTOM)); + } OfflineMeeting offlineMeeting = OfflineMeeting.builder() .meetingName(createOfflineMeetingRequestDto.getMeetingName()) .meetingDescription(createOfflineMeetingRequestDto.getMeetingDescription()) @@ -161,6 +169,7 @@ public CreateOfflineMeetingResponseDto createOfflineMeeting( .meetingDate(createOfflineMeetingRequestDto.getMeetingDate()) .offlineMeetingCategory(createOfflineMeetingRequestDto.getOfflineMeetingCategory()) .backgroundImage(backgroundImage) + .backgroundType(createOfflineMeetingRequestDto.getBackgroundImageType()) .build(); offlineMeeting = offlineMeetingRepository.save(offlineMeeting); @@ -284,14 +293,25 @@ public MeetingDetailResponseDto getMeetingDetail(Long meetingId, UserDetails use .genderRestriction(meeting.getGenderRestriction()) .location(getLocation(meeting)) .dueDateTime(getDueDateTime(meeting)) - .backgroundImage(Optional.ofNullable(meeting.getBackgroundImage()).map(Image::getImageUrl).orElse(null)) + .backgroundImage(getBackgroundImageUrl(meeting)) .isOwner(isOwner(meeting, member.getMemberId())) .isCurrentUser(isCurrentUser(meeting, member)) .participants(participants) + .offlineMeetingCategory( + meeting instanceof OfflineMeeting ? ((OfflineMeeting)meeting).getOfflineMeetingCategory() : null) .chatRoomId(meeting.getChatRoom().getId()) .build(); } + private String getBackgroundImageUrl(Meeting meeting) { + if (meeting.getBackgroundType() == MeetingBackgroundType.CUSTOM) { + return Optional.ofNullable(meeting.getBackgroundImage()) + .map(Image::getImageUrl) + .orElse(null); + } + return meeting.getBackgroundType().getImageUrl(); + } + private String getLocation(Meeting meeting) { if (meeting instanceof DeliveryMeeting) { return ((DeliveryMeeting)meeting).getPickupLocation(); @@ -501,9 +521,12 @@ public void updateOfflineMeeting(Long meetingId, UpdateOfflineMeetingRequestDto UserDetails userDetails) { Member member = securityUtils.getMember(userDetails); - Image backgroundImage = Optional.ofNullable(updateOfflineMeetingRequestDto.getBackgroundImage()) - .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) - .orElse(null); + Image backgroundImage = null; + if (updateOfflineMeetingRequestDto.getBackgroundImageType() == MeetingBackgroundType.CUSTOM) { + backgroundImage = Optional.ofNullable(updateOfflineMeetingRequestDto.getBackgroundImage()) + .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) + .orElseThrow(() -> new CommonException(ErrorCode.IMAGE_REQUIRED_FOR_CUSTOM)); + } OfflineMeeting offlineMeeting = offlineMeetingRepository.findById(meetingId) .orElseThrow(() -> new CommonException(ErrorCode.MEETING_NOT_FOUND)); @@ -518,6 +541,7 @@ public void updateOfflineMeeting(Long meetingId, UpdateOfflineMeetingRequestDto updateOfflineMeetingRequestDto.getMeetingPlace(), updateOfflineMeetingRequestDto.getMeetingDate(), updateOfflineMeetingRequestDto.getOfflineMeetingCategory(), + updateOfflineMeetingRequestDto.getBackgroundImageType(), backgroundImage ); } @@ -528,9 +552,12 @@ public void updateDeliveryMeeting(Long meetingId, UpdateDeliveryMeetingRequestDt UserDetails userDetails) { Member member = securityUtils.getMember(userDetails); - Image backgroundImage = Optional.ofNullable(updateDeliveryMeetingRequestDto.getBackgroundImage()) - .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) - .orElse(null); + Image backgroundImage = null; + if (updateDeliveryMeetingRequestDto.getBackgroundImageType() == MeetingBackgroundType.CUSTOM) { + backgroundImage = Optional.ofNullable(updateDeliveryMeetingRequestDto.getBackgroundImage()) + .map(image -> imageSaveService.uploadImage(image, MEETING_BACKGROUND)) + .orElseThrow(() -> new CommonException(ErrorCode.IMAGE_REQUIRED_FOR_CUSTOM)); + } DeliveryMeeting deliveryMeeting = deliveryMeetingRepository.findById(meetingId) .orElseThrow(() -> new CommonException(ErrorCode.MEETING_NOT_FOUND)); @@ -547,6 +574,7 @@ public void updateDeliveryMeeting(Long meetingId, UpdateDeliveryMeetingRequestDt updateDeliveryMeetingRequestDto.getPickupLocation(), updateDeliveryMeetingRequestDto.getAccountNumber(), updateDeliveryMeetingRequestDto.getBankName(), + updateDeliveryMeetingRequestDto.getBackgroundImageType(), backgroundImage ); } diff --git a/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java b/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java index 594f615e..ffcd38da 100644 --- a/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java +++ b/src/main/java/com/example/eatmate/global/config/error/ErrorCode.java @@ -14,6 +14,9 @@ public enum ErrorCode { NO_RESOURCE_FOUND(404, "NO_RESOURCE_FOUND", "해당 리소스를 찾을 수 없습니다."), INVALID_EMAIL_DOMAIN(400, "INVALID_EMAIL_DOMAIN", "가천대학교 이메일이 아닙니다."), INVALID_PARAMETER_TYPE(400, "INVALID_PARAMETER_TYPE", "적절하지 않은 파라미터 타입입니다."), + VALIDATION_ERROR(400, "VALIDATION_ERROR", "유효성 검사 오류입니다."), + INVALID_REQUEST_FORMAT(400, "INVALID_REQUEST_FORMAT", "올바르지 않은 요청 형식입니다."), + UNSUPPORTED_MEDIA_TYPE(415, "UNSUPPORTED_MEDIA_TYPE", "지원하지 않는 미디어 타입입니다."), //회원 USER_NOT_FOUND(404, "USER_NOT_FOUND", "유저를 찾을 수 없습니다."), @@ -42,6 +45,7 @@ public enum ErrorCode { ALREADY_DELETED_MEETING(400, "ALREADY_DELETED_MEETING", "이미 삭제된 모임입니다."), CANNOT_DELETE_MEETING_WITH_PARTICIPANTS(400, "CANNOT_DELETE_MEETING_WITH_PARTICIPANTS", "참가자가 있는 모임은 삭제할 수 없습니다."), INVALID_MEETING_TYPE(400, "INVALID_MEETING_TYPE", "올바르지 않은 모임 타입입니다."), + IMAGE_REQUIRED_FOR_CUSTOM(400, "IMAGE_REQUIRED_FOR_CUSTOM", "커스텀 배경 이미지가 필요합니다."), // 신고 SELF_REPORT_NOT_ALLOWED(400, "SELF_REPORT_NOT_ALLOWED", "자기 자신을 신고할 수 없습니다."), diff --git a/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java b/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java index f9868a46..ec92dc6b 100644 --- a/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/example/eatmate/global/config/error/exception/GlobalExceptionHandler.java @@ -5,9 +5,12 @@ import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.env.Environment; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.transaction.TransactionSystemException; +import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -19,6 +22,7 @@ import com.example.eatmate.global.config.error.ErrorCode; import com.example.eatmate.global.config.error.ErrorResponse; import com.example.eatmate.global.response.GlobalResponseDto; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; @@ -170,6 +174,42 @@ public ResponseEntity> handleMissingServletRequestPara .body(GlobalResponseDto.fail(errorCode, errorMessage)); } + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity> handleDataIntegrityViolationException( + DataIntegrityViolationException ex) { + ErrorCode errorCode = ErrorCode.VALIDATION_ERROR; + String errorMessage = "필수 입력 필드가 누락되었습니다."; + + return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) + .body(GlobalResponseDto.fail(errorCode, errorMessage)); + } + + @ExceptionHandler(HttpMessageConversionException.class) + public ResponseEntity> handleHttpMessageConversionException( + HttpMessageConversionException ex) { + ErrorCode errorCode = ErrorCode.INVALID_REQUEST_FORMAT; + String errorMessage = "잘못된 요청 형식입니다. multipart/form-data 형식으로 요청해주세요."; + + // MultipartFile 변환 실패 관련 에러인 경우 + if (ex.getCause() instanceof InvalidDefinitionException + && ex.getMessage().contains("MultipartFile")) { + errorMessage = "파일 업로드는 multipart/form-data 형식으로 요청해주세요."; + } + + return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) + .body(GlobalResponseDto.fail(errorCode, errorMessage)); + } + + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) + public ResponseEntity> handleHttpMediaTypeNotSupportedException( + HttpMediaTypeNotSupportedException ex) { + ErrorCode errorCode = ErrorCode.UNSUPPORTED_MEDIA_TYPE; + String errorMessage = "지원하지 않는 Content-Type입니다. multipart/form-data 형식으로 요청해주세요."; + + return ResponseEntity.status(HttpStatus.valueOf(errorCode.getStatus())) + .body(GlobalResponseDto.fail(errorCode, errorMessage)); + } + public void handleUnexpectedError(Exception ex) { if (isProd(environment.getActiveProfiles())) { githubIssueGenerator.create(ex);