diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionApi.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionApi.java
index 37fe8ae5..4b2388f1 100644
--- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionApi.java
+++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionApi.java
@@ -4,6 +4,7 @@
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.common.dto.ApiResponse;
@@ -19,7 +20,7 @@
*
* 구역은 운영 회차에 묶여 있지 않다. 요청의 회차는 "어느 회차의 배정 현황을 볼지"를 정하는 기준이다.
*/
-@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회")
+@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회와 사물함 상태 변경")
public interface AdminLockerSectionApi {
/** 구역별 전체·선택 가능·배정 사물함 수. */
@@ -47,4 +48,22 @@ ApiResponse getLockerSection(
Long sectionId,
@ParameterObject LockerPeriodParams params
);
+
+ /** 구역 안의 사물함 여러 개를 같은 상태로 바꾼다. */
+ @Operation(summary = "사물함 상태 일괄 변경",
+ description = "구역 안의 사물함 여러 개를 AVAILABLE(선택가능) 또는 DISABLED(선택불가)로 한 번에 바꾼다. "
+ + "하나라도 바꿀 수 없으면 아무것도 바뀌지 않는다. "
+ + "사용 기간이 끝나지 않은 회차에 배정된 사물함은 이용 중인 학생이 있으므로 DISABLED로 바꿀 수 없고, "
+ + "AVAILABLE로 되돌리는 것은 막지 않는다. "
+ + "구역 상세가 내려주는 ASSIGNED는 신청으로 생기는 표시용 값이라 이 경로로 바꿀 수 없다. "
+ + "응답 본문이 없으므로 바뀐 결과는 구역 상세를 다시 조회해 받는다. "
+ + "이용 중 검사와 학생 신청이 겹치면 서로 다른 테이블에 써서 둘 다 성공할 수 있고, "
+ + "그때는 배정된 칸이 사용 중지된 채로 남는다.")
+ @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"})
+ @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_NOT_IN_SECTION", "LOCKER_IN_USE"})
+ ApiResponse changeLockerStatus(
+ AdminApiUser apiUser,
+ Long sectionId,
+ LockerStatusChangeRequest request
+ );
}
diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionController.java
index 50a67087..1cbd5a8d 100644
--- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionController.java
+++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/AdminLockerSectionController.java
@@ -6,18 +6,22 @@
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.common.dto.ApiResponse;
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;
@@ -27,6 +31,7 @@
public class AdminLockerSectionController implements AdminLockerSectionApi {
private final LockerService lockerService;
+ private final AdminLockerService adminLockerService;
private final FileService fileService;
@Override
@@ -54,4 +59,15 @@ public ApiResponse getLockerSection(
return ApiResponse.success(
AdminLockerSectionDetailResponse.of(detail, photosById, appliedLockerIds));
}
+
+ @Override
+ @PatchMapping("/{sectionId}/lockers/status")
+ public ApiResponse changeLockerStatus(
+ AdminApiUser apiUser,
+ @PathVariable Long sectionId,
+ @Valid @RequestBody LockerStatusChangeRequest request
+ ) {
+ adminLockerService.changeLockerStatus(sectionId, request.distinctLockerIds(), request.lockerStatus());
+ return ApiResponse.success();
+ }
}
diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerStatusChangeRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerStatusChangeRequest.java
new file mode 100644
index 00000000..f3d79138
--- /dev/null
+++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerStatusChangeRequest.java
@@ -0,0 +1,28 @@
+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;
+
+/**
+ * 구역 안의 사물함 여러 개를 같은 상태로 바꾼다.
+ *
+ * 구역 상세가 내려주는 {@code ASSIGNED}는 신청으로 생기는 표시용 값이라 이 경로로는 바꿀 수 없다.
+ */
+public record LockerStatusChangeRequest(
+ @NotEmpty(message = "사물함을 하나 이상 선택해 주세요.")
+ List<@NotNull(message = "사물함 ID는 비워 둘 수 없습니다.") Long> lockerIds,
+
+ @NotNull(message = "변경할 상태를 입력해 주세요.")
+ LockerStatus lockerStatus
+) {
+
+ /**
+ * 중복을 걷어낸 사물함 식별자. 같은 칸이 여러 번 와도 결과는 같지만, 그대로 두면 이용 중 검사의 {@code IN} 절이
+ * 구역의 칸 수보다 커진다. 중복을 없애면 상한이 그 구역의 칸 수가 되어 따로 숫자를 둘 필요가 없다.
+ */
+ public List distinctLockerIds() {
+ return lockerIds.stream().distinct().toList();
+ }
+}
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java
index 552dc6f8..1df0b67b 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/Locker.java
@@ -40,6 +40,13 @@ public static Locker of(
* @param applied 해당 운영 회차에 이 사물함이 이미 신청되었는지
*/
public boolean isSelectable(boolean applied) {
- return status == LockerStatus.AVAILABLE && !applied;
+ return status.isUsable() && !applied;
+ }
+
+ /**
+ * 사물함 자체 상태를 바꾼다. 이미 그 상태여도 그대로 둔다 — 영속성 컨텍스트가 값이 바뀐 행만 UPDATE한다.
+ */
+ public void changeStatus(LockerStatus status) {
+ this.status = status;
}
}
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerErrorCode.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerErrorCode.java
index efcf6c63..83be320f 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerErrorCode.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerErrorCode.java
@@ -20,6 +20,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;
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java
index adba7ae9..a76f8d6e 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerStatus.java
@@ -1,6 +1,16 @@
package kr.ac.kookmin.stream.event.domain.locker.domain;
+/** 사물함 자체의 물리 상태. 회차 배정 여부는 담지 않는다. */
public enum LockerStatus {
+
AVAILABLE,
- DISABLED
+ DISABLED;
+
+ /**
+ * 학생에게 내줄 수 있는 상태인지. 쓸 수 있는 상태를 허용 목록으로 두어, 나중에 상태가 늘어도 그 값이 자동으로
+ * "쓸 수 없음"에 들어가게 한다. 상태를 차단 목록으로 보는 곳이 생기면 새 값만 조용히 빠져나간다.
+ */
+ public boolean isUsable() {
+ return this == AVAILABLE;
+ }
}
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerApplicationRepository.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerApplicationRepository.java
index 6ee3936d..0b72feb2 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerApplicationRepository.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerApplicationRepository.java
@@ -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;
@@ -35,6 +36,11 @@ public interface LockerApplicationRepository {
*/
Optional findAppliedLockerId(Long lockerPeriodId, Long memberId);
+ /**
+ * 해당 회차들에 이 사물함 중 하나라도 신청됐는지. (회차, 사물함) 유니크 인덱스를 그대로 탄다.
+ */
+ boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds);
+
/**
* 회원의 사물함 신청 전체를 신청 일시 최신순으로 조회한다.
*/
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerRepository.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerRepository.java
index 150da354..c75f15af 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerRepository.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerRepository.java
@@ -92,4 +92,14 @@ public interface LockerRepository {
* 식별자에 해당하는 사물함. 지난 신청 이력의 사물함 이름을 보여줘야 하므로 삭제된 사물함도 포함한다.
*/
List findLockersByIdsIncludingDeleted(Collection lockerIds);
+
+ /**
+ * 식별자에 해당하는 사물함. 삭제된 사물함은 없는 것으로 본다. 순서는 보장하지 않는다.
+ */
+ List findLockersByIds(Collection lockerIds);
+
+ /**
+ * 사물함 여러 건을 저장한다. 변경은 호출한 쪽 트랜잭션이 커밋될 때 반영된다.
+ */
+ void saveLockers(List lockers);
}
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/AdminLockerService.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/AdminLockerService.java
index 65ba2420..2cd2da49 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/AdminLockerService.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/AdminLockerService.java
@@ -4,6 +4,7 @@
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가 쓰는 서비스.
@@ -36,4 +37,12 @@ public interface AdminLockerService {
* 운영 회차를 소프트 삭제한다. 신청이 한 건이라도 있으면 거부한다.
*/
void deletePeriod(Long lockerPeriodId);
+
+ /**
+ * 구역 안의 사물함 여러 개의 상태를 한 번에 바꾼다.
+ *
+ * 하나라도 바꿀 수 없으면 아무것도 바꾸지 않는다. 사용 기간이 끝나지 않은 회차에 배정된 사물함은
+ * 이용 중인 학생이 있으므로 {@code DISABLED}로 바꿀 수 없다.
+ */
+ void changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status);
}
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/AdminLockerServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/AdminLockerServiceImpl.java
index 6247affd..ffcbeace 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/AdminLockerServiceImpl.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/AdminLockerServiceImpl.java
@@ -1,13 +1,20 @@
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.Set;
+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;
@@ -66,6 +73,59 @@ public void deletePeriod(Long lockerPeriodId) {
}
}
+ @Override
+ @Transactional
+ public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status) {
+ List lockers = getLockersInSection(sectionId, lockerIds);
+ if (!status.isUsable()) {
+ validateNotInUse(lockerIds);
+ }
+
+ lockers.forEach(locker -> locker.changeStatus(status));
+ lockerRepository.saveLockers(lockers);
+ }
+
+ /**
+ * 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다.
+ */
+ private List getLockersInSection(Long sectionId, List lockerIds) {
+ // 구역부터 확인한다. 없는 구역이면 같은 경로의 구역 상세 조회와 같이 404로 나가야 한다
+ if (lockerRepository.findSectionById(sectionId).isEmpty()) {
+ throw new BusinessException(LockerErrorCode.LOCKER_SECTION_NOT_FOUND);
+ }
+
+ // 구역 전체가 아니라 요청한 사물함만 읽는다. 36칸 구역에서 두 칸만 바꿔도 전체를 읽지 않도록 한다
+ List lockers = lockerRepository.findLockersByIds(lockerIds);
+ boolean allInSection = lockers.size() == lockerIds.size()
+ && lockers.stream().allMatch(locker -> locker.getSectionId().equals(sectionId));
+ if (!allInSection) {
+ // 없는 사물함과 다른 구역 사물함을 가르지 않는다. 운영 화면에서는 둘 다 "잘못 고른 칸"이다
+ throw new BusinessException(LockerErrorCode.LOCKER_NOT_IN_SECTION);
+ }
+ return lockers;
+ }
+
+ /**
+ * 아직 배정 상태인 신청이 걸린 사물함이 하나라도 있으면 끊는다.
+ *
+ * 배정인지 만료인지는 내 신청 내역 화면과 같은 기준을 써야 하므로 {@link LockerApplicationStatus}에 맡긴다.
+ * 저장소는 판정에 끼지 않고 회차를 읽고 신청 존재만 알려준다.
+ */
+ private void validateNotInUse(List lockerIds) {
+ LocalDate today = LocalDate.now();
+ // 회차를 먼저 좁힌다. 회차는 학기당 한두 건이라 전부 읽어도 되고, 그래야 신청 조회가 (회차, 사물함)
+ // 유니크 인덱스를 탄다. 사물함 식별자만으로 신청을 찾으면 탈 인덱스가 없어 이력 전체를 훑는다.
+ // 게시 여부로 거르지 않는다. 신청은 게시된 회차에만 들어와 지금은 차이가 없고, 거르려면 포트만 하나 더 는다
+ Set assignedPeriodIds = lockerRepository.findAllPeriods().stream()
+ .filter(period -> LockerApplicationStatus.from(period, today) == LockerApplicationStatus.ASSIGNED)
+ .map(LockerPeriod::getId)
+ .collect(Collectors.toSet());
+
+ if (lockerApplicationRepository.existsByPeriodIdsAndLockerIds(assignedPeriodIds, lockerIds)) {
+ throw new BusinessException(LockerErrorCode.LOCKER_IN_USE);
+ }
+ }
+
private LockerPeriod getPeriod(Long lockerPeriodId) {
return lockerRepository.findPeriodById(lockerPeriodId)
.orElseThrow(() -> new BusinessException(LockerErrorCode.LOCKER_PERIOD_NOT_FOUND));
diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImpl.java
index e0dcdbab..d47246ab 100644
--- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImpl.java
+++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImpl.java
@@ -89,7 +89,8 @@ private LockerSectionSummary summarize(
int availableCount = (int) lockers.stream()
.filter(locker -> locker.isSelectable(appliedLockerIds.contains(locker.getId())))
.count();
- // 사용 중지된 사물함도 배정돼 있으면 센다. 배정 후 고장 난 칸이라 운영진이 다른 자리를 줘야 하는 대상이다
+ // 사용 중지된 사물함도 배정돼 있으면 센다. 상태 변경 API가 이용 중인 칸의 사용 중지를 막아 그 조합은 흔치
+ // 않지만, DB 직접 수정이나 검사와 신청이 겹친 경우로 남을 수 있다
int assignedCount = (int) lockers.stream()
.filter(locker -> appliedLockerIds.contains(locker.getId()))
.count();
diff --git a/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java b/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java
index 6c301016..f8e218d2 100644
--- a/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java
+++ b/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java
@@ -360,6 +360,16 @@ public List findPublishedPeriodsUsageEndingOn(LocalDate usageEndAt
throw new UnsupportedOperationException();
}
+ @Override
+ public void saveLockers(List lockers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List findLockersByIds(Collection lockerIds) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public List findLockersByIdsIncludingDeleted(Collection lockerIds) {
batchLookups++;
@@ -428,6 +438,11 @@ private static final class FakeLockerApplicationRepository implements LockerAppl
private final List saved = new ArrayList<>();
private List memberApplications = List.of();
+ @Override
+ public boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public boolean existsByPeriod(Long lockerPeriodId) {
throw new UnsupportedOperationException();
diff --git a/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImplTest.java b/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImplTest.java
index e6b8ae9f..0103ffdd 100644
--- a/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImplTest.java
+++ b/core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImplTest.java
@@ -323,6 +323,16 @@ public List findPublishedPeriodsUsageEndingOn(LocalDate usageEndAt
throw new UnsupportedOperationException();
}
+ @Override
+ public void saveLockers(List lockers) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List findLockersByIds(Collection lockerIds) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public List findLockersByIdsIncludingDeleted(Collection lockerIds) {
throw new UnsupportedOperationException();
@@ -351,6 +361,11 @@ public Optional findAppliedLockerId(Long lockerPeriodId, Long memberId) {
// 아래는 사물함 신청(LockerApplicationService)용 메서드라 이 테스트에서는 쓰지 않는다
+ @Override
+ public boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public boolean existsByPeriod(Long lockerPeriodId) {
throw new UnsupportedOperationException();
diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaRepository.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaRepository.java
index e5694261..ecc0552f 100644
--- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaRepository.java
+++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaRepository.java
@@ -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;
@@ -30,6 +31,8 @@ public interface LockerApplicationJpaRepository extends JpaRepository findMemberIdsByLockerPeriodId(@Param("lockerPeriodId") Long lockerPeriodId);
+ boolean existsByLockerPeriodIdInAndLockerIdIn(Collection lockerPeriodIds, Collection lockerIds);
+
/**
* 회차당 회원 신청은 유니크 제약으로 한 건까지만 존재한다.
*/
diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationRepositoryImpl.java
index e91dcfce..374fc52a 100644
--- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationRepositoryImpl.java
+++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationRepositoryImpl.java
@@ -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;
@@ -45,6 +46,14 @@ public Optional findAppliedLockerId(Long lockerPeriodId, Long memberId) {
return lockerApplicationJpaRepository.findLockerIdByLockerPeriodIdAndMemberId(lockerPeriodId, memberId);
}
+ @Override
+ public boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds) {
+ if (lockerPeriodIds.isEmpty() || lockerIds.isEmpty()) {
+ return false;
+ }
+ return lockerApplicationJpaRepository.existsByLockerPeriodIdInAndLockerIdIn(lockerPeriodIds, lockerIds);
+ }
+
@Override
public List findByMemberId(Long memberId) {
return lockerApplicationJpaRepository.findAllByMemberIdOrderByAppliedAtDescIdDesc(memberId).stream()
diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java
index 9c38a8af..e19cd69a 100644
--- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java
+++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java
@@ -14,6 +14,7 @@
import kr.ac.kookmin.stream.event.domain.locker.domain.LockerStatus;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
+import org.hibernate.annotations.DynamicUpdate;
@Entity
@Table(
@@ -23,6 +24,10 @@
columnList = "section_id, is_deleted, row_no, column_no"
)
)
+// 바뀐 컬럼만 UPDATE한다. 상태 변경은 도메인에서 만든 엔티티를 merge하는데, 그 엔티티는 삭제 여부를 들고 있지 않아
+// is_deleted가 늘 false다. 사물함 삭제 기능이 붙은 뒤 수정과 삭제가 겹치면 읽어 둔 값으로 되써서 삭제가 되돌아간다
+// (LockerPeriodJpaEntity와 같은 이유).
+@DynamicUpdate
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LockerJpaEntity extends BaseSoftDeleteEntity {
diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaRepository.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaRepository.java
index bdcb4433..a98b8120 100644
--- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaRepository.java
+++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerJpaRepository.java
@@ -17,6 +17,8 @@ public interface LockerJpaRepository extends JpaRepository findAllByIdIn(Collection ids);
+ List findAllByIdInAndIsDeletedFalse(Collection ids);
+
/**
* 배치도 순서(행 → 열)로 조회한다. (row_no, column_no)에 유니크 제약이 없어 같은 자리가 둘 이상일 수 있으므로
* 식별자를 동점 기준으로 더해 같은 요청이 항상 같은 순서를 돌려주게 한다.
diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java
index 1e86220b..073c7c21 100644
--- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java
+++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java
@@ -138,6 +138,23 @@ public Optional findLockerById(Long lockerId) {
.map(LockerJpaEntity::toDomain);
}
+ @Override
+ public void saveLockers(List lockers) {
+ lockerJpaRepository.saveAll(lockers.stream()
+ .map(LockerJpaEntity::from)
+ .toList());
+ }
+
+ @Override
+ public List findLockersByIds(Collection lockerIds) {
+ if (lockerIds.isEmpty()) {
+ return List.of();
+ }
+ return lockerJpaRepository.findAllByIdInAndIsDeletedFalse(lockerIds).stream()
+ .map(LockerJpaEntity::toDomain)
+ .toList();
+ }
+
@Override
public List findLockersByIdsIncludingDeleted(Collection lockerIds) {
return lockerJpaRepository.findAllByIdIn(lockerIds).stream()