diff --git a/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java b/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java index ea7e5d9d..dfe6741e 100644 --- a/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java +++ b/src/main/java/com/project/dorumdorum/domain/calendar/domain/entity/CalendarEvent.java @@ -36,7 +36,6 @@ public class CalendarEvent extends BaseEntity { @Column(nullable = false) private String content; - @Column(nullable = false) private LocalTime eventTime; @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/project/dorumdorum/domain/checklist/application/mapper/UserChecklistMapper.java b/src/main/java/com/project/dorumdorum/domain/checklist/application/mapper/UserChecklistMapper.java index f5a21152..400aaf3c 100644 --- a/src/main/java/com/project/dorumdorum/domain/checklist/application/mapper/UserChecklistMapper.java +++ b/src/main/java/com/project/dorumdorum/domain/checklist/application/mapper/UserChecklistMapper.java @@ -15,6 +15,7 @@ public interface UserChecklistMapper { @Mapping(target = "userNo", source = "userNo") @Mapping(target = "createdAt", ignore = true) @Mapping(target = "updatedAt", ignore = true) + @Mapping(target = "deletedAt", ignore = true) UserChecklist toUserChecklist(String userNo, CreateUserChecklistRequest request); @Mapping(target = "userChecklistNo", ignore = true) diff --git a/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java b/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java index 4b7eeab3..d3bd3766 100644 --- a/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/RoomRuleRepository.java @@ -3,6 +3,7 @@ import com.project.dorumdorum.domain.checklist.domain.entity.RoomRule; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; +import java.util.List; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -16,4 +17,7 @@ public interface RoomRuleRepository extends JpaRepository { @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE RoomRule rr SET rr.deletedAt = CURRENT_TIMESTAMP WHERE rr.room.roomNo = :roomNo AND rr.deletedAt IS NULL") void deleteByRoomNo(@Param("roomNo") String roomNo); + + @Query("SELECT rr FROM RoomRule rr JOIN FETCH rr.room r WHERE r.roomStatus = 'CONFIRM_PENDING' AND r.deletedAt IS NULL AND rr.deletedAt IS NULL") + List findAllActiveWithRoom(); } diff --git a/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/UserChecklistRepository.java b/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/UserChecklistRepository.java index 36350ce5..4d2b7d6d 100644 --- a/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/UserChecklistRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/checklist/domain/repository/UserChecklistRepository.java @@ -9,4 +9,5 @@ public interface UserChecklistRepository extends JpaRepository findByUserNo(String userNo); + boolean existsByUserNo(String userNo); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/RoomCreateRequest.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/RoomCreateRequest.java index 2766b24b..e458830a 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/RoomCreateRequest.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/RoomCreateRequest.java @@ -12,5 +12,6 @@ public record RoomCreateRequest( @NotNull Integer capacity, @NotNull ResidencePeriod residencePeriod, @NotBlank String title, + String notes, @Valid @NotNull CreateRoomRuleRequest rule ) {} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/UpdateRoomTitleRequest.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/UpdateRoomTitleRequest.java index 3dbca70f..71dc9dc4 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/UpdateRoomTitleRequest.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/request/UpdateRoomTitleRequest.java @@ -3,5 +3,6 @@ import jakarta.validation.constraints.NotBlank; public record UpdateRoomTitleRequest( - @NotBlank String title + @NotBlank String title, + String notes ) {} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsPageResponse.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsPageResponse.java new file mode 100644 index 00000000..4226233a --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsPageResponse.java @@ -0,0 +1,16 @@ +package com.project.dorumdorum.domain.room.application.dto.response; + +import com.project.dorumdorum.global.pagination.CursorPage; + +import java.util.List; + +public record FindRoomsPageResponse( + List items, + String nextCursor, + boolean hasNext, + Long totalCount +) { + public static FindRoomsPageResponse of(CursorPage page, Long totalCount) { + return new FindRoomsPageResponse(page.items(), page.nextCursor(), page.hasNext(), totalCount); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsResponse.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsResponse.java index df468f70..ca3aca23 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsResponse.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/FindRoomsResponse.java @@ -14,8 +14,11 @@ public record FindRoomsResponse( Integer currentMateCount, LocalDateTime createdAt, String title, + String notes, String hostNickname, + String hostMajor, + String hostStudentYear, RoomStatus roomStatus, String residencePeriod, // 거주기간 (예: "학기(16주)", "반기(24주)", "계절학기") Integer remaining -) {} \ No newline at end of file +) {} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RecommendedRoomResponse.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RecommendedRoomResponse.java new file mode 100644 index 00000000..e471f586 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RecommendedRoomResponse.java @@ -0,0 +1,24 @@ +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; +import lombok.Builder; + +import java.util.List; + +@Builder +public record RecommendedRoomResponse( + String roomNo, + String title, + RoomType roomType, + Integer capacity, + Integer currentMateCount, + Integer remaining, + RoomStatus roomStatus, + String residencePeriod, + String hostNickname, + String matchLabel, + int matchedCount, + int totalCount, + List highlights +) {} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RecommendedRoomsPageResponse.java b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RecommendedRoomsPageResponse.java new file mode 100644 index 00000000..b83a0f85 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/application/dto/response/RecommendedRoomsPageResponse.java @@ -0,0 +1,8 @@ +package com.project.dorumdorum.domain.room.application.dto.response; + +import java.util.List; + +public record RecommendedRoomsPageResponse( + boolean hasChecklist, + List items +) {} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/FindRoomsUseCase.java b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/FindRoomsUseCase.java index a2585433..6d8bf542 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/FindRoomsUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/FindRoomsUseCase.java @@ -1,6 +1,7 @@ package com.project.dorumdorum.domain.room.application.usecase; import com.project.dorumdorum.domain.room.application.dto.request.ChecklistFilterRequest; +import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsPageResponse; import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsResponse; import com.project.dorumdorum.domain.room.domain.service.RoomService; import com.project.dorumdorum.domain.user.domain.entity.Gender; @@ -29,7 +30,7 @@ public class FindRoomsUseCase { * - 필터와 커서 기준으로 방 목록을 조회 * - 다음 페이지 커서를 포함한 결과를 반환 */ - public CursorPage execute(String userNo, ChecklistFilterRequest request) { + public FindRoomsPageResponse execute(String userNo, ChecklistFilterRequest request) { Gender gender = userService.findById(userNo).getGender(); CursorQueryParams params = PaginationHelper.prepareCursorQuery(request.cursor(), LIMIT); @@ -42,12 +43,18 @@ public CursorPage execute(String userNo, ChecklistFilterReque params.limitPlusOne() ); - return PaginationHelper.buildCursorPage( + CursorPage page = PaginationHelper.buildCursorPage( responses, LIMIT, last -> request.sortType() == ChecklistFilterRequest.SortType.REMAINING ? CursorCodec.encode(last.remaining(), last.createdAt(), last.roomNo()) : CursorCodec.encode(last.createdAt(), last.roomNo()) ); + + Long totalCount = request.cursor() == null + ? roomService.countSearchResults(gender, request) + : null; + + return FindRoomsPageResponse.of(page, totalCount); } } diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/LoadRecommendedRoomsUseCase.java b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/LoadRecommendedRoomsUseCase.java new file mode 100644 index 00000000..44c7910f --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/LoadRecommendedRoomsUseCase.java @@ -0,0 +1,125 @@ +package com.project.dorumdorum.domain.room.application.usecase; + +import com.project.dorumdorum.domain.checklist.domain.entity.ChecklistBase; +import com.project.dorumdorum.domain.checklist.domain.entity.RoomRule; +import com.project.dorumdorum.domain.checklist.domain.entity.UserChecklist; +import com.project.dorumdorum.domain.checklist.domain.repository.RoomRuleRepository; +import com.project.dorumdorum.domain.checklist.domain.repository.UserChecklistRepository; +import com.project.dorumdorum.domain.checklist.domain.service.UserChecklistService; +import com.project.dorumdorum.domain.room.application.dto.response.RecommendedRoomResponse; +import com.project.dorumdorum.domain.room.application.dto.response.RecommendedRoomsPageResponse; +import com.project.dorumdorum.domain.room.domain.entity.Room; +import com.project.dorumdorum.domain.user.domain.service.UserService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class LoadRecommendedRoomsUseCase { + + private static final double STRONG_MATCH_THRESHOLD = 0.85; + private static final double MATCH_THRESHOLD = 0.65; + + private final UserChecklistService userChecklistService; + private final UserChecklistRepository userChecklistRepository; + private final RoomRuleRepository roomRuleRepository; + private final UserService userService; + + public RecommendedRoomsPageResponse execute(String userNo) { + if (!userChecklistRepository.existsByUserNo(userNo)) { + return new RecommendedRoomsPageResponse(false, List.of()); + } + + UserChecklist my = userChecklistService.findByUserNo(userNo); + List roomRules = roomRuleRepository.findAllActiveWithRoom(); + + List items = roomRules.stream() + .map(rr -> buildResponse(my, rr)) + .filter(r -> r != null) + .sorted(Comparator.comparingDouble(r -> -(double) r.matchedCount() / Math.max(r.totalCount(), 1))) + .toList(); + + return new RecommendedRoomsPageResponse(true, items); + } + + private RecommendedRoomResponse buildResponse(UserChecklist my, RoomRule rr) { + List fields = compareFields(my, rr); + + int totalCount = (int) fields.stream().filter(FieldResult::filled).count(); + if (totalCount == 0) return null; + + int matchedCount = (int) fields.stream().filter(f -> f.filled() && f.match()).count(); + double score = (double) matchedCount / totalCount; + + String matchLabel; + if (score >= STRONG_MATCH_THRESHOLD) matchLabel = "잘 맞아요"; + else if (score >= MATCH_THRESHOLD) matchLabel = "괜찮아요"; + else return null; + + List highlights = fields.stream() + .filter(f -> f.filled() && f.match()) + .limit(3) + .map(FieldResult::label) + .toList(); + + Room room = rr.getRoom(); + String hostNickname = userService.findById(room.getHostUserNo()).getNickname(); + + return RecommendedRoomResponse.builder() + .roomNo(room.getRoomNo()) + .title(room.getTitle()) + .roomType(room.getRoomType()) + .capacity(room.getCapacity()) + .currentMateCount(room.getCurrentMateCount()) + .remaining(room.getRemaining()) + .roomStatus(room.getRoomStatus()) + .residencePeriod(room.getResidencePeriod().name()) + .hostNickname(hostNickname) + .matchLabel(matchLabel) + .matchedCount(matchedCount) + .totalCount(totalCount) + .highlights(highlights) + .build(); + } + + private List compareFields(UserChecklist my, ChecklistBase room) { + List results = new ArrayList<>(); + addField(results, "취침", my.getBedtime(), room.getBedtime()); + addField(results, "기상", my.getWakeUp(), room.getWakeUp()); + addField(results, "귀가", my.getReturnHome(), room.getReturnHome()); + addField(results, "청소", my.getCleaning(), room.getCleaning()); + addField(results, "방에서 전화", my.getPhoneCall(), room.getPhoneCall()); + addField(results, "잠귀", my.getSleepLight(), room.getSleepLight()); + addField(results, "잠버릇", my.getSleepHabit(), room.getSleepHabit()); + addField(results, "코골이", my.getSnoring(), room.getSnoring()); + addField(results, "샤워 시간", my.getShowerTime(), room.getShowerTime()); + addField(results, "방에서 취식", my.getEating(), room.getEating()); + addField(results, "소등", my.getLightsOut(), room.getLightsOut()); + addField(results, "본가 주기", my.getHomeVisit(), room.getHomeVisit()); + addField(results, "흡연", my.getSmoking(), room.getSmoking()); + addField(results, "냉장고", my.getRefrigerator(), room.getRefrigerator()); + addField(results, "알람", my.getAlarm(), room.getAlarm()); + addField(results, "이어폰", my.getEarphone(), room.getEarphone()); + addField(results, "키스킨", my.getKeyskin(), room.getKeyskin()); + addField(results, "더위", my.getHeat(), room.getHeat()); + addField(results, "추위", my.getCold(), room.getCold()); + addField(results, "공부", my.getStudy(), room.getStudy()); + addField(results, "쓰레기통", my.getTrashCan(), room.getTrashCan()); + return results; + } + + private void addField(List results, String label, Object myValue, Object roomValue) { + boolean filled = myValue != null && !(myValue instanceof String s && s.isBlank()); + boolean match = filled && Objects.equals(myValue, roomValue); + results.add(new FieldResult(label, filled, match)); + } + + private record FieldResult(String label, boolean filled, boolean match) {} +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/UpdateRoomTitleUseCase.java b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/UpdateRoomTitleUseCase.java index e09b283b..db0af936 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/application/usecase/UpdateRoomTitleUseCase.java +++ b/src/main/java/com/project/dorumdorum/domain/room/application/usecase/UpdateRoomTitleUseCase.java @@ -31,5 +31,6 @@ public void execute(String userNo, String roomNo, UpdateRoomTitleRequest request throw new RestApiException(NO_PERMISSION_ON_ROOM); room.updateTitle(request.title().trim()); + room.updateNotes(request.notes()); } } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/Room.java b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/Room.java index 7596aeaf..545ed2b9 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/Room.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/Room.java @@ -47,6 +47,9 @@ public class Room extends BaseEntity { @Column(nullable = false) private String title; + @Column(columnDefinition = "TEXT") + private String notes; + @Column(nullable = false) private String hostUserNo; @@ -127,4 +130,8 @@ public void updateResidencePeriod(ResidencePeriod residencePeriod) { public void updateTitle(String title) { this.title = title; } + + public void updateNotes(String notes) { + this.notes = notes; + } } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomType.java b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomType.java index 4af1a963..36961361 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomType.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/entity/RoomType.java @@ -1,5 +1,5 @@ package com.project.dorumdorum.domain.room.domain.entity; public enum RoomType { - TYPE_1, TYPE_2, TYPE_MEDICAL + TYPE_1, TYPE_2, TYPE_3, TYPE_MEDICAL } diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomQueryRepository.java b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomQueryRepository.java index a124acc1..12f9d16d 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomQueryRepository.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/repository/RoomQueryRepository.java @@ -19,6 +19,8 @@ List findByCursor( int limitPlusOne ); + long countByFilter(Gender gender, ChecklistFilterRequest request); + Optional findMyRoom(String userNo); List findLikedRooms(String userNo); diff --git a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java index ad6934a9..48ef3479 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java +++ b/src/main/java/com/project/dorumdorum/domain/room/domain/service/RoomService.java @@ -28,6 +28,7 @@ public Room create(String userNo, Gender gender, RoomCreateRequest request) { .roomType(request.roomType()) .residencePeriod(request.residencePeriod()) .title(request.title()) + .notes(request.notes()) .hostUserNo(userNo) .gender(gender) .build(); @@ -56,6 +57,10 @@ public List searchByCursor( return roomRepository.findByCursor(gender, request, cursorCreatedAt, cursorId, cursorRemaining, limitPlusOne); } + public long countSearchResults(Gender gender, ChecklistFilterRequest request) { + return roomRepository.countByFilter(gender, request); + } + public FindRoomsResponse findMyRoom(String userNo) { return roomRepository.findMyRoom(userNo).orElse(null); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/infra/repository/RoomRepositoryImpl.java b/src/main/java/com/project/dorumdorum/domain/room/infra/repository/RoomRepositoryImpl.java index 9556e24d..a824c862 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/infra/repository/RoomRepositoryImpl.java +++ b/src/main/java/com/project/dorumdorum/domain/room/infra/repository/RoomRepositoryImpl.java @@ -7,6 +7,7 @@ import com.project.dorumdorum.domain.room.domain.entity.RoomStatus; import com.project.dorumdorum.domain.room.domain.repository.RoomQueryRepository; import com.project.dorumdorum.domain.user.domain.entity.Gender; +import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQuery; @@ -59,7 +60,10 @@ public List findByCursor( room.currentMateCount, room.createdAt, room.title, + room.notes, user.nickname, + user.major, + user.studentNo.substring(2, 4), room.roomStatus, room.residencePeriod.stringValue(), room.remaining @@ -80,7 +84,12 @@ public List findByCursor( ); if (request.sortType() == ChecklistFilterRequest.SortType.REMAINING) { - q.orderBy(room.remaining.asc(), room.createdAt.desc(), room.roomNo.desc()); + q.orderBy( + new CaseBuilder().when(room.remaining.eq(0)).then(1).otherwise(0).asc(), + room.remaining.asc(), + room.createdAt.desc(), + room.roomNo.desc() + ); } else { q.orderBy(room.createdAt.desc(), room.roomNo.desc()); } @@ -88,6 +97,60 @@ public List findByCursor( return q.limit(limitPlusOne).fetch(); } + @Override + public long countByFilter(Gender gender, ChecklistFilterRequest request) { + if (hasChecklistFilters(request)) { + return countByFilterWithChecklist(gender, request); + } + + Long count = query + .select(room.count()) + .from(room) + .where( + room.roomStatus.eq(RoomStatus.CONFIRM_PENDING), + room.deletedAt.isNull(), + room.gender.eq(gender), + eqRoomType(request), + eqResidencePeriod(request), + eqCapacity(request) + ) + .fetchOne(); + + return count == null ? 0L : count; + } + + private long countByFilterWithChecklist(Gender gender, ChecklistFilterRequest request) { + StringBuilder sql = new StringBuilder(""" + SELECT COUNT(*) + FROM room r + WHERE r.room_status = :roomStatus + AND r.deleted_at IS NULL + AND r.gender = :gender + """); + + Map params = new LinkedHashMap<>(); + params.put("roomStatus", RoomStatus.CONFIRM_PENDING.name()); + params.put("gender", gender.name()); + + 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()); + sql.append(""" + + AND EXISTS ( + SELECT 1 + FROM room_rule rr + WHERE rr.room_no = r.room_no + """); + appendRoomRuleConditions(sql, params, request); + sql.append("\n )"); + + Query nativeQuery = entityManager.createNativeQuery(sql.toString()); + params.forEach(nativeQuery::setParameter); + + return ((Number) nativeQuery.getSingleResult()).longValue(); + } + private List findByCursorWithLateral( Gender gender, ChecklistFilterRequest request, @@ -104,7 +167,10 @@ private List findByCursorWithLateral( r.current_mate_count, r.created_at, r.title, + r.notes, u.nickname, + u.major, + SUBSTRING(u.student_no FROM 3 FOR 2), r.room_status, r.residence_period::text, r.remaining @@ -117,30 +183,7 @@ JOIN LATERAL ( Map params = new LinkedHashMap<>(); - appendCondition(sql, params, "rr.bedtime", "bedtime", request.bedtime()); - appendCondition(sql, params, "rr.wake_up", "wakeUp", request.wakeUp()); - appendCondition(sql, params, "rr.return_home", "returnHome", enumName(request.returnHome())); - appendCondition(sql, params, "rr.return_home_time", "returnHomeTime", request.returnHomeTime()); - appendCondition(sql, params, "rr.cleaning", "cleaning", enumName(request.cleaning())); - appendCondition(sql, params, "rr.phone_call", "phoneCall", enumName(request.phoneCall())); - appendCondition(sql, params, "rr.sleep_light", "sleepLight", enumName(request.sleepLight())); - appendCondition(sql, params, "rr.sleep_habit", "sleepHabit", enumName(request.sleepHabit())); - appendCondition(sql, params, "rr.snoring", "snoring", enumName(request.snoring())); - appendCondition(sql, params, "rr.shower_time", "showerTime", enumName(request.showerTime())); - appendCondition(sql, params, "rr.eating", "eating", enumName(request.eating())); - appendCondition(sql, params, "rr.lights_out", "lightsOut", enumName(request.lightsOut())); - appendCondition(sql, params, "rr.lights_out_time", "lightsOutTime", request.lightsOutTime()); - appendCondition(sql, params, "rr.home_visit", "homeVisit", enumName(request.homeVisit())); - appendCondition(sql, params, "rr.smoking", "smoking", enumName(request.smoking())); - appendCondition(sql, params, "rr.refrigerator", "refrigerator", enumName(request.refrigerator())); - appendCondition(sql, params, "rr.hair_dryer", "hairDryer", request.hairDryer()); - appendCondition(sql, params, "rr.alarm", "alarm", enumName(request.alarm())); - appendCondition(sql, params, "rr.earphone", "earphone", enumName(request.earphone())); - appendCondition(sql, params, "rr.keyskin", "keyskin", enumName(request.keyskin())); - appendCondition(sql, params, "rr.heat", "heat", enumName(request.heat())); - appendCondition(sql, params, "rr.cold", "cold", enumName(request.cold())); - appendCondition(sql, params, "rr.study", "study", enumName(request.study())); - appendCondition(sql, params, "rr.trash_can", "trashCan", enumName(request.trashCan())); + appendRoomRuleConditions(sql, params, request); sql.append(""" LIMIT 1 @@ -186,7 +229,10 @@ public Optional findMyRoom(String userNo) { room.currentMateCount, room.createdAt, room.title, + room.notes, user.nickname, + user.major, + user.studentNo.substring(2, 4), room.roomStatus, room.residencePeriod.stringValue(), room.remaining @@ -217,7 +263,10 @@ public List findLikedRooms(String userNo) { room.currentMateCount, room.createdAt, room.title, + room.notes, user.nickname, + user.major, + user.studentNo.substring(2, 4), room.roomStatus, room.residencePeriod.stringValue(), room.remaining @@ -245,7 +294,10 @@ public List findAppliedRooms(String userNo) { room.currentMateCount, room.createdAt, room.title, + room.notes, user.nickname, + user.major, + user.studentNo.substring(2, 4), room.roomStatus, room.residencePeriod.stringValue(), room.remaining @@ -271,9 +323,16 @@ private BooleanExpression cursorPredicate(LocalDateTime cursorCreatedAt, String private BooleanExpression remainingCursorPredicate(Integer cursorRemaining, LocalDateTime cursorCreatedAt, String cursorId) { if (cursorRemaining == null || cursorCreatedAt == null || cursorId == null) return null; - return room.remaining.gt(cursorRemaining) - .or(room.remaining.eq(cursorRemaining).and(room.createdAt.lt(cursorCreatedAt))) + BooleanExpression sameRemainingNext = room.remaining.eq(cursorRemaining).and(room.createdAt.lt(cursorCreatedAt)) .or(room.remaining.eq(cursorRemaining).and(room.createdAt.eq(cursorCreatedAt)).and(room.roomNo.lt(cursorId))); + + if (cursorRemaining == 0) { + return sameRemainingNext; + } + + return room.remaining.gt(cursorRemaining).and(room.remaining.ne(0)) + .or(room.remaining.eq(0)) + .or(sameRemainingNext); } private BooleanExpression eqRoomType(ChecklistFilterRequest request) { @@ -330,6 +389,37 @@ private void appendCondition( params.put(parameterName, value); } + private void appendRoomRuleConditions( + StringBuilder sql, + Map params, + ChecklistFilterRequest request + ) { + appendCondition(sql, params, "rr.bedtime", "bedtime", request.bedtime()); + appendCondition(sql, params, "rr.wake_up", "wakeUp", request.wakeUp()); + appendCondition(sql, params, "rr.return_home", "returnHome", enumName(request.returnHome())); + appendCondition(sql, params, "rr.return_home_time", "returnHomeTime", request.returnHomeTime()); + appendCondition(sql, params, "rr.cleaning", "cleaning", enumName(request.cleaning())); + appendCondition(sql, params, "rr.phone_call", "phoneCall", enumName(request.phoneCall())); + appendCondition(sql, params, "rr.sleep_light", "sleepLight", enumName(request.sleepLight())); + appendCondition(sql, params, "rr.sleep_habit", "sleepHabit", enumName(request.sleepHabit())); + appendCondition(sql, params, "rr.snoring", "snoring", enumName(request.snoring())); + appendCondition(sql, params, "rr.shower_time", "showerTime", enumName(request.showerTime())); + appendCondition(sql, params, "rr.eating", "eating", enumName(request.eating())); + appendCondition(sql, params, "rr.lights_out", "lightsOut", enumName(request.lightsOut())); + appendCondition(sql, params, "rr.lights_out_time", "lightsOutTime", request.lightsOutTime()); + appendCondition(sql, params, "rr.home_visit", "homeVisit", enumName(request.homeVisit())); + appendCondition(sql, params, "rr.smoking", "smoking", enumName(request.smoking())); + appendCondition(sql, params, "rr.refrigerator", "refrigerator", enumName(request.refrigerator())); + appendCondition(sql, params, "rr.hair_dryer", "hairDryer", request.hairDryer()); + appendCondition(sql, params, "rr.alarm", "alarm", enumName(request.alarm())); + appendCondition(sql, params, "rr.earphone", "earphone", enumName(request.earphone())); + appendCondition(sql, params, "rr.keyskin", "keyskin", enumName(request.keyskin())); + appendCondition(sql, params, "rr.heat", "heat", enumName(request.heat())); + appendCondition(sql, params, "rr.cold", "cold", enumName(request.cold())); + appendCondition(sql, params, "rr.study", "study", enumName(request.study())); + appendCondition(sql, params, "rr.trash_can", "trashCan", enumName(request.trashCan())); + } + private void appendCursorCondition( StringBuilder sql, Map params, @@ -343,15 +433,29 @@ private void appendCursorCondition( return; } - sql.append(""" - - AND ( - r.remaining > :cursorRemaining - OR (r.remaining = :cursorRemaining AND r.created_at < :cursorCreatedAt) - OR (r.remaining = :cursorRemaining AND r.created_at = :cursorCreatedAt AND r.room_no < :cursorId) - ) - """); - params.put("cursorRemaining", cursorRemaining); + if (cursorRemaining == 0) { + sql.append(""" + + AND ( + r.remaining = 0 + AND ( + r.created_at < :cursorCreatedAt + OR (r.created_at = :cursorCreatedAt AND r.room_no < :cursorId) + ) + ) + """); + } else { + sql.append(""" + + AND ( + (r.remaining > :cursorRemaining AND r.remaining <> 0) + OR r.remaining = 0 + OR (r.remaining = :cursorRemaining AND r.created_at < :cursorCreatedAt) + OR (r.remaining = :cursorRemaining AND r.created_at = :cursorCreatedAt AND r.room_no < :cursorId) + ) + """); + params.put("cursorRemaining", cursorRemaining); + } params.put("cursorCreatedAt", cursorCreatedAt); params.put("cursorId", cursorId); return; @@ -374,7 +478,8 @@ private void appendCursorCondition( private void appendOrderBy(StringBuilder sql, String alias, ChecklistFilterRequest.SortType sortType) { if (sortType == ChecklistFilterRequest.SortType.REMAINING) { - sql.append(alias).append(".remaining ASC, ") + sql.append("CASE WHEN ").append(alias).append(".remaining = 0 THEN 1 ELSE 0 END ASC, ") + .append(alias).append(".remaining ASC, ") .append(alias).append(".created_at DESC, ") .append(alias).append(".room_no DESC"); return; @@ -393,9 +498,12 @@ private FindRoomsResponse toFindRoomsResponse(Object[] row) { localDateTimeValue(row[4]), stringValue(row[5]), stringValue(row[6]), - enumValue(RoomStatus.class, row[7]), + stringValue(row[7]), stringValue(row[8]), - intValue(row[9]) + stringValue(row[9]), + enumValue(RoomStatus.class, row[10]), + stringValue(row[11]), + intValue(row[12]) ); } diff --git a/src/main/java/com/project/dorumdorum/domain/room/ui/FindRoomsController.java b/src/main/java/com/project/dorumdorum/domain/room/ui/FindRoomsController.java index b9f352a7..6ecca1b6 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/ui/FindRoomsController.java +++ b/src/main/java/com/project/dorumdorum/domain/room/ui/FindRoomsController.java @@ -1,12 +1,11 @@ package com.project.dorumdorum.domain.room.ui; import com.project.dorumdorum.domain.room.application.dto.request.ChecklistFilterRequest; -import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsResponse; +import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsPageResponse; import com.project.dorumdorum.domain.room.application.usecase.FindRoomsUseCase; import com.project.dorumdorum.domain.room.ui.spec.FindRoomsApiSpec; import com.project.dorumdorum.global.annotation.CurrentUser; import org.springframework.http.ResponseEntity; -import com.project.dorumdorum.global.pagination.CursorPage; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.RequestBody; @@ -19,7 +18,7 @@ public class FindRoomsController implements FindRoomsApiSpec { private final FindRoomsUseCase findRoomsUseCase; @Override - public ResponseEntity> loadAll( + public ResponseEntity loadAll( @CurrentUser String userNo, @Valid @RequestBody ChecklistFilterRequest request ) { diff --git a/src/main/java/com/project/dorumdorum/domain/room/ui/LoadRecommendedRoomsController.java b/src/main/java/com/project/dorumdorum/domain/room/ui/LoadRecommendedRoomsController.java new file mode 100644 index 00000000..8b57554c --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/ui/LoadRecommendedRoomsController.java @@ -0,0 +1,23 @@ +package com.project.dorumdorum.domain.room.ui; + +import com.project.dorumdorum.domain.room.application.dto.response.RecommendedRoomsPageResponse; +import com.project.dorumdorum.domain.room.application.usecase.LoadRecommendedRoomsUseCase; +import com.project.dorumdorum.domain.room.ui.spec.LoadRecommendedRoomsApiSpec; +import com.project.dorumdorum.global.annotation.CurrentUser; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LoadRecommendedRoomsController implements LoadRecommendedRoomsApiSpec { + + private final LoadRecommendedRoomsUseCase loadRecommendedRoomsUseCase; + + @Override + public ResponseEntity loadRecommended( + @CurrentUser String userNo + ) { + return ResponseEntity.ok(loadRecommendedRoomsUseCase.execute(userNo)); + } +} diff --git a/src/main/java/com/project/dorumdorum/domain/room/ui/spec/FindRoomsApiSpec.java b/src/main/java/com/project/dorumdorum/domain/room/ui/spec/FindRoomsApiSpec.java index 944bc087..866cbb03 100644 --- a/src/main/java/com/project/dorumdorum/domain/room/ui/spec/FindRoomsApiSpec.java +++ b/src/main/java/com/project/dorumdorum/domain/room/ui/spec/FindRoomsApiSpec.java @@ -1,8 +1,7 @@ package com.project.dorumdorum.domain.room.ui.spec; import com.project.dorumdorum.domain.room.application.dto.request.ChecklistFilterRequest; -import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsResponse; -import com.project.dorumdorum.global.pagination.CursorPage; +import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsPageResponse; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import io.swagger.v3.oas.annotations.Operation; @@ -17,10 +16,11 @@ public interface FindRoomsApiSpec { summary = "방 목록 조회 API", description = "체크리스트 및 방 조건을 기준으로 방 목록을 조회합니다. " + "커서 기반 페이지네이션을 지원합니다. " + + "첫 페이지에서는 검색 조건에 맞는 전체 방 개수를 반환합니다. " + "로그인한 사용자의 성별과 일치하는 방만 반환합니다." ) @PostMapping("/api/rooms/search") - ResponseEntity> loadAll( + ResponseEntity loadAll( @Parameter(hidden = true) String userNo, @Parameter(description = "체크리스트 기반 방 검색 조건", required = true) @Valid @RequestBody ChecklistFilterRequest request diff --git a/src/main/java/com/project/dorumdorum/domain/room/ui/spec/LoadRecommendedRoomsApiSpec.java b/src/main/java/com/project/dorumdorum/domain/room/ui/spec/LoadRecommendedRoomsApiSpec.java new file mode 100644 index 00000000..3f902e66 --- /dev/null +++ b/src/main/java/com/project/dorumdorum/domain/room/ui/spec/LoadRecommendedRoomsApiSpec.java @@ -0,0 +1,21 @@ +package com.project.dorumdorum.domain.room.ui.spec; + +import com.project.dorumdorum.domain.room.application.dto.response.RecommendedRoomsPageResponse; +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; + +@Tag(name = "Room") +public interface LoadRecommendedRoomsApiSpec { + + @Operation( + summary = "나와 잘 맞는 방 목록 조회 API", + description = "내 체크리스트와 비교해 65% 이상 일치하는 방을 매칭률 순으로 반환합니다. hasChecklist가 false면 체크리스트 미작성 상태입니다." + ) + @GetMapping("/api/rooms/recommended") + ResponseEntity loadRecommended( + @Parameter(hidden = true) String userNo + ); +} diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/infra/repository/RoomRepositoryImplTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/infra/repository/RoomRepositoryImplTest.java index 9338808e..88e98c02 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/infra/repository/RoomRepositoryImplTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/infra/repository/RoomRepositoryImplTest.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -55,7 +56,7 @@ void findByCursor_WhenRecruiting_FetchesWithLimit() { JPAQuery jpaQuery = mock(JPAQuery.class, RETURNS_SELF); List expected = List.of( new FindRoomsResponse("r1", RoomType.TYPE_1, 2, 1, LocalDateTime.now(), - "title", "host", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1) + "title", null, "host", "경영", "22", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1) ); when(queryFactory.select(org.mockito.ArgumentMatchers.>any())).thenReturn(jpaQuery); @@ -162,7 +163,10 @@ void findByCursor_WithChecklistFilters_UsesNativeLateralQuery() { 1, createdAt, "title", + null, "host", + "경영", + "22", RoomStatus.CONFIRM_PENDING.name(), ResidencePeriod.SEMESTER.name(), 1 @@ -178,13 +182,88 @@ void findByCursor_WithChecklistFilters_UsesNativeLateralQuery() { assertThat(result).containsExactly( new FindRoomsResponse("r1", RoomType.TYPE_1, 2, 1, createdAt, - "title", "host", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1) + "title", null, "host", "경영", "22", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1) ); - verify(entityManager).createNativeQuery(anyString()); + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + verify(entityManager).createNativeQuery(sqlCaptor.capture()); + assertThat(sqlCaptor.getValue()) + .contains("CASE WHEN r.remaining = 0 THEN 1 ELSE 0 END ASC") + .contains("OR r.remaining = 0"); verify(nativeQuery).getResultList(); verifyNoInteractions(queryFactory); } + @Test + @DisplayName("Should paginate within full rooms without unused remaining parameter") + void findByCursor_WithChecklistFiltersAndFullRoomCursor_UsesFullRoomCursor() { + RoomRepositoryImpl repository = new RoomRepositoryImpl(queryFactory, entityManager); + Query nativeQuery = mock(Query.class); + ChecklistFilterRequest request = new ChecklistFilterRequest( + ChecklistFilterRequest.SortType.REMAINING, + null, null, null, null, + null, null, null, null, null, null, null, null, null, null, + null, null, null, null, + SmokingType.NON_SMOKER, + null, null, null, null, null, null, null, null, null + ); + + when(entityManager.createNativeQuery(anyString())).thenReturn(nativeQuery); + when(nativeQuery.setParameter(anyString(), any())).thenReturn(nativeQuery); + when(nativeQuery.getResultList()).thenReturn(List.of()); + + repository.findByCursor(DEFAULT_GENDER, request, LocalDateTime.now(), "r1", 0, 51); + + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + verify(entityManager).createNativeQuery(sqlCaptor.capture()); + assertThat(sqlCaptor.getValue()) + .contains("r.remaining = 0") + .doesNotContain(":cursorRemaining"); + verify(nativeQuery, never()).setParameter("cursorRemaining", 0); + } + + @SuppressWarnings("unchecked") + @Test + @DisplayName("Should count rooms with standard filters") + void countByFilter_WithStandardFilters_UsesQueryDsl() { + RoomRepositoryImpl repository = new RoomRepositoryImpl(queryFactory, entityManager); + JPAQuery jpaQuery = mock(JPAQuery.class, RETURNS_SELF); + ChecklistFilterRequest request = request(ChecklistFilterRequest.SortType.LATEST); + + when(queryFactory.select(org.mockito.ArgumentMatchers.>any())).thenReturn(jpaQuery); + when(jpaQuery.fetchOne()).thenReturn(42L); + + long result = repository.countByFilter(DEFAULT_GENDER, request); + + assertThat(result).isEqualTo(42L); + verify(jpaQuery).fetchOne(); + verifyNoInteractions(entityManager); + } + + @Test + @DisplayName("Should count rooms with checklist filters using native query") + void countByFilter_WithChecklistFilters_UsesNativeQuery() { + RoomRepositoryImpl repository = new RoomRepositoryImpl(queryFactory, entityManager); + Query nativeQuery = mock(Query.class); + ChecklistFilterRequest request = new ChecklistFilterRequest( + ChecklistFilterRequest.SortType.LATEST, + null, null, null, null, + null, null, null, null, null, null, null, null, null, null, + null, null, null, null, + SmokingType.NON_SMOKER, + null, null, null, null, null, null, null, null, null + ); + + when(entityManager.createNativeQuery(anyString())).thenReturn(nativeQuery); + when(nativeQuery.setParameter(anyString(), any())).thenReturn(nativeQuery); + when(nativeQuery.getSingleResult()).thenReturn(7L); + + long result = repository.countByFilter(DEFAULT_GENDER, request); + + assertThat(result).isEqualTo(7L); + verify(nativeQuery).getSingleResult(); + verifyNoInteractions(queryFactory); + } + @SuppressWarnings("unchecked") @Test @DisplayName("Should return optional result for my room query") @@ -192,7 +271,7 @@ void findMyRoom_ReturnsOptional() { RoomRepositoryImpl repository = new RoomRepositoryImpl(queryFactory, entityManager); JPAQuery jpaQuery = mock(JPAQuery.class, RETURNS_SELF); FindRoomsResponse response = new FindRoomsResponse("r1", RoomType.TYPE_1, 2, 1, LocalDateTime.now(), - "title", "host", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1); + "title", null, "host", "경영", "22", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1); when(queryFactory.select(org.mockito.ArgumentMatchers.>any())).thenReturn(jpaQuery); when(jpaQuery.fetchOne()).thenReturn(response); diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java index 7f4441f4..d923a48b 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/service/RoomServiceTest.java @@ -39,7 +39,7 @@ class RoomServiceTest { @Test @DisplayName("Should create and save room with gender from request") void create_SavesRoom() { - RoomCreateRequest request = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", null); + RoomCreateRequest request = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", null, null); Room saved = Room.builder().roomNo("r1").hostUserNo("u1").gender(Gender.MALE).build(); when(roomRepository.save(any(Room.class))).thenReturn(saved); @@ -61,7 +61,7 @@ void findById_WhenMissing_Throws() { void searchByCursor_DelegatesToRepository() { List expected = List.of( new FindRoomsResponse("r1", RoomType.TYPE_1, 2, 1, LocalDateTime.now(), - "title", "host", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1) + "title", null, "host", "경영", "22", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1) ); ChecklistFilterRequest request = new ChecklistFilterRequest( ChecklistFilterRequest.SortType.LATEST, null, null, null, null, @@ -79,6 +79,23 @@ void searchByCursor_DelegatesToRepository() { assertThat(result).isEqualTo(expected); } + @Test + @DisplayName("Should delegate filtered room count to repository") + void countSearchResults_DelegatesToRepository() { + ChecklistFilterRequest request = new ChecklistFilterRequest( + 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 + ); + when(roomRepository.countByFilter(Gender.MALE, request)).thenReturn(42L); + + long result = service.countSearchResults(Gender.MALE, request); + + assertThat(result).isEqualTo(42L); + verify(roomRepository).countByFilter(Gender.MALE, request); + } + @Test @DisplayName("Should return null when my room is not found") void findMyRoom_WhenMissing_ReturnsNull() { diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/ui/CreateRoomControllerTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/ui/CreateRoomControllerTest.java index d82b2747..4c5543cf 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/ui/CreateRoomControllerTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/ui/CreateRoomControllerTest.java @@ -30,7 +30,7 @@ class CreateRoomControllerTest { @Test void create_CallsUseCase() { - RoomCreateRequest req = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", + RoomCreateRequest req = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", null, mock(CreateRoomRuleRequest.class)); when(useCase.execute("u1", req)).thenReturn("r1"); ResponseEntity response = controller.create("u1", req); 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 da15bfc8..83ca2706 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 @@ -1,10 +1,10 @@ package com.project.dorumdorum.domain.room.unit.ui; import com.project.dorumdorum.domain.room.application.dto.request.ChecklistFilterRequest; +import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsPageResponse; import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsResponse; import com.project.dorumdorum.domain.room.application.usecase.FindRoomsUseCase; import com.project.dorumdorum.domain.room.ui.FindRoomsController; -import com.project.dorumdorum.global.pagination.CursorPage; import org.springframework.http.ResponseEntity; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -36,10 +36,10 @@ void loadAll_ReturnsUseCaseResult() { null, null, null, null, null, null, null, null, null, null, null, null, null, null ); - CursorPage page = new CursorPage<>(List.of(), null, false); + FindRoomsPageResponse page = new FindRoomsPageResponse(List.of(), null, false, 0L); when(useCase.execute(USER_NO, request)).thenReturn(page); - ResponseEntity> response = + ResponseEntity response = controller.loadAll(USER_NO, request); verify(useCase).execute(USER_NO, request); diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/ui/UpdateRoomTitleControllerTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/ui/UpdateRoomTitleControllerTest.java index 70dc7f75..995a2358 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/ui/UpdateRoomTitleControllerTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/ui/UpdateRoomTitleControllerTest.java @@ -23,7 +23,7 @@ class UpdateRoomTitleControllerTest { @Test void update_CallsUseCase() { - UpdateRoomTitleRequest req = new UpdateRoomTitleRequest("new"); + UpdateRoomTitleRequest req = new UpdateRoomTitleRequest("new", null); ResponseEntity response = controller.update("u1", "r1", req); verify(useCase).execute("u1", "r1", req); assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/CreateRoomUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/CreateRoomUseCaseTest.java index 9a414451..e10aede0 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/CreateRoomUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/CreateRoomUseCaseTest.java @@ -43,7 +43,7 @@ class CreateRoomUseCaseTest { void execute_WhenUserAlreadyInRoom_Throws() { String userNo = "u1"; CreateRoomRuleRequest ruleRequest = mock(CreateRoomRuleRequest.class); - RoomCreateRequest request = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", ruleRequest); + RoomCreateRequest request = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", null, ruleRequest); when(roommateService.existsByUserNo(userNo)).thenReturn(true); @@ -59,7 +59,7 @@ void execute_CreatesRoomAndHostAndRule() { String userNo = "u1"; User host = User.builder().userNo(userNo).gender(Gender.MALE).build(); CreateRoomRuleRequest ruleRequest = mock(CreateRoomRuleRequest.class); - RoomCreateRequest request = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", ruleRequest); + RoomCreateRequest request = new RoomCreateRequest(RoomType.TYPE_1, 2, ResidencePeriod.SEMESTER, "title", null, ruleRequest); Room room = Room.builder().roomNo("r1").gender(Gender.MALE).build(); RoomRule roomRule = mock(RoomRule.class); diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/FindRoomsUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/FindRoomsUseCaseTest.java index 043e5454..e2bc389d 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/FindRoomsUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/FindRoomsUseCaseTest.java @@ -1,6 +1,7 @@ package com.project.dorumdorum.domain.room.unit.usecase; import com.project.dorumdorum.domain.room.application.dto.request.ChecklistFilterRequest; +import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsPageResponse; import com.project.dorumdorum.domain.room.application.dto.response.FindRoomsResponse; import com.project.dorumdorum.domain.room.application.usecase.FindRoomsUseCase; import com.project.dorumdorum.domain.room.domain.entity.ResidencePeriod; @@ -10,7 +11,6 @@ import com.project.dorumdorum.domain.user.domain.entity.Gender; import com.project.dorumdorum.domain.user.domain.entity.User; import com.project.dorumdorum.domain.user.domain.service.UserService; -import com.project.dorumdorum.global.pagination.CursorPage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,8 +46,8 @@ void setUp() { } private FindRoomsResponse room(String roomNo, int capacity, int current, LocalDateTime createdAt) { - return new FindRoomsResponse(roomNo, RoomType.TYPE_1, capacity, current, createdAt, "title", - "host", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), capacity - current); + return new FindRoomsResponse(roomNo, RoomType.TYPE_1, capacity, current, createdAt, "title", null, + "host", "경영", "22", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), capacity - current); } private ChecklistFilterRequest request(ChecklistFilterRequest.SortType sortType, String cursor) { @@ -68,12 +69,14 @@ void execute_WhenResponsesExceedLimit_ReturnsHasNextTrue() { ChecklistFilterRequest request = request(ChecklistFilterRequest.SortType.LATEST, null); when(roomService.searchByCursor(any(), any(), any(), any(), any(), anyInt())) .thenReturn(responses); + when(roomService.countSearchResults(any(), any())).thenReturn(120L); - CursorPage result = useCase.execute(USER_NO, request); + FindRoomsPageResponse result = useCase.execute(USER_NO, request); assertThat(result.items()).hasSize(50); assertThat(result.hasNext()).isTrue(); assertThat(result.nextCursor()).isNotBlank(); + assertThat(result.totalCount()).isEqualTo(120L); verify(roomService).searchByCursor(any(), any(), any(), any(), any(), anyInt()); } @@ -85,7 +88,7 @@ void execute_WhenResponsesWithinLimit_ReturnsHasNextFalse() { when(roomService.searchByCursor(any(), any(), any(), any(), any(), anyInt())) .thenReturn(responses); - CursorPage result = useCase.execute(USER_NO, request); + FindRoomsPageResponse result = useCase.execute(USER_NO, request); assertThat(result.items()).hasSize(1); assertThat(result.hasNext()).isFalse(); @@ -99,7 +102,7 @@ void execute_WhenNoResponse_ReturnsNullCursor() { when(roomService.searchByCursor(any(), any(), any(), any(), any(), anyInt())) .thenReturn(List.of()); - CursorPage result = useCase.execute(USER_NO, request); + FindRoomsPageResponse result = useCase.execute(USER_NO, request); assertThat(result.items()).isEmpty(); assertThat(result.nextCursor()).isNull(); @@ -115,9 +118,11 @@ void execute_WithCursor_UsesDecodedCursorPath() { when(roomService.searchByCursor(any(), any(), any(), any(), any(), anyInt())) .thenReturn(responses); - CursorPage result = useCase.execute(USER_NO, request); + FindRoomsPageResponse result = useCase.execute(USER_NO, request); assertThat(result.items()).hasSize(1); + assertThat(result.totalCount()).isNull(); verify(roomService).searchByCursor(any(), any(), any(), any(), any(), anyInt()); + verify(roomService, never()).countSearchResults(any(), any()); } } diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/LoadMyRoomsUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/LoadMyRoomsUseCaseTest.java index 5f7d0b5e..d1cf250e 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/LoadMyRoomsUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/LoadMyRoomsUseCaseTest.java @@ -30,7 +30,7 @@ class LoadMyRoomsUseCaseTest { @DisplayName("Should return my room from room service") void execute_ReturnsMyRoom() { FindRoomsResponse expected = new FindRoomsResponse("r1", RoomType.TYPE_1, 2, 1, LocalDateTime.now(), - "title", "host", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1); + "title", null, "host", "경영", "22", RoomStatus.CONFIRM_PENDING, ResidencePeriod.SEMESTER.name(), 1); when(roomService.findMyRoom("u1")).thenReturn(expected); FindRoomsResponse result = useCase.execute("u1"); diff --git a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/UpdateRoomTitleUseCaseTest.java b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/UpdateRoomTitleUseCaseTest.java index d9b3b79b..bdbef1f3 100644 --- a/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/UpdateRoomTitleUseCaseTest.java +++ b/src/test/java/com/project/dorumdorum/domain/room/unit/usecase/UpdateRoomTitleUseCaseTest.java @@ -32,9 +32,10 @@ void execute_WhenHost_UpdatesTitle() { when(roomService.findById("r1")).thenReturn(room); when(roommateService.isHost("u1", room)).thenReturn(true); - useCase.execute("u1", "r1", new UpdateRoomTitleRequest(" new title ")); + useCase.execute("u1", "r1", new UpdateRoomTitleRequest(" new title ", "notes")); verify(room).updateTitle("new title"); + verify(room).updateNotes("notes"); } @Test @@ -44,7 +45,7 @@ void execute_WhenNotHost_Throws() { when(roomService.findById("r1")).thenReturn(room); when(roommateService.isHost("u1", room)).thenReturn(false); - assertThatThrownBy(() -> useCase.execute("u1", "r1", new UpdateRoomTitleRequest("title"))) + assertThatThrownBy(() -> useCase.execute("u1", "r1", new UpdateRoomTitleRequest("title", null))) .isInstanceOf(RestApiException.class); } }