Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.ac.kookmin.stream.api.admin.AdminApiUser;
import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerPeriodParams;
import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerStatusChangeRequest;
import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerSectionDetailResponse;
import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerSectionListResponse;
import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerStatusChangeResponse;
import kr.ac.kookmin.stream.api.common.dto.ApiResponse;
import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode;
import kr.ac.kookmin.stream.common.CommonErrorCode;
Expand All @@ -19,7 +21,7 @@
* <p>
* 구역은 운영 회차에 묶여 있지 않다. 요청의 회차는 "어느 회차의 배정 현황을 볼지"를 정하는 기준이다.
*/
@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회")
@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회와 사물함 상태 변경")
public interface AdminLockerSectionApi {

/** 구역별 전체·선택 가능·배정 사물함 수. */
Expand Down Expand Up @@ -47,4 +49,20 @@ ApiResponse<AdminLockerSectionDetailResponse> getLockerSection(
Long sectionId,
@ParameterObject LockerPeriodParams params
);

/** 구역 안의 사물함 여러 개를 같은 상태로 바꾼다. */
@Operation(summary = "사물함 상태 일괄 변경",
description = "구역 안의 사물함 여러 개를 AVAILABLE(선택가능) 또는 DISABLED(선택불가)로 한 번에 바꾼다. "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 질문 하나 드려요. 학생 쪽 구역 상태(SectionAvailabilityStatus.from)는 사용 중지 칸도 분모에 넣고 있어서, 이 API로 DISABLED가 흔해지면 예를 들어 36칸 중 30칸 사용 중지·6칸 빈 구역이 6/36(약 17%)으로 계산되어 학생에게 "거의 마감"으로 보일 것 같아요. 의도하신 동작인지 궁금합니다!

+ "하나라도 바꿀 수 없으면 아무것도 바뀌지 않는다. "
+ "사용 기간이 끝나지 않은 회차에 배정된 사물함은 이용 중인 학생이 있으므로 DISABLED로 바꿀 수 없고, "
+ "AVAILABLE로 되돌리는 것은 막지 않는다. "
+ "응답은 요청한 사물함 전체로, 이미 그 상태였던 사물함도 담긴다. "
+ "구역 상세가 내려주는 ASSIGNED는 신청으로 생기는 표시용 값이라 이 경로로 바꿀 수 없다.")
@ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"})
@ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_NOT_IN_SECTION", "LOCKER_IN_USE"})
ApiResponse<AdminLockerStatusChangeResponse> changeLockerStatus(
AdminApiUser apiUser,
Long sectionId,
LockerStatusChangeRequest request
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,24 @@
import java.util.Set;
import kr.ac.kookmin.stream.api.admin.AdminApiUser;
import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerPeriodParams;
import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerStatusChangeRequest;
import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerSectionDetailResponse;
import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerSectionListResponse;
import kr.ac.kookmin.stream.api.admin.event.locker.response.AdminLockerStatusChangeResponse;
import kr.ac.kookmin.stream.api.common.dto.ApiResponse;
import kr.ac.kookmin.stream.event.domain.locker.domain.Locker;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerSectionDetail;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerSectionSummary;
import kr.ac.kookmin.stream.event.domain.locker.service.AdminLockerService;
import kr.ac.kookmin.stream.event.domain.locker.service.LockerService;
import kr.ac.kookmin.stream.file.domain.File;
import kr.ac.kookmin.stream.file.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -27,6 +33,7 @@
public class AdminLockerSectionController implements AdminLockerSectionApi {

private final LockerService lockerService;
private final AdminLockerService adminLockerService;
private final FileService fileService;

@Override
Expand Down Expand Up @@ -56,4 +63,16 @@ public ApiResponse<AdminLockerSectionDetailResponse> getLockerSection(
return ApiResponse.success(
AdminLockerSectionDetailResponse.of(detail, photosById, appliedLockerIds));
}

@Override
@PatchMapping("/{sectionId}/lockers/status")
public ApiResponse<AdminLockerStatusChangeResponse> changeLockerStatus(
AdminApiUser apiUser,
@PathVariable Long sectionId,
@Valid @RequestBody LockerStatusChangeRequest request
) {
List<Locker> lockers = adminLockerService.changeLockerStatus(
sectionId, request.lockerIds(), request.lockerStatus());
return ApiResponse.success(AdminLockerStatusChangeResponse.from(lockers));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package kr.ac.kookmin.stream.api.admin.event.locker.request;

import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus;

/**
* 구역 안의 사물함 여러 개를 같은 상태로 바꾼다.
* <p>
* 구역 상세가 내려주는 {@code ASSIGNED}는 신청으로 생기는 표시용 값이라 이 경로로는 바꿀 수 없다.
*/
public record LockerStatusChangeRequest(
@NotEmpty(message = "사물함을 하나 이상 선택해 주세요.")
List<@NotNull(message = "사물함 ID는 비워 둘 수 없습니다.") Long> lockerIds,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 같은 id가 여러 번 오면 응답에도 같은 칸이 여러 번 담겨서, lockerIds.stream().distinct()로 한 번 걸러주면 좋을 것 같아요. 한 구역의 칸 수를 넘는 요청은 의미가 없으니 @Size(max = ...) 상한도 하나 있으면 안전할 것 같고요.


@NotNull(message = "변경할 상태를 입력해 주세요.")
LockerStatus lockerStatus
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kr.ac.kookmin.stream.api.admin.event.locker.response;

import java.util.List;
import kr.ac.kookmin.stream.event.domain.locker.domain.Locker;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus;

/**
* 요청한 사물함 전체를 바뀐 뒤 상태로 돌려준다. 이미 그 상태였던 사물함도 담긴다.
*/
public record AdminLockerStatusChangeResponse(List<LockerItem> lockers) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] 리뷰 포인트 주신 응답 이름은 coding-style.md 1절 표({Domain}{Action}Response)와 공지·회비 관리자 API(NoticeCreateResponse, FeeStatusUpdateResponse)가 접두사 없이 가고 있어서, 요청에 맞춰 LockerStatusChangeResponse로 짝을 맞추는 쪽이 어떨까요? 위 코멘트대로 Void로 바꾸시면 자연히 없어지는 고민이기도 해요.


public static AdminLockerStatusChangeResponse from(List<Locker> lockers) {
return new AdminLockerStatusChangeResponse(lockers.stream()
.map(LockerItem::from)
.toList());
}

/** 도메인 {@link Locker}와 이름이 겹치지 않도록 {@code LockerItem}으로 둔다. */
public record LockerItem(Long lockerId, int lockerNumber, LockerStatus lockerStatus) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] 응답 관련 리뷰 포인트([r] 두 번째, [a])에 대한 의견이에요. 구역 상세 화면은 LockerDisplayStatus(AVAILABLE/DISABLED/ASSIGNED, 회차 기준)를 쓰는데 이 응답은 물리 상태인 LockerStatus를 줘서, 응답으로 칸을 다시 그리면 어긋날 수 있을 것 같아요. 예를 들어 배정된 칸(물리 상태 AVAILABLE)을 포함해 AVAILABLE로 일괄 변경하면 응답에 AVAILABLE이 와서, 화면이 그 칸을 "선택 가능"으로 그리게 돼요. 필드 이름도 상세는 status, 여기는 lockerStatus라 다르고요.

회차 관리 쓰기 API처럼 ApiResponse<Void>로 돌려주고 화면이 상세를 다시 조회하게 하면, "바뀐 것만 담을지"와 "lockerNumber를 넣을지" 고민도 같이 없어질 것 같아요.


public static LockerItem from(Locker locker) {
return new LockerItem(locker.getId(), locker.getLockerNumber(), locker.getStatus());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,17 @@ public static Locker of(
public boolean isSelectable(boolean applied) {
return status == LockerStatus.AVAILABLE && !applied;
}

/**
* 사물함 자체 상태를 바꾼다. 바뀐 값이 있으면 true를 돌려준다.
* <p>
* 운영진이 여러 칸을 한 번에 바꾸므로, 이미 그 상태인 사물함은 저장하지 않도록 바뀌었는지 알려준다.
*/
public boolean changeStatus(LockerStatus status) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 바뀌었는지를 boolean으로 돌려주고 서비스에서 filter 안에서 상태를 바꾸는 구조라, 스트림의 non-interference 원칙에 어긋나고 나중에 anyMatch처럼 중간에 멈추는 연산으로 바뀌면 변경이 일부 빠질 수 있을 것 같아요. 사물함들이 같은 트랜잭션에서 findLockersBySectionId로 이미 영속성 컨텍스트에 올라와 있어서, 전부 넘겨도 실제로 값이 바뀐 행만 UPDATE될 거예요. 그래서 boolean 없이 단순하게 가도 될 것 같습니다.

public void changeStatus(LockerStatus status) {
    this.status = status;
}
lockers.forEach(locker -> locker.changeStatus(status));
lockerRepository.saveLockers(lockers);

if (this.status == status) {
return false;
}
this.status = status;
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public enum LockerErrorCode implements ErrorCode {
LOCKER_PERIOD_APPLY_ALREADY_STARTED(ErrorStatus.CONFLICT, "신청이 시작된 회차는 신청 시작일을 바꿀 수 없습니다."),
LOCKER_PERIOD_HAS_APPLICATIONS(ErrorStatus.CONFLICT, "신청이 있는 회차는 삭제할 수 없습니다."),
LOCKER_APPLY_NOT_OPEN(ErrorStatus.CONFLICT, "사물함 신청 기간이 아닙니다."),
LOCKER_NOT_IN_SECTION(ErrorStatus.BAD_REQUEST, "해당 구역의 사물함이 아닙니다."),
LOCKER_IN_USE(ErrorStatus.CONFLICT, "이용 중인 사물함은 사용 중지할 수 없습니다."),
LOCKER_ALREADY_ASSIGNED(ErrorStatus.CONFLICT, "다른 사용자가 먼저 신청한 사물함입니다.");

private final int status;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.ac.kookmin.stream.event.domain.locker.repository;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -35,6 +36,11 @@ public interface LockerApplicationRepository {
*/
Optional<Long> findAppliedLockerId(Long lockerPeriodId, Long memberId);

/**
* 해당 사물함들의 신청 전체. 어느 운영 회차에 배정됐는지 보려고 읽는다.
*/
List<LockerApplication> findByLockerIds(Collection<Long> lockerIds);

/**
* 회원의 사물함 신청 전체를 신청 일시 최신순으로 조회한다.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,9 @@ public interface LockerRepository {
* 식별자에 해당하는 사물함. 지난 신청 이력의 사물함 이름을 보여줘야 하므로 삭제된 사물함도 포함한다.
*/
List<Locker> findLockersByIdsIncludingDeleted(Collection<Long> lockerIds);

/**
* 사물함 여러 건을 저장한다. 변경은 호출한 쪽 트랜잭션이 커밋될 때 반영된다.
*/
void saveLockers(List<Locker> lockers);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package kr.ac.kookmin.stream.event.domain.locker.service;

import java.util.List;
import kr.ac.kookmin.stream.event.domain.locker.domain.Locker;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodCreateCommand;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodSummary;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodUpdateCommand;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus;

/**
* 운영진 사물함 API가 쓰는 서비스.
Expand Down Expand Up @@ -36,4 +38,14 @@ public interface AdminLockerService {
* 운영 회차를 소프트 삭제한다. 신청이 한 건이라도 있으면 거부한다.
*/
void deletePeriod(Long lockerPeriodId);

/**
* 구역 안의 사물함 여러 개의 상태를 한 번에 바꾸고, 바뀐 사물함을 돌려준다.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 본문은 "바뀐 사물함을 돌려준다"인데 @return은 "요청한 사물함 전체. 이미 그 상태였던 사물함도 포함한다"라서 한쪽으로 맞춰주시면 좋겠어요.

* <p>
* 하나라도 바꿀 수 없으면 아무것도 바꾸지 않는다. 사용 기간이 끝나지 않은 회차에 배정된 사물함은
* 이용 중인 학생이 있으므로 {@code DISABLED}로 바꿀 수 없다.
*
* @return 요청한 사물함 전체. 이미 그 상태였던 사물함도 포함한다
*/
List<Locker> changeLockerStatus(Long sectionId, List<Long> lockerIds, LockerStatus status);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
package kr.ac.kookmin.stream.event.domain.locker.service.impl;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import kr.ac.kookmin.stream.common.BusinessException;
import kr.ac.kookmin.stream.event.domain.locker.domain.Locker;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplication;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerApplicationStatus;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerErrorCode;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriod;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodCreateCommand;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodSummary;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerPeriodUpdateCommand;
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus;
import kr.ac.kookmin.stream.event.domain.locker.repository.LockerApplicationRepository;
import kr.ac.kookmin.stream.event.domain.locker.repository.LockerRepository;
import kr.ac.kookmin.stream.event.domain.locker.service.AdminLockerService;
Expand Down Expand Up @@ -66,6 +75,66 @@ public void deletePeriod(Long lockerPeriodId) {
}
}

@Override
@Transactional
public List<Locker> changeLockerStatus(Long sectionId, List<Long> lockerIds, LockerStatus status) {
List<Locker> lockers = findLockersInSection(sectionId, lockerIds);
if (status == LockerStatus.DISABLED) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] DISABLED일 때만 검사하는 방식이라, 나중에 REPAIRING 같은 상태가 생기면 학생은 고를 수 없는데 이용 중 검사는 그냥 통과하게 될 것 같아요. Locker.isSelectable은 status == AVAILABLE(허용 목록)로 보고 있으니, "사용할 수 있는 상태인지"를 enum에 두고 양쪽이 같이 쓰면 상태가 늘어도 안전할 것 같아요.

public enum LockerStatus {
    AVAILABLE,
    DISABLED;

    public boolean isUsable() {
        return this == AVAILABLE;
    }
}
// Locker.isSelectable: status.isUsable() && !applied
// 여기: if (!status.isUsable()) { validateNotInUse(lockerIds); }

requireNotInUse(lockerIds);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[r] 리뷰 포인트 주신 "이용 중인 칸은 DISABLED 불가" 기준에 대한 의견이에요. 날짜 기준 자체(사용 종료일 당일까지 사용 중, LockerApplicationStatus 재사용)는 좋은데, 이 규칙이 #141의 설계와 부딪히는 것 같아요.

  • #141은 "배정된 뒤 고장 나서 사용 중지된 칸"을 정상 상태로 보고 assignedCount 주석, LockerDisplayStatus의 ASSIGNED 우선 규칙, Swagger 설명을 만들어 뒀는데,
  • 이 PR 이후로는 진행 중인 회차에서 그 상태를 API로 만들 수 없어요.
  • 신청 취소·변경 기능도 없다 보니, 배정된 칸이 실제로 고장 나면 결국 DB를 직접 고쳐야 하는 상황이 남고요.

두 방향 중 하나로 맞추면 좋을 것 같아요.

운영 기준은 팀에서 한번 같이 정하면 좋겠습니다!

}

// 이미 그 상태인 사물함은 저장하지 않는다. 36칸 구역에서 두 칸만 바꿔도 전체를 쓰지 않도록 한다
lockerRepository.saveLockers(lockers.stream()
.filter(locker -> locker.changeStatus(status))
.toList());
return lockers;
}

/**
* 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다.
*/
private List<Locker> findLockersInSection(Long sectionId, List<Long> lockerIds) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 같은 파일 관행에 맞추면, throw하는 검증인 requireNotInUse는 validateNotInUse(같은 파일의 validateNotOverlapped처럼), 없으면 던지는 조회인 findLockersInSection은 getLockersInSection(getPeriod처럼)이 어떨까요?

Map<Long, Locker> lockersInSection = lockerRepository.findLockersBySectionId(sectionId).stream()
.collect(Collectors.toMap(Locker::getId, Function.identity()));

return lockerIds.stream()
.map(lockerId -> {
Locker locker = lockersInSection.get(lockerId);
if (locker == null) {
throw new BusinessException(LockerErrorCode.LOCKER_NOT_IN_SECTION);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] 리뷰 포인트 주신 "없는 사물함과 다른 구역 사물함을 하나로 묶은 것"은 운영 화면에서는 하나로 둬도 괜찮아 보여요. 다만 가르려면 조회가 하나 더 든다고 적어주셨는데, 구역 전체 대신 요청한 id로 조회하면(findAllByIdInAndIsDeletedFalse 같은) 쿼리 하나로 "결과에 없음 = 없는 사물함 / sectionId가 다름 = 다른 구역"을 가를 수 있고 읽는 행도 줄어들 것 같아요.

그리고 없는 sectionId가 들어오면 같은 경로의 GET은 404(LOCKER_SECTION_NOT_FOUND)인데 여기는 400(LOCKER_NOT_IN_SECTION)이라, 이 부분은 맞춰두면 좋겠습니다.

}
return locker;
})
.toList();
}

/**
* 아직 배정 상태인 신청이 걸린 사물함이 하나라도 있으면 끊는다.
* <p>
* 배정인지 만료인지는 내 신청 내역 화면과 같은 기준을 써야 하므로 {@link LockerApplicationStatus}에 맡긴다.
* 저장소는 신청과 회차를 읽어 오기만 한다.
*/
private void requireNotInUse(List<Long> lockerIds) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[c] 학생 신청과 이 변경이 동시에 실행되면 "사용 중지인데 배정된" 칸이 생길 수 있을 것 같아요. 예를 들어 여기서 신청을 조회(0건)한 직후 학생이 같은 칸을 AVAILABLE로 읽고 신청을 커밋하면, 두 트랜잭션이 서로 다른 테이블에 써서 충돌 없이 둘 다 성공해요. 유니크 제약은 중복 배정만 막아서 이 경우는 못 잡고요.

막으려면 사물함 행을 비관적 락으로 같이 잡아야 하는데 저장소에 선례가 없는 패턴이라, 위 [r]에서 (A)로 가면 자연히 해결되고, (B)로 가면 최소한 API 설명과 PR 한계에 적어두면 좋을 것 같아요.

List<LockerApplication> applications = lockerApplicationRepository.findByLockerIds(lockerIds);
if (applications.isEmpty()) {
return;
}

LocalDate today = LocalDate.now();
Set<Long> assignedPeriodIds = lockerRepository.findPublishedPeriodsByIds(applications.stream()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[a] 사소한 부분 두 가지예요.

  1. findPublishedPeriodsByIds라서 미게시 회차에 걸린 신청은 "이용 중 아님"으로 판정되어 검사를 통과해요. 운영진 쪽 판단이니 findAllPeriods처럼 미게시도 포함하는 조회가 맞을 것 같아요.
  2. locker_applications에 locker_id로 시작하는 인덱스가 없어서 findAllByLockerIdIn이 학기마다 쌓이는 신청 이력을 전부 훑게 돼요. 회차를 먼저 읽어 도메인에서 "배정 중"인 회차만 고른 뒤, (locker_period_id, locker_id) 유니크 인덱스를 타는 existsByLockerPeriodIdInAndLockerIdIn으로 확인하면 쿼리 수는 그대로 두 개면서 인덱스도 탈 수 있을 것 같아요.

.map(LockerApplication::getLockerPeriodId)
.collect(Collectors.toSet())).stream()
.filter(period -> LockerApplicationStatus.from(period, today) == LockerApplicationStatus.ASSIGNED)
.map(LockerPeriod::getId)
.collect(Collectors.toSet());

boolean inUse = applications.stream()
.anyMatch(application -> assignedPeriodIds.contains(application.getLockerPeriodId()));
if (inUse) {
throw new BusinessException(LockerErrorCode.LOCKER_IN_USE);
}
}

private LockerPeriod getPeriod(Long lockerPeriodId) {
return lockerRepository.findPeriodById(lockerPeriodId)
.orElseThrow(() -> new BusinessException(LockerErrorCode.LOCKER_PERIOD_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ public List<LockerPeriod> findPublishedPeriodsUsageEndingOn(LocalDate usageEndAt
throw new UnsupportedOperationException();
}

@Override
public void saveLockers(List<Locker> lockers) {
throw new UnsupportedOperationException();
}

@Override
public List<Locker> findLockersByIdsIncludingDeleted(Collection<Long> lockerIds) {
batchLookups++;
Expand Down Expand Up @@ -427,6 +432,11 @@ private static final class FakeLockerApplicationRepository implements LockerAppl
private final List<LockerApplication> saved = new ArrayList<>();
private List<LockerApplication> memberApplications = List.of();

@Override
public List<LockerApplication> findByLockerIds(Collection<Long> lockerIds) {
throw new UnsupportedOperationException();
}

@Override
public boolean existsByPeriod(Long lockerPeriodId) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,11 @@ public List<LockerPeriod> findPublishedPeriodsUsageEndingOn(LocalDate usageEndAt
throw new UnsupportedOperationException();
}

@Override
public void saveLockers(List<Locker> lockers) {
throw new UnsupportedOperationException();
}

@Override
public List<Locker> findLockersByIdsIncludingDeleted(Collection<Long> lockerIds) {
throw new UnsupportedOperationException();
Expand Down Expand Up @@ -349,6 +354,11 @@ public Optional<Long> findAppliedLockerId(Long lockerPeriodId, Long memberId) {

// 아래는 사물함 신청(LockerApplicationService)용 메서드라 이 테스트에서는 쓰지 않는다

@Override
public List<LockerApplication> findByLockerIds(Collection<Long> lockerIds) {
throw new UnsupportedOperationException();
}

@Override
public boolean existsByPeriod(Long lockerPeriodId) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.ac.kookmin.stream.db.event;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -30,6 +31,8 @@ public interface LockerApplicationJpaRepository extends JpaRepository<LockerAppl
""")
List<Long> findMemberIdsByLockerPeriodId(@Param("lockerPeriodId") Long lockerPeriodId);

List<LockerApplicationJpaEntity> findAllByLockerIdIn(Collection<Long> lockerIds);

/**
* 회차당 회원 신청은 유니크 제약으로 한 건까지만 존재한다.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.ac.kookmin.stream.db.event;

import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
Expand Down Expand Up @@ -45,6 +46,16 @@ public Optional<Long> findAppliedLockerId(Long lockerPeriodId, Long memberId) {
return lockerApplicationJpaRepository.findLockerIdByLockerPeriodIdAndMemberId(lockerPeriodId, memberId);
}

@Override
public List<LockerApplication> findByLockerIds(Collection<Long> lockerIds) {
if (lockerIds.isEmpty()) {
return List.of();
}
return lockerApplicationJpaRepository.findAllByLockerIdIn(lockerIds).stream()
.map(LockerApplicationJpaEntity::toDomain)
.toList();
}

@Override
public List<LockerApplication> findByMemberId(Long memberId) {
return lockerApplicationJpaRepository.findAllByMemberIdOrderByAppliedAtDescIdDesc(memberId).stream()
Expand Down
Loading
Loading