From e8a984437d590b49b912245e266fb36e67b7e89e Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 29 Oct 2026 17:32:12 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=95=84=EC=B9=B4=EC=9D=B4?= =?UTF-8?q?=EB=B9=99=20=EB=93=B1=EB=A1=9D=C2=B7=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=8F=99=EC=9E=91=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/domain/archive/domain/Archive.java | 58 +++++++++++++++++++ .../archive/domain/ArchiveCreateCommand.java | 24 ++++++++ .../archive/domain/ArchiveErrorCode.java | 3 +- .../archive/domain/ArchiveUpdateCommand.java | 24 ++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java create mode 100644 core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java index 8c718288a..8a57e625a 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java @@ -3,6 +3,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import kr.ac.kookmin.stream.common.BusinessException; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; @@ -45,6 +46,63 @@ public static Archive of( exposed, relatedEventId, createdAt); } + /** + * 아카이빙을 새로 만든다. 등록 일시는 DB 기본값으로 채워져 저장 전에는 비어 있다. + */ + public static Archive create(ArchiveCreateCommand command) { + validatePeriod(command.startDate(), command.endDate()); + + return new Archive( + null, + command.title(), + command.content(), + command.startDate(), + command.endDate(), + command.location(), + command.departmentName(), + command.imageIds(), + command.exposed(), + command.relatedEventId(), + null + ); + } + + /** + * 전체 필드를 통째로 바꾼다. 사진과 연결 행사도 받은 값으로 교체하므로, 빈 사진 목록은 전체 삭제이고 + * {@code relatedEventId}가 {@code null}이면 연결을 끊는다. + */ + public void update(ArchiveUpdateCommand command) { + validatePeriod(command.startDate(), command.endDate()); + + this.title = command.title(); + this.content = command.content(); + this.startDate = command.startDate(); + this.endDate = command.endDate(); + this.location = command.location(); + this.departmentName = command.departmentName(); + this.imageIds = command.imageIds(); + this.exposed = command.exposed(); + this.relatedEventId = command.relatedEventId(); + } + + /** + * 노출 여부를 바꾼다. 이미 그 값이어도 그대로 둔다 — 영속성 컨텍스트가 값이 바뀐 행만 UPDATE한다. + */ + public void changeExposure(boolean exposed) { + this.exposed = exposed; + } + + /** + * 당일 활동은 종료일이 없다. 종료일을 줬다면 시작일보다 앞설 수 없다. + *

+ * 값 하나의 형식이 아니라 두 값 사이의 규칙이라 요청 어노테이션으로는 표현할 수 없어 도메인에서 본다. + */ + private static void validatePeriod(LocalDate startDate, LocalDate endDate) { + if (endDate != null && endDate.isBefore(startDate)) { + throw new BusinessException(ArchiveErrorCode.INVALID_ARCHIVE_PERIOD); + } + } + /** * 대표 이미지 파일 id. 사진 배열의 첫 장을 대표로 쓴다. *

diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java new file mode 100644 index 000000000..1a4a58bb9 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java @@ -0,0 +1,24 @@ +package kr.ac.kookmin.stream.event.domain.archive.domain; + +import java.time.LocalDate; +import java.util.List; + +/** + * 아카이빙 등록 입력. + * + * @param endDate 당일 활동이면 {@code null} + * @param imageIds 활동 사진. 배열 순서가 노출 순서이고 첫 장이 대표 이미지다 + * @param relatedEventId 연결할 행사. 연결하지 않으면 {@code null} + */ +public record ArchiveCreateCommand( + String title, + String content, + LocalDate startDate, + LocalDate endDate, + String location, + String departmentName, + List imageIds, + boolean exposed, + Long relatedEventId +) { +} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveErrorCode.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveErrorCode.java index 9561f9bdc..65c5288fb 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveErrorCode.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveErrorCode.java @@ -11,7 +11,8 @@ @AllArgsConstructor public enum ArchiveErrorCode implements ErrorCode { - ARCHIVE_NOT_FOUND(ErrorStatus.NOT_FOUND, "아카이빙을 찾을 수 없습니다."); + ARCHIVE_NOT_FOUND(ErrorStatus.NOT_FOUND, "아카이빙을 찾을 수 없습니다."), + INVALID_ARCHIVE_PERIOD(ErrorStatus.BAD_REQUEST, "활동 종료일이 시작일보다 앞설 수 없습니다."); private final int status; private final String message; diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java new file mode 100644 index 000000000..d696b72e3 --- /dev/null +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java @@ -0,0 +1,24 @@ +package kr.ac.kookmin.stream.event.domain.archive.domain; + +import java.time.LocalDate; +import java.util.List; + +/** + * 아카이빙 수정 입력. 전체 필드를 받아 통째로 바꾼다. + * + * @param endDate 당일 활동이면 {@code null} + * @param imageIds 활동 사진. 배열 순서가 노출 순서이고 첫 장이 대표 이미지다 + * @param relatedEventId 연결할 행사. {@code null}이면 연결을 끊는다 + */ +public record ArchiveUpdateCommand( + String title, + String content, + LocalDate startDate, + LocalDate endDate, + String location, + String departmentName, + List imageIds, + boolean exposed, + Long relatedEventId +) { +} From 755c61388cf3a5fba03d858563edcefab8f1cc12 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 29 Oct 2026 17:34:01 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=EC=95=84=EC=B9=B4=EC=9D=B4?= =?UTF-8?q?=EB=B9=99=20=EC=93=B0=EA=B8=B0=20=EC=A0=80=EC=9E=A5=EC=86=8C=20?= =?UTF-8?q?=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 --- .../archive/repository/ArchiveRepository.java | 15 ++++++++ .../stream/db/event/ArchiveJpaEntity.java | 5 +++ .../stream/db/event/ArchiveJpaRepository.java | 2 ++ .../db/event/ArchiveRepositoryImpl.java | 34 +++++++++++++++++++ 4 files changed, 56 insertions(+) diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/repository/ArchiveRepository.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/repository/ArchiveRepository.java index 638c21354..0ec1808e9 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/repository/ArchiveRepository.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/repository/ArchiveRepository.java @@ -29,4 +29,19 @@ public interface ArchiveRepository { PageResult search(String keyword, Boolean exposed, PageOffset pageOffset); Optional findById(Long id); + + /** + * 식별자 목록에 드는 아카이빙. 삭제된 것은 담지 않는다. 노출 여부 일괄 변경이 요청한 전부가 살아 있는지 + * 확인하려고 쓰므로, 결과 수가 요청 수와 다르면 부르는 쪽이 끊는다. + */ + List findAllByIds(List archiveIds); + + Archive save(Archive archive); + + void saveAll(List archives); + + /** + * 소프트 삭제한다. 이미 없거나 지워진 아카이빙이면 {@code false}다. + */ + boolean delete(Long id); } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java index 0d19036f4..e6bde77ec 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java @@ -12,10 +12,15 @@ import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; import lombok.AccessLevel; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; +// 바뀐 컬럼만 UPDATE한다(@DynamicUpdate). 운영진 둘이 서로 다른 필드를 고칠 때 읽어 둔 나머지 컬럼까지 되쓰는 것을 +// 막는다. 다만 삭제 표시(is_deleted)는 이것으로 지켜지지 않는다 — 도메인이 그 값을 들고 있지 않아 merge가 항상 +// false를 싣고, 수정과 삭제가 겹치면 삭제가 되돌아갈 수 있다. 저장소의 다른 소프트 삭제 엔티티도 같은 상태다. @Entity +@DynamicUpdate @Table(name = "archives") @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ArchiveJpaEntity extends BaseSoftDeleteEntity { diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaRepository.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaRepository.java index 8ba7984ad..6a702d652 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaRepository.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaRepository.java @@ -40,6 +40,8 @@ ORDER BY YEAR(a.startDate) DESC Optional findByIdAndIsDeletedFalse(Long id); + List findAllByIdInAndIsDeletedFalse(List ids); + /** * 운영진 목록. 제목 검색은 {@code LIKE '%...%'}라 인덱스를 타지 못하고 테이블을 훑는다 — 아카이빙이 연간 * 수십 건 규모라 그대로 둔다. 검색어 이스케이프는 {@code LikePatterns}가 한다. diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRepositoryImpl.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRepositoryImpl.java index 0b463851e..bfd37943d 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRepositoryImpl.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveRepositoryImpl.java @@ -56,4 +56,38 @@ public PageResult search(String keyword, Boolean exposed, PageOffset pa public Optional findById(Long id) { return archiveJpaRepository.findByIdAndIsDeletedFalse(id).map(ArchiveJpaEntity::toDomain); } + + @Override + public List findAllByIds(List archiveIds) { + if (archiveIds.isEmpty()) { + return List.of(); + } + return archiveJpaRepository.findAllByIdInAndIsDeletedFalse(archiveIds).stream() + .map(ArchiveJpaEntity::toDomain) + .toList(); + } + + @Override + public Archive save(Archive archive) { + return archiveJpaRepository.save(ArchiveJpaEntity.from(archive)).toDomain(); + } + + @Override + public void saveAll(List archives) { + archiveJpaRepository.saveAll(archives.stream().map(ArchiveJpaEntity::from).toList()); + } + + /** + * 관리 상태 엔티티를 읽어 표시만 바꾼다. 더티 체킹이 is_deleted 한 컬럼만 UPDATE하므로, 도메인이 들고 있지 않은 + * 삭제 표시를 쓰기 경로로 되돌릴 일이 없다 (NoticeRepositoryImpl.delete와 같은 방식). + */ + @Override + public boolean delete(Long id) { + return archiveJpaRepository.findByIdAndIsDeletedFalse(id) + .map(entity -> { + entity.delete(); + return true; + }) + .orElse(false); + } } From b87f9590a049868995117af20b60d5e37e13fcb2 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 29 Oct 2026 17:35:34 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=95=84=EC=B9=B4=EC=9D=B4?= =?UTF-8?q?=EB=B9=99=20=EB=93=B1=EB=A1=9D=C2=B7=EC=88=98=EC=A0=95=C2=B7?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=C2=B7=EB=85=B8=EC=B6=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=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 --- .../archive/service/AdminArchiveService.java | 29 ++++++++++++ .../service/impl/AdminArchiveServiceImpl.java | 47 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java index cd46badb1..ec4ec1133 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java @@ -2,7 +2,10 @@ import kr.ac.kookmin.stream.common.PageOffset; import kr.ac.kookmin.stream.common.PageResult; +import java.util.List; import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; /** * 운영진 아카이빙 API가 쓰는 서비스. @@ -19,4 +22,30 @@ public interface AdminArchiveService { * @param exposed 슬랑제 노출 여부. {@code null}이면 노출 여부로 좁히지 않는다 */ PageResult getArchives(String keyword, Boolean exposed, PageOffset pageOffset); + + /** + * 아카이빙을 새로 등록한다. + *

+ * 사진 파일과 연결할 행사가 실제로 있는지는 이 서비스가 보지 않는다. 교차 도메인이라 + * {@code api:admin-api}의 UseCase가 확인한다. + */ + void create(ArchiveCreateCommand command); + + /** + * 아카이빙을 통째로 바꾼다. 사진과 연결 행사도 받은 값으로 교체한다. 삭제된 아카이빙은 없는 것으로 본다. + */ + void update(Long archiveId, ArchiveUpdateCommand command); + + /** + * 아카이빙을 소프트 삭제한다. 학생 앱 조회에서 바로 빠진다. + */ + void delete(Long archiveId); + + /** + * 여러 아카이빙의 슬랑제 페이지 노출 여부를 한 번에 바꾼다. 목록의 체크박스 일괄 변경과 행별 드롭다운 변경이 + * 같은 동작이라 하나로 받는다 — 행별 변경은 식별자가 하나인 요청이다. + *

+ * 하나라도 없거나 지워진 아카이빙이 섞이면 아무것도 바꾸지 않는다. + */ + void changeExposure(List archiveIds, boolean exposed); } diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java index 455a1a92f..15ce6958b 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java @@ -1,12 +1,18 @@ package kr.ac.kookmin.stream.event.domain.archive.service.impl; +import java.util.List; +import kr.ac.kookmin.stream.common.BusinessException; import kr.ac.kookmin.stream.common.PageOffset; import kr.ac.kookmin.stream.common.PageResult; import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveErrorCode; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; import kr.ac.kookmin.stream.event.domain.archive.repository.ArchiveRepository; import kr.ac.kookmin.stream.event.domain.archive.service.AdminArchiveService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -18,4 +24,45 @@ class AdminArchiveServiceImpl implements AdminArchiveService { public PageResult getArchives(String keyword, Boolean exposed, PageOffset pageOffset) { return archiveRepository.search(keyword, exposed, pageOffset); } + + @Override + @Transactional + public void create(ArchiveCreateCommand command) { + archiveRepository.save(Archive.create(command)); + } + + @Override + @Transactional + public void update(Long archiveId, ArchiveUpdateCommand command) { + Archive archive = getArchive(archiveId); + archive.update(command); + + archiveRepository.save(archive); + } + + @Override + @Transactional + public void delete(Long archiveId) { + if (!archiveRepository.delete(archiveId)) { + throw new BusinessException(ArchiveErrorCode.ARCHIVE_NOT_FOUND); + } + } + + @Override + @Transactional + public void changeExposure(List archiveIds, boolean exposed) { + List archives = archiveRepository.findAllByIds(archiveIds); + // 하나라도 없으면 아무것도 바꾸지 않는다. 삭제된 아카이빙은 findAllByIds가 빼므로 수가 모자란다 + if (archives.size() != archiveIds.size()) { + throw new BusinessException(ArchiveErrorCode.ARCHIVE_NOT_FOUND); + } + + archives.forEach(archive -> archive.changeExposure(exposed)); + archiveRepository.saveAll(archives); + } + + private Archive getArchive(Long archiveId) { + return archiveRepository.findById(archiveId) + .orElseThrow(() -> new BusinessException(ArchiveErrorCode.ARCHIVE_NOT_FOUND)); + } } From 10163ee46adb3f9de9b98d615eb76776861b3a3e Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 29 Oct 2026 17:40:00 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=EC=95=84=EC=B9=B4=EC=9D=B4?= =?UTF-8?q?=EB=B9=99=20=EB=93=B1=EB=A1=9D=C2=B7=EC=88=98=EC=A0=95=C2=B7?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=C2=B7=EB=85=B8=EC=B6=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=20=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 --- .../admin/event/archive/AdminArchiveApi.java | 46 +++++++++++- .../event/archive/AdminArchiveController.java | 47 ++++++++++++ .../request/ArchiveExposureChangeRequest.java | 26 +++++++ .../archive/request/ArchiveWriteRequest.java | 73 +++++++++++++++++++ .../usecase/AdminArchiveWriteUseCase.java | 67 +++++++++++++++++ 5 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java create mode 100644 api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java index 6718116bb..23eeed00b 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java @@ -3,7 +3,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import kr.ac.kookmin.stream.api.admin.AdminApiUser; +import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveExposureChangeRequest; import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveSearchParams; +import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveWriteRequest; import kr.ac.kookmin.stream.api.admin.event.archive.response.AdminArchiveDetailResponse; import kr.ac.kookmin.stream.api.admin.event.archive.response.AdminArchiveListItemResponse; import kr.ac.kookmin.stream.api.common.dto.ApiResponse; @@ -12,6 +14,8 @@ import kr.ac.kookmin.stream.api.common.openapi.ApiErrorCode; import kr.ac.kookmin.stream.common.CommonErrorCode; import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveErrorCode; +import kr.ac.kookmin.stream.event.domain.event.domain.EventErrorCode; +import kr.ac.kookmin.stream.file.domain.FileErrorCode; import org.springdoc.core.annotations.ParameterObject; /** @@ -19,7 +23,7 @@ *

* 접근은 {@code ADMIN} role이면 된다. 부서 제한은 두지 않았다. */ -@Tag(name = "아카이빙 관리", description = "운영진 아카이빙 목록·상세 조회") +@Tag(name = "아카이빙 관리", description = "운영진 아카이빙 목록·상세 조회와 등록·수정·삭제") public interface AdminArchiveApi { /** 아카이빙 목록. 제목으로 검색하고 슬랑제 노출 여부로 거르며, 등록일 최신순으로 페이지 단위 조회한다. */ @@ -44,4 +48,44 @@ ApiResponse> getArchives( + "연결하지 않았거나 행사가 삭제됐으면 null이다 — 운영진 화면이므로 아직 게시하지 않은 행사도 보여준다.") @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"}) ApiResponse getArchive(AdminApiUser apiUser, Long archiveId); + + /** 아카이빙 등록. */ + @Operation(summary = "아카이빙 등록", + description = "아카이빙을 새로 등록한다. 활동 사진은 한 장 이상이어야 하고 첫 장이 대표 이미지다. " + + "당일 활동은 endDate를 생략한다 — 종료일을 주면 시작일보다 앞설 수 없다. " + + "relatedEventId로 행사를 연결하며, 아직 게시하지 않은 행사도 연결할 수 있다. " + + "응답 본문이 없으므로 등록 결과는 목록을 다시 조회해 받는다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"INVALID_ARCHIVE_PERIOD"}) + @ApiErrorCode(type = FileErrorCode.class, codes = {"FILE_NOT_FOUND"}) + @ApiErrorCode(type = EventErrorCode.class, codes = {"EVENT_NOT_FOUND"}) + ApiResponse createArchive(AdminApiUser apiUser, ArchiveWriteRequest request); + + /** 아카이빙 수정. 전체 필드를 통째로 바꾼다. */ + @Operation(summary = "아카이빙 수정", + description = "아카이빙을 통째로 바꾼다. 보낸 값이 그대로 저장되므로 고치지 않을 필드도 함께 보내야 한다. " + + "imageIds는 최종 사진 목록이고, relatedEventId를 null로 주면 행사 연결이 끊긴다. " + + "응답 본문이 없으므로 바뀐 결과는 상세를 다시 조회해 받는다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND", "INVALID_ARCHIVE_PERIOD"}) + @ApiErrorCode(type = FileErrorCode.class, codes = {"FILE_NOT_FOUND"}) + @ApiErrorCode(type = EventErrorCode.class, codes = {"EVENT_NOT_FOUND"}) + ApiResponse updateArchive(AdminApiUser apiUser, Long archiveId, ArchiveWriteRequest request); + + /** 아카이빙 삭제. 행은 남기고 표시만 바꾼다. */ + @Operation(summary = "아카이빙 삭제", + description = "아카이빙을 삭제한다. 행은 남기고 표시만 바꿔 되살릴 수 있게 하며, 학생 앱 조회에서는 바로 빠진다. " + + "이미 삭제된 아카이빙은 없는 것으로 본다.") + @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"}) + ApiResponse deleteArchive(AdminApiUser apiUser, Long archiveId); + + /** 슬랑제 페이지 노출 여부 일괄 변경. */ + @Operation(summary = "아카이빙 슬랑제 노출 여부 변경", + description = "여러 아카이빙의 슬랑제 페이지 노출 여부를 한 번에 바꾼다. 목록의 체크박스 일괄 변경과 " + + "행별 드롭다운 변경에 함께 쓰며, 행별 변경은 archiveIds에 하나만 담아 보낸다. " + + "한 번에 1개 이상 100개 이하로 보내고, 하나라도 없거나 삭제된 아카이빙이 섞이면 아무것도 바뀌지 않는다. " + + "응답 본문이 없으므로 바뀐 결과는 목록을 다시 조회해 받는다.") + @ApiErrorCode(type = CommonErrorCode.class, codes = {"INVALID_INPUT"}) + @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"}) + ApiResponse changeExposure(AdminApiUser apiUser, ArchiveExposureChangeRequest request); } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java index c14438290..3a38b2ada 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java @@ -3,10 +3,13 @@ import jakarta.validation.Valid; import java.util.Map; import kr.ac.kookmin.stream.api.admin.AdminApiUser; +import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveExposureChangeRequest; import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveSearchParams; +import kr.ac.kookmin.stream.api.admin.event.archive.request.ArchiveWriteRequest; import kr.ac.kookmin.stream.api.admin.event.archive.response.AdminArchiveDetailResponse; import kr.ac.kookmin.stream.api.admin.event.archive.response.AdminArchiveListItemResponse; import kr.ac.kookmin.stream.api.admin.event.archive.usecase.AdminArchiveDetailUseCase; +import kr.ac.kookmin.stream.api.admin.event.archive.usecase.AdminArchiveWriteUseCase; import kr.ac.kookmin.stream.api.admin.event.archive.usecase.ArchiveWithRelatedEvent; import kr.ac.kookmin.stream.api.common.dto.ApiResponse; import kr.ac.kookmin.stream.api.common.dto.PageParams; @@ -17,7 +20,12 @@ 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.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -34,6 +42,7 @@ public class AdminArchiveController implements AdminArchiveApi { private final AdminArchiveService adminArchiveService; private final AdminArchiveDetailUseCase adminArchiveDetailUseCase; + private final AdminArchiveWriteUseCase adminArchiveWriteUseCase; private final FileService fileService; @Override @@ -61,4 +70,42 @@ public ApiResponse getArchive( return ApiResponse.success( AdminArchiveDetailResponse.of(archive.detail(), archive.relatedEventTitle(), imagesById)); } + + @Override + @PostMapping + public ApiResponse createArchive( + AdminApiUser apiUser, + @Valid @RequestBody ArchiveWriteRequest request + ) { + adminArchiveWriteUseCase.create(request.toCreateCommand()); + return ApiResponse.success(); + } + + @Override + @PutMapping("/{archiveId}") + public ApiResponse updateArchive( + AdminApiUser apiUser, + @PathVariable Long archiveId, + @Valid @RequestBody ArchiveWriteRequest request + ) { + adminArchiveWriteUseCase.update(archiveId, request.toUpdateCommand()); + return ApiResponse.success(); + } + + @Override + @DeleteMapping("/{archiveId}") + public ApiResponse deleteArchive(AdminApiUser apiUser, @PathVariable Long archiveId) { + adminArchiveService.delete(archiveId); + return ApiResponse.success(); + } + + @Override + @PatchMapping("/exposure") + public ApiResponse changeExposure( + AdminApiUser apiUser, + @Valid @RequestBody ArchiveExposureChangeRequest request + ) { + adminArchiveService.changeExposure(request.distinctArchiveIds(), request.isExposed()); + return ApiResponse.success(); + } } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java new file mode 100644 index 000000000..366c1fde4 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java @@ -0,0 +1,26 @@ +package kr.ac.kookmin.stream.api.admin.event.archive.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.List; + +/** + * 여러 아카이빙의 슬랑제 페이지 노출 여부를 한 번에 바꾼다. + *

+ * 목록의 체크박스 일괄 변경과 행별 드롭다운 변경이 같은 동작이라 하나로 받는다 — 행별 변경은 식별자가 하나인 요청이다. + */ +public record ArchiveExposureChangeRequest( + + // 목록에서 체크박스로 고르는 동작이라 한 번에 고를 수 있는 최대는 한 페이지 최대와 같다(PageParams의 size 상한) + @Size(min = 1, max = 100, message = "아카이빙을 1개 이상 100개 이하로 선택해 주세요.") + List<@NotNull(message = "아카이빙 ID는 비워 둘 수 없습니다.") Long> archiveIds, + + @NotNull(message = "슬랑제 페이지 노출 여부를 입력해 주세요.") + Boolean isExposed +) { + + /** 중복을 걷어낸 아카이빙 식별자. 서비스가 "요청한 수와 조회된 수"를 견줘 없는 아카이빙을 가려내기 때문이다. */ + public List distinctArchiveIds() { + return archiveIds.stream().distinct().toList(); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java new file mode 100644 index 000000000..7a5e40bf3 --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java @@ -0,0 +1,73 @@ +package kr.ac.kookmin.stream.api.admin.event.archive.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.LocalDate; +import java.util.List; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; + +/** + * 아카이빙 등록과 수정이 같은 본문을 받는다. 수정이 전체 필드를 통째로 바꾸는 방식이라 등록과 모양이 같아, + * 두 record로 나누면 같은 선언이 두 벌 생긴다. + *

+ * 종료일이 시작일보다 앞서는지는 여기서 보지 않는다. 값 하나의 형식이 아니라 두 값 사이의 규칙이라 + * {@code Archive}가 본다. + * + * @param endDate 당일 활동이면 생략하거나 {@code null} + * @param imageIds 활동 사진. 배열 순서가 노출 순서이고 첫 장이 대표 이미지다 + * @param relatedEventId 연결할 행사. 연결하지 않으면 {@code null}이고, 수정에서 {@code null}이면 연결을 끊는다 + */ +public record ArchiveWriteRequest( + + @NotBlank(message = "활동 제목을 입력해 주세요.") + @Size(max = 200, message = "활동 제목은 200자 이하여야 합니다.") + String title, + + @NotBlank(message = "활동 내용을 입력해 주세요.") + String content, + + @NotNull(message = "활동 시작일을 입력해 주세요.") + LocalDate startDate, + + LocalDate endDate, + + @Size(max = 255, message = "활동 장소는 255자 이하여야 합니다.") + String location, + + @NotBlank(message = "담당 부서를 입력해 주세요.") + @Size(max = 100, message = "담당 부서는 100자 이하여야 합니다.") + String departmentName, + + // 사진 없는 아카이빙은 목록에서 썸네일이 빈 칸으로 보인다. 첫 장이 대표 이미지라 한 장은 있어야 한다 + @NotEmpty(message = "활동 사진을 한 장 이상 등록해 주세요.") + List<@NotNull(message = "파일 ID는 비워 둘 수 없습니다.") Long> imageIds, + + @NotNull(message = "슬랑제 페이지 노출 여부를 입력해 주세요.") + Boolean isExposed, + + Long relatedEventId +) { + + public ArchiveCreateCommand toCreateCommand() { + return new ArchiveCreateCommand( + title, content, startDate, endDate, location, departmentName, distinctImageIds(), isExposed, + relatedEventId); + } + + public ArchiveUpdateCommand toUpdateCommand() { + return new ArchiveUpdateCommand( + title, content, startDate, endDate, location, departmentName, distinctImageIds(), isExposed, + relatedEventId); + } + + /** + * 중복을 걷어낸 사진 식별자. 같은 사진을 두 번 보여줄 일이 없고, 파일 존재 확인이 개수를 견주므로 중복이 + * 남아 있으면 있는 파일도 없다고 판단한다. 처음 나온 자리를 지켜 노출 순서는 그대로 둔다. + */ + private List distinctImageIds() { + return imageIds.stream().distinct().toList(); + } +} diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java new file mode 100644 index 000000000..c6868021a --- /dev/null +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java @@ -0,0 +1,67 @@ +package kr.ac.kookmin.stream.api.admin.event.archive.usecase; + +import java.util.List; +import java.util.Set; +import kr.ac.kookmin.stream.common.BusinessException; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; +import kr.ac.kookmin.stream.event.domain.archive.service.AdminArchiveService; +import kr.ac.kookmin.stream.event.domain.event.domain.EventErrorCode; +import kr.ac.kookmin.stream.event.domain.event.service.EventService; +import kr.ac.kookmin.stream.file.domain.FileErrorCode; +import kr.ac.kookmin.stream.file.service.FileService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 아카이빙을 쓰기 전에 연결할 사진과 행사가 실제로 있는지 확인한다. 사진(file)과 행사는 아카이빙과 다른 도메인이라 + * 도메인끼리 직접 의존하지 않고 이 UseCase가 조합한다({@code architecture.md} 6-1절). + *

+ * 확인과 저장을 한 트랜잭션으로 묶지 않는다. 확인 직후 사진이 지워져도 상세 조회가 목록에서 조용히 빼고, 행사가 + * 지워지면 연결만 비어 보인다 — 둘 다 아카이빙을 못 쓰게 만들지 않는다. 공지와 같은 판단이다 + * ({@code AdminNoticeWriteUseCase}). + */ +@Component +@RequiredArgsConstructor +public class AdminArchiveWriteUseCase { + + private final FileService fileService; + private final EventService eventService; + private final AdminArchiveService adminArchiveService; + + public void create(ArchiveCreateCommand command) { + validateImagesExist(command.imageIds()); + validateRelatedEventExists(command.relatedEventId()); + + adminArchiveService.create(command); + } + + public void update(Long archiveId, ArchiveUpdateCommand command) { + validateImagesExist(command.imageIds()); + validateRelatedEventExists(command.relatedEventId()); + + adminArchiveService.update(archiveId, command); + } + + /** + * 조회된 개수를 견줘 전부 있는지 본다. 중복이 남아 있으면 있는 파일도 없다고 오판하므로 호출부에 맡기지 않고 + * 여기서 묶는다. + */ + private void validateImagesExist(List imageIds) { + Set fileIds = Set.copyOf(imageIds); + + if (fileService.findAllByIdIn(List.copyOf(fileIds)).size() != fileIds.size()) { + throw new BusinessException(FileErrorCode.FILE_NOT_FOUND); + } + } + + /** + * 없는 행사를 연결하면 상세에서 조용히 비어 보여 운영진이 눈치채지 못한다. 쓰는 자리에서 막는다. + * 아직 게시하지 않은 행사는 연결할 수 있다 — 운영진 화면은 미게시 행사도 고를 수 있어야 한다. + */ + private void validateRelatedEventExists(Long relatedEventId) { + if (relatedEventId != null && eventService.getByIdOrNull(relatedEventId) == null) { + throw new BusinessException(EventErrorCode.EVENT_NOT_FOUND); + } + } +} From 292fe5efcc25ed27a4a0f2e2dbeb2e5a2accd0fd Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 29 Oct 2026 17:44:07 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor:=20=EC=9A=94=EC=95=BD=EA=B3=BC=20?= =?UTF-8?q?=EA=B2=B9=EC=B9=98=EB=8A=94=20=EC=95=84=EC=B9=B4=EC=9D=B4?= =?UTF-8?q?=EB=B9=99=20=EC=93=B0=EA=B8=B0=20=EC=A3=BC=EC=84=9D=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 --- .../api/admin/event/archive/AdminArchiveApi.java | 4 ---- .../stream/event/domain/archive/domain/Archive.java | 4 +--- .../domain/archive/service/AdminArchiveService.java | 10 ++-------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java index 23eeed00b..ec9efd972 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveApi.java @@ -49,7 +49,6 @@ ApiResponse> getArchives( @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"}) ApiResponse getArchive(AdminApiUser apiUser, Long archiveId); - /** 아카이빙 등록. */ @Operation(summary = "아카이빙 등록", description = "아카이빙을 새로 등록한다. 활동 사진은 한 장 이상이어야 하고 첫 장이 대표 이미지다. " + "당일 활동은 endDate를 생략한다 — 종료일을 주면 시작일보다 앞설 수 없다. " @@ -61,7 +60,6 @@ ApiResponse> getArchives( @ApiErrorCode(type = EventErrorCode.class, codes = {"EVENT_NOT_FOUND"}) ApiResponse createArchive(AdminApiUser apiUser, ArchiveWriteRequest request); - /** 아카이빙 수정. 전체 필드를 통째로 바꾼다. */ @Operation(summary = "아카이빙 수정", description = "아카이빙을 통째로 바꾼다. 보낸 값이 그대로 저장되므로 고치지 않을 필드도 함께 보내야 한다. " + "imageIds는 최종 사진 목록이고, relatedEventId를 null로 주면 행사 연결이 끊긴다. " @@ -72,14 +70,12 @@ ApiResponse> getArchives( @ApiErrorCode(type = EventErrorCode.class, codes = {"EVENT_NOT_FOUND"}) ApiResponse updateArchive(AdminApiUser apiUser, Long archiveId, ArchiveWriteRequest request); - /** 아카이빙 삭제. 행은 남기고 표시만 바꾼다. */ @Operation(summary = "아카이빙 삭제", description = "아카이빙을 삭제한다. 행은 남기고 표시만 바꿔 되살릴 수 있게 하며, 학생 앱 조회에서는 바로 빠진다. " + "이미 삭제된 아카이빙은 없는 것으로 본다.") @ApiErrorCode(type = ArchiveErrorCode.class, codes = {"ARCHIVE_NOT_FOUND"}) ApiResponse deleteArchive(AdminApiUser apiUser, Long archiveId); - /** 슬랑제 페이지 노출 여부 일괄 변경. */ @Operation(summary = "아카이빙 슬랑제 노출 여부 변경", description = "여러 아카이빙의 슬랑제 페이지 노출 여부를 한 번에 바꾼다. 목록의 체크박스 일괄 변경과 " + "행별 드롭다운 변경에 함께 쓰며, 행별 변경은 archiveIds에 하나만 담아 보낸다. " diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java index 8a57e625a..b7655e7be 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java @@ -46,9 +46,7 @@ public static Archive of( exposed, relatedEventId, createdAt); } - /** - * 아카이빙을 새로 만든다. 등록 일시는 DB 기본값으로 채워져 저장 전에는 비어 있다. - */ + /** 등록 일시는 DB 기본값으로 채워져 저장 전에는 비어 있다. */ public static Archive create(ArchiveCreateCommand command) { validatePeriod(command.startDate(), command.endDate()); diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java index ec4ec1133..ecb77e00e 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java @@ -24,21 +24,15 @@ public interface AdminArchiveService { PageResult getArchives(String keyword, Boolean exposed, PageOffset pageOffset); /** - * 아카이빙을 새로 등록한다. - *

* 사진 파일과 연결할 행사가 실제로 있는지는 이 서비스가 보지 않는다. 교차 도메인이라 * {@code api:admin-api}의 UseCase가 확인한다. */ void create(ArchiveCreateCommand command); - /** - * 아카이빙을 통째로 바꾼다. 사진과 연결 행사도 받은 값으로 교체한다. 삭제된 아카이빙은 없는 것으로 본다. - */ + /** 사진과 연결 행사도 받은 값으로 교체한다. 삭제된 아카이빙은 없는 것으로 본다. */ void update(Long archiveId, ArchiveUpdateCommand command); - /** - * 아카이빙을 소프트 삭제한다. 학생 앱 조회에서 바로 빠진다. - */ + /** 행은 남기고 표시만 바꾼다. 학생 앱 조회에서는 바로 빠진다. */ void delete(Long archiveId); /** From 76a70c3d00cb2b838eb4b1a4623fb29e6b60fb09 Mon Sep 17 00:00:00 2001 From: sangrae Date: Thu, 29 Oct 2026 23:22:01 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=EC=95=84=EC=B9=B4=EC=9D=B4=EB=B9=99?= =?UTF-8?q?=20=EB=85=B8=EC=B6=9C=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EB=88=84=EB=9D=BD=20=EA=B2=80=EC=A6=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @Size는 null을 통과시켜 archiveIds를 빠뜨린 요청이 NPE로 500이 났다. @NotEmpty를 함께 걸어 400 INVALID_INPUT으로 나가게 한다. 같은 PR의 imageIds는 이미 @NotEmpty를 쓰고 있어 기준이 갈려 있었다. 함께 정리한 것 - 중복 식별자 제거를 요청에서 서비스로 옮겨, 스스로 보장하는 파일 존재 확인과 기준을 맞춘다 - 등록·수정 Command가 필드까지 같아 ArchiveWriteCommand 하나로 합친다 - ArchiveJpaEntity의 @DynamicUpdate 주석을 바로잡는다. 수정은 같은 트랜잭션에서 엔티티를 올린 뒤 merge하므로 is_deleted가 변경으로 잡히지 않아 UPDATE에서 빠진다 — 삭제가 되돌아가지 않는다 - import 순서 정리 --- .../event/archive/AdminArchiveController.java | 10 ++++---- .../request/ArchiveExposureChangeRequest.java | 12 ++++------ .../archive/request/ArchiveWriteRequest.java | 13 +++------- .../usecase/AdminArchiveWriteUseCase.java | 7 +++--- .../event/domain/archive/domain/Archive.java | 4 ++-- .../archive/domain/ArchiveUpdateCommand.java | 24 ------------------- ...eCommand.java => ArchiveWriteCommand.java} | 6 ++--- .../archive/service/AdminArchiveService.java | 9 ++++--- .../service/impl/AdminArchiveServiceImpl.java | 13 +++++----- .../stream/db/event/ArchiveJpaEntity.java | 7 +++--- 10 files changed, 36 insertions(+), 69 deletions(-) delete mode 100644 core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java rename core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/{ArchiveCreateCommand.java => ArchiveWriteCommand.java} (68%) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java index 3a38b2ada..f60e70dd3 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/AdminArchiveController.java @@ -22,12 +22,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.DeleteMapping; 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.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -77,7 +77,7 @@ public ApiResponse createArchive( AdminApiUser apiUser, @Valid @RequestBody ArchiveWriteRequest request ) { - adminArchiveWriteUseCase.create(request.toCreateCommand()); + adminArchiveWriteUseCase.create(request.toCommand()); return ApiResponse.success(); } @@ -88,7 +88,7 @@ public ApiResponse updateArchive( @PathVariable Long archiveId, @Valid @RequestBody ArchiveWriteRequest request ) { - adminArchiveWriteUseCase.update(archiveId, request.toUpdateCommand()); + adminArchiveWriteUseCase.update(archiveId, request.toCommand()); return ApiResponse.success(); } @@ -105,7 +105,7 @@ public ApiResponse changeExposure( AdminApiUser apiUser, @Valid @RequestBody ArchiveExposureChangeRequest request ) { - adminArchiveService.changeExposure(request.distinctArchiveIds(), request.isExposed()); + adminArchiveService.changeExposure(request.archiveIds(), request.isExposed()); return ApiResponse.success(); } } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java index 366c1fde4..e15dc6281 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveExposureChangeRequest.java @@ -1,5 +1,6 @@ package kr.ac.kookmin.stream.api.admin.event.archive.request; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import java.util.List; @@ -11,16 +12,13 @@ */ public record ArchiveExposureChangeRequest( - // 목록에서 체크박스로 고르는 동작이라 한 번에 고를 수 있는 최대는 한 페이지 최대와 같다(PageParams의 size 상한) - @Size(min = 1, max = 100, message = "아카이빙을 1개 이상 100개 이하로 선택해 주세요.") + // @Size는 null을 통과시키므로 @NotEmpty가 같이 있어야 목록을 빠뜨린 요청이 400으로 나간다. + // 상한은 목록에서 체크박스로 고르는 동작이라 한 페이지 최대와 같게 둔다(PageParams의 size 상한) + @NotEmpty(message = "아카이빙을 하나 이상 선택해 주세요.") + @Size(max = 100, message = "아카이빙은 한 번에 100개까지 바꿀 수 있습니다.") List<@NotNull(message = "아카이빙 ID는 비워 둘 수 없습니다.") Long> archiveIds, @NotNull(message = "슬랑제 페이지 노출 여부를 입력해 주세요.") Boolean isExposed ) { - - /** 중복을 걷어낸 아카이빙 식별자. 서비스가 "요청한 수와 조회된 수"를 견줘 없는 아카이빙을 가려내기 때문이다. */ - public List distinctArchiveIds() { - return archiveIds.stream().distinct().toList(); - } } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java index 7a5e40bf3..dddf0748c 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java @@ -6,8 +6,7 @@ import jakarta.validation.constraints.Size; import java.time.LocalDate; import java.util.List; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveWriteCommand; /** * 아카이빙 등록과 수정이 같은 본문을 받는다. 수정이 전체 필드를 통째로 바꾸는 방식이라 등록과 모양이 같아, @@ -51,14 +50,8 @@ public record ArchiveWriteRequest( Long relatedEventId ) { - public ArchiveCreateCommand toCreateCommand() { - return new ArchiveCreateCommand( - title, content, startDate, endDate, location, departmentName, distinctImageIds(), isExposed, - relatedEventId); - } - - public ArchiveUpdateCommand toUpdateCommand() { - return new ArchiveUpdateCommand( + public ArchiveWriteCommand toCommand() { + return new ArchiveWriteCommand( title, content, startDate, endDate, location, departmentName, distinctImageIds(), isExposed, relatedEventId); } diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java index c6868021a..e9685d027 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/usecase/AdminArchiveWriteUseCase.java @@ -3,8 +3,7 @@ import java.util.List; import java.util.Set; import kr.ac.kookmin.stream.common.BusinessException; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveWriteCommand; import kr.ac.kookmin.stream.event.domain.archive.service.AdminArchiveService; import kr.ac.kookmin.stream.event.domain.event.domain.EventErrorCode; import kr.ac.kookmin.stream.event.domain.event.service.EventService; @@ -29,14 +28,14 @@ public class AdminArchiveWriteUseCase { private final EventService eventService; private final AdminArchiveService adminArchiveService; - public void create(ArchiveCreateCommand command) { + public void create(ArchiveWriteCommand command) { validateImagesExist(command.imageIds()); validateRelatedEventExists(command.relatedEventId()); adminArchiveService.create(command); } - public void update(Long archiveId, ArchiveUpdateCommand command) { + public void update(Long archiveId, ArchiveWriteCommand command) { validateImagesExist(command.imageIds()); validateRelatedEventExists(command.relatedEventId()); diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java index b7655e7be..5634b29d6 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java @@ -47,7 +47,7 @@ public static Archive of( } /** 등록 일시는 DB 기본값으로 채워져 저장 전에는 비어 있다. */ - public static Archive create(ArchiveCreateCommand command) { + public static Archive create(ArchiveWriteCommand command) { validatePeriod(command.startDate(), command.endDate()); return new Archive( @@ -69,7 +69,7 @@ public static Archive create(ArchiveCreateCommand command) { * 전체 필드를 통째로 바꾼다. 사진과 연결 행사도 받은 값으로 교체하므로, 빈 사진 목록은 전체 삭제이고 * {@code relatedEventId}가 {@code null}이면 연결을 끊는다. */ - public void update(ArchiveUpdateCommand command) { + public void update(ArchiveWriteCommand command) { validatePeriod(command.startDate(), command.endDate()); this.title = command.title(); diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java deleted file mode 100644 index d696b72e3..000000000 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveUpdateCommand.java +++ /dev/null @@ -1,24 +0,0 @@ -package kr.ac.kookmin.stream.event.domain.archive.domain; - -import java.time.LocalDate; -import java.util.List; - -/** - * 아카이빙 수정 입력. 전체 필드를 받아 통째로 바꾼다. - * - * @param endDate 당일 활동이면 {@code null} - * @param imageIds 활동 사진. 배열 순서가 노출 순서이고 첫 장이 대표 이미지다 - * @param relatedEventId 연결할 행사. {@code null}이면 연결을 끊는다 - */ -public record ArchiveUpdateCommand( - String title, - String content, - LocalDate startDate, - LocalDate endDate, - String location, - String departmentName, - List imageIds, - boolean exposed, - Long relatedEventId -) { -} diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveWriteCommand.java similarity index 68% rename from core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java rename to core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveWriteCommand.java index 1a4a58bb9..025db7497 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveCreateCommand.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/ArchiveWriteCommand.java @@ -4,13 +4,13 @@ import java.util.List; /** - * 아카이빙 등록 입력. + * 아카이빙 등록·수정 입력. 수정이 전체 필드를 통째로 바꾸는 방식이라 등록과 담는 값이 같아 하나로 둔다. * * @param endDate 당일 활동이면 {@code null} * @param imageIds 활동 사진. 배열 순서가 노출 순서이고 첫 장이 대표 이미지다 - * @param relatedEventId 연결할 행사. 연결하지 않으면 {@code null} + * @param relatedEventId 연결할 행사. 연결하지 않으면 {@code null}이고, 수정에서 {@code null}이면 연결을 끊는다 */ -public record ArchiveCreateCommand( +public record ArchiveWriteCommand( String title, String content, LocalDate startDate, diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java index ecb77e00e..ceae56b28 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/AdminArchiveService.java @@ -1,11 +1,10 @@ package kr.ac.kookmin.stream.event.domain.archive.service; +import java.util.List; import kr.ac.kookmin.stream.common.PageOffset; import kr.ac.kookmin.stream.common.PageResult; -import java.util.List; import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveWriteCommand; /** * 운영진 아카이빙 API가 쓰는 서비스. @@ -27,10 +26,10 @@ public interface AdminArchiveService { * 사진 파일과 연결할 행사가 실제로 있는지는 이 서비스가 보지 않는다. 교차 도메인이라 * {@code api:admin-api}의 UseCase가 확인한다. */ - void create(ArchiveCreateCommand command); + void create(ArchiveWriteCommand command); /** 사진과 연결 행사도 받은 값으로 교체한다. 삭제된 아카이빙은 없는 것으로 본다. */ - void update(Long archiveId, ArchiveUpdateCommand command); + void update(Long archiveId, ArchiveWriteCommand command); /** 행은 남기고 표시만 바꾼다. 학생 앱 조회에서는 바로 빠진다. */ void delete(Long archiveId); diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java index 15ce6958b..e5e1567e9 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java @@ -5,9 +5,8 @@ import kr.ac.kookmin.stream.common.PageOffset; import kr.ac.kookmin.stream.common.PageResult; import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveCreateCommand; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveWriteCommand; import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveErrorCode; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveUpdateCommand; import kr.ac.kookmin.stream.event.domain.archive.repository.ArchiveRepository; import kr.ac.kookmin.stream.event.domain.archive.service.AdminArchiveService; import lombok.RequiredArgsConstructor; @@ -27,13 +26,13 @@ public PageResult getArchives(String keyword, Boolean exposed, PageOffs @Override @Transactional - public void create(ArchiveCreateCommand command) { + public void create(ArchiveWriteCommand command) { archiveRepository.save(Archive.create(command)); } @Override @Transactional - public void update(Long archiveId, ArchiveUpdateCommand command) { + public void update(Long archiveId, ArchiveWriteCommand command) { Archive archive = getArchive(archiveId); archive.update(command); @@ -51,9 +50,11 @@ public void delete(Long archiveId) { @Override @Transactional public void changeExposure(List archiveIds, boolean exposed) { - List archives = archiveRepository.findAllByIds(archiveIds); + // 같은 식별자가 두 번 오면 아래 개수 비교가 어긋나므로, 호출부에 맡기지 않고 여기서 걷어낸다 + List distinctIds = archiveIds.stream().distinct().toList(); + List archives = archiveRepository.findAllByIds(distinctIds); // 하나라도 없으면 아무것도 바꾸지 않는다. 삭제된 아카이빙은 findAllByIds가 빼므로 수가 모자란다 - if (archives.size() != archiveIds.size()) { + if (archives.size() != distinctIds.size()) { throw new BusinessException(ArchiveErrorCode.ARCHIVE_NOT_FOUND); } diff --git a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java index e6bde77ec..eeebc014a 100644 --- a/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java +++ b/infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/ArchiveJpaEntity.java @@ -16,9 +16,10 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.type.SqlTypes; -// 바뀐 컬럼만 UPDATE한다(@DynamicUpdate). 운영진 둘이 서로 다른 필드를 고칠 때 읽어 둔 나머지 컬럼까지 되쓰는 것을 -// 막는다. 다만 삭제 표시(is_deleted)는 이것으로 지켜지지 않는다 — 도메인이 그 값을 들고 있지 않아 merge가 항상 -// false를 싣고, 수정과 삭제가 겹치면 삭제가 되돌아갈 수 있다. 저장소의 다른 소프트 삭제 엔티티도 같은 상태다. +// 바뀐 컬럼만 UPDATE한다(@DynamicUpdate). 수정은 같은 트랜잭션에서 findById로 엔티티를 영속성 컨텍스트에 올린 뒤 +// merge하므로, 도메인이 들고 있지 않은 is_deleted는 읽어 둔 값 그대로여서 변경으로 잡히지 않고 UPDATE에서 빠진다. +// 덕분에 운영진 A가 수정하는 사이 B가 삭제를 커밋해도 A의 UPDATE가 삭제를 되돌리지 않는다. +// (NoticeJpaEntity·LockerJpaEntity가 같은 이유로 붙이고 있다) @Entity @DynamicUpdate @Table(name = "archives") From 359840038a09604ff1caff27664ecce8b0f74947 Mon Sep 17 00:00:00 2001 From: sangrae Date: Tue, 3 Nov 2026 01:07:43 +0900 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=EC=95=84=EC=B9=B4=EC=9D=B4=EB=B9=99?= =?UTF-8?q?=20=EC=93=B0=EA=B8=B0=20=EB=A6=AC=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/event/archive/request/ArchiveWriteRequest.java | 6 ++++-- .../kookmin/stream/event/domain/archive/domain/Archive.java | 6 +++--- .../archive/service/impl/AdminArchiveServiceImpl.java | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java index dddf0748c..828d43361 100644 --- a/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java +++ b/api/admin-api/src/main/java/kr/ac/kookmin/stream/api/admin/event/archive/request/ArchiveWriteRequest.java @@ -25,7 +25,9 @@ public record ArchiveWriteRequest( @Size(max = 200, message = "활동 제목은 200자 이하여야 합니다.") String title, + // content는 TEXT 컬럼(최대 65,535바이트)이다. 글자당 최대 4바이트로 잡아도 넘치지 않는 길이로 막는다 @NotBlank(message = "활동 내용을 입력해 주세요.") + @Size(max = 16000, message = "활동 내용은 16000자 이하여야 합니다.") String content, @NotNull(message = "활동 시작일을 입력해 주세요.") @@ -57,8 +59,8 @@ title, content, startDate, endDate, location, departmentName, distinctImageIds() } /** - * 중복을 걷어낸 사진 식별자. 같은 사진을 두 번 보여줄 일이 없고, 파일 존재 확인이 개수를 견주므로 중복이 - * 남아 있으면 있는 파일도 없다고 판단한다. 처음 나온 자리를 지켜 노출 순서는 그대로 둔다. + * 중복을 걷어낸 사진 식별자. 같은 사진을 두 번 보여줄 일이 없어 저장 전에 뺀다. 처음 나온 자리를 지켜 노출 + * 순서는 그대로 둔다. */ private List distinctImageIds() { return imageIds.stream().distinct().toList(); diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java index 5634b29d6..8ecc22007 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/domain/Archive.java @@ -66,8 +66,8 @@ public static Archive create(ArchiveWriteCommand command) { } /** - * 전체 필드를 통째로 바꾼다. 사진과 연결 행사도 받은 값으로 교체하므로, 빈 사진 목록은 전체 삭제이고 - * {@code relatedEventId}가 {@code null}이면 연결을 끊는다. + * 전체 필드를 통째로 바꾼다. 사진과 연결 행사도 받은 값으로 교체하므로, {@code relatedEventId}가 + * {@code null}이면 연결을 끊는다. */ public void update(ArchiveWriteCommand command) { validatePeriod(command.startDate(), command.endDate()); @@ -104,7 +104,7 @@ private static void validatePeriod(LocalDate startDate, LocalDate endDate) { /** * 대표 이미지 파일 id. 사진 배열의 첫 장을 대표로 쓴다. *

- * 사진 없는 아카이빙은 등록되지 않는다는 전제지만 최소 1장 제약이 없어, 비어 있으면 예외 대신 null을 돌린다. + * 등록·수정은 사진을 한 장 이상 받지만 그 전에 DB로 직접 넣은 행은 비어 있을 수 있어, 예외 대신 null을 돌린다. */ public Long thumbnailFileId() { return imageIds == null || imageIds.isEmpty() ? null : imageIds.getFirst(); diff --git a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java index e5e1567e9..cc268faef 100644 --- a/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java +++ b/core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/archive/service/impl/AdminArchiveServiceImpl.java @@ -5,8 +5,8 @@ import kr.ac.kookmin.stream.common.PageOffset; import kr.ac.kookmin.stream.common.PageResult; import kr.ac.kookmin.stream.event.domain.archive.domain.Archive; -import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveWriteCommand; import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveErrorCode; +import kr.ac.kookmin.stream.event.domain.archive.domain.ArchiveWriteCommand; import kr.ac.kookmin.stream.event.domain.archive.repository.ArchiveRepository; import kr.ac.kookmin.stream.event.domain.archive.service.AdminArchiveService; import lombok.RequiredArgsConstructor;