From f9a9030bc8336914ece29e7e881cb7b08ed3133e Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 01/12] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=8F=99=EC=9E=91=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stream/event/domain/locker/domain/Locker.java | 13 +++++++++++++ .../event/domain/locker/domain/LockerErrorCode.java | 2 ++ 2 files changed, 15 insertions(+) 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..5bf1ef7b 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 @@ -42,4 +42,17 @@ public static Locker of( public boolean isSelectable(boolean applied) { return status == LockerStatus.AVAILABLE && !applied; } + + /** + * 사물함 자체 상태를 바꾼다. 바뀐 값이 있으면 true를 돌려준다. + *

+ * 운영진이 여러 칸을 한 번에 바꾸므로, 이미 그 상태인 사물함은 저장하지 않도록 바뀌었는지 알려준다. + */ + public boolean changeStatus(LockerStatus status) { + if (this.status == status) { + return false; + } + this.status = status; + return true; + } } 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; From 5bcfc2ed35b5cab757be178005e069f3f2080a59 Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20=EA=B2=BD=EB=A1=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/LockerApplicationRepository.java | 6 ++++++ .../domain/locker/repository/LockerRepository.java | 5 +++++ .../impl/LockerApplicationServiceImplTest.java | 10 ++++++++++ .../locker/service/impl/LockerServiceImplTest.java | 10 ++++++++++ .../db/event/LockerApplicationJpaRepository.java | 3 +++ .../db/event/LockerApplicationRepositoryImpl.java | 11 +++++++++++ .../kookmin/stream/db/event/LockerRepositoryImpl.java | 7 +++++++ 7 files changed, 52 insertions(+) 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..248f8365 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); + /** + * 해당 사물함들의 신청 전체. 어느 운영 회차에 배정됐는지 보려고 읽는다. + */ + List findByLockerIds(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..5ea2fb69 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,9 @@ public interface LockerRepository { * 식별자에 해당하는 사물함. 지난 신청 이력의 사물함 이름을 보여줘야 하므로 삭제된 사물함도 포함한다. */ List findLockersByIdsIncludingDeleted(Collection lockerIds); + + /** + * 사물함 여러 건을 저장한다. 변경은 호출한 쪽 트랜잭션이 커밋될 때 반영된다. + */ + void saveLockers(List lockers); } 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..14d7bf7a 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,11 @@ public List findPublishedPeriodsUsageEndingOn(LocalDate usageEndAt throw new UnsupportedOperationException(); } + @Override + public void saveLockers(List lockers) { + throw new UnsupportedOperationException(); + } + @Override public List findLockersByIdsIncludingDeleted(Collection lockerIds) { batchLookups++; @@ -428,6 +433,11 @@ private static final class FakeLockerApplicationRepository implements LockerAppl private final List saved = new ArrayList<>(); private List memberApplications = List.of(); + @Override + public List findByLockerIds(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..11141687 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,11 @@ public List findPublishedPeriodsUsageEndingOn(LocalDate usageEndAt throw new UnsupportedOperationException(); } + @Override + public void saveLockers(List lockers) { + throw new UnsupportedOperationException(); + } + @Override public List findLockersByIdsIncludingDeleted(Collection lockerIds) { throw new UnsupportedOperationException(); @@ -351,6 +356,11 @@ public Optional findAppliedLockerId(Long lockerPeriodId, Long memberId) { // 아래는 사물함 신청(LockerApplicationService)용 메서드라 이 테스트에서는 쓰지 않는다 + @Override + public List findByLockerIds(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..254ead19 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); + List findAllByLockerIdIn(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..c8dab7c7 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,16 @@ public Optional findAppliedLockerId(Long lockerPeriodId, Long memberId) { return lockerApplicationJpaRepository.findLockerIdByLockerPeriodIdAndMemberId(lockerPeriodId, memberId); } + @Override + public List findByLockerIds(Collection lockerIds) { + if (lockerIds.isEmpty()) { + return List.of(); + } + return lockerApplicationJpaRepository.findAllByLockerIdIn(lockerIds).stream() + .map(LockerApplicationJpaEntity::toDomain) + .toList(); + } + @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/LockerRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java index 1e86220b..460d5405 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,13 @@ 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 findLockersByIdsIncludingDeleted(Collection lockerIds) { return lockerJpaRepository.findAllByIdIn(lockerIds).stream() From 4c5b2ef9db5ed6932aef29d55c42335168b19b48 Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=9D=BC=EA=B4=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../locker/service/AdminLockerService.java | 12 ++++ .../service/impl/AdminLockerServiceImpl.java | 69 +++++++++++++++++++ 2 files changed, 81 insertions(+) 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..ca9df54e 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 @@ -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가 쓰는 서비스. @@ -36,4 +38,14 @@ public interface AdminLockerService { * 운영 회차를 소프트 삭제한다. 신청이 한 건이라도 있으면 거부한다. */ void deletePeriod(Long lockerPeriodId); + + /** + * 구역 안의 사물함 여러 개의 상태를 한 번에 바꾸고, 바뀐 사물함을 돌려준다. + *

+ * 하나라도 바꿀 수 없으면 아무것도 바꾸지 않는다. 사용 기간이 끝나지 않은 회차에 배정된 사물함은 + * 이용 중인 학생이 있으므로 {@code DISABLED}로 바꿀 수 없다. + * + * @return 요청한 사물함 전체. 이미 그 상태였던 사물함도 포함한다 + */ + List 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..d2d45112 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,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; @@ -66,6 +75,66 @@ public void deletePeriod(Long lockerPeriodId) { } } + @Override + @Transactional + public List changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status) { + List lockers = findLockersInSection(sectionId, lockerIds); + if (status == LockerStatus.DISABLED) { + requireNotInUse(lockerIds); + } + + // 이미 그 상태인 사물함은 저장하지 않는다. 36칸 구역에서 두 칸만 바꿔도 전체를 쓰지 않도록 한다 + lockerRepository.saveLockers(lockers.stream() + .filter(locker -> locker.changeStatus(status)) + .toList()); + return lockers; + } + + /** + * 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다. + */ + private List findLockersInSection(Long sectionId, List lockerIds) { + Map 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); + } + return locker; + }) + .toList(); + } + + /** + * 아직 배정 상태인 신청이 걸린 사물함이 하나라도 있으면 끊는다. + *

+ * 배정인지 만료인지는 내 신청 내역 화면과 같은 기준을 써야 하므로 {@link LockerApplicationStatus}에 맡긴다. + * 저장소는 신청과 회차를 읽어 오기만 한다. + */ + private void requireNotInUse(List lockerIds) { + List applications = lockerApplicationRepository.findByLockerIds(lockerIds); + if (applications.isEmpty()) { + return; + } + + LocalDate today = LocalDate.now(); + Set assignedPeriodIds = lockerRepository.findPublishedPeriodsByIds(applications.stream() + .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)); From 9654f0c09cedc4ac33343c689f6c2255f38c735f Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=9D=BC=EA=B4=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/locker/AdminLockerSectionApi.java | 20 ++++++++++++++- .../locker/AdminLockerSectionController.java | 19 ++++++++++++++ .../request/LockerStatusChangeRequest.java | 20 +++++++++++++++ .../AdminLockerStatusChangeResponse.java | 25 +++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerStatusChangeRequest.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java 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..1dc041bc 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,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; @@ -19,7 +21,7 @@ *

* 구역은 운영 회차에 묶여 있지 않다. 요청의 회차는 "어느 회차의 배정 현황을 볼지"를 정하는 기준이다. */ -@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회") +@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회와 사물함 상태 변경") public interface AdminLockerSectionApi { /** 구역별 전체·선택 가능·배정 사물함 수. */ @@ -47,4 +49,20 @@ 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..504262a2 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,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; @@ -27,6 +33,7 @@ public class AdminLockerSectionController implements AdminLockerSectionApi { private final LockerService lockerService; + private final AdminLockerService adminLockerService; private final FileService fileService; @Override @@ -54,4 +61,16 @@ 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 + ) { + List lockers = adminLockerService.changeLockerStatus( + sectionId, request.lockerIds(), request.lockerStatus()); + return ApiResponse.success(AdminLockerStatusChangeResponse.from(lockers)); + } } 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..a1759a0e --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerStatusChangeRequest.java @@ -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; + +/** + * 구역 안의 사물함 여러 개를 같은 상태로 바꾼다. + *

+ * 구역 상세가 내려주는 {@code ASSIGNED}는 신청으로 생기는 표시용 값이라 이 경로로는 바꿀 수 없다. + */ +public record LockerStatusChangeRequest( + @NotEmpty(message = "사물함을 하나 이상 선택해 주세요.") + List<@NotNull(message = "사물함 ID는 비워 둘 수 없습니다.") Long> lockerIds, + + @NotNull(message = "변경할 상태를 입력해 주세요.") + LockerStatus lockerStatus +) { +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java new file mode 100644 index 00000000..037233ac --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java @@ -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 lockers) { + + public static AdminLockerStatusChangeResponse from(List lockers) { + return new AdminLockerStatusChangeResponse(lockers.stream() + .map(LockerItem::from) + .toList()); + } + + /** 도메인 {@link Locker}와 이름이 겹치지 않도록 {@code LockerItem}으로 둔다. */ + public record LockerItem(Long lockerId, int lockerNumber, LockerStatus lockerStatus) { + + public static LockerItem from(Locker locker) { + return new LockerItem(locker.getId(), locker.getLockerNumber(), locker.getStatus()); + } + } +} From e39dca0645a166f4a0239f532fa84a62029a08e9 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 20:55:07 +0900 Subject: [PATCH 05/12] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=84=20=EC=97=86=EC=95=A0=EA=B3=A0=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=EC=9D=84=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/locker/AdminLockerSectionApi.java | 7 +++--- .../locker/AdminLockerSectionController.java | 9 +++---- .../AdminLockerStatusChangeResponse.java | 25 ------------------- .../event/domain/locker/domain/Locker.java | 10 ++------ .../locker/service/AdminLockerService.java | 7 ++---- .../service/impl/AdminLockerServiceImpl.java | 9 +++---- 6 files changed, 13 insertions(+), 54 deletions(-) delete mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java 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 1dc041bc..5f6bd29a 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 @@ -7,7 +7,6 @@ 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; @@ -56,11 +55,11 @@ ApiResponse getLockerSection( + "하나라도 바꿀 수 없으면 아무것도 바뀌지 않는다. " + "사용 기간이 끝나지 않은 회차에 배정된 사물함은 이용 중인 학생이 있으므로 DISABLED로 바꿀 수 없고, " + "AVAILABLE로 되돌리는 것은 막지 않는다. " - + "응답은 요청한 사물함 전체로, 이미 그 상태였던 사물함도 담긴다. " - + "구역 상세가 내려주는 ASSIGNED는 신청으로 생기는 표시용 값이라 이 경로로 바꿀 수 없다.") + + "구역 상세가 내려주는 ASSIGNED는 신청으로 생기는 표시용 값이라 이 경로로 바꿀 수 없다. " + + "응답 본문이 없으므로 바뀐 결과는 구역 상세를 다시 조회해 받는다.") @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_NOT_IN_SECTION", "LOCKER_IN_USE"}) - ApiResponse changeLockerStatus( + 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 504262a2..33732f0a 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 @@ -9,9 +9,7 @@ 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; @@ -64,13 +62,12 @@ public ApiResponse getLockerSection( @Override @PatchMapping("/{sectionId}/lockers/status") - public ApiResponse changeLockerStatus( + public ApiResponse changeLockerStatus( AdminApiUser apiUser, @PathVariable Long sectionId, @Valid @RequestBody LockerStatusChangeRequest request ) { - List lockers = adminLockerService.changeLockerStatus( - sectionId, request.lockerIds(), request.lockerStatus()); - return ApiResponse.success(AdminLockerStatusChangeResponse.from(lockers)); + adminLockerService.changeLockerStatus(sectionId, request.lockerIds(), request.lockerStatus()); + return ApiResponse.success(); } } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java deleted file mode 100644 index 037233ac..00000000 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/response/AdminLockerStatusChangeResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -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 lockers) { - - public static AdminLockerStatusChangeResponse from(List lockers) { - return new AdminLockerStatusChangeResponse(lockers.stream() - .map(LockerItem::from) - .toList()); - } - - /** 도메인 {@link Locker}와 이름이 겹치지 않도록 {@code LockerItem}으로 둔다. */ - public record LockerItem(Long lockerId, int lockerNumber, LockerStatus lockerStatus) { - - public static LockerItem from(Locker locker) { - return new LockerItem(locker.getId(), locker.getLockerNumber(), locker.getStatus()); - } - } -} 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 5bf1ef7b..c724433f 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 @@ -44,15 +44,9 @@ public boolean isSelectable(boolean applied) { } /** - * 사물함 자체 상태를 바꾼다. 바뀐 값이 있으면 true를 돌려준다. - *

- * 운영진이 여러 칸을 한 번에 바꾸므로, 이미 그 상태인 사물함은 저장하지 않도록 바뀌었는지 알려준다. + * 사물함 자체 상태를 바꾼다. 이미 그 상태여도 그대로 둔다 — 영속성 컨텍스트가 값이 바뀐 행만 UPDATE한다. */ - public boolean changeStatus(LockerStatus status) { - if (this.status == status) { - return false; - } + public void changeStatus(LockerStatus status) { this.status = status; - return true; } } 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 ca9df54e..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 @@ -1,7 +1,6 @@ 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; @@ -40,12 +39,10 @@ public interface AdminLockerService { void deletePeriod(Long lockerPeriodId); /** - * 구역 안의 사물함 여러 개의 상태를 한 번에 바꾸고, 바뀐 사물함을 돌려준다. + * 구역 안의 사물함 여러 개의 상태를 한 번에 바꾼다. *

* 하나라도 바꿀 수 없으면 아무것도 바꾸지 않는다. 사용 기간이 끝나지 않은 회차에 배정된 사물함은 * 이용 중인 학생이 있으므로 {@code DISABLED}로 바꿀 수 없다. - * - * @return 요청한 사물함 전체. 이미 그 상태였던 사물함도 포함한다 */ - List changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status); + 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 d2d45112..bb7c83b5 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 @@ -77,17 +77,14 @@ public void deletePeriod(Long lockerPeriodId) { @Override @Transactional - public List changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status) { + public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status) { List lockers = findLockersInSection(sectionId, lockerIds); if (status == LockerStatus.DISABLED) { requireNotInUse(lockerIds); } - // 이미 그 상태인 사물함은 저장하지 않는다. 36칸 구역에서 두 칸만 바꿔도 전체를 쓰지 않도록 한다 - lockerRepository.saveLockers(lockers.stream() - .filter(locker -> locker.changeStatus(status)) - .toList()); - return lockers; + lockers.forEach(locker -> locker.changeStatus(status)); + lockerRepository.saveLockers(lockers); } /** From 502217149aae0b185a5760236555d260d1c6d4d6 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:00:32 +0900 Subject: [PATCH 06/12] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=20=ED=8C=90=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20LockerStatus.isUsable=EB=A1=9C=20=EB=AA=A8=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stream/event/domain/locker/domain/Locker.java | 2 +- .../event/domain/locker/domain/LockerStatus.java | 12 +++++++++++- .../locker/service/impl/AdminLockerServiceImpl.java | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) 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 c724433f..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,7 +40,7 @@ public static Locker of( * @param applied 해당 운영 회차에 이 사물함이 이미 신청되었는지 */ public boolean isSelectable(boolean applied) { - return status == LockerStatus.AVAILABLE && !applied; + return status.isUsable() && !applied; } /** 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/service/impl/AdminLockerServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/AdminLockerServiceImpl.java index bb7c83b5..e3f14817 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 @@ -79,7 +79,7 @@ public void deletePeriod(Long lockerPeriodId) { @Transactional public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status) { List lockers = findLockersInSection(sectionId, lockerIds); - if (status == LockerStatus.DISABLED) { + if (!status.isUsable()) { requireNotInUse(lockerIds); } From a54d64c0a46ffaf83833f87e1fd887007cb2f5a2 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:11:34 +0900 Subject: [PATCH 07/12] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=9D=B4=EC=9A=A9=20=EC=A4=91=20=ED=8C=90=EC=A0=95=EC=9D=84?= =?UTF-8?q?=20=ED=9A=8C=EC=B0=A8=20=EB=A8=BC=EC=A0=80=20=EC=A2=81=ED=9E=88?= =?UTF-8?q?=EB=8A=94=20=EC=88=9C=EC=84=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/LockerApplicationRepository.java | 4 ++-- .../service/impl/AdminLockerServiceImpl.java | 16 +++++----------- .../impl/LockerApplicationServiceImplTest.java | 2 +- .../service/impl/LockerServiceImplTest.java | 2 +- .../db/event/LockerApplicationJpaRepository.java | 2 +- .../event/LockerApplicationRepositoryImpl.java | 10 ++++------ 6 files changed, 14 insertions(+), 22 deletions(-) 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 248f8365..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 @@ -37,9 +37,9 @@ public interface LockerApplicationRepository { Optional findAppliedLockerId(Long lockerPeriodId, Long memberId); /** - * 해당 사물함들의 신청 전체. 어느 운영 회차에 배정됐는지 보려고 읽는다. + * 해당 회차들에 이 사물함 중 하나라도 신청됐는지. (회차, 사물함) 유니크 인덱스를 그대로 탄다. */ - List findByLockerIds(Collection lockerIds); + boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds); /** * 회원의 사물함 신청 전체를 신청 일시 최신순으로 조회한다. 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 e3f14817..807dbf5e 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 @@ -112,22 +112,16 @@ private List findLockersInSection(Long sectionId, List lockerIds) * 저장소는 신청과 회차를 읽어 오기만 한다. */ private void requireNotInUse(List lockerIds) { - List applications = lockerApplicationRepository.findByLockerIds(lockerIds); - if (applications.isEmpty()) { - return; - } - LocalDate today = LocalDate.now(); - Set assignedPeriodIds = lockerRepository.findPublishedPeriodsByIds(applications.stream() - .map(LockerApplication::getLockerPeriodId) - .collect(Collectors.toSet())).stream() + // 회차를 먼저 좁힌다. 회차는 학기당 한두 건이라 전부 읽어도 되고, 그래야 신청 조회가 (회차, 사물함) + // 유니크 인덱스를 탄다. 사물함 식별자만으로 신청을 찾으면 탈 인덱스가 없어 이력 전체를 훑는다. + // 게시 여부로 거르지 않는다. 신청은 게시된 회차에만 들어와 지금은 차이가 없고, 거르려면 포트만 하나 더 는다 + Set assignedPeriodIds = lockerRepository.findAllPeriods().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) { + if (lockerApplicationRepository.existsByPeriodIdsAndLockerIds(assignedPeriodIds, lockerIds)) { throw new BusinessException(LockerErrorCode.LOCKER_IN_USE); } } 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 14d7bf7a..66c31e3b 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 @@ -434,7 +434,7 @@ private static final class FakeLockerApplicationRepository implements LockerAppl private List memberApplications = List.of(); @Override - public List findByLockerIds(Collection lockerIds) { + public boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds) { 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 11141687..91d37bfe 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 @@ -357,7 +357,7 @@ public Optional findAppliedLockerId(Long lockerPeriodId, Long memberId) { // 아래는 사물함 신청(LockerApplicationService)용 메서드라 이 테스트에서는 쓰지 않는다 @Override - public List findByLockerIds(Collection lockerIds) { + public boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds) { 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 254ead19..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 @@ -31,7 +31,7 @@ public interface LockerApplicationJpaRepository extends JpaRepository findMemberIdsByLockerPeriodId(@Param("lockerPeriodId") Long lockerPeriodId); - List findAllByLockerIdIn(Collection lockerIds); + 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 c8dab7c7..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 @@ -47,13 +47,11 @@ public Optional findAppliedLockerId(Long lockerPeriodId, Long memberId) { } @Override - public List findByLockerIds(Collection lockerIds) { - if (lockerIds.isEmpty()) { - return List.of(); + public boolean existsByPeriodIdsAndLockerIds(Collection lockerPeriodIds, Collection lockerIds) { + if (lockerPeriodIds.isEmpty() || lockerIds.isEmpty()) { + return false; } - return lockerApplicationJpaRepository.findAllByLockerIdIn(lockerIds).stream() - .map(LockerApplicationJpaEntity::toDomain) - .toList(); + return lockerApplicationJpaRepository.existsByLockerPeriodIdInAndLockerIdIn(lockerPeriodIds, lockerIds); } @Override From e09fcccd25da58e1c24585a7844e4733fd6eae18 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:13:50 +0900 Subject: [PATCH 08/12] =?UTF-8?q?refactor:=20=EC=82=AC=EB=AC=BC=ED=95=A8?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=9D=98=20private=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=AA=85=EC=9D=84=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80=ED=96=89?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../locker/service/impl/AdminLockerServiceImpl.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 807dbf5e..dcdf219f 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 @@ -78,9 +78,9 @@ public void deletePeriod(Long lockerPeriodId) { @Override @Transactional public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status) { - List lockers = findLockersInSection(sectionId, lockerIds); + List lockers = getLockersInSection(sectionId, lockerIds); if (!status.isUsable()) { - requireNotInUse(lockerIds); + validateNotInUse(lockerIds); } lockers.forEach(locker -> locker.changeStatus(status)); @@ -90,7 +90,7 @@ public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatu /** * 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다. */ - private List findLockersInSection(Long sectionId, List lockerIds) { + private List getLockersInSection(Long sectionId, List lockerIds) { Map lockersInSection = lockerRepository.findLockersBySectionId(sectionId).stream() .collect(Collectors.toMap(Locker::getId, Function.identity())); @@ -109,9 +109,9 @@ private List findLockersInSection(Long sectionId, List lockerIds) * 아직 배정 상태인 신청이 걸린 사물함이 하나라도 있으면 끊는다. *

* 배정인지 만료인지는 내 신청 내역 화면과 같은 기준을 써야 하므로 {@link LockerApplicationStatus}에 맡긴다. - * 저장소는 신청과 회차를 읽어 오기만 한다. + * 저장소는 판정에 끼지 않고 회차를 읽고 신청 존재만 알려준다. */ - private void requireNotInUse(List lockerIds) { + private void validateNotInUse(List lockerIds) { LocalDate today = LocalDate.now(); // 회차를 먼저 좁힌다. 회차는 학기당 한두 건이라 전부 읽어도 되고, 그래야 신청 조회가 (회차, 사물함) // 유니크 인덱스를 탄다. 사물함 식별자만으로 신청을 찾으면 탈 인덱스가 없어 이력 전체를 훑는다. From 92b7df292f450ac075cf52f99aaadfbaaf48df0d Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:19:55 +0900 Subject: [PATCH 09/12] =?UTF-8?q?fix:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=EC=9D=B4=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=ED=91=9C=EC=8B=9C=EB=A5=BC=20=EB=90=98=EB=8F=8C?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20@DynamicUpda?= =?UTF-8?q?te=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/ac/kookmin/stream/db/event/LockerJpaEntity.java | 5 +++++ 1 file changed, 5 insertions(+) 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 { From 09061dace08468fca66cd8e59d33db2851d73f93 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:20:52 +0900 Subject: [PATCH 10/12] =?UTF-8?q?fix:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=9D=98=20=EC=A4=91=EB=B3=B5=20=EC=8B=9D=EB=B3=84=EC=9E=90=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/event/locker/AdminLockerSectionController.java | 2 +- .../event/locker/request/LockerStatusChangeRequest.java | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) 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 33732f0a..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 @@ -67,7 +67,7 @@ public ApiResponse changeLockerStatus( @PathVariable Long sectionId, @Valid @RequestBody LockerStatusChangeRequest request ) { - adminLockerService.changeLockerStatus(sectionId, request.lockerIds(), request.lockerStatus()); + 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 index a1759a0e..f3d79138 100644 --- 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 @@ -17,4 +17,12 @@ public record LockerStatusChangeRequest( @NotNull(message = "변경할 상태를 입력해 주세요.") LockerStatus lockerStatus ) { + + /** + * 중복을 걷어낸 사물함 식별자. 같은 칸이 여러 번 와도 결과는 같지만, 그대로 두면 이용 중 검사의 {@code IN} 절이 + * 구역의 칸 수보다 커진다. 중복을 없애면 상한이 그 구역의 칸 수가 되어 따로 숫자를 둘 필요가 없다. + */ + public List distinctLockerIds() { + return lockerIds.stream().distinct().toList(); + } } From 5cb627dba6f96569d463a1dcc1b0ea9366282294 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:29:15 +0900 Subject: [PATCH 11/12] =?UTF-8?q?fix:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=97=86=EB=8A=94=20=EA=B5=AC=EC=97=AD=EC=9D=84=20404=EB=A1=9C?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=ED=95=98=EA=B3=A0=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=ED=95=9C=20=EC=82=AC=EB=AC=BC=ED=95=A8=EB=A7=8C=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../locker/repository/LockerRepository.java | 5 ++++ .../service/impl/AdminLockerServiceImpl.java | 28 +++++++++---------- .../LockerApplicationServiceImplTest.java | 5 ++++ .../service/impl/LockerServiceImplTest.java | 5 ++++ .../stream/db/event/LockerJpaRepository.java | 2 ++ .../stream/db/event/LockerRepositoryImpl.java | 10 +++++++ 6 files changed, 41 insertions(+), 14 deletions(-) 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 5ea2fb69..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 @@ -93,6 +93,11 @@ public interface LockerRepository { */ List findLockersByIdsIncludingDeleted(Collection lockerIds); + /** + * 식별자에 해당하는 사물함. 삭제된 사물함은 없는 것으로 본다. 순서는 보장하지 않는다. + */ + List findLockersByIds(Collection lockerIds); + /** * 사물함 여러 건을 저장한다. 변경은 호출한 쪽 트랜잭션이 커밋될 때 반영된다. */ 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 dcdf219f..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 @@ -3,9 +3,7 @@ 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; @@ -91,18 +89,20 @@ public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatu * 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다. */ private List getLockersInSection(Long sectionId, List lockerIds) { - Map 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); - } - return locker; - }) - .toList(); + // 구역부터 확인한다. 없는 구역이면 같은 경로의 구역 상세 조회와 같이 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; } /** 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 66c31e3b..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 @@ -365,6 +365,11 @@ public void saveLockers(List lockers) { throw new UnsupportedOperationException(); } + @Override + public List findLockersByIds(Collection lockerIds) { + throw new UnsupportedOperationException(); + } + @Override public List findLockersByIdsIncludingDeleted(Collection lockerIds) { batchLookups++; 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 91d37bfe..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 @@ -328,6 +328,11 @@ 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(); 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 460d5405..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 @@ -145,6 +145,16 @@ public void saveLockers(List lockers) { .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() From d20a6ad8a4beb7c4272d1302d76efd8ad4666e08 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 27 Oct 2026 21:31:54 +0900 Subject: [PATCH 12/12] =?UTF-8?q?docs:=20=EC=82=AC=EB=AC=BC=ED=95=A8=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=B3=80=EA=B2=BD=EC=9D=B4=20=EB=AA=BB=20?= =?UTF-8?q?=EB=A7=89=EB=8A=94=20=EA=B2=BD=ED=95=A9=EA=B3=BC=20=EB=B0=B0?= =?UTF-8?q?=EC=A0=95=20=EC=A7=91=EA=B3=84=20=EA=B7=BC=EA=B1=B0=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stream/api/admin/event/locker/AdminLockerSectionApi.java | 4 +++- .../event/domain/locker/service/impl/LockerServiceImpl.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 5f6bd29a..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 @@ -56,7 +56,9 @@ ApiResponse getLockerSection( + "사용 기간이 끝나지 않은 회차에 배정된 사물함은 이용 중인 학생이 있으므로 DISABLED로 바꿀 수 없고, " + "AVAILABLE로 되돌리는 것은 막지 않는다. " + "구역 상세가 내려주는 ASSIGNED는 신청으로 생기는 표시용 값이라 이 경로로 바꿀 수 없다. " - + "응답 본문이 없으므로 바뀐 결과는 구역 상세를 다시 조회해 받는다.") + + "응답 본문이 없으므로 바뀐 결과는 구역 상세를 다시 조회해 받는다. " + + "이용 중 검사와 학생 신청이 겹치면 서로 다른 테이블에 써서 둘 다 성공할 수 있고, " + + "그때는 배정된 칸이 사용 중지된 채로 남는다.") @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_NOT_IN_SECTION", "LOCKER_IN_USE"}) ApiResponse changeLockerStatus( 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();