From c3223aa77e1eafad2137999e9d02b161fefe69ba Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 1/4] =?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 213d2239..1d8ea824 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 @@ -21,6 +21,8 @@ public enum LockerErrorCode implements ErrorCode { LOCKER_PERIOD_APPLY_ALREADY_STARTED(ErrorStatus.CONFLICT, "신청이 시작된 회차는 신청 시작일을 바꿀 수 없습니다."), LOCKER_PERIOD_HAS_APPLICATIONS(ErrorStatus.CONFLICT, "신청이 있는 회차는 삭제할 수 없습니다."), LOCKER_APPLY_NOT_OPEN(ErrorStatus.CONFLICT, "사물함 신청 기간이 아닙니다."), + LOCKER_NOT_IN_SECTION(ErrorStatus.BAD_REQUEST, "해당 구역의 사물함이 아닙니다."), + LOCKER_IN_USE(ErrorStatus.CONFLICT, "이용 중인 사물함은 사용 중지할 수 없습니다."), LOCKER_ALREADY_ASSIGNED(ErrorStatus.CONFLICT, "다른 사용자가 먼저 신청한 사물함입니다."); private final int status; From 43e0e08447c03a405cac8a13649d22b1c6e6f129 Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 2/4] =?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 00f82834..c59c000e 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++; @@ -427,6 +432,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 2bee7381..c715f8c0 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 @@ -322,6 +322,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(); @@ -349,6 +354,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 c1f6d7c6469fa14c7a4cd3fb3d23d22ca8a06c08 Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 3/4] =?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 5e96754d4a4fc54663ecca431cafd8973a51c83c Mon Sep 17 00:00:00 2001 From: sangrae Date: Mon, 19 Oct 2026 03:32:53 +0900 Subject: [PATCH 4/4] =?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 a001e5fe..34630947 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 @@ -56,4 +63,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()); + } + } +}