diff --git a/src/main/java/com/project/dorumdorum/domain/notice/application/usecase/LoadNoticeDetailUseCase.java b/src/main/java/com/project/dorumdorum/domain/notice/application/usecase/LoadNoticeDetailUseCase.java new file mode 100644 index 00000000..d6cc37d7 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notice/application/usecase/LoadNoticeDetailUseCase.java @@ -0,0 +1,32 @@ +package com.project.dorumdorum.domain.notice.application.usecase; + +import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse; +import com.project.dorumdorum.domain.notice.domain.entity.Notice; +import com.project.dorumdorum.domain.notice.domain.repository.NoticeRepository; +import com.project.dorumdorum.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.project.dorumdorum.global.exception.code.status.CommonErrorStatus._NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class LoadNoticeDetailUseCase { + + private final NoticeRepository noticeRepository; + + @Transactional(readOnly = true) + public NoticeResponse execute(String noticeNo) { + Notice notice = noticeRepository.findById(noticeNo) + .orElseThrow(() -> new RestApiException(_NOT_FOUND)); + + return new NoticeResponse( + notice.getNoticeNo(), + notice.getTitle(), + notice.getContent(), + notice.getWrittenDate(), + notice.getOriginalLink() + ); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notice/ui/LoadNoticeDetailController.java b/src/main/java/com/project/dorumdorum/domain/notice/ui/LoadNoticeDetailController.java new file mode 100644 index 00000000..fbb92b3f --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notice/ui/LoadNoticeDetailController.java @@ -0,0 +1,21 @@ +package com.project.dorumdorum.domain.notice.ui; + +import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse; +import com.project.dorumdorum.domain.notice.application.usecase.LoadNoticeDetailUseCase; +import com.project.dorumdorum.domain.notice.ui.spec.LoadNoticeDetailApiSpec; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LoadNoticeDetailController implements LoadNoticeDetailApiSpec { + + private final LoadNoticeDetailUseCase loadNoticeDetailUseCase; + + @Override + public ResponseEntity loadNotice(@PathVariable String noticeNo) { + return ResponseEntity.ok(loadNoticeDetailUseCase.execute(noticeNo)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notice/ui/spec/LoadNoticeDetailApiSpec.java b/src/main/java/com/project/dorumdorum/domain/notice/ui/spec/LoadNoticeDetailApiSpec.java new file mode 100644 index 00000000..afcab243 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notice/ui/spec/LoadNoticeDetailApiSpec.java @@ -0,0 +1,16 @@ +package com.project.dorumdorum.domain.notice.ui.spec; + +import com.project.dorumdorum.domain.notice.application.dto.response.NoticeResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag(name = "Notice", description = "공지사항 API") +public interface LoadNoticeDetailApiSpec { + + @Operation(summary = "공지사항 상세 조회", description = "공지사항을 단건 조회합니다.") + @GetMapping("/api/notices/{noticeNo}") + ResponseEntity loadNotice(@PathVariable String noticeNo); +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/application/dto/request/NotificationSettingRequest.java b/src/main/java/com/project/dorumdorum/domain/notification/application/dto/request/NotificationSettingRequest.java new file mode 100644 index 00000000..0887d3e9 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/application/dto/request/NotificationSettingRequest.java @@ -0,0 +1,11 @@ +package com.project.dorumdorum.domain.notification.application.dto.request; + +public record NotificationSettingRequest( + boolean enabled, + boolean applicants, + boolean applicantResult, + boolean chat, + boolean notice, + boolean schedule +) { +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/application/dto/response/NotificationSettingResponse.java b/src/main/java/com/project/dorumdorum/domain/notification/application/dto/response/NotificationSettingResponse.java new file mode 100644 index 00000000..f5fb4ff0 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/application/dto/response/NotificationSettingResponse.java @@ -0,0 +1,23 @@ +package com.project.dorumdorum.domain.notification.application.dto.response; + +import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; + +public record NotificationSettingResponse( + boolean enabled, + boolean applicants, + boolean applicantResult, + boolean chat, + boolean notice, + boolean schedule +) { + public static NotificationSettingResponse from(NotificationSetting setting) { + return new NotificationSettingResponse( + setting.isEnabled(), + setting.isApplicants(), + setting.isApplicantResult(), + setting.isChat(), + setting.isNotice(), + setting.isSchedule() + ); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/application/usecase/LoadNotificationSettingUseCase.java b/src/main/java/com/project/dorumdorum/domain/notification/application/usecase/LoadNotificationSettingUseCase.java new file mode 100644 index 00000000..d12d7f64 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/application/usecase/LoadNotificationSettingUseCase.java @@ -0,0 +1,22 @@ +package com.project.dorumdorum.domain.notification.application.usecase; + +import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; +import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; +import com.project.dorumdorum.domain.notification.domain.repository.NotificationSettingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class LoadNotificationSettingUseCase { + + private final NotificationSettingRepository notificationSettingRepository; + + @Transactional(readOnly = true) + public NotificationSettingResponse execute(String userNo) { + NotificationSetting setting = notificationSettingRepository.findByUserNo(userNo) + .orElseGet(() -> NotificationSetting.defaultFor(userNo)); + return NotificationSettingResponse.from(setting); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/application/usecase/UpdateNotificationSettingUseCase.java b/src/main/java/com/project/dorumdorum/domain/notification/application/usecase/UpdateNotificationSettingUseCase.java new file mode 100644 index 00000000..fbcc3430 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/application/usecase/UpdateNotificationSettingUseCase.java @@ -0,0 +1,25 @@ +package com.project.dorumdorum.domain.notification.application.usecase; + +import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; +import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; +import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; +import com.project.dorumdorum.domain.notification.domain.repository.NotificationSettingRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UpdateNotificationSettingUseCase { + + private final NotificationSettingRepository notificationSettingRepository; + + @Transactional + public NotificationSettingResponse execute(String userNo, NotificationSettingRequest request) { + NotificationSetting setting = notificationSettingRepository.findByUserNo(userNo) + .orElseGet(() -> NotificationSetting.defaultFor(userNo)); + setting.update(request); + NotificationSetting saved = notificationSettingRepository.save(setting); + return NotificationSettingResponse.from(saved); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationSetting.java b/src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationSetting.java new file mode 100644 index 00000000..f7b2e073 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationSetting.java @@ -0,0 +1,69 @@ +package com.project.dorumdorum.domain.notification.domain.entity; + +import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; +import com.project.dorumdorum.global.common.BaseEntity; +import io.hypersistence.utils.hibernate.id.Tsid; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@Table(name = "notification_settings") +public class NotificationSetting extends BaseEntity { + + @Id + @Tsid + private String notificationSettingNo; + + @Column(nullable = false, unique = true) + private String userNo; + + @Column(nullable = false) + private boolean enabled; + + @Column(nullable = false) + private boolean applicants; + + @Column(nullable = false) + private boolean applicantResult; + + @Column(nullable = false) + private boolean chat; + + @Column(nullable = false) + private boolean notice; + + @Column(nullable = false) + private boolean schedule; + + public static NotificationSetting defaultFor(String userNo) { + return NotificationSetting.builder() + .userNo(userNo) + .enabled(true) + .applicants(true) + .applicantResult(true) + .chat(true) + .notice(true) + .schedule(false) + .build(); + } + + public void update(NotificationSettingRequest request) { + this.enabled = request.enabled(); + this.applicants = request.applicants(); + this.applicantResult = request.applicantResult(); + this.chat = request.chat(); + this.notice = request.notice(); + this.schedule = request.schedule(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationDeviceRepository.java b/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationDeviceRepository.java index fe78e487..f72fab6a 100644 --- a/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationDeviceRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationDeviceRepository.java @@ -2,6 +2,9 @@ import com.project.dorumdorum.domain.notification.domain.entity.Device; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -13,5 +16,8 @@ public interface NotificationDeviceRepository extends JpaRepository findByUserNo(String userNo); List findByFcmTokenIn(List fcmTokens); -} + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Device d SET d.deletedAt = CURRENT_TIMESTAMP WHERE d.userNo = :userNo AND d.deletedAt IS NULL") + void deleteAllByUserNo(@Param("userNo") String userNo); +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationRepository.java b/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationRepository.java index 40b92cb6..2703316c 100644 --- a/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationRepository.java @@ -2,10 +2,17 @@ import com.project.dorumdorum.domain.notification.domain.entity.Notification; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; public interface NotificationRepository extends JpaRepository, NotificationQueryRepository { Optional findByNotificationNoAndRecipientNo(String notificationNo, String recipientNo); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Notification n SET n.deletedAt = CURRENT_TIMESTAMP WHERE n.recipientNo = :userNo AND n.deletedAt IS NULL") + void deleteAllByRecipientNo(@Param("userNo") String userNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationSettingRepository.java b/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationSettingRepository.java new file mode 100644 index 00000000..f5b7b718 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationSettingRepository.java @@ -0,0 +1,10 @@ +package com.project.dorumdorum.domain.notification.domain.repository; + +import com.project.dorumdorum.domain.notification.domain.entity.NotificationSetting; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface NotificationSettingRepository extends JpaRepository { + Optional findByUserNo(String userNo); +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/ui/NotificationSettingController.java b/src/main/java/com/project/dorumdorum/domain/notification/ui/NotificationSettingController.java new file mode 100644 index 00000000..120299ba --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/ui/NotificationSettingController.java @@ -0,0 +1,33 @@ +package com.project.dorumdorum.domain.notification.ui; + +import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; +import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; +import com.project.dorumdorum.domain.notification.application.usecase.LoadNotificationSettingUseCase; +import com.project.dorumdorum.domain.notification.application.usecase.UpdateNotificationSettingUseCase; +import com.project.dorumdorum.domain.notification.ui.spec.NotificationSettingApiSpec; +import com.project.dorumdorum.global.annotation.CurrentUser; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class NotificationSettingController implements NotificationSettingApiSpec { + + private final LoadNotificationSettingUseCase loadNotificationSettingUseCase; + private final UpdateNotificationSettingUseCase updateNotificationSettingUseCase; + + @Override + public ResponseEntity load(@CurrentUser String userNo) { + return ResponseEntity.ok(loadNotificationSettingUseCase.execute(userNo)); + } + + @Override + public ResponseEntity update( + @CurrentUser String userNo, + @RequestBody NotificationSettingRequest request + ) { + return ResponseEntity.ok(updateNotificationSettingUseCase.execute(userNo, request)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/notification/ui/spec/NotificationSettingApiSpec.java b/src/main/java/com/project/dorumdorum/domain/notification/ui/spec/NotificationSettingApiSpec.java new file mode 100644 index 00000000..6fc7d71f --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/notification/ui/spec/NotificationSettingApiSpec.java @@ -0,0 +1,26 @@ +package com.project.dorumdorum.domain.notification.ui.spec; + +import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; +import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "Notification") +public interface NotificationSettingApiSpec { + + @Operation(summary = "내 알림 설정 조회 API", description = "사용자별 알림 수신 설정을 조회합니다. 저장된 설정이 없으면 기본값을 반환합니다.") + @GetMapping("/api/users/me/notification-settings") + ResponseEntity load(@Parameter(hidden = true) String userNo); + + @Operation(summary = "내 알림 설정 저장 API", description = "사용자별 알림 수신 설정을 저장합니다.") + @PutMapping("/api/users/me/notification-settings") + ResponseEntity update( + @Parameter(hidden = true) String userNo, + @RequestBody NotificationSettingRequest request + ); +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/ChecklistFilterRequest.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/ChecklistFilterRequest.java index 0451f1b9..6e7859ec 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/ChecklistFilterRequest.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/ChecklistFilterRequest.java @@ -8,6 +8,7 @@ public record ChecklistFilterRequest( @NotNull SortType sortType, String cursor, + String keyword, RoomType roomType, ResidencePeriod residencePeriod, Integer capacity, @@ -36,6 +37,71 @@ public record ChecklistFilterRequest( StudyType study, TrashCanType trashCan ) { + public ChecklistFilterRequest( + SortType sortType, + String cursor, + RoomType roomType, + ResidencePeriod residencePeriod, + Integer capacity, + String bedtime, + String wakeUp, + ReturnHomeType returnHome, + String returnHomeTime, + CleaningType cleaning, + PhoneCallType phoneCall, + SleepLightType sleepLight, + SleepHabitType sleepHabit, + SnoringType snoring, + ShowerTimeType showerTime, + EatingType eating, + LightsOutType lightsOut, + String lightsOutTime, + HomeVisitType homeVisit, + SmokingType smoking, + RefrigeratorType refrigerator, + String hairDryer, + AlarmType alarm, + EarphoneType earphone, + KeyskinType keyskin, + HeatType heat, + ColdType cold, + StudyType study, + TrashCanType trashCan + ) { + this( + sortType, + cursor, + null, + roomType, + residencePeriod, + capacity, + bedtime, + wakeUp, + returnHome, + returnHomeTime, + cleaning, + phoneCall, + sleepLight, + sleepHabit, + snoring, + showerTime, + eating, + lightsOut, + lightsOutTime, + homeVisit, + smoking, + refrigerator, + hairDryer, + alarm, + earphone, + keyskin, + heat, + cold, + study, + trashCan + ); + } + public enum SortType { LATEST, REMAINING diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RoomDetailResponse.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RoomDetailResponse.java new file mode 100644 index 00000000..378eaa18 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RoomDetailResponse.java @@ -0,0 +1,30 @@ +package com.project.dorumdorum.domain.room.application.dto.response; + +import com.project.dorumdorum.domain.room.domain.entity.RoomStatus; +import com.project.dorumdorum.domain.room.domain.entity.RoomType; + +public record RoomDetailResponse( + String roomNo, + RoomType roomType, + Integer capacity, + Integer currentMateCount, + Integer remaining, + String title, + String notes, + String hostUserNo, + String hostName, + String hostNickname, + String hostMajor, + String hostStudentYear, + String residencePeriod, + RoomStatus roomStatus, + boolean liked, + AppliedStatus appliedStatus, + boolean isMyRoom +) { + public enum AppliedStatus { + NONE, + WAITING, + APPROVED + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/LoadRoomDetailUseCase.java b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/LoadRoomDetailUseCase.java new file mode 100644 index 00000000..820013c4 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/LoadRoomDetailUseCase.java @@ -0,0 +1,70 @@ +package com.project.dorumdorum.domain.room.application.usecase; + +import com.project.dorumdorum.domain.room.application.dto.response.RoomDetailResponse; +import com.project.dorumdorum.domain.room.domain.entity.Direction; +import com.project.dorumdorum.domain.room.domain.entity.Room; +import com.project.dorumdorum.domain.room.domain.repository.RoomLikeRepository; +import com.project.dorumdorum.domain.room.domain.repository.RoomRequestRepository; +import com.project.dorumdorum.domain.room.domain.service.RoomService; +import com.project.dorumdorum.domain.roommate.domain.service.RoommateService; +import com.project.dorumdorum.domain.user.domain.entity.User; +import com.project.dorumdorum.domain.user.domain.service.UserService; +import com.project.dorumdorum.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.project.dorumdorum.global.exception.code.status.RoomErrorStatus.ROOM_NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class LoadRoomDetailUseCase { + + private final RoomService roomService; + private final UserService userService; + private final RoomLikeRepository roomLikeRepository; + private final RoomRequestRepository roomRequestRepository; + private final RoommateService roommateService; + + @Transactional(readOnly = true) + public RoomDetailResponse execute(String userNo, String roomNo) { + Room room = roomService.findById(roomNo); + if (room.isDeleted()) { + throw new RestApiException(ROOM_NOT_FOUND); + } + + User host = userService.findById(room.getHostUserNo()); + boolean liked = roomLikeRepository.existsByUserNoAndRoom(userNo, room); + boolean isMyRoom = room.isHost(userNo) || roommateService.isUserRoommate(userNo, roomNo); + RoomDetailResponse.AppliedStatus appliedStatus = resolveAppliedStatus(userNo, room, isMyRoom); + + return new RoomDetailResponse( + room.getRoomNo(), + room.getRoomType(), + room.getCapacity(), + room.getCurrentMateCount(), + room.getRemaining(), + room.getTitle(), + room.getNotes(), + room.getHostUserNo(), + host.getName(), + host.getNickname(), + host.getMajor(), + host.getStudentNo().substring(2, 4), + room.getResidencePeriod().name(), + room.getRoomStatus(), + liked, + appliedStatus, + isMyRoom + ); + } + + private RoomDetailResponse.AppliedStatus resolveAppliedStatus(String userNo, Room room, boolean isMyRoom) { + if (isMyRoom) { + return RoomDetailResponse.AppliedStatus.APPROVED; + } + return roomRequestRepository.findByUserNoAndRoomAndDirection(userNo, room, Direction.USER_TO_ROOM).isPresent() + ? RoomDetailResponse.AppliedStatus.WAITING + : RoomDetailResponse.AppliedStatus.NONE; + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java index 3cbc3e75..272f8a57 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomLikeRepository.java @@ -18,5 +18,8 @@ public interface RoomLikeRepository extends JpaRepository { @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE RoomLike l SET l.deletedAt = CURRENT_TIMESTAMP WHERE l.room = :room AND l.deletedAt IS NULL") void deleteAllByRoom(@Param("room") Room room); -} + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RoomLike l SET l.deletedAt = CURRENT_TIMESTAMP WHERE l.userNo = :userNo AND l.deletedAt IS NULL") + void deleteAllByUserNo(@Param("userNo") String userNo); +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRepository.java b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRepository.java index 497e0fc5..6e335bcb 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRepository.java @@ -15,4 +15,6 @@ public interface RoomRepository extends JpaRepository, RoomQueryRe @Query("select room from Room room where room.roomNo = :roomNo and room.deletedAt is null") Optional findByRoomNoForUpdate(@Param("roomNo") String roomNo); + boolean existsByHostUserNoAndDeletedAtIsNull(String hostUserNo); + } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java index 8c28d784..739aa76b 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomRequestRepository.java @@ -18,5 +18,8 @@ public interface RoomRequestRepository extends JpaRepository findByCursor( eqRoomType(request), eqResidencePeriod(request), eqCapacity(request), + keywordContains(request), request.sortType() == ChecklistFilterRequest.SortType.REMAINING ? remainingCursorPredicate(cursorRemaining, cursorCreatedAt, cursorId) : cursorPredicate(cursorCreatedAt, cursorId) @@ -112,7 +113,8 @@ public long countByFilter(Gender gender, ChecklistFilterRequest request) { room.gender.eq(gender), eqRoomType(request), eqResidencePeriod(request), - eqCapacity(request) + eqCapacity(request), + keywordContains(request) ) .fetchOne(); @@ -123,6 +125,8 @@ private long countByFilterWithChecklist(Gender gender, ChecklistFilterRequest re StringBuilder sql = new StringBuilder(""" SELECT COUNT(*) FROM room r + LEFT JOIN users u + ON u.user_no = r.host_user_no WHERE r.room_status = :roomStatus AND r.deleted_at IS NULL AND r.gender = :gender @@ -135,6 +139,7 @@ SELECT COUNT(*) appendCondition(sql, params, "r.room_type", "roomType", enumName(request.roomType())); appendCondition(sql, params, "r.residence_period", "residencePeriod", enumName(request.residencePeriod())); appendCondition(sql, params, "r.capacity", "capacity", request.capacity()); + appendKeywordCondition(sql, params, "r", "u", request.keyword()); sql.append(""" AND EXISTS ( @@ -200,6 +205,7 @@ JOIN LATERAL ( appendCondition(sql, params, "r.room_type", "roomType", enumName(request.roomType())); appendCondition(sql, params, "r.residence_period", "residencePeriod", enumName(request.residencePeriod())); appendCondition(sql, params, "r.capacity", "capacity", request.capacity()); + appendKeywordCondition(sql, params, "r", "u", request.keyword()); appendCursorCondition(sql, params, request.sortType(), cursorCreatedAt, cursorId, cursorRemaining); sql.append("\n ORDER BY "); @@ -347,6 +353,15 @@ private BooleanExpression eqCapacity(ChecklistFilterRequest request) { return request.capacity() == null ? null : room.capacity.eq(request.capacity()); } + private BooleanExpression keywordContains(ChecklistFilterRequest request) { + if (isBlank(request.keyword())) { + return null; + } + String keyword = request.keyword().trim(); + return room.title.containsIgnoreCase(keyword) + .or(user.nickname.containsIgnoreCase(keyword)); + } + private boolean hasChecklistFilters(ChecklistFilterRequest request) { return !isBlank(request.bedtime()) || !isBlank(request.wakeUp()) @@ -420,6 +435,25 @@ private void appendRoomRuleConditions( appendCondition(sql, params, "rr.trash_can", "trashCan", enumName(request.trashCan())); } + private void appendKeywordCondition( + StringBuilder sql, + Map params, + String roomAlias, + String userAlias, + String keyword + ) { + if (isBlank(keyword)) { + return; + } + + sql.append("\n AND (LOWER(") + .append(roomAlias) + .append(".title) LIKE :keyword OR LOWER(") + .append(userAlias) + .append(".nickname) LIKE :keyword)"); + params.put("keyword", "%" + keyword.trim().toLowerCase() + "%"); + } + private void appendCursorCondition( StringBuilder sql, Map params, diff --git a/src/main/java/com/project/dorumdorum/domain/room/ui/LoadRoomDetailController.java b/src/main/java/com/project/dorumdorum/domain/room/ui/LoadRoomDetailController.java new file mode 100644 index 00000000..4c3d002b --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/ui/LoadRoomDetailController.java @@ -0,0 +1,25 @@ +package com.project.dorumdorum.domain.room.ui; + +import com.project.dorumdorum.domain.room.application.dto.response.RoomDetailResponse; +import com.project.dorumdorum.domain.room.application.usecase.LoadRoomDetailUseCase; +import com.project.dorumdorum.domain.room.ui.spec.LoadRoomDetailApiSpec; +import com.project.dorumdorum.global.annotation.CurrentUser; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LoadRoomDetailController implements LoadRoomDetailApiSpec { + + private final LoadRoomDetailUseCase loadRoomDetailUseCase; + + @Override + public ResponseEntity load( + @CurrentUser String userNo, + @PathVariable String roomNo + ) { + return ResponseEntity.ok(loadRoomDetailUseCase.execute(userNo, roomNo)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/ui/spec/LoadRoomDetailApiSpec.java b/src/main/java/com/project/dorumdorum/domain/room/ui/spec/LoadRoomDetailApiSpec.java new file mode 100644 index 00000000..3b8a290a --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/ui/spec/LoadRoomDetailApiSpec.java @@ -0,0 +1,20 @@ +package com.project.dorumdorum.domain.room.ui.spec; + +import com.project.dorumdorum.domain.room.application.dto.response.RoomDetailResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag(name = "Room") +public interface LoadRoomDetailApiSpec { + + @Operation(summary = "방 상세 조회 API", description = "방 상세 화면에 필요한 모집방 정보를 단건 조회합니다.") + @GetMapping("/api/rooms/{roomNo}") + ResponseEntity load( + @Parameter(hidden = true) String userNo, + @PathVariable String roomNo + ); +} diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/application/dto/response/RoommateHistoryResponse.java b/src/main/java/com/project/dorumdorum/domain/roommate/application/dto/response/RoommateHistoryResponse.java new file mode 100644 index 00000000..3093f3ab --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/roommate/application/dto/response/RoommateHistoryResponse.java @@ -0,0 +1,27 @@ +package com.project.dorumdorum.domain.roommate.application.dto.response; + +import com.project.dorumdorum.domain.room.domain.entity.RoomType; + +import java.time.LocalDateTime; + +public record RoommateHistoryResponse( + String historyNo, + String roomNo, + String roomTitle, + RoomType roomType, + Integer capacity, + String roommateUserNo, + String name, + String nickname, + String studentNo, + String major, + String studentYear, + LocalDateTime startedAt, + LocalDateTime endedAt, + Relation relation +) { + public enum Relation { + CURRENT, + PAST + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/application/usecase/LoadMyRoommateHistoryUseCase.java b/src/main/java/com/project/dorumdorum/domain/roommate/application/usecase/LoadMyRoommateHistoryUseCase.java new file mode 100644 index 00000000..7e5fd1a8 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/roommate/application/usecase/LoadMyRoommateHistoryUseCase.java @@ -0,0 +1,21 @@ +package com.project.dorumdorum.domain.roommate.application.usecase; + +import com.project.dorumdorum.domain.roommate.application.dto.response.RoommateHistoryResponse; +import com.project.dorumdorum.domain.roommate.domain.service.RoommateService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class LoadMyRoommateHistoryUseCase { + + private final RoommateService roommateService; + + @Transactional(readOnly = true) + public List execute(String userNo) { + return roommateService.findMyRoommateHistory(userNo); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateQueryRepository.java b/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateQueryRepository.java index d8c79677..e2393dc0 100644 --- a/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateQueryRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateQueryRepository.java @@ -1,10 +1,13 @@ package com.project.dorumdorum.domain.roommate.domain.repository; import com.project.dorumdorum.domain.roommate.application.dto.response.MyRoommateResponse; +import com.project.dorumdorum.domain.roommate.application.dto.response.RoommateHistoryResponse; import java.util.List; public interface RoommateQueryRepository { List findMyRoommates(String userNo); + + List findMyRoommateHistory(String userNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateRepository.java b/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateRepository.java index 9410ca71..838efe05 100644 --- a/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateRepository.java @@ -3,6 +3,7 @@ import com.project.dorumdorum.domain.room.domain.entity.Room; import com.project.dorumdorum.domain.roommate.domain.entity.Roommate; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -26,4 +27,8 @@ public interface RoommateRepository extends JpaRepository, Roo Optional findByUserNo(String userNo); boolean existsByUserNo(String userNo); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE Roommate rm SET rm.deletedAt = CURRENT_TIMESTAMP WHERE rm.userNo = :userNo AND rm.deletedAt IS NULL") + void deleteAllByUserNo(@Param("userNo") String userNo); + } diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java b/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java index 54c135ca..f2a84307 100644 --- a/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java +++ b/src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java @@ -2,6 +2,7 @@ import com.project.dorumdorum.domain.room.domain.entity.Room; import com.project.dorumdorum.domain.roommate.application.dto.response.MyRoommateResponse; +import com.project.dorumdorum.domain.roommate.application.dto.response.RoommateHistoryResponse; import com.project.dorumdorum.domain.roommate.domain.entity.ConfirmStatus; import com.project.dorumdorum.domain.roommate.domain.entity.RoomRole; import com.project.dorumdorum.domain.roommate.domain.entity.Roommate; @@ -73,4 +74,8 @@ public void leaveRoom(String userNo, String roomNo) { public List findMyRoommates(String userNo) { return roommateRepository.findMyRoommates(userNo); } + + public List findMyRoommateHistory(String userNo) { + return roommateRepository.findMyRoommateHistory(userNo); + } } diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/infra/repository/RoommateRepositoryImpl.java b/src/main/java/com/project/dorumdorum/domain/roommate/infra/repository/RoommateRepositoryImpl.java index 2999d279..f2049377 100644 --- a/src/main/java/com/project/dorumdorum/domain/roommate/infra/repository/RoommateRepositoryImpl.java +++ b/src/main/java/com/project/dorumdorum/domain/roommate/infra/repository/RoommateRepositoryImpl.java @@ -1,12 +1,18 @@ package com.project.dorumdorum.domain.roommate.infra.repository; import com.project.dorumdorum.domain.roommate.application.dto.response.MyRoommateResponse; +import com.project.dorumdorum.domain.roommate.application.dto.response.RoommateHistoryResponse; +import com.project.dorumdorum.domain.room.domain.entity.RoomType; import com.project.dorumdorum.domain.roommate.domain.entity.QRoommate; import com.project.dorumdorum.domain.roommate.domain.repository.RoommateQueryRepository; import com.querydsl.jpa.impl.JPAQueryFactory; -import lombok.RequiredArgsConstructor; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Query; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; +import java.sql.Timestamp; +import java.time.LocalDateTime; import java.util.List; import static com.project.dorumdorum.domain.roommate.domain.entity.QRoommate.roommate; @@ -14,10 +20,21 @@ import static com.querydsl.core.types.Projections.constructor; @Repository -@RequiredArgsConstructor public class RoommateRepositoryImpl implements RoommateQueryRepository { private final JPAQueryFactory query; + private final EntityManager entityManager; + + @Autowired + public RoommateRepositoryImpl(JPAQueryFactory query, EntityManager entityManager) { + this.query = query; + this.entityManager = entityManager; + } + + public RoommateRepositoryImpl(JPAQueryFactory query) { + this.query = query; + this.entityManager = null; + } @Override public List findMyRoommates(String userNo) { @@ -53,4 +70,96 @@ public List findMyRoommates(String userNo) { ) .fetch(); } + + @Override + public List findMyRoommateHistory(String userNo) { + Query nativeQuery = entityManager.createNativeQuery(""" + SELECT + rm.roommate_no, + r.room_no, + r.title, + r.room_type, + r.capacity, + rm.user_no, + u.name, + u.nickname, + u.student_no, + u.major, + SUBSTRING(u.student_no FROM 3 FOR 2), + GREATEST(COALESCE(my_rm.created_at, r.created_at), COALESCE(r.created_at, my_rm.created_at)), + CASE + WHEN my_rm.deleted_at IS NULL AND rm.deleted_at IS NULL AND r.deleted_at IS NULL THEN NULL + ELSE COALESCE(rm.deleted_at, my_rm.deleted_at, r.deleted_at) + END, + CASE + WHEN my_rm.deleted_at IS NULL AND rm.deleted_at IS NULL AND r.deleted_at IS NULL THEN 'CURRENT' + ELSE 'PAST' + END + FROM roommate my_rm + JOIN roommate rm + ON rm.room_no = my_rm.room_no + AND rm.user_no <> :userNo + JOIN room r + ON r.room_no = my_rm.room_no + LEFT JOIN users u + ON u.user_no = rm.user_no + WHERE my_rm.user_no = :userNo + ORDER BY + CASE + WHEN my_rm.deleted_at IS NULL AND rm.deleted_at IS NULL AND r.deleted_at IS NULL THEN 0 + ELSE 1 + END, + COALESCE(rm.deleted_at, my_rm.deleted_at, r.deleted_at, rm.created_at) DESC, + rm.roommate_no DESC + """); + nativeQuery.setParameter("userNo", userNo); + + @SuppressWarnings("unchecked") + List rows = nativeQuery.getResultList(); + return rows.stream().map(this::toRoommateHistoryResponse).toList(); + } + + private RoommateHistoryResponse toRoommateHistoryResponse(Object[] row) { + return new RoommateHistoryResponse( + stringValue(row[0]), + stringValue(row[1]), + stringValue(row[2]), + enumValue(RoomType.class, row[3]), + intValue(row[4]), + stringValue(row[5]), + stringValue(row[6]), + stringValue(row[7]), + stringValue(row[8]), + stringValue(row[9]), + stringValue(row[10]), + localDateTimeValue(row[11]), + localDateTimeValue(row[12]), + enumValue(RoommateHistoryResponse.Relation.class, row[13]) + ); + } + + private > T enumValue(Class enumType, Object value) { + return value == null ? null : Enum.valueOf(enumType, value.toString()); + } + + private String stringValue(Object value) { + return value == null ? null : value.toString(); + } + + private Integer intValue(Object value) { + return value == null ? null : ((Number) value).intValue(); + } + + private LocalDateTime localDateTimeValue(Object value) { + if (value == null) { + return null; + } + if (value instanceof LocalDateTime localDateTime) { + return localDateTime; + } + if (value instanceof Timestamp timestamp) { + return timestamp.toLocalDateTime(); + } + return LocalDateTime.parse(value.toString().replace(' ', 'T')); + } } diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/ui/LoadMyRoommateHistoryController.java b/src/main/java/com/project/dorumdorum/domain/roommate/ui/LoadMyRoommateHistoryController.java new file mode 100644 index 00000000..79888b7b --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/roommate/ui/LoadMyRoommateHistoryController.java @@ -0,0 +1,23 @@ +package com.project.dorumdorum.domain.roommate.ui; + +import com.project.dorumdorum.domain.roommate.application.dto.response.RoommateHistoryResponse; +import com.project.dorumdorum.domain.roommate.application.usecase.LoadMyRoommateHistoryUseCase; +import com.project.dorumdorum.domain.roommate.ui.spec.LoadMyRoommateHistoryApiSpec; +import com.project.dorumdorum.global.annotation.CurrentUser; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class LoadMyRoommateHistoryController implements LoadMyRoommateHistoryApiSpec { + + private final LoadMyRoommateHistoryUseCase loadMyRoommateHistoryUseCase; + + @Override + public ResponseEntity> loadHistory(@CurrentUser String userNo) { + return ResponseEntity.ok(loadMyRoommateHistoryUseCase.execute(userNo)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/roommate/ui/spec/LoadMyRoommateHistoryApiSpec.java b/src/main/java/com/project/dorumdorum/domain/roommate/ui/spec/LoadMyRoommateHistoryApiSpec.java new file mode 100644 index 00000000..ca997f54 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/roommate/ui/spec/LoadMyRoommateHistoryApiSpec.java @@ -0,0 +1,18 @@ +package com.project.dorumdorum.domain.roommate.ui.spec; + +import com.project.dorumdorum.domain.roommate.application.dto.response.RoommateHistoryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; + +import java.util.List; + +@Tag(name = "Roommate") +public interface LoadMyRoommateHistoryApiSpec { + + @Operation(summary = "내 룸메이트 기록 조회 API", description = "현재 및 과거에 함께 방을 확정했던 룸메이트 기록을 조회합니다.") + @GetMapping("/api/users/me/roommate-history") + ResponseEntity> loadHistory(@Parameter(hidden = true) String userNo); +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/application/dto/request/CreateSupportInquiryRequest.java b/src/main/java/com/project/dorumdorum/domain/support/application/dto/request/CreateSupportInquiryRequest.java new file mode 100644 index 00000000..26ff63b6 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/application/dto/request/CreateSupportInquiryRequest.java @@ -0,0 +1,12 @@ +package com.project.dorumdorum.domain.support.application.dto.request; + +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiryCategory; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record CreateSupportInquiryRequest( + @NotNull SupportInquiryCategory category, + @NotBlank @Size(max = 500) String message +) { +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/application/dto/response/SupportInquiryResponse.java b/src/main/java/com/project/dorumdorum/domain/support/application/dto/response/SupportInquiryResponse.java new file mode 100644 index 00000000..7f3430c2 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/application/dto/response/SupportInquiryResponse.java @@ -0,0 +1,25 @@ +package com.project.dorumdorum.domain.support.application.dto.response; + +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiry; +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiryCategory; +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiryStatus; + +import java.time.LocalDateTime; + +public record SupportInquiryResponse( + String inquiryNo, + SupportInquiryCategory category, + String message, + SupportInquiryStatus status, + LocalDateTime createdAt +) { + public static SupportInquiryResponse from(SupportInquiry inquiry) { + return new SupportInquiryResponse( + inquiry.getInquiryNo(), + inquiry.getCategory(), + inquiry.getMessage(), + inquiry.getStatus(), + inquiry.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/application/usecase/CreateSupportInquiryUseCase.java b/src/main/java/com/project/dorumdorum/domain/support/application/usecase/CreateSupportInquiryUseCase.java new file mode 100644 index 00000000..fd9fb29c --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/application/usecase/CreateSupportInquiryUseCase.java @@ -0,0 +1,28 @@ +package com.project.dorumdorum.domain.support.application.usecase; + +import com.project.dorumdorum.domain.support.application.dto.request.CreateSupportInquiryRequest; +import com.project.dorumdorum.domain.support.application.dto.response.SupportInquiryResponse; +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiry; +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiryStatus; +import com.project.dorumdorum.domain.support.domain.repository.SupportInquiryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CreateSupportInquiryUseCase { + + private final SupportInquiryRepository supportInquiryRepository; + + @Transactional + public SupportInquiryResponse execute(String userNo, CreateSupportInquiryRequest request) { + SupportInquiry inquiry = SupportInquiry.builder() + .userNo(userNo) + .category(request.category()) + .message(request.message().trim()) + .status(SupportInquiryStatus.RECEIVED) + .build(); + return SupportInquiryResponse.from(supportInquiryRepository.save(inquiry)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/application/usecase/LoadMySupportInquiriesUseCase.java b/src/main/java/com/project/dorumdorum/domain/support/application/usecase/LoadMySupportInquiriesUseCase.java new file mode 100644 index 00000000..1bbe3abe --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/application/usecase/LoadMySupportInquiriesUseCase.java @@ -0,0 +1,24 @@ +package com.project.dorumdorum.domain.support.application.usecase; + +import com.project.dorumdorum.domain.support.application.dto.response.SupportInquiryResponse; +import com.project.dorumdorum.domain.support.domain.repository.SupportInquiryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class LoadMySupportInquiriesUseCase { + + private final SupportInquiryRepository supportInquiryRepository; + + @Transactional(readOnly = true) + public List execute(String userNo) { + return supportInquiryRepository.findByUserNoAndDeletedAtIsNullOrderByCreatedAtDescInquiryNoDesc(userNo) + .stream() + .map(SupportInquiryResponse::from) + .toList(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/application/usecase/LoadSupportInquiryDetailUseCase.java b/src/main/java/com/project/dorumdorum/domain/support/application/usecase/LoadSupportInquiryDetailUseCase.java new file mode 100644 index 00000000..eec85473 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/application/usecase/LoadSupportInquiryDetailUseCase.java @@ -0,0 +1,24 @@ +package com.project.dorumdorum.domain.support.application.usecase; + +import com.project.dorumdorum.domain.support.application.dto.response.SupportInquiryResponse; +import com.project.dorumdorum.domain.support.domain.repository.SupportInquiryRepository; +import com.project.dorumdorum.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.project.dorumdorum.global.exception.code.status.CommonErrorStatus._NOT_FOUND; + +@Service +@RequiredArgsConstructor +public class LoadSupportInquiryDetailUseCase { + + private final SupportInquiryRepository supportInquiryRepository; + + @Transactional(readOnly = true) + public SupportInquiryResponse execute(String userNo, String inquiryNo) { + return supportInquiryRepository.findByInquiryNoAndUserNoAndDeletedAtIsNull(inquiryNo, userNo) + .map(SupportInquiryResponse::from) + .orElseThrow(() -> new RestApiException(_NOT_FOUND)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiry.java b/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiry.java new file mode 100644 index 00000000..2b0f988d --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiry.java @@ -0,0 +1,50 @@ +package com.project.dorumdorum.domain.support.domain.entity; + +import com.project.dorumdorum.global.common.BaseEntity; +import io.hypersistence.utils.hibernate.id.Tsid; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@Table(name = "support_inquiries") +public class SupportInquiry extends BaseEntity { + + @Id + @Tsid + private String inquiryNo; + + @Column(nullable = false) + private String userNo; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SupportInquiryCategory category; + + @Column(nullable = false, columnDefinition = "TEXT") + private String message; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private SupportInquiryStatus status; + + @PrePersist + public void init() { + if (status == null) { + status = SupportInquiryStatus.RECEIVED; + } + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiryCategory.java b/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiryCategory.java new file mode 100644 index 00000000..cf98ac27 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiryCategory.java @@ -0,0 +1,9 @@ +package com.project.dorumdorum.domain.support.domain.entity; + +public enum SupportInquiryCategory { + APP_USAGE, + MATCHING, + ACCOUNT, + BUG_REPORT, + ETC +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiryStatus.java b/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiryStatus.java new file mode 100644 index 00000000..310a9eca --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/domain/entity/SupportInquiryStatus.java @@ -0,0 +1,8 @@ +package com.project.dorumdorum.domain.support.domain.entity; + +public enum SupportInquiryStatus { + RECEIVED, + IN_PROGRESS, + ANSWERED, + CLOSED +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/domain/repository/SupportInquiryRepository.java b/src/main/java/com/project/dorumdorum/domain/support/domain/repository/SupportInquiryRepository.java new file mode 100644 index 00000000..e31fa476 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/domain/repository/SupportInquiryRepository.java @@ -0,0 +1,13 @@ +package com.project.dorumdorum.domain.support.domain.repository; + +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiry; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface SupportInquiryRepository extends JpaRepository { + List findByUserNoAndDeletedAtIsNullOrderByCreatedAtDescInquiryNoDesc(String userNo); + + Optional findByInquiryNoAndUserNoAndDeletedAtIsNull(String inquiryNo, String userNo); +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/ui/SupportInquiryController.java b/src/main/java/com/project/dorumdorum/domain/support/ui/SupportInquiryController.java new file mode 100644 index 00000000..b91db7d8 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/ui/SupportInquiryController.java @@ -0,0 +1,47 @@ +package com.project.dorumdorum.domain.support.ui; + +import com.project.dorumdorum.domain.support.application.dto.request.CreateSupportInquiryRequest; +import com.project.dorumdorum.domain.support.application.dto.response.SupportInquiryResponse; +import com.project.dorumdorum.domain.support.application.usecase.CreateSupportInquiryUseCase; +import com.project.dorumdorum.domain.support.application.usecase.LoadMySupportInquiriesUseCase; +import com.project.dorumdorum.domain.support.application.usecase.LoadSupportInquiryDetailUseCase; +import com.project.dorumdorum.domain.support.ui.spec.SupportInquiryApiSpec; +import com.project.dorumdorum.global.annotation.CurrentUser; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class SupportInquiryController implements SupportInquiryApiSpec { + + private final CreateSupportInquiryUseCase createSupportInquiryUseCase; + private final LoadMySupportInquiriesUseCase loadMySupportInquiriesUseCase; + private final LoadSupportInquiryDetailUseCase loadSupportInquiryDetailUseCase; + + @Override + public ResponseEntity create( + @CurrentUser String userNo, + @Valid @RequestBody CreateSupportInquiryRequest request + ) { + return ResponseEntity.ok(createSupportInquiryUseCase.execute(userNo, request)); + } + + @Override + public ResponseEntity> loadAll(@CurrentUser String userNo) { + return ResponseEntity.ok(loadMySupportInquiriesUseCase.execute(userNo)); + } + + @Override + public ResponseEntity load( + @CurrentUser String userNo, + @PathVariable String inquiryNo + ) { + return ResponseEntity.ok(loadSupportInquiryDetailUseCase.execute(userNo, inquiryNo)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/support/ui/spec/SupportInquiryApiSpec.java b/src/main/java/com/project/dorumdorum/domain/support/ui/spec/SupportInquiryApiSpec.java new file mode 100644 index 00000000..72812c66 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/support/ui/spec/SupportInquiryApiSpec.java @@ -0,0 +1,37 @@ +package com.project.dorumdorum.domain.support.ui.spec; + +import com.project.dorumdorum.domain.support.application.dto.request.CreateSupportInquiryRequest; +import com.project.dorumdorum.domain.support.application.dto.response.SupportInquiryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; + +import java.util.List; + +@Tag(name = "Support") +public interface SupportInquiryApiSpec { + + @Operation(summary = "문의 접수 API", description = "사용자의 고객 문의를 접수합니다.") + @PostMapping("/api/support/inquiries") + ResponseEntity create( + @Parameter(hidden = true) String userNo, + @Valid @RequestBody CreateSupportInquiryRequest request + ); + + @Operation(summary = "내 문의 목록 조회 API", description = "내가 접수한 고객 문의 목록을 조회합니다.") + @GetMapping("/api/support/inquiries") + ResponseEntity> loadAll(@Parameter(hidden = true) String userNo); + + @Operation(summary = "내 문의 상세 조회 API", description = "내가 접수한 고객 문의를 단건 조회합니다.") + @GetMapping("/api/support/inquiries/{inquiryNo}") + ResponseEntity load( + @Parameter(hidden = true) String userNo, + @PathVariable String inquiryNo + ); +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/DeleteAccountRequest.java b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/DeleteAccountRequest.java new file mode 100644 index 00000000..55cb6ce3 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/application/dto/request/DeleteAccountRequest.java @@ -0,0 +1,8 @@ +package com.project.dorumdorum.domain.user.application.dto.request; + +import jakarta.validation.constraints.Size; + +public record DeleteAccountRequest( + @Size(max = 100) String reason +) { +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/application/usecase/DeleteAccountUseCase.java b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/DeleteAccountUseCase.java new file mode 100644 index 00000000..b6df4b7a --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/application/usecase/DeleteAccountUseCase.java @@ -0,0 +1,46 @@ +package com.project.dorumdorum.domain.user.application.usecase; + +import com.project.dorumdorum.domain.notification.domain.repository.NotificationDeviceRepository; +import com.project.dorumdorum.domain.notification.domain.repository.NotificationRepository; +import com.project.dorumdorum.domain.room.domain.repository.RoomLikeRepository; +import com.project.dorumdorum.domain.room.domain.repository.RoomRepository; +import com.project.dorumdorum.domain.room.domain.repository.RoomRequestRepository; +import com.project.dorumdorum.domain.roommate.domain.repository.RoommateRepository; +import com.project.dorumdorum.domain.user.application.dto.request.DeleteAccountRequest; +import com.project.dorumdorum.domain.user.domain.entity.User; +import com.project.dorumdorum.domain.user.domain.service.UserService; +import com.project.dorumdorum.global.exception.RestApiException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.project.dorumdorum.global.exception.code.status.UserErrorStatus.ACTIVE_ROOM_OWNER; + +@Service +@RequiredArgsConstructor +public class DeleteAccountUseCase { + + private final UserService userService; + private final RoomRepository roomRepository; + private final RoomLikeRepository roomLikeRepository; + private final RoomRequestRepository roomRequestRepository; + private final RoommateRepository roommateRepository; + private final NotificationRepository notificationRepository; + private final NotificationDeviceRepository notificationDeviceRepository; + + @Transactional + public void execute(String userNo, DeleteAccountRequest request) { + if (roomRepository.existsByHostUserNoAndDeletedAtIsNull(userNo)) { + throw new RestApiException(ACTIVE_ROOM_OWNER); + } + + User user = userService.findById(userNo); + roomLikeRepository.deleteAllByUserNo(userNo); + roomRequestRepository.deleteAllByUserNo(userNo); + roommateRepository.deleteAllByUserNo(userNo); + notificationRepository.deleteAllByRecipientNo(userNo); + notificationDeviceRepository.deleteAllByUserNo(userNo); + user.anonymizeForWithdrawal(); + user.delete(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java b/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java index 8aac584e..8e8f1de8 100644 --- a/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java +++ b/src/main/java/com/project/dorumdorum/domain/user/domain/entity/User.java @@ -71,4 +71,17 @@ public void updatePassword(String encodedPassword) { this.password = encodedPassword; } + public void anonymizeForWithdrawal() { + this.nickname = "탈퇴한 사용자"; + this.name = "탈퇴한 사용자"; + this.email = "deleted-" + this.userNo + "@dorumdorum.invalid"; + this.password = ""; + this.studentNo = "deleted-" + this.userNo; + this.major = null; + this.grade = null; + this.birth = null; + this.age = null; + this.firebaseToken = null; + } + } diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/DeleteAccountController.java b/src/main/java/com/project/dorumdorum/domain/user/ui/DeleteAccountController.java new file mode 100644 index 00000000..8b58e3f4 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/DeleteAccountController.java @@ -0,0 +1,37 @@ +package com.project.dorumdorum.domain.user.ui; + +import com.project.dorumdorum.domain.user.application.dto.request.DeleteAccountRequest; +import com.project.dorumdorum.domain.user.application.usecase.DeleteAccountUseCase; +import com.project.dorumdorum.domain.user.application.usecase.LogoutUseCase; +import com.project.dorumdorum.domain.user.ui.spec.DeleteAccountApiSpec; +import com.project.dorumdorum.global.annotation.AccessToken; +import com.project.dorumdorum.global.annotation.CurrentUser; +import com.project.dorumdorum.global.security.cookie.AuthCookieWriter; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class DeleteAccountController implements DeleteAccountApiSpec { + + private final DeleteAccountUseCase deleteAccountUseCase; + private final LogoutUseCase logoutUseCase; + private final AuthCookieWriter authCookieWriter; + + @Override + public ResponseEntity delete( + HttpServletResponse response, + @CurrentUser String userNo, + @AccessToken String accessToken, + @Valid @RequestBody(required = false) DeleteAccountRequest request + ) { + deleteAccountUseCase.execute(userNo, request == null ? new DeleteAccountRequest(null) : request); + logoutUseCase.execute(accessToken); + authCookieWriter.expireAuthCookies(response); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/user/ui/spec/DeleteAccountApiSpec.java b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/DeleteAccountApiSpec.java new file mode 100644 index 00000000..ef360eb4 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/user/ui/spec/DeleteAccountApiSpec.java @@ -0,0 +1,24 @@ +package com.project.dorumdorum.domain.user.ui.spec; + +import com.project.dorumdorum.domain.user.application.dto.request.DeleteAccountRequest; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestBody; + +@Tag(name = "User") +public interface DeleteAccountApiSpec { + + @Operation(summary = "회원 탈퇴 API", description = "로그인한 사용자를 soft delete하고 인증 쿠키와 토큰을 폐기합니다.") + @DeleteMapping("/api/users/me") + ResponseEntity delete( + HttpServletResponse response, + @Parameter(hidden = true) String userNo, + @Parameter(hidden = true) String accessToken, + @Valid @RequestBody(required = false) DeleteAccountRequest request + ); +} diff --git a/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java b/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java index a4b23d44..532fb7b1 100644 --- a/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java +++ b/src/main/java/com/project/dorumdorum/global/exception/code/status/UserErrorStatus.java @@ -16,6 +16,7 @@ public enum UserErrorStatus implements BaseCodeInterface { AGE_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON400", "나이 계산 파싱 오류입니다."), FAILED_SEND_VERIFY_CODE(HttpStatus.INTERNAL_SERVER_ERROR, "MAIL001", "인증번호 전송에 실패하였습니다."), EMAIL_NOT_VERIFIED(HttpStatus.BAD_REQUEST, "USER002", "이메일 인증이 완료되지 않았습니다."), + ACTIVE_ROOM_OWNER(HttpStatus.CONFLICT, "USER003", "운영 중인 방이 있어 탈퇴할 수 없습니다."), ; private final HttpStatus httpStatus; diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index bd343ded..4c87301b 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -59,3 +59,31 @@ CREATE UNIQUE INDEX IF NOT EXISTS uk_chat_room_direct CREATE INDEX IF NOT EXISTS idx_chat_room_room_no ON chat_room (room_no); + +CREATE TABLE IF NOT EXISTS notification_settings ( + notification_setting_no VARCHAR(255) PRIMARY KEY, + user_no VARCHAR(255) NOT NULL UNIQUE, + enabled BOOLEAN NOT NULL, + applicants BOOLEAN NOT NULL, + applicant_result BOOLEAN NOT NULL, + chat BOOLEAN NOT NULL, + notice BOOLEAN NOT NULL, + schedule BOOLEAN NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS support_inquiries ( + inquiry_no VARCHAR(255) PRIMARY KEY, + user_no VARCHAR(255) NOT NULL, + category VARCHAR(255) NOT NULL, + message TEXT NOT NULL, + status VARCHAR(255) NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + deleted_at TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_support_inquiry_user_created + ON support_inquiries (user_no, created_at DESC, inquiry_no DESC); diff --git a/src/test/java/com/project/dorumdorum/domain/notification/ui/NotificationSettingControllerTest.java b/src/test/java/com/project/dorumdorum/domain/notification/ui/NotificationSettingControllerTest.java new file mode 100644 index 00000000..70fe114c --- /dev/null +++ b/src/test/java/com/project/dorumdorum/domain/notification/ui/NotificationSettingControllerTest.java @@ -0,0 +1,56 @@ +package com.project.dorumdorum.domain.notification.ui; + +import com.project.dorumdorum.domain.notification.application.dto.request.NotificationSettingRequest; +import com.project.dorumdorum.domain.notification.application.dto.response.NotificationSettingResponse; +import com.project.dorumdorum.domain.notification.application.usecase.LoadNotificationSettingUseCase; +import com.project.dorumdorum.domain.notification.application.usecase.UpdateNotificationSettingUseCase; +import org.junit.jupiter.api.DisplayName; +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.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("NotificationSettingController 단위 테스트") +class NotificationSettingControllerTest { + + @Mock + private LoadNotificationSettingUseCase loadUseCase; + + @Mock + private UpdateNotificationSettingUseCase updateUseCase; + + @InjectMocks + private NotificationSettingController controller; + + @Test + @DisplayName("load는 알림 설정 조회 결과를 반환한다") + void load_ReturnsSettings() { + NotificationSettingResponse response = new NotificationSettingResponse(true, true, true, true, true, false); + when(loadUseCase.execute("user-1")).thenReturn(response); + + ResponseEntity result = controller.load("user-1"); + + verify(loadUseCase).execute("user-1"); + assertThat(result.getBody()).isEqualTo(response); + } + + @Test + @DisplayName("update는 알림 설정 저장 결과를 반환한다") + void update_ReturnsSavedSettings() { + NotificationSettingRequest request = new NotificationSettingRequest(false, true, true, false, true, false); + NotificationSettingResponse response = new NotificationSettingResponse(false, true, true, false, true, false); + when(updateUseCase.execute("user-1", request)).thenReturn(response); + + ResponseEntity result = controller.update("user-1", request); + + verify(updateUseCase).execute("user-1", request); + assertThat(result.getBody()).isEqualTo(response); + } +} diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/ui/FindRoomsControllerTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/ui/FindRoomsControllerTest.java index 83ca2706..1bb6c336 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/ui/FindRoomsControllerTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/ui/FindRoomsControllerTest.java @@ -31,10 +31,10 @@ class FindRoomsControllerTest { @Test void loadAll_ReturnsUseCaseResult() { ChecklistFilterRequest request = new ChecklistFilterRequest( - ChecklistFilterRequest.SortType.LATEST, null, null, null, null, + ChecklistFilterRequest.SortType.LATEST, null, "조용한", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null + null, null, null, null, null ); FindRoomsPageResponse page = new FindRoomsPageResponse(List.of(), null, false, 0L); when(useCase.execute(USER_NO, request)).thenReturn(page); diff --git a/src/test/java/com/project/dorumdorum/domain/support/ui/SupportInquiryControllerTest.java b/src/test/java/com/project/dorumdorum/domain/support/ui/SupportInquiryControllerTest.java new file mode 100644 index 00000000..fe90b191 --- /dev/null +++ b/src/test/java/com/project/dorumdorum/domain/support/ui/SupportInquiryControllerTest.java @@ -0,0 +1,65 @@ +package com.project.dorumdorum.domain.support.ui; + +import com.project.dorumdorum.domain.support.application.dto.request.CreateSupportInquiryRequest; +import com.project.dorumdorum.domain.support.application.dto.response.SupportInquiryResponse; +import com.project.dorumdorum.domain.support.application.usecase.CreateSupportInquiryUseCase; +import com.project.dorumdorum.domain.support.application.usecase.LoadMySupportInquiriesUseCase; +import com.project.dorumdorum.domain.support.application.usecase.LoadSupportInquiryDetailUseCase; +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiryCategory; +import com.project.dorumdorum.domain.support.domain.entity.SupportInquiryStatus; +import org.junit.jupiter.api.DisplayName; +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.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@DisplayName("SupportInquiryController 단위 테스트") +class SupportInquiryControllerTest { + + @Mock + private CreateSupportInquiryUseCase createUseCase; + + @Mock + private LoadMySupportInquiriesUseCase loadAllUseCase; + + @Mock + private LoadSupportInquiryDetailUseCase loadDetailUseCase; + + @InjectMocks + private SupportInquiryController controller; + + @Test + @DisplayName("create는 문의 접수 결과를 반환한다") + void create_ReturnsCreatedInquiry() { + CreateSupportInquiryRequest request = new CreateSupportInquiryRequest(SupportInquiryCategory.APP_USAGE, "문의합니다"); + SupportInquiryResponse response = new SupportInquiryResponse("1", SupportInquiryCategory.APP_USAGE, "문의합니다", SupportInquiryStatus.RECEIVED, LocalDateTime.now()); + when(createUseCase.execute("user-1", request)).thenReturn(response); + + ResponseEntity result = controller.create("user-1", request); + + verify(createUseCase).execute("user-1", request); + assertThat(result.getBody()).isEqualTo(response); + } + + @Test + @DisplayName("loadAll은 내 문의 목록을 반환한다") + void loadAll_ReturnsMyInquiries() { + List responses = List.of(); + when(loadAllUseCase.execute("user-1")).thenReturn(responses); + + ResponseEntity> result = controller.loadAll("user-1"); + + verify(loadAllUseCase).execute("user-1"); + assertThat(result.getBody()).isEqualTo(responses); + } +} diff --git a/src/test/java/com/project/dorumdorum/domain/user/unit/ui/DeleteAccountControllerTest.java b/src/test/java/com/project/dorumdorum/domain/user/unit/ui/DeleteAccountControllerTest.java new file mode 100644 index 00000000..1af6652f --- /dev/null +++ b/src/test/java/com/project/dorumdorum/domain/user/unit/ui/DeleteAccountControllerTest.java @@ -0,0 +1,52 @@ +package com.project.dorumdorum.domain.user.unit.ui; + +import com.project.dorumdorum.domain.user.application.dto.request.DeleteAccountRequest; +import com.project.dorumdorum.domain.user.application.usecase.DeleteAccountUseCase; +import com.project.dorumdorum.domain.user.application.usecase.LogoutUseCase; +import com.project.dorumdorum.domain.user.ui.DeleteAccountController; +import com.project.dorumdorum.global.security.cookie.AuthCookieWriter; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.DisplayName; +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.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +@DisplayName("DeleteAccountController 단위 테스트") +class DeleteAccountControllerTest { + + @Mock + private DeleteAccountUseCase deleteAccountUseCase; + + @Mock + private LogoutUseCase logoutUseCase; + + @Mock + private AuthCookieWriter authCookieWriter; + + @Mock + private HttpServletResponse servletResponse; + + @InjectMocks + private DeleteAccountController controller; + + @Test + @DisplayName("delete는 탈퇴 처리 후 토큰과 쿠키를 정리하고 204를 반환한다") + void delete_DeletesAccountAndExpiresAuth() { + DeleteAccountRequest request = new DeleteAccountRequest("서비스를 자주 쓰지 않아요"); + + ResponseEntity response = controller.delete(servletResponse, "user-1", "access-token", request); + + verify(deleteAccountUseCase).execute("user-1", request); + verify(logoutUseCase).execute("access-token"); + verify(authCookieWriter).expireAuthCookies(servletResponse); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } +}