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()