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