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 4b2388f1f..89950b7a2 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,8 @@ 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.LockerSectionLayoutUpdateRequest; +import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerSectionPhotoUpdateRequest; 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; @@ -11,6 +13,7 @@ import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; import kr.ac.kookmin.stream.common.CommonErrorCode; import kr.ac.kookmin.stream.event.domain.locker.domain.LockerErrorCode; +import kr.ac.kookmin.stream.file.domain.FileErrorCode; import org.springdoc.core.annotations.ParameterObject; /** @@ -20,7 +23,7 @@ *

* 구역은 운영 회차에 묶여 있지 않다. 요청의 회차는 "어느 회차의 배정 현황을 볼지"를 정하는 기준이다. */ -@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회와 사물함 상태 변경") +@Tag(name = "사물함 구역 관리", description = "운영진 사물함 구역 목록·상세 조회, 사물함 상태 변경, 칸 배치·사진 수정") public interface AdminLockerSectionApi { /** 구역별 전체·선택 가능·배정 사물함 수. */ @@ -66,4 +69,32 @@ ApiResponse changeLockerStatus( Long sectionId, LockerStatusChangeRequest request ); + + @Operation(summary = "사물함 구역 칸 배치 수정", + description = "구역의 칸 배치 구조(layout)를 바꾼다. 배치를 아직 등록하지 않은 구역이면 이 요청이 첫 등록이다. " + + "보낸 JSON은 해석하지 않고 그대로 저장하며, 칸 번호가 구역의 사물함과 맞는지도 보지 않는다 — " + + "칸 번호가 어긋나면 없는 칸이 그려지거나 있는 사물함이 화면에서 빠진다. " + + "배치 형식 버전은 받지 않고 서버가 박는다. " + + "응답 본문이 없으므로 바뀐 결과는 구역 상세를 다시 조회해 받는다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_SECTION_NOT_FOUND"}) + ApiResponse changeSectionLayout( + AdminApiUser apiUser, + Long sectionId, + LockerSectionLayoutUpdateRequest request + ); + + @Operation(summary = "사물함 구역 사진 수정", + description = "구역 사진을 보낸 목록으로 통째로 바꾼다. 배열 순서가 노출 순서이고, 한 장 이상이어야 한다. " + + "지울 사진은 목록에서 빼고 남길 사진까지 함께 보낸다 — 빠진 사진은 연결만 끊기고 파일 자체는 지워지지 않는다. " + + "사진은 칸 배치와 같은 행에 저장되므로 배치를 등록하지 않은 구역에는 사진만 올릴 수 없다. " + + "응답 본문이 없으므로 바뀐 결과는 구역 상세를 다시 조회해 받는다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = LockerErrorCode.class, codes = {"LOCKER_SECTION_NOT_FOUND", "LOCKER_SECTION_LAYOUT_NOT_FOUND"}) + @ApiErrorCode(type = FileErrorCode.class, codes = {"FILE_NOT_FOUND"}) + ApiResponse changeSectionPhotos( + AdminApiUser apiUser, + Long sectionId, + LockerSectionPhotoUpdateRequest 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 1cbd5a8d9..e55b97460 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,9 +6,12 @@ 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.LockerSectionLayoutUpdateRequest; +import kr.ac.kookmin.stream.api.admin.event.locker.request.LockerSectionPhotoUpdateRequest; 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.usecase.AdminLockerPhotoUpdateUseCase; 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; @@ -21,6 +24,7 @@ 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -33,6 +37,7 @@ public class AdminLockerSectionController implements AdminLockerSectionApi { private final LockerService lockerService; private final AdminLockerService adminLockerService; private final FileService fileService; + private final AdminLockerPhotoUpdateUseCase adminLockerPhotoUpdateUseCase; @Override @GetMapping @@ -70,4 +75,26 @@ public ApiResponse changeLockerStatus( adminLockerService.changeLockerStatus(sectionId, request.distinctLockerIds(), request.lockerStatus()); return ApiResponse.success(); } + + @Override + @PutMapping("/{sectionId}/layout") + public ApiResponse changeSectionLayout( + AdminApiUser apiUser, + @PathVariable Long sectionId, + @Valid @RequestBody LockerSectionLayoutUpdateRequest request + ) { + adminLockerService.changeSectionLayout(sectionId, request.layout()); + return ApiResponse.success(); + } + + @Override + @PutMapping("/{sectionId}/photos") + public ApiResponse changeSectionPhotos( + AdminApiUser apiUser, + @PathVariable Long sectionId, + @Valid @RequestBody LockerSectionPhotoUpdateRequest request + ) { + adminLockerPhotoUpdateUseCase.changePhotos(sectionId, request.distinctPhotoFileIds()); + return ApiResponse.success(); + } } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerSectionLayoutUpdateRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerSectionLayoutUpdateRequest.java new file mode 100644 index 000000000..ab9e47003 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerSectionLayoutUpdateRequest.java @@ -0,0 +1,47 @@ +package kr.ac.kookmin.stream.api.admin.event.locker.request; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.NotNull; + +/** + * 구역의 칸 배치 구조를 바꾼다. + *

+ * 배치 형식 버전은 받지 않는다. 형식 버전은 서버와 앱 사이의 약속이라 운영진이 화면에서 고를 값이 아니고, + * 잘못 올리면 앱이 멀쩡한 배치를 모르는 형식으로 보고 그리지 않는다. 저장할 때 서버가 박는다. + */ +public record LockerSectionLayoutUpdateRequest( + @NotNull(message = "칸 배치를 입력해 주세요.") + @Schema(description = "root 블록 트리. 칸 번호는 구역 상세의 lockers[].lockerNumber와 같다") + ObjectNode root +) { + + /** + * 저장할 JSON 원문. + *

+ * 블록을 들여다보지 않고 공백만 걷어낸 문자열로 바꾼다. {@code String}으로 받으면 깨진 JSON이 DB까지 내려가 + * 500이 되고, 블록 타입으로 받으면 서버가 형식을 알아야 한다. {@code ObjectNode}는 Jackson이 파싱해 + * 유효한 JSON 객체임만 보장하고 내용은 모른 채 넘긴다. + */ + public String layout() { + return root.toString(); + } + + /** + * 빈 객체를 거른다. 편집기를 비우고 저장하면 {@code {}}가 올라오는데 {@code @NotNull}은 이를 막지 못하고 + * ({@code @NotEmpty}는 {@code ObjectNode}를 지원하지 않는다), 저장되면 앱이 아무것도 그리지 않는다. + * 배치 화면에는 배치를 지우는 수단이 없어 빈 배치는 운영진의 선택이 아니다. + *

+ * 필드가 하나라도 있는지만 보므로 블록 형식은 알지 못한다. 그래서 {@code children}이 빈 블록처럼 + * 비어 있지 않으면서 아무것도 그리지 않는 배치는 걸러내지 못한다. + */ + @JsonIgnore + @Schema(hidden = true) + @AssertTrue(message = "칸 배치가 비어 있습니다.") + public boolean isRootNotEmpty() { + // null은 @NotNull이 따로 알린다. 여기서 false를 내면 메시지가 둘 붙는다 + return root == null || !root.isEmpty(); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerSectionPhotoUpdateRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerSectionPhotoUpdateRequest.java new file mode 100644 index 000000000..ba190bf50 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/request/LockerSectionPhotoUpdateRequest.java @@ -0,0 +1,24 @@ +package kr.ac.kookmin.stream.api.admin.event.locker.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +/** + * 구역 사진을 받은 목록으로 통째로 바꾼다. + *

+ * 화면이 사진을 지우고 추가한 뒤 저장 버튼 하나로 올리므로 개별 추가·삭제가 아니라 전체 교체다. + */ +public record LockerSectionPhotoUpdateRequest( + @NotEmpty(message = "구역 사진을 한 장 이상 등록해 주세요.") + List<@NotNull(message = "파일 ID는 비워 둘 수 없습니다.") Long> photoFileIds +) { + + /** + * 중복을 걷어낸 파일 식별자. 같은 사진이 두 번 와도 한 번만 보여주면 되고, 파일 존재 확인이 개수를 견주므로 + * 중복이 남아 있으면 있는 파일인데도 없다고 판단한다. 처음 나온 자리를 지켜 노출 순서는 그대로 둔다. + */ + public List distinctPhotoFileIds() { + return photoFileIds.stream().distinct().toList(); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/usecase/AdminLockerPhotoUpdateUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/usecase/AdminLockerPhotoUpdateUseCase.java new file mode 100644 index 000000000..f2b543264 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/locker/usecase/AdminLockerPhotoUpdateUseCase.java @@ -0,0 +1,42 @@ +package kr.ac.kookmin.stream.api.admin.event.locker.usecase; + +import java.util.List; +import java.util.Set; +import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.event.domain.locker.service.AdminLockerService; +import kr.ac.kookmin.stream.file.domain.FileErrorCode; +import kr.ac.kookmin.stream.file.service.FileService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 구역 사진을 바꾸기 전에 연결할 파일이 실제로 있는지 확인한다. 사물함(event)과 파일(file)은 서로 다른 도메인이라 + * 도메인끼리 직접 의존하지 않고 이 UseCase가 두 Service를 조합한다({@code architecture.md} 6-1절). + *

+ * 파일 확인과 사진 저장을 한 트랜잭션으로 묶지 않는다. 확인 직후 파일이 지워져도 구역 상세 조회가 삭제된 파일을 + * 목록에서 조용히 제외하므로 원자성이 필요하지 않다. 공지와 같은 판단이다({@code AdminNoticeWriteUseCase}). + */ +@Component +@RequiredArgsConstructor +public class AdminLockerPhotoUpdateUseCase { + + private final FileService fileService; + private final AdminLockerService adminLockerService; + + public void changePhotos(Long sectionId, List photoFileIds) { + validateFilesExist(photoFileIds); + adminLockerService.changeSectionPhotos(sectionId, photoFileIds); + } + + /** + * 조회된 개수를 견줘 전부 있는지 본다. 중복이 남아 있으면 있는 파일도 없다고 오판하므로 호출부에 맡기지 않고 + * 여기서 묶는다. + */ + private void validateFilesExist(List photoFileIds) { + Set fileIds = Set.copyOf(photoFileIds); + + if (fileService.findAllByIdIn(List.copyOf(fileIds)).size() != fileIds.size()) { + throw new BusinessException(FileErrorCode.FILE_NOT_FOUND); + } + } +} 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 95bae6a73..599d6caef 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 @@ -13,6 +13,7 @@ public enum LockerErrorCode implements ErrorCode { LOCKER_PERIOD_NOT_FOUND(ErrorStatus.NOT_FOUND, "사물함 운영 회차를 찾을 수 없습니다."), LOCKER_SECTION_NOT_FOUND(ErrorStatus.NOT_FOUND, "사물함 구역을 찾을 수 없습니다."), + LOCKER_SECTION_LAYOUT_NOT_FOUND(ErrorStatus.NOT_FOUND, "구역의 칸 배치가 등록되지 않았습니다."), INVALID_LOCKER_PERIOD_RANGE(ErrorStatus.BAD_REQUEST, "신청 기간과 사용 기간의 순서가 올바르지 않습니다."), LOCKER_PERIOD_SEMESTER_DUPLICATED(ErrorStatus.CONFLICT, "이미 등록된 학기입니다."), LOCKER_PERIOD_OVERLAPPED(ErrorStatus.CONFLICT, "다른 회차와 기간이 겹칩니다."), diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerSectionLayout.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerSectionLayout.java index f8cc70023..e6ba0fd39 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerSectionLayout.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/domain/LockerSectionLayout.java @@ -17,6 +17,13 @@ @AllArgsConstructor(access = AccessLevel.PRIVATE) public class LockerSectionLayout { + /** + * 지금 서버가 쓰는 배치 형식 버전. 블록 종류·필드가 바뀌면 올린다. + *

+ * 운영진이 화면에서 고르는 값이 아니다. 형식 버전은 서버와 앱 사이의 약속이라 저장할 때 서버가 박는다. + */ + public static final int CURRENT_VERSION = 1; + private Long id; private Long sectionId; /** root 블록 트리 JSON 원문. 서버는 해석하지 않고 그대로 내려준다. */ @@ -35,4 +42,31 @@ public static LockerSectionLayout of( ) { return new LockerSectionLayout(id, sectionId, layout, version, photoFileIds); } + + /** + * 배치 구조를 아직 등록하지 않은 구역에 쓸 빈 행. + *

+ * 배치 JSON과 형식 버전은 {@link #changeLayout(String)}이 함께 채운다 — 두 값을 넣는 자리를 한 곳으로 모은다. + * 사진은 따로 올리므로 빈 목록으로 시작한다. + */ + public static LockerSectionLayout createEmpty(Long sectionId) { + return new LockerSectionLayout(null, sectionId, null, 0, List.of()); + } + + /** + * 배치 구조를 바꾼다. 형식 버전도 지금 버전으로 맞춘다 — 저장되는 JSON은 현재 콘솔이 만든 형식이다. + */ + public void changeLayout(String layout) { + this.layout = layout; + this.version = CURRENT_VERSION; + } + + /** + * 구역 사진을 받은 목록으로 통째로 바꾼다. 배열 순서가 노출 순서다. + *

+ * 화면이 사진을 지우고 추가한 뒤 저장 버튼 하나로 올리므로 개별 추가·삭제가 아니라 전체 교체다. + */ + public void changePhotos(List photoFileIds) { + this.photoFileIds = photoFileIds; + } } 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 c75f15afb..4ad5fa569 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 @@ -73,6 +73,11 @@ public interface LockerRepository { */ Optional findLayoutBySectionId(Long sectionId); + /** + * 구역의 배치 구조와 사진을 저장한다. 식별자가 없으면 새로 만든다. + */ + void saveLayout(LockerSectionLayout sectionLayout); + /** * 삭제되지 않은 사물함 전체를 조회한다. 구역별 집계는 서비스가 이 목록을 묶어 센다. */ 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 b188e33df..280911576 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 @@ -49,6 +49,25 @@ public interface AdminLockerService { */ void changeLockerStatus(Long sectionId, List lockerIds, LockerStatus status); + /** + * 구역의 칸 배치 구조를 바꾼다. 아직 배치가 없는 구역이면 새로 만든다. + *

+ * 배치 JSON은 해석하지 않고 그대로 저장한다. 칸 번호가 구역의 사물함과 맞는지도 보지 않는다 — 블록 종류와 + * 필드는 운영진 콘솔과 앱만 아는 약속이라, 서버가 대조하려면 그 형식에 묶인다. 형식 버전은 서버가 박는다. + * + * @param layout root 블록 트리 JSON 원문 + */ + void changeSectionLayout(Long sectionId, String layout); + + /** + * 구역 사진을 받은 목록으로 통째로 바꾼다. 배열 순서가 노출 순서다. + *

+ * 사진은 배치와 같은 행에 있고 배치 쪽이 비어 있을 수 없어, 배치를 등록하지 않은 구역에는 사진만 올릴 수 없다. + *

+ * 파일이 실제로 있는지는 이 서비스가 보지 않는다. 교차 도메인이라 {@code api:admin-api}의 UseCase가 확인한다. + */ + void changeSectionPhotos(Long sectionId, List photoFileIds); + /** * 운영 회차의 신청 현황을 신청 일시 최신순으로 조회한다. 취소된 신청도 담는다. * 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 1eced3d8e..74f4692b0 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 @@ -22,6 +22,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.LockerSectionLayout; 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; @@ -94,6 +95,32 @@ public void changeLockerStatus(Long sectionId, List lockerIds, LockerStatu lockerRepository.saveLockers(lockers); } + @Override + @Transactional + public void changeSectionLayout(Long sectionId, String layout) { + validateSectionExists(sectionId); + + // 배치를 아직 등록하지 않은 구역이면 이 저장이 첫 등록이다 + LockerSectionLayout sectionLayout = lockerRepository.findLayoutBySectionId(sectionId) + .orElseGet(() -> LockerSectionLayout.createEmpty(sectionId)); + sectionLayout.changeLayout(layout); + + lockerRepository.saveLayout(sectionLayout); + } + + @Override + @Transactional + public void changeSectionPhotos(Long sectionId, List photoFileIds) { + validateSectionExists(sectionId); + + // 사진은 배치와 같은 행에 있고 배치 쪽이 비어 있을 수 없다. 배치부터 등록해야 사진을 올릴 수 있다 + LockerSectionLayout sectionLayout = lockerRepository.findLayoutBySectionId(sectionId) + .orElseThrow(() -> new BusinessException(LockerErrorCode.LOCKER_SECTION_LAYOUT_NOT_FOUND)); + sectionLayout.changePhotos(photoFileIds); + + lockerRepository.saveLayout(sectionLayout); + } + @Override public PageResult getApplications( Long lockerPeriodId, List memberIds, PageOffset pageOffset @@ -159,10 +186,7 @@ private void validateUsageNotEnded(Long lockerPeriodId, LocalDate today) { * 요청한 사물함을 구역 소속으로 확인하고 돌려준다. 하나라도 그 구역에 없으면 아무것도 바꾸지 않도록 여기서 끊는다. */ private List getLockersInSection(Long sectionId, List lockerIds) { - // 구역부터 확인한다. 없는 구역이면 같은 경로의 구역 상세 조회와 같이 404로 나가야 한다 - if (lockerRepository.findSectionById(sectionId).isEmpty()) { - throw new BusinessException(LockerErrorCode.LOCKER_SECTION_NOT_FOUND); - } + validateSectionExists(sectionId); // 구역 전체가 아니라 요청한 사물함만 읽는다. 36칸 구역에서 두 칸만 바꿔도 전체를 읽지 않도록 한다 List lockers = lockerRepository.findLockersByIds(lockerIds); @@ -196,6 +220,15 @@ private void validateNotInUse(List lockerIds) { } } + /** + * 없는 구역이면 같은 경로의 구역 상세 조회와 같이 404로 나가야 한다. 구역을 먼저 확인해 아무것도 바꾸지 않고 끊는다. + */ + private void validateSectionExists(Long sectionId) { + if (lockerRepository.findSectionById(sectionId).isEmpty()) { + throw new BusinessException(LockerErrorCode.LOCKER_SECTION_NOT_FOUND); + } + } + private LockerPeriod getPeriod(Long lockerPeriodId) { return lockerRepository.findPeriodById(lockerPeriodId) .orElseThrow(() -> new BusinessException(LockerErrorCode.LOCKER_PERIOD_NOT_FOUND)); 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 9cbd4921e..731c641d9 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 @@ -420,6 +420,11 @@ public Optional findLayoutBySectionId(Long sectionId) { throw new UnsupportedOperationException(); } + @Override + public void saveLayout(LockerSectionLayout sectionLayout) { + throw new UnsupportedOperationException(); + } + @Override public List findAllLockers() { 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 de252ec1a..2fb2ca1d5 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 @@ -285,6 +285,11 @@ public Optional findLayoutBySectionId(Long sectionId) { return Optional.empty(); } + @Override + public void saveLayout(LockerSectionLayout sectionLayout) { + throw new UnsupportedOperationException(); + } + @Override public List findAllLockers() { return allLockers; 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 073c7c215..d2daa2e14 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 @@ -118,6 +118,11 @@ public Optional findLayoutBySectionId(Long sectionId) { .map(LockerSectionLayoutJpaEntity::toDomain); } + @Override + public void saveLayout(LockerSectionLayout sectionLayout) { + lockerSectionLayoutJpaRepository.save(LockerSectionLayoutJpaEntity.from(sectionLayout)); + } + @Override public List findAllLockers() { return lockerJpaRepository.findAllByIsDeletedFalse().stream()