From 8821b07c93d740b129789e3071969c28e43a903a Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 13:41:39 +0900 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20=EC=97=85=EC=9E=A5=20=EB=8C=80?= =?UTF-8?q?=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EB=B0=8F=20repository=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대표이미지(최대 5개) 관리를 위한 기반 구조 추가. 순서 관리가 필요해 File과 별도로 WorkspaceImage(라이브 업장), WorkspaceRequestImage(신청 단계) 조인 엔티티를 도입. FileTargetType에 WORKSPACE_REPRESENTATIVE_IMAGE, ErrorCode에 FILE_LIMIT_EXCEEDED 추가. --- .../WorkspaceImageJpaRepository.java | 12 ++++ .../WorkspaceImageQueryRepositoryImpl.java | 27 +++++++ .../WorkspaceImageRepositoryImpl.java | 27 +++++++ .../WorkspaceRequestImageJpaRepository.java | 11 +++ ...kspaceRequestImageQueryRepositoryImpl.java | 22 ++++++ .../WorkspaceRequestImageRepositoryImpl.java | 22 ++++++ .../alter/common/exception/ErrorCode.java | 1 + .../domain/file/type/FileTargetType.java | 1 + .../workspace/entity/WorkspaceImage.java | 71 +++++++++++++++++++ .../entity/WorkspaceRequestImage.java | 67 +++++++++++++++++ .../WorkspaceImageQueryRepository.java | 10 +++ .../outbound/WorkspaceImageRepository.java | 10 +++ .../WorkspaceRequestImageQueryRepository.java | 9 +++ .../WorkspaceRequestImageRepository.java | 9 +++ 14 files changed, 299 insertions(+) create mode 100644 src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageRepositoryImpl.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageJpaRepository.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageQueryRepositoryImpl.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageRepositoryImpl.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageRepository.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageQueryRepository.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageRepository.java diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java new file mode 100644 index 00000000..1c6e68d7 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java @@ -0,0 +1,12 @@ +package com.dreamteam.alter.adapter.outbound.workspace.persistence; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; + +public interface WorkspaceImageJpaRepository extends JpaRepository { + List findByWorkspaceIdOrderBySortOrderAsc(Long workspaceId); + long countByWorkspaceId(Long workspaceId); +} diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java new file mode 100644 index 00000000..98449bb2 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java @@ -0,0 +1,27 @@ +package com.dreamteam.alter.adapter.outbound.workspace.persistence; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageQueryRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class WorkspaceImageQueryRepositoryImpl implements WorkspaceImageQueryRepository { + + private final WorkspaceImageJpaRepository workspaceImageJpaRepository; + + @Override + public List findAllByWorkspaceId(Long workspaceId) { + return workspaceImageJpaRepository.findByWorkspaceIdOrderBySortOrderAsc(workspaceId); + } + + @Override + public long countByWorkspaceId(Long workspaceId) { + return workspaceImageJpaRepository.countByWorkspaceId(workspaceId); + } +} diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageRepositoryImpl.java new file mode 100644 index 00000000..01372c09 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageRepositoryImpl.java @@ -0,0 +1,27 @@ +package com.dreamteam.alter.adapter.outbound.workspace.persistence; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class WorkspaceImageRepositoryImpl implements WorkspaceImageRepository { + + private final WorkspaceImageJpaRepository workspaceImageJpaRepository; + + @Override + public void saveAll(List workspaceImages) { + workspaceImageJpaRepository.saveAll(workspaceImages); + } + + @Override + public void deleteAll(List workspaceImages) { + workspaceImageJpaRepository.deleteAll(workspaceImages); + } +} diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageJpaRepository.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageJpaRepository.java new file mode 100644 index 00000000..5c6bfa3c --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageJpaRepository.java @@ -0,0 +1,11 @@ +package com.dreamteam.alter.adapter.outbound.workspace.persistence; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; + +public interface WorkspaceRequestImageJpaRepository extends JpaRepository { + List findByWorkspaceRequestIdOrderBySortOrderAsc(Long workspaceRequestId); +} diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageQueryRepositoryImpl.java new file mode 100644 index 00000000..390d5cc6 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageQueryRepositoryImpl.java @@ -0,0 +1,22 @@ +package com.dreamteam.alter.adapter.outbound.workspace.persistence; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageQueryRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class WorkspaceRequestImageQueryRepositoryImpl implements WorkspaceRequestImageQueryRepository { + + private final WorkspaceRequestImageJpaRepository workspaceRequestImageJpaRepository; + + @Override + public List findAllByWorkspaceRequestId(Long workspaceRequestId) { + return workspaceRequestImageJpaRepository.findByWorkspaceRequestIdOrderBySortOrderAsc(workspaceRequestId); + } +} diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageRepositoryImpl.java new file mode 100644 index 00000000..c58a444f --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestImageRepositoryImpl.java @@ -0,0 +1,22 @@ +package com.dreamteam.alter.adapter.outbound.workspace.persistence; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageRepository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class WorkspaceRequestImageRepositoryImpl implements WorkspaceRequestImageRepository { + + private final WorkspaceRequestImageJpaRepository workspaceRequestImageJpaRepository; + + @Override + public void saveAll(List workspaceRequestImages) { + workspaceRequestImageJpaRepository.saveAll(workspaceRequestImages); + } +} diff --git a/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java b/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java index 442b9c8d..1cc23a39 100644 --- a/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java +++ b/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java @@ -51,6 +51,7 @@ public enum ErrorCode { INVALID_FILE_TYPE(400, "B023", "허용되지 않는 파일 형식입니다."), FILE_SIZE_EXCEEDED(400, "B024", "파일 크기가 제한을 초과합니다."), FILE_ALREADY_ATTACHED(409, "B025", "이미 연결된 파일입니다."), + FILE_LIMIT_EXCEEDED(400, "B026", "대표이미지는 최대 5개까지 등록할 수 있습니다."), TOO_MANY_REQUESTS(429, "E001", "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."), diff --git a/src/main/java/com/dreamteam/alter/domain/file/type/FileTargetType.java b/src/main/java/com/dreamteam/alter/domain/file/type/FileTargetType.java index 327ea99f..787192ed 100644 --- a/src/main/java/com/dreamteam/alter/domain/file/type/FileTargetType.java +++ b/src/main/java/com/dreamteam/alter/domain/file/type/FileTargetType.java @@ -5,6 +5,7 @@ public enum FileTargetType { USER_CERTIFICATE, POSTING, WORKSPACE, + WORKSPACE_REPRESENTATIVE_IMAGE, // 업장 대표이미지 (최대 5개) WORKSPACE_CERTIFICATE, // 사업자등록증명원 WORKSPACE_OWN_IDENTITY, // 대표자 신분증 사본 WORKSPACE_WARRANT, // 위임 확인서 diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java new file mode 100644 index 00000000..b0fd9d7a --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java @@ -0,0 +1,71 @@ +package com.dreamteam.alter.domain.workspace.entity; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table( + name = "workspace_images", + uniqueConstraints = @UniqueConstraint(name = "uk_workspace_images_workspace_file", columnNames = {"workspace_id", "file_id"}) +) +@Builder(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EntityListeners(AuditingEntityListener.class) +public class WorkspaceImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "workspace_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Workspace workspace; + + @Column(name = "file_id", nullable = false) + private String fileId; + + @Column(name = "sort_order", nullable = false) + private int sortOrder; + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public static WorkspaceImage create(Workspace workspace, String fileId, int sortOrder) { + return WorkspaceImage.builder() + .workspace(workspace) + .fileId(fileId) + .sortOrder(sortOrder) + .build(); + } + + public void updateSortOrder(int sortOrder) { + this.sortOrder = sortOrder; + } +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java new file mode 100644 index 00000000..c32773f6 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java @@ -0,0 +1,67 @@ +package com.dreamteam.alter.domain.workspace.entity; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table( + name = "workspace_request_images", + uniqueConstraints = @UniqueConstraint(name = "uk_workspace_request_images_request_file", columnNames = {"workspace_request_id", "file_id"}) +) +@Builder(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EntityListeners(AuditingEntityListener.class) +public class WorkspaceRequestImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(name = "workspace_request_id", nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private WorkspaceRequest workspaceRequest; + + @Column(name = "file_id", nullable = false) + private String fileId; + + @Column(name = "sort_order", nullable = false) + private int sortOrder; + + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + public static WorkspaceRequestImage create(WorkspaceRequest workspaceRequest, String fileId, int sortOrder) { + return WorkspaceRequestImage.builder() + .workspaceRequest(workspaceRequest) + .fileId(fileId) + .sortOrder(sortOrder) + .build(); + } +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java new file mode 100644 index 00000000..f045c87f --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java @@ -0,0 +1,10 @@ +package com.dreamteam.alter.domain.workspace.port.outbound; + +import java.util.List; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; + +public interface WorkspaceImageQueryRepository { + List findAllByWorkspaceId(Long workspaceId); + long countByWorkspaceId(Long workspaceId); +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageRepository.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageRepository.java new file mode 100644 index 00000000..9ca04617 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageRepository.java @@ -0,0 +1,10 @@ +package com.dreamteam.alter.domain.workspace.port.outbound; + +import java.util.List; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; + +public interface WorkspaceImageRepository { + void saveAll(List workspaceImages); + void deleteAll(List workspaceImages); +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageQueryRepository.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageQueryRepository.java new file mode 100644 index 00000000..85d152a1 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageQueryRepository.java @@ -0,0 +1,9 @@ +package com.dreamteam.alter.domain.workspace.port.outbound; + +import java.util.List; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; + +public interface WorkspaceRequestImageQueryRepository { + List findAllByWorkspaceRequestId(Long workspaceRequestId); +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageRepository.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageRepository.java new file mode 100644 index 00000000..8e1f5c46 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceRequestImageRepository.java @@ -0,0 +1,9 @@ +package com.dreamteam.alter.domain.workspace.port.outbound; + +import java.util.List; + +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; + +public interface WorkspaceRequestImageRepository { + void saveAll(List workspaceRequestImages); +} From f6e8887370bd030db518bb65db47ce0ef76357d3 Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 13:41:48 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=EC=97=85=EC=9E=A5=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=A0=EC=B2=AD/=EC=8A=B9=EC=9D=B8=20=EC=8B=9C?= =?UTF-8?q?=20=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 신청 시 대표이미지 파일을 첨부하고 순서를 보관(requestId 기준). 승인 시 생성된 업장(workspaceId)으로 파일을 재첨부하고 WorkspaceImage로 순서를 그대로 옮김. --- .../dto/CreateWorkspaceRequestDto.java | 6 +++ .../usecase/CreateWorkspaceRequest.java | 22 ++++++++++ .../usecase/UpdateWorkspaceRequestStatus.java | 40 +++++++++++++++++++ 3 files changed, 68 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java index ba6b9a6e..b5773e0d 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java @@ -1,10 +1,12 @@ package com.dreamteam.alter.adapter.inbound.general.workspace.dto; import java.math.BigDecimal; +import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -65,4 +67,8 @@ public class CreateWorkspaceRequestDto { @Schema(description = "위임 확인서 파일 ID", example = "01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e") private String workspaceWarrantFileId; + + @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") + @Schema(description = "업장 대표이미지 파일 ID 목록 (최대 5개, 목록 순서가 노출 순서)", example = "[\"01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e\"]") + private List representativeImageFileIds; } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java index a0e8bf55..f7445c8e 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java @@ -1,17 +1,22 @@ package com.dreamteam.alter.application.workspace.usecase; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; import com.dreamteam.alter.domain.file.type.FileTargetType; import com.dreamteam.alter.domain.user.entity.User; import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequest; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; import com.dreamteam.alter.domain.workspace.port.inbound.CreateWorkspaceRequestUseCase; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageRepository; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestRepository; import lombok.RequiredArgsConstructor; @@ -22,6 +27,7 @@ public class CreateWorkspaceRequest implements CreateWorkspaceRequestUseCase { private final WorkspaceRequestRepository workspaceRequestRepository; + private final WorkspaceRequestImageRepository workspaceRequestImageRepository; private final AttachFilesUseCase attachFiles; @Override @@ -49,5 +55,21 @@ public void execute(User user, CreateWorkspaceRequestDto request) { fileMap.put(request.getWorkspaceWarrantFileId(), FileTargetType.WORKSPACE_WARRANT); } attachFiles.executeMap(fileMap, savedWorkspaceRequestId.toString(), user.getId()); + + List representativeImageFileIds = request.getRepresentativeImageFileIds(); + if (!CollectionUtils.isEmpty(representativeImageFileIds)) { + attachFiles.execute( + representativeImageFileIds, + FileTargetType.WORKSPACE_REPRESENTATIVE_IMAGE, + savedWorkspaceRequestId.toString(), + user.getId() + ); + + List images = new ArrayList<>(); + for (int sortOrder = 0; sortOrder < representativeImageFileIds.size(); sortOrder++) { + images.add(WorkspaceRequestImage.create(workspaceRequest, representativeImageFileIds.get(sortOrder), sortOrder)); + } + workspaceRequestImageRepository.saveAll(images); + } } } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java index b0512cb0..3e1ce168 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java @@ -1,6 +1,11 @@ package com.dreamteam.alter.application.workspace.usecase; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,9 +21,13 @@ import com.dreamteam.alter.domain.user.port.outbound.ManagerUserRepository; import com.dreamteam.alter.domain.user.type.ManagerUserStatus; import com.dreamteam.alter.domain.workspace.entity.Workspace; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequest; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; import com.dreamteam.alter.domain.workspace.port.inbound.UpdateWorkspaceRequestStatusUseCase; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageQueryRepository; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestQueryRepository; import com.dreamteam.alter.domain.workspace.type.WorkspaceRequestStatus; import com.dreamteam.alter.domain.workspace.type.WorkspaceStatus; @@ -34,6 +43,8 @@ public class UpdateWorkspaceRequestStatus implements UpdateWorkspaceRequestStatu private final WorkspaceRepository workspaceRepository; private final ManagerUserQueryRepository managerUserQueryRepository; private final ManagerUserRepository managerUserRepository; + private final WorkspaceImageRepository workspaceImageRepository; + private final WorkspaceRequestImageQueryRepository workspaceRequestImageQueryRepository; private final FileQueryRepository fileQueryRepository; private final FileDeleteService fileDeleteService; @@ -77,9 +88,38 @@ private void approve(WorkspaceRequest workspaceRequest, Long workspaceRequestId) workspaceRepository.save(workspace); + attachRepresentativeImages(workspaceRequestId, workspace); + Optional file = fileQueryRepository.findByTargetTypeAndTargetId( FileTargetType.WORKSPACE_OWN_IDENTITY, String.valueOf(workspaceRequestId)); file.ifPresent(fileDeleteService::delete); } + + private void attachRepresentativeImages(Long workspaceRequestId, Workspace workspace) { + List requestImages = + workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(workspaceRequestId); + + if (requestImages.isEmpty()) { + return; + } + + List fileIds = requestImages.stream() + .map(WorkspaceRequestImage::getFileId) + .toList(); + Map fileMap = fileQueryRepository.findAllByIdIn(fileIds).stream() + .collect(Collectors.toMap(File::getId, Function.identity())); + + List workspaceImages = new ArrayList<>(); + for (WorkspaceRequestImage requestImage : requestImages) { + File file = fileMap.get(requestImage.getFileId()); + if (file == null) { + continue; + } + // 신청(requestId)에 붙어있던 파일을 생성된 업장(workspaceId)으로 재첨부 + file.attach(String.valueOf(workspace.getId())); + workspaceImages.add(WorkspaceImage.create(workspace, requestImage.getFileId(), requestImage.getSortOrder())); + } + workspaceImageRepository.saveAll(workspaceImages); + } } From 1ec2aba577fbfe98799bb011101185c138e43515 Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 13:41:58 +0900 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=97=85=EC=9E=A5=20=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=A1=B0=ED=9A=8C/=EC=88=98=EC=A0=95=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /manager/workspaces/{id}/images 목록 조회, PUT /manager/workspaces/{id}/images 전체 교체(추가/삭제/순서변경 원자 처리). 전달 목록 순서가 노출 순서, 최대 5개 제한. --- .../ManagerWorkspaceImageController.java | 60 ++++++++++ .../ManagerWorkspaceImageControllerSpec.java | 84 ++++++++++++++ .../dto/UpdateWorkspaceImagesRequestDto.java | 22 ++++ .../dto/WorkspaceImageResponseDto.java | 33 ++++++ .../usecase/ManagerGetWorkspaceImages.java | 60 ++++++++++ .../usecase/ManagerUpdateWorkspaceImages.java | 106 ++++++++++++++++++ .../ManagerGetWorkspaceImagesUseCase.java | 10 ++ .../ManagerUpdateWorkspaceImagesUseCase.java | 8 ++ 8 files changed, 383 insertions(+) create mode 100644 src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageController.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java create mode 100644 src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/WorkspaceImageResponseDto.java create mode 100644 src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java create mode 100644 src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java create mode 100644 src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerUpdateWorkspaceImagesUseCase.java diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageController.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageController.java new file mode 100644 index 00000000..65f18376 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageController.java @@ -0,0 +1,60 @@ +package com.dreamteam.alter.adapter.inbound.manager.workspace.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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; + +import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse; +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.WorkspaceImageResponseDto; +import com.dreamteam.alter.application.aop.ManagerActionContext; +import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.workspace.port.inbound.ManagerGetWorkspaceImagesUseCase; +import com.dreamteam.alter.domain.workspace.port.inbound.ManagerUpdateWorkspaceImagesUseCase; + +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/manager/workspaces") +@PreAuthorize("hasAnyRole('MANAGER')") +@RequiredArgsConstructor +@Validated +public class ManagerWorkspaceImageController implements ManagerWorkspaceImageControllerSpec { + + @Resource(name = "managerGetWorkspaceImages") + private final ManagerGetWorkspaceImagesUseCase managerGetWorkspaceImages; + + @Resource(name = "managerUpdateWorkspaceImages") + private final ManagerUpdateWorkspaceImagesUseCase managerUpdateWorkspaceImages; + + @Override + @GetMapping("/{workspaceId}/images") + public ResponseEntity>> getWorkspaceImages( + @PathVariable Long workspaceId + ) { + ManagerActor actor = ManagerActionContext.getInstance().getActor(); + + return ResponseEntity.ok(CommonApiResponse.of(managerGetWorkspaceImages.execute(actor, workspaceId))); + } + + @Override + @PutMapping("/{workspaceId}/images") + public ResponseEntity> updateWorkspaceImages( + @PathVariable Long workspaceId, + @RequestBody @Valid UpdateWorkspaceImagesRequestDto request + ) { + ManagerActor actor = ManagerActionContext.getInstance().getActor(); + managerUpdateWorkspaceImages.execute(actor, workspaceId, request); + return ResponseEntity.ok(CommonApiResponse.empty()); + } +} diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java new file mode 100644 index 00000000..a5f5c2fe --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java @@ -0,0 +1,84 @@ +package com.dreamteam.alter.adapter.inbound.manager.workspace.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import com.dreamteam.alter.adapter.inbound.common.dto.CommonApiResponse; +import com.dreamteam.alter.adapter.inbound.common.dto.ErrorResponse; +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.WorkspaceImageResponseDto; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "MANAGER - 업장 대표이미지 API") +public interface ManagerWorkspaceImageControllerSpec { + + @Operation(summary = "매니저 - 업장 대표이미지 목록 조회", description = "노출 순서(sortOrder) 오름차순으로 조회합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "대표이미지 목록 조회 성공"), + @ApiResponse(responseCode = "400", description = "존재하지 않는 업장", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "존재하지 않는 업장입니다.", + value = "{\"code\" : \"B008\"}" + ), + })), + }) + ResponseEntity>> getWorkspaceImages( + @PathVariable Long workspaceId + ); + + @Operation(summary = "매니저 - 업장 대표이미지 수정 (전체 교체)", description = "전달한 파일 ID 목록으로 대표이미지를 전체 교체합니다. 목록 순서가 노출 순서이며, 추가/삭제/순서변경을 한 번에 처리합니다. 최대 5개.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "대표이미지 수정 성공"), + @ApiResponse(responseCode = "400", description = "400 Error 실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "존재하지 않는 업장입니다.", + value = "{\"code\" : \"B008\"}" + ), + @ExampleObject( + name = "대표이미지는 최대 5개까지 등록할 수 있습니다.", + value = "{\"code\" : \"B026\"}" + ), + @ExampleObject( + name = "존재하지 않는 파일입니다.", + value = "{\"code\" : \"B021\"}" + ), + @ExampleObject( + name = "유효하지 않은 파일입니다.", + value = "{\"code\" : \"B022\"}" + ), + })), + @ApiResponse(responseCode = "409", description = "이미 연결된 파일", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "이미 연결된 파일입니다.", + value = "{\"code\" : \"B025\"}" + ), + })), + }) + ResponseEntity> updateWorkspaceImages( + @PathVariable Long workspaceId, + @RequestBody @Valid UpdateWorkspaceImagesRequestDto request + ); +} diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java new file mode 100644 index 00000000..a3566a03 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java @@ -0,0 +1,22 @@ +package com.dreamteam.alter.adapter.inbound.manager.workspace.dto; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "업장 대표이미지 수정 요청 DTO (전체 교체)") +public class UpdateWorkspaceImagesRequestDto { + + @NotNull + @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") + @Schema(description = "대표이미지로 설정할 파일 ID 목록 (목록 순서가 노출 순서, 비우면 전체 삭제)", example = "[\"01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e\"]") + private List fileIds; +} diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/WorkspaceImageResponseDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/WorkspaceImageResponseDto.java new file mode 100644 index 00000000..99d5b249 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/WorkspaceImageResponseDto.java @@ -0,0 +1,33 @@ +package com.dreamteam.alter.adapter.inbound.manager.workspace.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Builder(access = AccessLevel.PRIVATE) +@Schema(description = "업장 대표이미지 응답 DTO") +public class WorkspaceImageResponseDto { + + @Schema(description = "파일 ID", example = "01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e") + private String fileId; + + @Schema(description = "이미지 URL", example = "https://cdn.example.com/workspace_representative_image/abc.jpg") + private String url; + + @Schema(description = "노출 순서 (0부터 시작)", example = "0") + private int sortOrder; + + public static WorkspaceImageResponseDto of(String fileId, String url, int sortOrder) { + return WorkspaceImageResponseDto.builder() + .fileId(fileId) + .url(url) + .sortOrder(sortOrder) + .build(); + } +} diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java new file mode 100644 index 00000000..2219688d --- /dev/null +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java @@ -0,0 +1,60 @@ +package com.dreamteam.alter.application.workspace.usecase; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.WorkspaceImageResponseDto; +import com.dreamteam.alter.application.file.FileUrlService; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.file.entity.File; +import com.dreamteam.alter.domain.file.port.outbound.FileQueryRepository; +import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.port.inbound.ManagerGetWorkspaceImagesUseCase; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageQueryRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; + +import lombok.RequiredArgsConstructor; + +@Service("managerGetWorkspaceImages") +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ManagerGetWorkspaceImages implements ManagerGetWorkspaceImagesUseCase { + + private final WorkspaceQueryRepository workspaceQueryRepository; + private final WorkspaceImageQueryRepository workspaceImageQueryRepository; + private final FileQueryRepository fileQueryRepository; + private final FileUrlService fileUrlService; + + @Override + public List execute(ManagerActor actor, Long workspaceId) { + if (!workspaceQueryRepository.existsByIdAndManagerUser(workspaceId, actor.getManagerUser())) { + throw new CustomException(ErrorCode.WORKSPACE_NOT_FOUND); + } + + List images = workspaceImageQueryRepository.findAllByWorkspaceId(workspaceId); + if (images.isEmpty()) { + return List.of(); + } + + List fileIds = images.stream() + .map(WorkspaceImage::getFileId) + .toList(); + Map fileMap = fileQueryRepository.findAllByIdIn(fileIds).stream() + .collect(Collectors.toMap(File::getId, Function.identity())); + + return images.stream() + .filter(image -> fileMap.containsKey(image.getFileId())) + .map(image -> { + File file = fileMap.get(image.getFileId()); + return WorkspaceImageResponseDto.of(file.getId(), fileUrlService.resolve(file).getUrl(), image.getSortOrder()); + }) + .toList(); + } +} diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java new file mode 100644 index 00000000..e76931b2 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java @@ -0,0 +1,106 @@ +package com.dreamteam.alter.application.workspace.usecase; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; + +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; +import com.dreamteam.alter.application.file.FileDeleteService; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.file.entity.File; +import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; +import com.dreamteam.alter.domain.file.port.outbound.FileQueryRepository; +import com.dreamteam.alter.domain.file.type.FileTargetType; +import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.workspace.entity.Workspace; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.port.inbound.ManagerUpdateWorkspaceImagesUseCase; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageQueryRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; + +import lombok.RequiredArgsConstructor; + +@Service("managerUpdateWorkspaceImages") +@RequiredArgsConstructor +@Transactional +public class ManagerUpdateWorkspaceImages implements ManagerUpdateWorkspaceImagesUseCase { + + private static final int MAX_IMAGE_COUNT = 5; + + private final WorkspaceQueryRepository workspaceQueryRepository; + private final WorkspaceImageQueryRepository workspaceImageQueryRepository; + private final WorkspaceImageRepository workspaceImageRepository; + private final FileQueryRepository fileQueryRepository; + private final FileDeleteService fileDeleteService; + private final AttachFilesUseCase attachFiles; + + @Override + public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesRequestDto request) { + List newFileIds = CollectionUtils.isEmpty(request.getFileIds()) + ? List.of() + : request.getFileIds(); + + if (newFileIds.size() > MAX_IMAGE_COUNT) { + throw new CustomException(ErrorCode.FILE_LIMIT_EXCEEDED); + } + + Workspace workspace = workspaceQueryRepository.findById(workspaceId) + .orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND)); + if (!Objects.equals(workspace.getManagerUser().getId(), actor.getManagerUser().getId())) { + throw new CustomException(ErrorCode.WORKSPACE_NOT_FOUND); + } + + List existingImages = workspaceImageQueryRepository.findAllByWorkspaceId(workspaceId); + Map existingByFileId = existingImages.stream() + .collect(Collectors.toMap(WorkspaceImage::getFileId, Function.identity())); + + // 1. 빠진 이미지 제거 (파일 soft-delete + WorkspaceImage 삭제) + List removedImages = existingImages.stream() + .filter(image -> !newFileIds.contains(image.getFileId())) + .toList(); + if (!removedImages.isEmpty()) { + List removedFileIds = removedImages.stream() + .map(WorkspaceImage::getFileId) + .toList(); + fileQueryRepository.findAllByIdIn(removedFileIds).forEach(fileDeleteService::delete); + workspaceImageRepository.deleteAll(removedImages); + } + + // 2. 신규 파일 attach (requestId 단계 없이 바로 workspaceId 로 연결) + List addFileIds = newFileIds.stream() + .filter(fileId -> !existingByFileId.containsKey(fileId)) + .toList(); + if (!addFileIds.isEmpty()) { + attachFiles.execute( + addFileIds, + FileTargetType.WORKSPACE_REPRESENTATIVE_IMAGE, + String.valueOf(workspaceId), + actor.getUserId() + ); + } + + // 3. 순서 재계산: 유지 이미지는 sortOrder 갱신, 신규는 생성 + List newImages = new ArrayList<>(); + for (int sortOrder = 0; sortOrder < newFileIds.size(); sortOrder++) { + String fileId = newFileIds.get(sortOrder); + WorkspaceImage existing = existingByFileId.get(fileId); + if (existing != null) { + existing.updateSortOrder(sortOrder); + } else { + newImages.add(WorkspaceImage.create(workspace, fileId, sortOrder)); + } + } + if (!newImages.isEmpty()) { + workspaceImageRepository.saveAll(newImages); + } + } +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java new file mode 100644 index 00000000..7bc2d1db --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java @@ -0,0 +1,10 @@ +package com.dreamteam.alter.domain.workspace.port.inbound; + +import java.util.List; + +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.WorkspaceImageResponseDto; +import com.dreamteam.alter.domain.user.context.ManagerActor; + +public interface ManagerGetWorkspaceImagesUseCase { + List execute(ManagerActor actor, Long workspaceId); +} diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerUpdateWorkspaceImagesUseCase.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerUpdateWorkspaceImagesUseCase.java new file mode 100644 index 00000000..861aa3d4 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerUpdateWorkspaceImagesUseCase.java @@ -0,0 +1,8 @@ +package com.dreamteam.alter.domain.workspace.port.inbound; + +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; +import com.dreamteam.alter.domain.user.context.ManagerActor; + +public interface ManagerUpdateWorkspaceImagesUseCase { + void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesRequestDto request); +} From a6a4dadb3ad34cdd9bc34d333a474931f98c6000 Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 13:42:10 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EC=97=85=EC=9E=A5=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 매니저 업장 상세 조회 시 대표이미지를 노출 순서대로 반환. --- .../workspace/dto/ManagerWorkspaceResponseDto.java | 10 +++++++++- .../workspace/usecase/ManagerGetWorkspace.java | 4 +++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceResponseDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceResponseDto.java index 615b6251..6fd0f799 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceResponseDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceResponseDto.java @@ -11,6 +11,7 @@ import java.math.BigDecimal; import java.time.LocalDateTime; +import java.util.List; @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -69,7 +70,13 @@ public class ManagerWorkspaceResponseDto { @Schema(description = "업장 평판 요약 정보") private SelfReputationSummaryDto reputationSummary; - public static ManagerWorkspaceResponseDto of(ManagerWorkspaceResponse entity) { + @Schema(description = "업장 대표이미지 목록 (노출 순서 오름차순, 최대 5개)") + private List representativeImages; + + public static ManagerWorkspaceResponseDto of( + ManagerWorkspaceResponse entity, + List representativeImages + ) { return ManagerWorkspaceResponseDto.builder() .id(entity.getId()) .businessRegistrationNo(entity.getBusinessRegistrationNo()) @@ -84,6 +91,7 @@ public static ManagerWorkspaceResponseDto of(ManagerWorkspaceResponse entity) { .nextMonthShiftGenDay(entity.getNextMonthShiftGenDay()) .createdAt(entity.getCreatedAt()) .reputationSummary(SelfReputationSummaryDto.from(entity.getReputationSummary())) + .representativeImages(representativeImages) .build(); } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java index 86b9d2ce..16c3a208 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java @@ -5,6 +5,7 @@ import com.dreamteam.alter.common.exception.CustomException; import com.dreamteam.alter.common.exception.ErrorCode; import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.workspace.port.inbound.ManagerGetWorkspaceImagesUseCase; import com.dreamteam.alter.domain.workspace.port.inbound.ManagerGetWorkspaceUseCase; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; import lombok.RequiredArgsConstructor; @@ -18,6 +19,7 @@ public class ManagerGetWorkspace implements ManagerGetWorkspaceUseCase { private final WorkspaceQueryRepository workspaceQueryRepository; + private final ManagerGetWorkspaceImagesUseCase managerGetWorkspaceImages; @Override public ManagerWorkspaceResponseDto execute(ManagerActor actor, Long workspaceId) { @@ -28,7 +30,7 @@ public ManagerWorkspaceResponseDto execute(ManagerActor actor, Long workspaceId) throw new CustomException(ErrorCode.WORKSPACE_NOT_FOUND); } - return ManagerWorkspaceResponseDto.of(workspace); + return ManagerWorkspaceResponseDto.of(workspace, managerGetWorkspaceImages.execute(actor, workspaceId)); } } From 93c034375ae67e157c9c4e0b64715cc719efe3de Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:07:38 +0900 Subject: [PATCH 05/22] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20fileId=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 동일 fileId 중복 전달 시 WorkspaceImage 중복 생성으로 uk_workspace_images_workspace_file 제약 위반 발생. distinct()로 선제거. --- .../workspace/usecase/ManagerUpdateWorkspaceImages.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java index e76931b2..68946acc 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java @@ -47,7 +47,7 @@ public class ManagerUpdateWorkspaceImages implements ManagerUpdateWorkspaceImage public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesRequestDto request) { List newFileIds = CollectionUtils.isEmpty(request.getFileIds()) ? List.of() - : request.getFileIds(); + : request.getFileIds().stream().distinct().toList(); if (newFileIds.size() > MAX_IMAGE_COUNT) { throw new CustomException(ErrorCode.FILE_LIMIT_EXCEEDED); From af40b87af43b646e1d2472505a55628b838a325e Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:07:47 +0900 Subject: [PATCH 06/22] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=85=B8=EC=B6=9C=20=EC=88=9C=EC=84=9C(so?= =?UTF-8?q?rtOrder)=20=EC=97=B0=EC=86=8D=EC=84=B1=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File 누락분을 건너뛸 때 sortOrder에 빈 구간이 생기던 문제 수정. 조회/승인 시 노출 순서를 0부터 연속 재계산. --- .../usecase/ManagerGetWorkspaceImages.java | 18 +++++++++++------- .../usecase/UpdateWorkspaceRequestStatus.java | 4 +++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java index 2219688d..a164644e 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java @@ -1,5 +1,6 @@ package com.dreamteam.alter.application.workspace.usecase; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -49,12 +50,15 @@ public List execute(ManagerActor actor, Long workspac Map fileMap = fileQueryRepository.findAllByIdIn(fileIds).stream() .collect(Collectors.toMap(File::getId, Function.identity())); - return images.stream() - .filter(image -> fileMap.containsKey(image.getFileId())) - .map(image -> { - File file = fileMap.get(image.getFileId()); - return WorkspaceImageResponseDto.of(file.getId(), fileUrlService.resolve(file).getUrl(), image.getSortOrder()); - }) - .toList(); + List result = new ArrayList<>(); + int sortOrder = 0; + for (WorkspaceImage image : images) { + File file = fileMap.get(image.getFileId()); + if (file == null) { + continue; + } + result.add(WorkspaceImageResponseDto.of(file.getId(), fileUrlService.resolve(file).getUrl(), sortOrder++)); + } + return result; } } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java index 3e1ce168..82cffef6 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java @@ -111,6 +111,7 @@ private void attachRepresentativeImages(Long workspaceRequestId, Workspace works .collect(Collectors.toMap(File::getId, Function.identity())); List workspaceImages = new ArrayList<>(); + int sortOrder = 0; for (WorkspaceRequestImage requestImage : requestImages) { File file = fileMap.get(requestImage.getFileId()); if (file == null) { @@ -118,7 +119,8 @@ private void attachRepresentativeImages(Long workspaceRequestId, Workspace works } // 신청(requestId)에 붙어있던 파일을 생성된 업장(workspaceId)으로 재첨부 file.attach(String.valueOf(workspace.getId())); - workspaceImages.add(WorkspaceImage.create(workspace, requestImage.getFileId(), requestImage.getSortOrder())); + // 누락 파일이 있어도 노출 순서가 비연속되지 않도록 재계산 + workspaceImages.add(WorkspaceImage.create(workspace, requestImage.getFileId(), sortOrder++)); } workspaceImageRepository.saveAll(workspaceImages); } From 0f571f3031c17d8cebcbcf1c78c2dbea8b4ef439 Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:07:56 +0900 Subject: [PATCH 07/22] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20fileId=20=EB=AA=A9=EB=A1=9D=EC=97=90=20?= =?UTF-8?q?=EB=B9=88=20=EB=AC=B8=EC=9E=90=EC=97=B4=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit List<@NotBlank String>로 빈/공백 fileId 유입 차단. --- .../general/workspace/dto/CreateWorkspaceRequestDto.java | 2 +- .../manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java index b5773e0d..2d1409c1 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java @@ -70,5 +70,5 @@ public class CreateWorkspaceRequestDto { @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") @Schema(description = "업장 대표이미지 파일 ID 목록 (최대 5개, 목록 순서가 노출 순서)", example = "[\"01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e\"]") - private List representativeImageFileIds; + private List<@NotBlank String> representativeImageFileIds; } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java index a3566a03..16b5192c 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java @@ -3,6 +3,7 @@ import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; @@ -18,5 +19,5 @@ public class UpdateWorkspaceImagesRequestDto { @NotNull @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") @Schema(description = "대표이미지로 설정할 파일 ID 목록 (목록 순서가 노출 순서, 비우면 전체 삭제)", example = "[\"01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e\"]") - private List fileIds; + private List<@NotBlank String> fileIds; } From 87bb2ebeafa7105555b7f1fd341b1a23d3f462b1 Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:08:04 +0900 Subject: [PATCH 08/22] =?UTF-8?q?chore:=20=EB=AF=B8=EC=82=AC=EC=9A=A9=20co?= =?UTF-8?q?untByWorkspaceId=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 호출처 없는 데드코드 제거 (port/impl/jpa). --- .../workspace/persistence/WorkspaceImageJpaRepository.java | 1 - .../persistence/WorkspaceImageQueryRepositoryImpl.java | 5 ----- .../port/outbound/WorkspaceImageQueryRepository.java | 1 - 3 files changed, 7 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java index 1c6e68d7..1436aebb 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageJpaRepository.java @@ -8,5 +8,4 @@ public interface WorkspaceImageJpaRepository extends JpaRepository { List findByWorkspaceIdOrderBySortOrderAsc(Long workspaceId); - long countByWorkspaceId(Long workspaceId); } diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java index 98449bb2..5ac12353 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceImageQueryRepositoryImpl.java @@ -19,9 +19,4 @@ public class WorkspaceImageQueryRepositoryImpl implements WorkspaceImageQueryRep public List findAllByWorkspaceId(Long workspaceId) { return workspaceImageJpaRepository.findByWorkspaceIdOrderBySortOrderAsc(workspaceId); } - - @Override - public long countByWorkspaceId(Long workspaceId) { - return workspaceImageJpaRepository.countByWorkspaceId(workspaceId); - } } diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java index f045c87f..7e5cec79 100644 --- a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceImageQueryRepository.java @@ -6,5 +6,4 @@ public interface WorkspaceImageQueryRepository { List findAllByWorkspaceId(Long workspaceId); - long countByWorkspaceId(Long workspaceId); } From 52d4cf975fbf2febb3521f1d712d0657adf4bdcf Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:14:36 +0900 Subject: [PATCH 09/22] =?UTF-8?q?fix:=20=EC=97=85=EC=9E=A5=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=A0=EC=B2=AD=20=EC=8B=9C=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=20=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20ID=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 리뷰 반영. 매니저 수정 플로우와 동일하게 distinct 처리. --- .../workspace/usecase/CreateWorkspaceRequest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java index f7445c8e..06291333 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java @@ -56,8 +56,10 @@ public void execute(User user, CreateWorkspaceRequestDto request) { } attachFiles.executeMap(fileMap, savedWorkspaceRequestId.toString(), user.getId()); - List representativeImageFileIds = request.getRepresentativeImageFileIds(); - if (!CollectionUtils.isEmpty(representativeImageFileIds)) { + List representativeImageFileIds = CollectionUtils.isEmpty(request.getRepresentativeImageFileIds()) + ? List.of() + : request.getRepresentativeImageFileIds().stream().distinct().toList(); + if (!representativeImageFileIds.isEmpty()) { attachFiles.execute( representativeImageFileIds, FileTargetType.WORKSPACE_REPRESENTATIVE_IMAGE, From 6fcb0b4b18888faf9c9d75916d504244c9b3fddd Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:14:46 +0900 Subject: [PATCH 10/22] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EB=85=B8=EC=B6=9C=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EC=9D=8C=EC=88=98=20=EA=B0=92=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 코드 리뷰 반영. sortOrder는 0 이상이어야 하는 불변식 보장. --- .../alter/domain/workspace/entity/WorkspaceImage.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java index b0fd9d7a..898128fd 100644 --- a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java +++ b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java @@ -2,6 +2,9 @@ import java.time.LocalDateTime; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; + import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -58,6 +61,7 @@ public class WorkspaceImage { private LocalDateTime updatedAt; public static WorkspaceImage create(Workspace workspace, String fileId, int sortOrder) { + validateSortOrder(sortOrder); return WorkspaceImage.builder() .workspace(workspace) .fileId(fileId) @@ -66,6 +70,13 @@ public static WorkspaceImage create(Workspace workspace, String fileId, int sort } public void updateSortOrder(int sortOrder) { + validateSortOrder(sortOrder); this.sortOrder = sortOrder; } + + private static void validateSortOrder(int sortOrder) { + if (sortOrder < 0) { + throw new CustomException(ErrorCode.ILLEGAL_ARGUMENT, "노출 순서는 0 이상이어야 합니다."); + } + } } From c13cfcfc5a35157bd1382a1afbad1f0d464337f2 Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:14:58 +0900 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20=EC=8A=B9=EC=9D=B8=20=EC=8B=9C=20?= =?UTF-8?q?=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=88=84=EB=9D=BD=20=EA=B2=BD=EA=B3=A0=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 리뷰 반영. 신청~승인 사이 파일 삭제로 일부만 마이그레이션되는 상황을 운영팀이 인지하도록 log.warn 추가. --- .../workspace/usecase/UpdateWorkspaceRequestStatus.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java index 82cffef6..e93f173c 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java @@ -33,7 +33,9 @@ import com.dreamteam.alter.domain.workspace.type.WorkspaceStatus; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Service("updateWorkspaceRequestStatus") @RequiredArgsConstructor @Transactional @@ -115,6 +117,9 @@ private void attachRepresentativeImages(Long workspaceRequestId, Workspace works for (WorkspaceRequestImage requestImage : requestImages) { File file = fileMap.get(requestImage.getFileId()); if (file == null) { + // 신청~승인 사이에 파일이 삭제된 경우. 일부만 마이그레이션되므로 운영 추적용 경고 + log.warn("업장 대표이미지 마이그레이션 중 파일 누락. workspaceRequestId={}, workspaceId={}, fileId={}", + workspaceRequestId, workspace.getId(), requestImage.getFileId()); continue; } // 신청(requestId)에 붙어있던 파일을 생성된 업장(workspaceId)으로 재첨부 From 21183f6cfa397b661dc8258f4af31dfaf417635c Mon Sep 17 00:00:00 2001 From: hodoon Date: Mon, 15 Jun 2026 14:32:37 +0900 Subject: [PATCH 12/22] =?UTF-8?q?test:=20=EC=97=85=EC=9E=A5=20=EB=8C=80?= =?UTF-8?q?=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 엔티티 sortOrder 검증, 매니저 조회/수정(전체교체), 신청 시 중복제거, 승인 시 마이그레이션 케이스 커버. --- .../usecase/ApproveWorkspaceRequestTest.java | 153 ++++++++++++ .../usecase/CreateWorkspaceRequestTest.java | 112 +++++++++ .../ManagerGetWorkspaceImagesTest.java | 122 ++++++++++ .../ManagerUpdateWorkspaceImagesTest.java | 218 ++++++++++++++++++ .../workspace/entity/WorkspaceImageTest.java | 57 +++++ 5 files changed, 662 insertions(+) create mode 100644 src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTest.java create mode 100644 src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java create mode 100644 src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTest.java create mode 100644 src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java create mode 100644 src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTest.java diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTest.java new file mode 100644 index 00000000..5ed1c64b --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTest.java @@ -0,0 +1,153 @@ +package com.dreamteam.alter.application.workspace.usecase; + +import com.dreamteam.alter.application.file.FileDeleteService; +import com.dreamteam.alter.domain.file.entity.File; +import com.dreamteam.alter.domain.file.port.outbound.FileQueryRepository; +import com.dreamteam.alter.domain.user.entity.ManagerUser; +import com.dreamteam.alter.domain.user.entity.User; +import com.dreamteam.alter.domain.user.port.outbound.ManagerUserQueryRepository; +import com.dreamteam.alter.domain.user.port.outbound.ManagerUserRepository; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequest; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageQueryRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestQueryRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ApproveWorkspaceRequest 테스트") +class ApproveWorkspaceRequestTest { + + @Mock + private WorkspaceRepository workspaceRepository; + + @Mock + private ManagerUserQueryRepository managerUserQueryRepository; + + @Mock + private ManagerUserRepository managerUserRepository; + + @Mock + private WorkspaceRequestQueryRepository workspaceRequestQueryRepository; + + @Mock + private WorkspaceImageRepository workspaceImageRepository; + + @Mock + private WorkspaceRequestImageQueryRepository workspaceRequestImageQueryRepository; + + @Mock + private FileQueryRepository fileQueryRepository; + + @Mock + private FileDeleteService fileDeleteService; + + @InjectMocks + private ApproveWorkspaceRequest approveWorkspaceRequest; + + @Captor + private ArgumentCaptor> imagesCaptor; + + private WorkspaceRequest mockApprovedRequest() { + WorkspaceRequest request = mock(WorkspaceRequest.class); + User user = mock(User.class); + given(user.getId()).willReturn(1L); + given(request.getUser()).willReturn(user); + given(workspaceRequestQueryRepository.findByIdWithUser(10L)).willReturn(Optional.of(request)); + given(managerUserQueryRepository.findByUserId(1L)).willReturn(Optional.of(mock(ManagerUser.class))); + return request; + } + + private WorkspaceRequestImage requestImage(String fileId) { + WorkspaceRequestImage image = mock(WorkspaceRequestImage.class); + given(image.getFileId()).willReturn(fileId); + return image; + } + + @Test + @DisplayName("대표이미지가 없으면 WorkspaceImage 저장을 호출하지 않는다") + void execute_대표이미지없음() { + // given + mockApprovedRequest(); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)).willReturn(List.of()); + given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); + + // when + approveWorkspaceRequest.execute(10L); + + // then + then(workspaceImageRepository).should(never()).saveAll(any()); + } + + @Test + @DisplayName("신청 단계 파일을 업장으로 재첨부하고 순서대로 WorkspaceImage를 생성한다") + void execute_대표이미지마이그레이션() { + // given + mockApprovedRequest(); + WorkspaceRequestImage riA = requestImage("fileA"); + WorkspaceRequestImage riB = requestImage("fileB"); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)) + .willReturn(List.of(riA, riB)); + + File fileA = mock(File.class); + File fileB = mock(File.class); + given(fileA.getId()).willReturn("fileA"); + given(fileB.getId()).willReturn("fileB"); + given(fileQueryRepository.findAllByIdIn(List.of("fileA", "fileB"))).willReturn(List.of(fileA, fileB)); + given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); + + // when + approveWorkspaceRequest.execute(10L); + + // then + then(fileA).should().attach(anyString()); + then(fileB).should().attach(anyString()); + then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("fileA", "fileB"); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getSortOrder).containsExactly(0, 1); + } + + @Test + @DisplayName("신청~승인 사이 삭제된 파일은 건너뛰고 존재하는 파일만 마이그레이션한다") + void execute_일부파일누락() { + // given + mockApprovedRequest(); + WorkspaceRequestImage riA = requestImage("fileA"); + WorkspaceRequestImage riB = requestImage("fileB"); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)) + .willReturn(List.of(riA, riB)); + + File fileA = mock(File.class); + given(fileA.getId()).willReturn("fileA"); + given(fileQueryRepository.findAllByIdIn(List.of("fileA", "fileB"))).willReturn(List.of(fileA)); + given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); + + // when + approveWorkspaceRequest.execute(10L); + + // then + then(fileA).should().attach(anyString()); + then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("fileA"); + } +} diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java new file mode 100644 index 00000000..e9e4e518 --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java @@ -0,0 +1,112 @@ +package com.dreamteam.alter.application.workspace.usecase; + +import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; +import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; +import com.dreamteam.alter.domain.file.type.FileTargetType; +import com.dreamteam.alter.domain.user.entity.User; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +@DisplayName("CreateWorkspaceRequest 테스트") +class CreateWorkspaceRequestTest { + + @Mock + private WorkspaceRequestRepository workspaceRequestRepository; + + @Mock + private WorkspaceRequestImageRepository workspaceRequestImageRepository; + + @Mock + private AttachFilesUseCase attachFiles; + + @InjectMocks + private CreateWorkspaceRequest createWorkspaceRequest; + + @Captor + private ArgumentCaptor> fileIdsCaptor; + + @Captor + private ArgumentCaptor> imagesCaptor; + + private CreateWorkspaceRequestDto baseRequest() { + CreateWorkspaceRequestDto dto = new CreateWorkspaceRequestDto(); + dto.setBizName("세븐일레븐"); + dto.setBrn("123-45-12345"); + dto.setAddress("서울특별시 구로구 고척동 123"); + dto.setProvince("서울특별시"); + dto.setDistrict("구로구"); + dto.setTown("고척동"); + dto.setType("음식점"); + dto.setContact("02-1234-5678"); + dto.setWorkspaceCertFileId("cert-file"); + dto.setWorkspaceOwnIdentityFileId("identity-file"); + return dto; + } + + @Test + @DisplayName("대표이미지 없이도 정상 처리되며 대표이미지 저장은 호출되지 않는다") + void execute_대표이미지없음() { + // given + User user = mock(User.class); + given(user.getId()).willReturn(100L); + given(workspaceRequestRepository.save(any())).willReturn(1L); + + // when + createWorkspaceRequest.execute(user, baseRequest()); + + // then + then(attachFiles).should().executeMap(any(), eq("1"), eq(100L)); + then(attachFiles).should(never()).execute(any(), any(), any(), any()); + then(workspaceRequestImageRepository).should(never()).saveAll(any()); + } + + @Test + @DisplayName("중복 대표이미지 ID는 제거되어 순서대로 저장된다") + void execute_대표이미지중복제거() { + // given + User user = mock(User.class); + given(user.getId()).willReturn(100L); + given(workspaceRequestRepository.save(any())).willReturn(1L); + + CreateWorkspaceRequestDto dto = baseRequest(); + dto.setRepresentativeImageFileIds(List.of("img1", "img1", "img2")); + + // when + createWorkspaceRequest.execute(user, dto); + + // then + then(attachFiles).should().execute( + fileIdsCaptor.capture(), + eq(FileTargetType.WORKSPACE_REPRESENTATIVE_IMAGE), + eq("1"), + eq(100L) + ); + assertThat(fileIdsCaptor.getValue()).containsExactly("img1", "img2"); + + then(workspaceRequestImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceRequestImage::getFileId) + .containsExactly("img1", "img2"); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceRequestImage::getSortOrder) + .containsExactly(0, 1); + } +} diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTest.java new file mode 100644 index 00000000..362d94d5 --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTest.java @@ -0,0 +1,122 @@ +package com.dreamteam.alter.application.workspace.usecase; + +import com.dreamteam.alter.adapter.inbound.common.dto.FileResponseDto; +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.WorkspaceImageResponseDto; +import com.dreamteam.alter.application.file.FileUrlService; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.file.entity.File; +import com.dreamteam.alter.domain.file.port.outbound.FileQueryRepository; +import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.user.entity.ManagerUser; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageQueryRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ManagerGetWorkspaceImages 테스트") +class ManagerGetWorkspaceImagesTest { + + @Mock + private WorkspaceQueryRepository workspaceQueryRepository; + + @Mock + private WorkspaceImageQueryRepository workspaceImageQueryRepository; + + @Mock + private FileQueryRepository fileQueryRepository; + + @Mock + private FileUrlService fileUrlService; + + @InjectMocks + private ManagerGetWorkspaceImages managerGetWorkspaceImages; + + @Test + @DisplayName("관리 업장이 아니면 WORKSPACE_NOT_FOUND 예외가 발생한다") + void execute_관리업장아님_예외() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + given(actor.getManagerUser()).willReturn(managerUser); + given(workspaceQueryRepository.existsByIdAndManagerUser(1L, managerUser)).willReturn(false); + + // when & then + assertThatThrownBy(() -> managerGetWorkspaceImages.execute(actor, 1L)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.WORKSPACE_NOT_FOUND)); + } + + @Test + @DisplayName("대표이미지가 없으면 빈 목록을 반환한다") + void execute_이미지없음_빈목록() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + given(actor.getManagerUser()).willReturn(managerUser); + given(workspaceQueryRepository.existsByIdAndManagerUser(1L, managerUser)).willReturn(true); + given(workspaceImageQueryRepository.findAllByWorkspaceId(1L)).willReturn(List.of()); + + // when + List result = managerGetWorkspaceImages.execute(actor, 1L); + + // then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("이미지 순서대로 매핑하며 파일이 없는 항목은 건너뛰고 노출 순서를 0부터 재계산한다") + void execute_정상_순서매핑() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + given(actor.getManagerUser()).willReturn(managerUser); + given(workspaceQueryRepository.existsByIdAndManagerUser(1L, managerUser)).willReturn(true); + + WorkspaceImage imageA = mock(WorkspaceImage.class); + WorkspaceImage imageMissing = mock(WorkspaceImage.class); + WorkspaceImage imageB = mock(WorkspaceImage.class); + given(imageA.getFileId()).willReturn("file-A"); + given(imageMissing.getFileId()).willReturn("file-missing"); + given(imageB.getFileId()).willReturn("file-B"); + given(workspaceImageQueryRepository.findAllByWorkspaceId(1L)) + .willReturn(List.of(imageA, imageMissing, imageB)); + + File fileA = mock(File.class); + File fileB = mock(File.class); + given(fileA.getId()).willReturn("file-A"); + given(fileB.getId()).willReturn("file-B"); + given(fileQueryRepository.findAllByIdIn(List.of("file-A", "file-missing", "file-B"))) + .willReturn(List.of(fileA, fileB)); + + FileResponseDto dtoA = FileResponseDto.of(fileA, "https://url/A"); + FileResponseDto dtoB = FileResponseDto.of(fileB, "https://url/B"); + given(fileUrlService.resolve(fileA)).willReturn(dtoA); + given(fileUrlService.resolve(fileB)).willReturn(dtoB); + + // when + List result = managerGetWorkspaceImages.execute(actor, 1L); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting(WorkspaceImageResponseDto::getFileId) + .containsExactly("file-A", "file-B"); + assertThat(result).extracting(WorkspaceImageResponseDto::getUrl) + .containsExactly("https://url/A", "https://url/B"); + assertThat(result).extracting(WorkspaceImageResponseDto::getSortOrder) + .containsExactly(0, 1); + } +} diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java new file mode 100644 index 00000000..ee50d49b --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java @@ -0,0 +1,218 @@ +package com.dreamteam.alter.application.workspace.usecase; + +import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; +import com.dreamteam.alter.application.file.FileDeleteService; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.file.entity.File; +import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; +import com.dreamteam.alter.domain.file.port.outbound.FileQueryRepository; +import com.dreamteam.alter.domain.file.type.FileTargetType; +import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.user.entity.ManagerUser; +import com.dreamteam.alter.domain.workspace.entity.Workspace; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageQueryRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ManagerUpdateWorkspaceImages 테스트") +class ManagerUpdateWorkspaceImagesTest { + + @Mock + private WorkspaceQueryRepository workspaceQueryRepository; + + @Mock + private WorkspaceImageQueryRepository workspaceImageQueryRepository; + + @Mock + private WorkspaceImageRepository workspaceImageRepository; + + @Mock + private FileQueryRepository fileQueryRepository; + + @Mock + private FileDeleteService fileDeleteService; + + @Mock + private AttachFilesUseCase attachFiles; + + @InjectMocks + private ManagerUpdateWorkspaceImages managerUpdateWorkspaceImages; + + @Captor + private ArgumentCaptor> imagesCaptor; + + @Captor + private ArgumentCaptor> fileIdsCaptor; + + private UpdateWorkspaceImagesRequestDto request(String... fileIds) { + return new UpdateWorkspaceImagesRequestDto(List.of(fileIds)); + } + + @Test + @DisplayName("5개를 초과하면 FILE_LIMIT_EXCEEDED 예외가 발생한다") + void execute_5개초과_예외() { + // given + ManagerActor actor = mock(ManagerActor.class); + + // when & then + assertThatThrownBy(() -> managerUpdateWorkspaceImages.execute( + actor, 1L, request("f1", "f2", "f3", "f4", "f5", "f6"))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.FILE_LIMIT_EXCEEDED)); + then(workspaceImageRepository).should(never()).saveAll(any()); + } + + @Test + @DisplayName("존재하지 않는 업장이면 WORKSPACE_NOT_FOUND 예외가 발생한다") + void execute_업장없음_예외() { + // given + ManagerActor actor = mock(ManagerActor.class); + given(workspaceQueryRepository.findById(1L)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> managerUpdateWorkspaceImages.execute(actor, 1L, request("f1"))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.WORKSPACE_NOT_FOUND)); + } + + @Test + @DisplayName("다른 매니저의 업장이면 WORKSPACE_NOT_FOUND 예외가 발생한다") + void execute_다른매니저_예외() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser actorManager = mock(ManagerUser.class); + given(actor.getManagerUser()).willReturn(actorManager); + given(actorManager.getId()).willReturn(2L); + + Workspace workspace = mock(Workspace.class); + ManagerUser owner = mock(ManagerUser.class); + given(workspace.getManagerUser()).willReturn(owner); + given(owner.getId()).willReturn(1L); + given(workspaceQueryRepository.findById(1L)).willReturn(Optional.of(workspace)); + + // when & then + assertThatThrownBy(() -> managerUpdateWorkspaceImages.execute(actor, 1L, request("f1"))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.WORKSPACE_NOT_FOUND)); + then(attachFiles).should(never()).execute(any(), any(), any(), any()); + } + + @Test + @DisplayName("신규 파일은 attach 후 순서대로 WorkspaceImage를 생성한다") + void execute_신규추가() { + // given + ManagerActor actor = mockOwnerActor(1L); + given(actor.getUserId()).willReturn(100L); + + Workspace workspace = mockOwnerWorkspace(1L); + given(workspaceQueryRepository.findById(1L)).willReturn(Optional.of(workspace)); + given(workspaceImageQueryRepository.findAllByWorkspaceId(1L)).willReturn(List.of()); + + // when + managerUpdateWorkspaceImages.execute(actor, 1L, request("f1", "f2")); + + // then + then(attachFiles).should().execute( + fileIdsCaptor.capture(), + eq(FileTargetType.WORKSPACE_REPRESENTATIVE_IMAGE), + eq("1"), + eq(100L) + ); + assertThat(fileIdsCaptor.getValue()).containsExactly("f1", "f2"); + + then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).hasSize(2); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("f1", "f2"); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getSortOrder).containsExactly(0, 1); + + then(workspaceImageRepository).should(never()).deleteAll(any()); + then(fileDeleteService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("빠진 이미지는 파일 soft-delete 후 제거하고, 유지 이미지는 순서를 갱신한다") + void execute_삭제및순서변경() { + // given + ManagerActor actor = mockOwnerActor(1L); + Workspace workspace = mockOwnerWorkspace(1L); + given(workspaceQueryRepository.findById(1L)).willReturn(Optional.of(workspace)); + + WorkspaceImage imageA = mock(WorkspaceImage.class); // 삭제 대상 + WorkspaceImage imageB = mock(WorkspaceImage.class); // 유지 + 순서 변경 + given(imageA.getFileId()).willReturn("fileA"); + given(imageB.getFileId()).willReturn("fileB"); + given(workspaceImageQueryRepository.findAllByWorkspaceId(1L)).willReturn(List.of(imageA, imageB)); + + File fileA = mock(File.class); + given(fileQueryRepository.findAllByIdIn(List.of("fileA"))).willReturn(List.of(fileA)); + + // when: fileB만 유지 (fileA 삭제), fileB는 0번으로 이동 + managerUpdateWorkspaceImages.execute(actor, 1L, request("fileB")); + + // then + then(fileDeleteService).should().delete(fileA); + then(workspaceImageRepository).should().deleteAll(List.of(imageA)); + then(imageB).should().updateSortOrder(0); + then(attachFiles).should(never()).execute(any(), any(), any(), any()); + then(workspaceImageRepository).should(never()).saveAll(any()); + } + + @Test + @DisplayName("중복 파일 ID는 제거되어 한 번만 반영된다") + void execute_중복제거() { + // given + ManagerActor actor = mockOwnerActor(1L); + given(actor.getUserId()).willReturn(100L); + Workspace workspace = mockOwnerWorkspace(1L); + given(workspaceQueryRepository.findById(1L)).willReturn(Optional.of(workspace)); + given(workspaceImageQueryRepository.findAllByWorkspaceId(1L)).willReturn(List.of()); + + // when + managerUpdateWorkspaceImages.execute(actor, 1L, request("f1", "f1", "f2")); + + // then + then(attachFiles).should().execute(fileIdsCaptor.capture(), any(), any(), any()); + assertThat(fileIdsCaptor.getValue()).containsExactly("f1", "f2"); + then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).hasSize(2); + } + + private ManagerActor mockOwnerActor(Long managerId) { + ManagerActor actor = mock(ManagerActor.class); + ManagerUser actorManager = mock(ManagerUser.class); + given(actor.getManagerUser()).willReturn(actorManager); + given(actorManager.getId()).willReturn(managerId); + return actor; + } + + private Workspace mockOwnerWorkspace(Long managerId) { + Workspace workspace = mock(Workspace.class); + ManagerUser owner = mock(ManagerUser.class); + given(workspace.getManagerUser()).willReturn(owner); + given(owner.getId()).willReturn(managerId); + return workspace; + } +} diff --git a/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTest.java b/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTest.java new file mode 100644 index 00000000..f9054fff --- /dev/null +++ b/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTest.java @@ -0,0 +1,57 @@ +package com.dreamteam.alter.domain.workspace.entity; + +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("WorkspaceImage 엔티티 테스트") +class WorkspaceImageTest { + + @Test + @DisplayName("정상 sortOrder로 생성된다") + void create_정상() { + // when + WorkspaceImage image = WorkspaceImage.create(null, "file-1", 0); + + // then + assertThat(image.getFileId()).isEqualTo("file-1"); + assertThat(image.getSortOrder()).isZero(); + } + + @Test + @DisplayName("음수 sortOrder로 생성하면 ILLEGAL_ARGUMENT 예외가 발생한다") + void create_음수sortOrder_예외() { + // when & then + assertThatThrownBy(() -> WorkspaceImage.create(null, "file-1", -1)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.ILLEGAL_ARGUMENT)); + } + + @Test + @DisplayName("updateSortOrder는 0 이상 값을 허용한다") + void updateSortOrder_정상() { + // given + WorkspaceImage image = WorkspaceImage.create(null, "file-1", 0); + + // when & then + assertThatCode(() -> image.updateSortOrder(3)).doesNotThrowAnyException(); + assertThat(image.getSortOrder()).isEqualTo(3); + } + + @Test + @DisplayName("updateSortOrder에 음수를 주면 ILLEGAL_ARGUMENT 예외가 발생한다") + void updateSortOrder_음수_예외() { + // given + WorkspaceImage image = WorkspaceImage.create(null, "file-1", 0); + + // when & then + assertThatThrownBy(() -> image.updateSortOrder(-1)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.ILLEGAL_ARGUMENT)); + } +} From 4a25399812237faabe2b12ed7752686f442d95be Mon Sep 17 00:00:00 2001 From: hodoon Date: Tue, 16 Jun 2026 15:53:43 +0900 Subject: [PATCH 13/22] =?UTF-8?q?refactor:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=94=ED=8B=B0=ED=8B=B0=20UNIQUE=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?(DDL=20=EA=B4=80=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../alter/domain/workspace/entity/WorkspaceImage.java | 6 +----- .../domain/workspace/entity/WorkspaceRequestImage.java | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java index 898128fd..60970ad7 100644 --- a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java +++ b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImage.java @@ -19,7 +19,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -28,10 +27,7 @@ @Entity @Getter -@Table( - name = "workspace_images", - uniqueConstraints = @UniqueConstraint(name = "uk_workspace_images_workspace_file", columnNames = {"workspace_id", "file_id"}) -) +@Table(name = "workspace_images") @Builder(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java index c32773f6..c740566b 100644 --- a/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java +++ b/src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestImage.java @@ -16,7 +16,6 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -25,10 +24,7 @@ @Entity @Getter -@Table( - name = "workspace_request_images", - uniqueConstraints = @UniqueConstraint(name = "uk_workspace_request_images_request_file", columnNames = {"workspace_request_id", "file_id"}) -) +@Table(name = "workspace_request_images") @Builder(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) From aee30a589c5ea74f8f40867bf7c4aa7fc8d186e8 Mon Sep 17 00:00:00 2001 From: hodoon Date: Tue, 16 Jun 2026 15:54:46 +0900 Subject: [PATCH 14/22] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=88=98=EC=A0=95=20=ED=8A=B8=EB=9E=9C?= =?UTF-8?q?=EC=9E=AD=EC=85=98=20=EC=A0=95=ED=95=A9=EC=84=B1=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20ErrorCode=20=EC=9E=AC=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - S3 물리 삭제는 롤백 불가하므로 신규 파일 attach/검증 성공 이후 마지막 단계로 이동 - 신규 ErrorCode(FILE_LIMIT_EXCEEDED) 제거 후 기존 ILLEGAL_ARGUMENT 재활용 --- .../usecase/ManagerUpdateWorkspaceImages.java | 32 ++++++++++--------- .../alter/common/exception/ErrorCode.java | 1 - 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java index 68946acc..d1544ff4 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java @@ -50,7 +50,7 @@ public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesR : request.getFileIds().stream().distinct().toList(); if (newFileIds.size() > MAX_IMAGE_COUNT) { - throw new CustomException(ErrorCode.FILE_LIMIT_EXCEEDED); + throw new CustomException(ErrorCode.ILLEGAL_ARGUMENT, "대표이미지는 최대 " + MAX_IMAGE_COUNT + "개까지 등록할 수 있습니다."); } Workspace workspace = workspaceQueryRepository.findById(workspaceId) @@ -63,19 +63,8 @@ public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesR Map existingByFileId = existingImages.stream() .collect(Collectors.toMap(WorkspaceImage::getFileId, Function.identity())); - // 1. 빠진 이미지 제거 (파일 soft-delete + WorkspaceImage 삭제) - List removedImages = existingImages.stream() - .filter(image -> !newFileIds.contains(image.getFileId())) - .toList(); - if (!removedImages.isEmpty()) { - List removedFileIds = removedImages.stream() - .map(WorkspaceImage::getFileId) - .toList(); - fileQueryRepository.findAllByIdIn(removedFileIds).forEach(fileDeleteService::delete); - workspaceImageRepository.deleteAll(removedImages); - } - - // 2. 신규 파일 attach (requestId 단계 없이 바로 workspaceId 로 연결) + // 1. 신규 파일 attach (requestId 단계 없이 바로 workspaceId 로 연결) + // 검증 실패(FILE_NOT_FOUND 등)를 S3 물리 삭제 이전에 조기에 걸러내 정합성 보장 List addFileIds = newFileIds.stream() .filter(fileId -> !existingByFileId.containsKey(fileId)) .toList(); @@ -88,7 +77,7 @@ public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesR ); } - // 3. 순서 재계산: 유지 이미지는 sortOrder 갱신, 신규는 생성 + // 2. 순서 재계산: 유지 이미지는 sortOrder 갱신, 신규는 생성 List newImages = new ArrayList<>(); for (int sortOrder = 0; sortOrder < newFileIds.size(); sortOrder++) { String fileId = newFileIds.get(sortOrder); @@ -102,5 +91,18 @@ public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesR if (!newImages.isEmpty()) { workspaceImageRepository.saveAll(newImages); } + + // 3. 빠진 이미지 제거 (파일 soft-delete + WorkspaceImage 삭제) + // S3 물리 삭제는 롤백 불가하므로 attach/검증이 모두 성공한 마지막 단계에서 수행 + List removedImages = existingImages.stream() + .filter(image -> !newFileIds.contains(image.getFileId())) + .toList(); + if (!removedImages.isEmpty()) { + List removedFileIds = removedImages.stream() + .map(WorkspaceImage::getFileId) + .toList(); + fileQueryRepository.findAllByIdIn(removedFileIds).forEach(fileDeleteService::delete); + workspaceImageRepository.deleteAll(removedImages); + } } } diff --git a/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java b/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java index 1cc23a39..442b9c8d 100644 --- a/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java +++ b/src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java @@ -51,7 +51,6 @@ public enum ErrorCode { INVALID_FILE_TYPE(400, "B023", "허용되지 않는 파일 형식입니다."), FILE_SIZE_EXCEEDED(400, "B024", "파일 크기가 제한을 초과합니다."), FILE_ALREADY_ATTACHED(409, "B025", "이미 연결된 파일입니다."), - FILE_LIMIT_EXCEEDED(400, "B026", "대표이미지는 최대 5개까지 등록할 수 있습니다."), TOO_MANY_REQUESTS(429, "E001", "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."), From 78e88419b30df7033eb00b659403c2895d6048c6 Mon Sep 17 00:00:00 2001 From: hodoon Date: Tue, 16 Jun 2026 15:54:57 +0900 Subject: [PATCH 15/22] =?UTF-8?q?fix:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=88=84=EB=9D=BD=20=EA=B2=BD=EA=B3=A0=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../workspace/usecase/ManagerGetWorkspaceImages.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java index a164644e..85bc4f08 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java @@ -22,7 +22,9 @@ import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @Service("managerGetWorkspaceImages") @RequiredArgsConstructor @Transactional(readOnly = true) @@ -55,6 +57,8 @@ public List execute(ManagerActor actor, Long workspac for (WorkspaceImage image : images) { File file = fileMap.get(image.getFileId()); if (file == null) { + // WorkspaceImage가 참조하는 파일이 조회되지 않음. 데이터 정합성 이상 신호이므로 추적용 경고 + log.warn("업장 대표이미지 조회 중 파일 누락. workspaceId={}, fileId={}", workspaceId, image.getFileId()); continue; } result.add(WorkspaceImageResponseDto.of(file.getId(), fileUrlService.resolve(file).getUrl(), sortOrder++)); From d7086693402e1aa8a076ea747bf9b1bc0be54a5d Mon Sep 17 00:00:00 2001 From: hodoon Date: Tue, 16 Jun 2026 16:13:09 +0900 Subject: [PATCH 16/22] =?UTF-8?q?refactor:=20=EB=8C=80=ED=91=9C=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=9A=94=EC=B2=AD=EC=9D=84=20{fileId,=20s?= =?UTF-8?q?ortOrder}=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - List → Set로 변경, fileId 기준 중복 자동 제거 - 노출 순서를 목록 인덱스 대신 sortOrder 필드로 명시 (미지정 시 후순위) - 수정 시 S3 물리 삭제를 attach/검증 성공 이후 마지막 단계로 이동 (트랜잭션 정합성) - 신규 ErrorCode(FILE_LIMIT_EXCEEDED) 제거 후 기존 ILLEGAL_ARGUMENT 재활용 --- .../common/dto/WorkspaceImageRequestDto.java | 43 +++++++++++++++++++ .../dto/CreateWorkspaceRequestDto.java | 9 ++-- .../ManagerWorkspaceImageControllerSpec.java | 4 +- .../dto/UpdateWorkspaceImagesRequestDto.java | 10 +++-- .../usecase/CreateWorkspaceRequest.java | 7 ++- .../usecase/ManagerUpdateWorkspaceImages.java | 6 +-- .../usecase/CreateWorkspaceRequestTest.java | 9 +++- .../ManagerUpdateWorkspaceImagesTest.java | 13 ++++-- 8 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 src/main/java/com/dreamteam/alter/adapter/inbound/common/dto/WorkspaceImageRequestDto.java diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/common/dto/WorkspaceImageRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/common/dto/WorkspaceImageRequestDto.java new file mode 100644 index 00000000..90cd2843 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/common/dto/WorkspaceImageRequestDto.java @@ -0,0 +1,43 @@ +package com.dreamteam.alter.adapter.inbound.common.dto; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = "fileId") +@Schema(description = "업장 대표이미지 요청 항목 DTO") +public class WorkspaceImageRequestDto { + + @NotBlank + @Schema(description = "대표이미지 파일 ID", example = "01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e") + private String fileId; + + @Schema(description = "노출 순서 (작을수록 먼저, 미지정 시 마지막)", example = "0") + private Integer sortOrder; + + /** + * sortOrder 오름차순(미지정은 후순위)으로 정렬한 파일 ID 목록을 반환한다. + * fileId 기준 equals/hashCode 로 Set 단계에서 이미 중복이 제거된다. + */ + public static List toOrderedFileIds(Collection images) { + if (images == null || images.isEmpty()) { + return List.of(); + } + return images.stream() + .sorted(Comparator + .comparing(WorkspaceImageRequestDto::getSortOrder, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(WorkspaceImageRequestDto::getFileId)) + .map(WorkspaceImageRequestDto::getFileId) + .toList(); + } +} diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java index 2d1409c1..95ef2885 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java @@ -1,9 +1,12 @@ package com.dreamteam.alter.adapter.inbound.general.workspace.dto; import java.math.BigDecimal; -import java.util.List; +import java.util.Set; + +import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -69,6 +72,6 @@ public class CreateWorkspaceRequestDto { private String workspaceWarrantFileId; @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") - @Schema(description = "업장 대표이미지 파일 ID 목록 (최대 5개, 목록 순서가 노출 순서)", example = "[\"01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e\"]") - private List<@NotBlank String> representativeImageFileIds; + @Schema(description = "업장 대표이미지 목록 (최대 5개, fileId 중복 자동 제거, sortOrder 오름차순으로 노출)") + private Set<@Valid WorkspaceImageRequestDto> representativeImages; } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java index a5f5c2fe..fed0b7ee 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceImageControllerSpec.java @@ -41,7 +41,7 @@ ResponseEntity>> getWorkspaceI @PathVariable Long workspaceId ); - @Operation(summary = "매니저 - 업장 대표이미지 수정 (전체 교체)", description = "전달한 파일 ID 목록으로 대표이미지를 전체 교체합니다. 목록 순서가 노출 순서이며, 추가/삭제/순서변경을 한 번에 처리합니다. 최대 5개.") + @Operation(summary = "매니저 - 업장 대표이미지 수정 (전체 교체)", description = "전달한 대표이미지 목록으로 전체 교체합니다. sortOrder 오름차순이 노출 순서이며(미지정 시 후순위), 추가/삭제/순서변경을 한 번에 처리합니다. 최대 5개.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "대표이미지 수정 성공"), @ApiResponse(responseCode = "400", description = "400 Error 실패 케이스", @@ -55,7 +55,7 @@ ResponseEntity>> getWorkspaceI ), @ExampleObject( name = "대표이미지는 최대 5개까지 등록할 수 있습니다.", - value = "{\"code\" : \"B026\"}" + value = "{\"code\" : \"B001\"}" ), @ExampleObject( name = "존재하지 않는 파일입니다.", diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java index 16b5192c..d07b8fa6 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java @@ -1,9 +1,11 @@ package com.dreamteam.alter.adapter.inbound.manager.workspace.dto; -import java.util.List; +import java.util.Set; + +import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; @@ -18,6 +20,6 @@ public class UpdateWorkspaceImagesRequestDto { @NotNull @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") - @Schema(description = "대표이미지로 설정할 파일 ID 목록 (목록 순서가 노출 순서, 비우면 전체 삭제)", example = "[\"01959b4e-4e5f-7c3a-8d9e-0f1a2b3c4d5e\"]") - private List<@NotBlank String> fileIds; + @Schema(description = "대표이미지 목록 (fileId 중복 자동 제거, sortOrder 오름차순으로 노출, 비우면 전체 삭제)") + private Set<@Valid WorkspaceImageRequestDto> images; } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java index 06291333..f19a166b 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java @@ -7,8 +7,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; +import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; import com.dreamteam.alter.domain.file.type.FileTargetType; @@ -56,9 +56,8 @@ public void execute(User user, CreateWorkspaceRequestDto request) { } attachFiles.executeMap(fileMap, savedWorkspaceRequestId.toString(), user.getId()); - List representativeImageFileIds = CollectionUtils.isEmpty(request.getRepresentativeImageFileIds()) - ? List.of() - : request.getRepresentativeImageFileIds().stream().distinct().toList(); + List representativeImageFileIds = + WorkspaceImageRequestDto.toOrderedFileIds(request.getRepresentativeImages()); if (!representativeImageFileIds.isEmpty()) { attachFiles.execute( representativeImageFileIds, diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java index d1544ff4..c6bc623f 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java @@ -9,8 +9,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.CollectionUtils; +import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; import com.dreamteam.alter.application.file.FileDeleteService; import com.dreamteam.alter.common.exception.CustomException; @@ -45,9 +45,7 @@ public class ManagerUpdateWorkspaceImages implements ManagerUpdateWorkspaceImage @Override public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesRequestDto request) { - List newFileIds = CollectionUtils.isEmpty(request.getFileIds()) - ? List.of() - : request.getFileIds().stream().distinct().toList(); + List newFileIds = WorkspaceImageRequestDto.toOrderedFileIds(request.getImages()); if (newFileIds.size() > MAX_IMAGE_COUNT) { throw new CustomException(ErrorCode.ILLEGAL_ARGUMENT, "대표이미지는 최대 " + MAX_IMAGE_COUNT + "개까지 등록할 수 있습니다."); diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java index e9e4e518..6ec6502a 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java @@ -1,5 +1,6 @@ package com.dreamteam.alter.application.workspace.usecase; +import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; import com.dreamteam.alter.domain.file.type.FileTargetType; @@ -16,7 +17,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -89,7 +92,11 @@ private CreateWorkspaceRequestDto baseRequest() { given(workspaceRequestRepository.save(any())).willReturn(1L); CreateWorkspaceRequestDto dto = baseRequest(); - dto.setRepresentativeImageFileIds(List.of("img1", "img1", "img2")); + Set images = new LinkedHashSet<>(); + images.add(new WorkspaceImageRequestDto("img1", 0)); + images.add(new WorkspaceImageRequestDto("img1", 1)); // 동일 fileId → 무시 + images.add(new WorkspaceImageRequestDto("img2", 2)); + dto.setRepresentativeImages(images); // when createWorkspaceRequest.execute(user, dto); diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java index ee50d49b..da674600 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java @@ -1,5 +1,6 @@ package com.dreamteam.alter.application.workspace.usecase; +import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; import com.dreamteam.alter.application.file.FileDeleteService; import com.dreamteam.alter.common.exception.CustomException; @@ -24,8 +25,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -68,11 +71,15 @@ class ManagerUpdateWorkspaceImagesTest { private ArgumentCaptor> fileIdsCaptor; private UpdateWorkspaceImagesRequestDto request(String... fileIds) { - return new UpdateWorkspaceImagesRequestDto(List.of(fileIds)); + Set images = new LinkedHashSet<>(); + for (int i = 0; i < fileIds.length; i++) { + images.add(new WorkspaceImageRequestDto(fileIds[i], i)); + } + return new UpdateWorkspaceImagesRequestDto(images); } @Test - @DisplayName("5개를 초과하면 FILE_LIMIT_EXCEEDED 예외가 발생한다") + @DisplayName("5개를 초과하면 ILLEGAL_ARGUMENT 예외가 발생한다") void execute_5개초과_예외() { // given ManagerActor actor = mock(ManagerActor.class); @@ -81,7 +88,7 @@ private UpdateWorkspaceImagesRequestDto request(String... fileIds) { assertThatThrownBy(() -> managerUpdateWorkspaceImages.execute( actor, 1L, request("f1", "f2", "f3", "f4", "f5", "f6"))) .isInstanceOf(CustomException.class) - .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.FILE_LIMIT_EXCEEDED)); + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.ILLEGAL_ARGUMENT)); then(workspaceImageRepository).should(never()).saveAll(any()); } From 5012899cbc3f8309deffb08c73fea8431d7832c8 Mon Sep 17 00:00:00 2001 From: hodoon Date: Wed, 17 Jun 2026 23:49:05 +0900 Subject: [PATCH 17/22] =?UTF-8?q?fix:=20=EC=97=85=EC=9E=A5=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=9C=20=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B5=9C=EB=8C=80=205=EA=B0=9C=20=EC=83=81?= =?UTF-8?q?=ED=95=9C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컨트롤러 검증을 우회한 호출에서도 비즈니스 규칙이 깨지지 않도록 관리자 교체 유스케이스와 동일하게 유스케이스 단계에서 상한을 강제 --- ...orkspaceRequestTest.java => ApproveWorkspaceRequestTests.java} | 0 ...WorkspaceRequestTest.java => CreateWorkspaceRequestTests.java} | 0 ...rkspaceImagesTest.java => ManagerGetWorkspaceImagesTests.java} | 0 ...paceImagesTest.java => ManagerUpdateWorkspaceImagesTests.java} | 0 .../entity/{WorkspaceImageTest.java => WorkspaceImageTests.java} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename src/test/java/com/dreamteam/alter/application/workspace/usecase/{ApproveWorkspaceRequestTest.java => ApproveWorkspaceRequestTests.java} (100%) rename src/test/java/com/dreamteam/alter/application/workspace/usecase/{CreateWorkspaceRequestTest.java => CreateWorkspaceRequestTests.java} (100%) rename src/test/java/com/dreamteam/alter/application/workspace/usecase/{ManagerGetWorkspaceImagesTest.java => ManagerGetWorkspaceImagesTests.java} (100%) rename src/test/java/com/dreamteam/alter/application/workspace/usecase/{ManagerUpdateWorkspaceImagesTest.java => ManagerUpdateWorkspaceImagesTests.java} (100%) rename src/test/java/com/dreamteam/alter/domain/workspace/entity/{WorkspaceImageTest.java => WorkspaceImageTests.java} (100%) diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java similarity index 100% rename from src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTest.java rename to src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java similarity index 100% rename from src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTest.java rename to src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java similarity index 100% rename from src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTest.java rename to src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTests.java similarity index 100% rename from src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTest.java rename to src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTests.java diff --git a/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTest.java b/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTests.java similarity index 100% rename from src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTest.java rename to src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTests.java From 9931d8c47acf546432159c38368bf159cc061b1a Mon Sep 17 00:00:00 2001 From: hodoon Date: Wed, 17 Jun 2026 23:49:13 +0900 Subject: [PATCH 18/22] =?UTF-8?q?fix:=20=EC=97=85=EC=9E=A5=20=EC=8B=A0?= =?UTF-8?q?=EC=B2=AD=20=EC=8B=9C=20=EB=8C=80=ED=91=9C=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B5=9C=EB=8C=80=205=EA=B0=9C=20=EC=83=81?= =?UTF-8?q?=ED=95=9C=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컨트롤러 검증을 우회한 호출에서도 비즈니스 규칙이 깨지지 않도록 관리자 교체 유스케이스와 동일하게 유스케이스 단계에서 상한을 강제 --- .../workspace/usecase/CreateWorkspaceRequest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java index f19a166b..86f6e96e 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java @@ -10,6 +10,8 @@ import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; import com.dreamteam.alter.domain.file.type.FileTargetType; import com.dreamteam.alter.domain.user.entity.User; @@ -26,6 +28,8 @@ @Transactional public class CreateWorkspaceRequest implements CreateWorkspaceRequestUseCase { + private static final int MAX_IMAGE_COUNT = 5; + private final WorkspaceRequestRepository workspaceRequestRepository; private final WorkspaceRequestImageRepository workspaceRequestImageRepository; private final AttachFilesUseCase attachFiles; @@ -58,6 +62,9 @@ public void execute(User user, CreateWorkspaceRequestDto request) { List representativeImageFileIds = WorkspaceImageRequestDto.toOrderedFileIds(request.getRepresentativeImages()); + if (representativeImageFileIds.size() > MAX_IMAGE_COUNT) { + throw new CustomException(ErrorCode.ILLEGAL_ARGUMENT, "대표이미지는 최대 " + MAX_IMAGE_COUNT + "개까지 등록할 수 있습니다."); + } if (!representativeImageFileIds.isEmpty()) { attachFiles.execute( representativeImageFileIds, From 0dde6000faf0af4f89093a167fd868638067a67c Mon Sep 17 00:00:00 2001 From: hodoon Date: Wed, 17 Jun 2026 23:51:22 +0900 Subject: [PATCH 19/22] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4=EB=AA=85=20Tests=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=ED=86=B5=EC=9D=BC=20=EB=B0=8F=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=EB=AA=85=20=EC=98=81=EB=AC=B8=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 신규 테스트 5개 클래스/파일명을 [ClassName]Tests 규칙으로 변경 - WorkspaceImageTests 메서드명을 action_condition_expectedResult 스타일로 변경 --- .../usecase/ApproveWorkspaceRequestTests.java | 2 +- .../workspace/usecase/CreateWorkspaceRequestTests.java | 2 +- .../usecase/ManagerGetWorkspaceImagesTests.java | 2 +- .../usecase/ManagerUpdateWorkspaceImagesTests.java | 2 +- .../domain/workspace/entity/WorkspaceImageTests.java | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java index 5ed1c64b..2daacfca 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java @@ -36,7 +36,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("ApproveWorkspaceRequest 테스트") -class ApproveWorkspaceRequestTest { +class ApproveWorkspaceRequestTests { @Mock private WorkspaceRepository workspaceRepository; diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java index 6ec6502a..dff1e3bc 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java @@ -31,7 +31,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("CreateWorkspaceRequest 테스트") -class CreateWorkspaceRequestTest { +class CreateWorkspaceRequestTests { @Mock private WorkspaceRequestRepository workspaceRequestRepository; diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java index 362d94d5..cf60f895 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java @@ -28,7 +28,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("ManagerGetWorkspaceImages 테스트") -class ManagerGetWorkspaceImagesTest { +class ManagerGetWorkspaceImagesTests { @Mock private WorkspaceQueryRepository workspaceQueryRepository; diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTests.java index da674600..0a24fdba 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImagesTests.java @@ -41,7 +41,7 @@ @ExtendWith(MockitoExtension.class) @DisplayName("ManagerUpdateWorkspaceImages 테스트") -class ManagerUpdateWorkspaceImagesTest { +class ManagerUpdateWorkspaceImagesTests { @Mock private WorkspaceQueryRepository workspaceQueryRepository; diff --git a/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTests.java b/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTests.java index f9054fff..837249e7 100644 --- a/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTests.java +++ b/src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceImageTests.java @@ -10,11 +10,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("WorkspaceImage 엔티티 테스트") -class WorkspaceImageTest { +class WorkspaceImageTests { @Test @DisplayName("정상 sortOrder로 생성된다") - void create_정상() { + void create_validSortOrder_createsSuccessfully() { // when WorkspaceImage image = WorkspaceImage.create(null, "file-1", 0); @@ -25,7 +25,7 @@ class WorkspaceImageTest { @Test @DisplayName("음수 sortOrder로 생성하면 ILLEGAL_ARGUMENT 예외가 발생한다") - void create_음수sortOrder_예외() { + void create_negativeSortOrder_throwsIllegalArgumentException() { // when & then assertThatThrownBy(() -> WorkspaceImage.create(null, "file-1", -1)) .isInstanceOf(CustomException.class) @@ -34,7 +34,7 @@ class WorkspaceImageTest { @Test @DisplayName("updateSortOrder는 0 이상 값을 허용한다") - void updateSortOrder_정상() { + void updateSortOrder_validValue_updatesSuccessfully() { // given WorkspaceImage image = WorkspaceImage.create(null, "file-1", 0); @@ -45,7 +45,7 @@ class WorkspaceImageTest { @Test @DisplayName("updateSortOrder에 음수를 주면 ILLEGAL_ARGUMENT 예외가 발생한다") - void updateSortOrder_음수_예외() { + void updateSortOrder_negativeValue_throwsIllegalArgumentException() { // given WorkspaceImage image = WorkspaceImage.create(null, "file-1", 0); From e10d44248983f5995a8f33c24f4a2c5e90db92cd Mon Sep 17 00:00:00 2001 From: hodoon Date: Thu, 18 Jun 2026 00:05:13 +0900 Subject: [PATCH 20/22] =?UTF-8?q?refactor:=20=EC=97=85=EC=9E=A5=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=8C=80?= =?UTF-8?q?=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=86=8C=EC=9C=A0=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EC=A4=91=EB=B3=B5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 상세 조회는 getByManagerUserAndId로 이미 소유를 검증하므로, 이미지 합성 단계의 소유 재검증(existsByIdAndManagerUser)을 생략하도록 검증 없는 조회 경로(getImagesWithoutOwnershipCheck)를 분리. 컨트롤러 직접 호출 경로는 기존 소유 검증을 유지. --- .../workspace/usecase/ManagerGetWorkspace.java | 3 ++- .../usecase/ManagerGetWorkspaceImages.java | 5 +++++ .../inbound/ManagerGetWorkspaceImagesUseCase.java | 3 +++ .../usecase/ManagerGetWorkspaceImagesTests.java | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java index 16c3a208..e0e89763 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspace.java @@ -30,7 +30,8 @@ public ManagerWorkspaceResponseDto execute(ManagerActor actor, Long workspaceId) throw new CustomException(ErrorCode.WORKSPACE_NOT_FOUND); } - return ManagerWorkspaceResponseDto.of(workspace, managerGetWorkspaceImages.execute(actor, workspaceId)); + return ManagerWorkspaceResponseDto.of( + workspace, managerGetWorkspaceImages.getImagesWithoutOwnershipCheck(workspaceId)); } } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java index 85bc4f08..82a2548e 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImages.java @@ -41,6 +41,11 @@ public List execute(ManagerActor actor, Long workspac throw new CustomException(ErrorCode.WORKSPACE_NOT_FOUND); } + return getImagesWithoutOwnershipCheck(workspaceId); + } + + @Override + public List getImagesWithoutOwnershipCheck(Long workspaceId) { List images = workspaceImageQueryRepository.findAllByWorkspaceId(workspaceId); if (images.isEmpty()) { return List.of(); diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java index 7bc2d1db..73563d46 100644 --- a/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ManagerGetWorkspaceImagesUseCase.java @@ -7,4 +7,7 @@ public interface ManagerGetWorkspaceImagesUseCase { List execute(ManagerActor actor, Long workspaceId); + + // 호출 측에서 이미 업장 소유를 검증한 경우 사용 (소유 재검증 생략) + List getImagesWithoutOwnershipCheck(Long workspaceId); } diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java index cf60f895..4a84dda5 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ManagerGetWorkspaceImagesTests.java @@ -25,6 +25,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; @ExtendWith(MockitoExtension.class) @DisplayName("ManagerGetWorkspaceImages 테스트") @@ -119,4 +120,18 @@ class ManagerGetWorkspaceImagesTests { assertThat(result).extracting(WorkspaceImageResponseDto::getSortOrder) .containsExactly(0, 1); } + + @Test + @DisplayName("getImagesWithoutOwnershipCheck는 소유 검증 없이 이미지를 조회한다") + void getImagesWithoutOwnershipCheck_소유검증생략_이미지조회() { + // given + given(workspaceImageQueryRepository.findAllByWorkspaceId(1L)).willReturn(List.of()); + + // when + List result = managerGetWorkspaceImages.getImagesWithoutOwnershipCheck(1L); + + // then + assertThat(result).isEmpty(); + verifyNoInteractions(workspaceQueryRepository); + } } From fc33ea519bb82ee6ed62aaaca2bf9fd3add0d68c Mon Sep 17 00:00:00 2001 From: hodoon Date: Thu, 18 Jun 2026 00:18:03 +0900 Subject: [PATCH 21/22] =?UTF-8?q?refactor:=20=EC=9C=A0=EC=8A=A4=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EC=9D=98=20=EC=96=B4=EB=8C=91=ED=84=B0=20DTO?= =?UTF-8?q?=20=EC=A7=81=EC=A0=91=20=EC=9D=98=EC=A1=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 대표이미지 fileId 정렬/추출 변환을 request DTO 내부 메서드로 캡슐화하여 CreateWorkspaceRequest·ManagerUpdateWorkspaceImages 유스케이스가 WorkspaceImageRequestDto(adapter/inbound)에 직접 결합되지 않도록 변경. 업장 신청 대표이미지 5개 상한 검증 테스트도 추가. --- .../dto/CreateWorkspaceRequestDto.java | 5 ++++ .../dto/UpdateWorkspaceImagesRequestDto.java | 5 ++++ .../usecase/CreateWorkspaceRequest.java | 4 +-- .../usecase/ManagerUpdateWorkspaceImages.java | 3 +-- .../usecase/CreateWorkspaceRequestTests.java | 25 +++++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java index 95ef2885..7a0c0f41 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java @@ -1,6 +1,7 @@ package com.dreamteam.alter.adapter.inbound.general.workspace.dto; import java.math.BigDecimal; +import java.util.List; import java.util.Set; import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; @@ -74,4 +75,8 @@ public class CreateWorkspaceRequestDto { @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") @Schema(description = "업장 대표이미지 목록 (최대 5개, fileId 중복 자동 제거, sortOrder 오름차순으로 노출)") private Set<@Valid WorkspaceImageRequestDto> representativeImages; + + public List getOrderedRepresentativeImageFileIds() { + return WorkspaceImageRequestDto.toOrderedFileIds(representativeImages); + } } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java index d07b8fa6..bb816b37 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/UpdateWorkspaceImagesRequestDto.java @@ -1,5 +1,6 @@ package com.dreamteam.alter.adapter.inbound.manager.workspace.dto; +import java.util.List; import java.util.Set; import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; @@ -22,4 +23,8 @@ public class UpdateWorkspaceImagesRequestDto { @Size(max = 5, message = "대표이미지는 최대 5개까지 등록할 수 있습니다.") @Schema(description = "대표이미지 목록 (fileId 중복 자동 제거, sortOrder 오름차순으로 노출, 비우면 전체 삭제)") private Set<@Valid WorkspaceImageRequestDto> images; + + public List getOrderedImageFileIds() { + return WorkspaceImageRequestDto.toOrderedFileIds(images); + } } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java index 86f6e96e..8b741867 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java @@ -8,7 +8,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; import com.dreamteam.alter.common.exception.CustomException; import com.dreamteam.alter.common.exception.ErrorCode; @@ -60,8 +59,7 @@ public void execute(User user, CreateWorkspaceRequestDto request) { } attachFiles.executeMap(fileMap, savedWorkspaceRequestId.toString(), user.getId()); - List representativeImageFileIds = - WorkspaceImageRequestDto.toOrderedFileIds(request.getRepresentativeImages()); + List representativeImageFileIds = request.getOrderedRepresentativeImageFileIds(); if (representativeImageFileIds.size() > MAX_IMAGE_COUNT) { throw new CustomException(ErrorCode.ILLEGAL_ARGUMENT, "대표이미지는 최대 " + MAX_IMAGE_COUNT + "개까지 등록할 수 있습니다."); } diff --git a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java index c6bc623f..838401f1 100644 --- a/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java +++ b/src/main/java/com/dreamteam/alter/application/workspace/usecase/ManagerUpdateWorkspaceImages.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.manager.workspace.dto.UpdateWorkspaceImagesRequestDto; import com.dreamteam.alter.application.file.FileDeleteService; import com.dreamteam.alter.common.exception.CustomException; @@ -45,7 +44,7 @@ public class ManagerUpdateWorkspaceImages implements ManagerUpdateWorkspaceImage @Override public void execute(ManagerActor actor, Long workspaceId, UpdateWorkspaceImagesRequestDto request) { - List newFileIds = WorkspaceImageRequestDto.toOrderedFileIds(request.getImages()); + List newFileIds = request.getOrderedImageFileIds(); if (newFileIds.size() > MAX_IMAGE_COUNT) { throw new CustomException(ErrorCode.ILLEGAL_ARGUMENT, "대표이미지는 최대 " + MAX_IMAGE_COUNT + "개까지 등록할 수 있습니다."); diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java index dff1e3bc..cc68513c 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java @@ -2,6 +2,8 @@ import com.dreamteam.alter.adapter.inbound.common.dto.WorkspaceImageRequestDto; import com.dreamteam.alter.adapter.inbound.general.workspace.dto.CreateWorkspaceRequestDto; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; import com.dreamteam.alter.domain.file.port.inbound.AttachFilesUseCase; import com.dreamteam.alter.domain.file.type.FileTargetType; import com.dreamteam.alter.domain.user.entity.User; @@ -22,6 +24,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; @@ -116,4 +119,26 @@ private CreateWorkspaceRequestDto baseRequest() { assertThat(imagesCaptor.getValue()).extracting(WorkspaceRequestImage::getSortOrder) .containsExactly(0, 1); } + + @Test + @DisplayName("대표이미지가 5개를 초과하면 ILLEGAL_ARGUMENT 예외가 발생한다") + void execute_대표이미지5개초과_예외() { + // given + User user = mock(User.class); + given(user.getId()).willReturn(100L); + given(workspaceRequestRepository.save(any())).willReturn(1L); + + CreateWorkspaceRequestDto dto = baseRequest(); + Set images = new LinkedHashSet<>(); + for (int i = 1; i <= 6; i++) { + images.add(new WorkspaceImageRequestDto("img" + i, i)); + } + dto.setRepresentativeImages(images); + + // when & then + assertThatThrownBy(() -> createWorkspaceRequest.execute(user, dto)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.ILLEGAL_ARGUMENT)); + then(workspaceRequestImageRepository).should(never()).saveAll(any()); + } } From 9a86b7ed97376bd43ed2aeb898527882d1613942 Mon Sep 17 00:00:00 2001 From: hodoon Date: Thu, 18 Jun 2026 00:36:46 +0900 Subject: [PATCH 22/22] =?UTF-8?q?test:=20dev=20=EB=A6=AC=EB=84=A4=EC=9E=84?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=B0=20=EC=8A=B9=EC=9D=B8=20=EB=8C=80?= =?UTF-8?q?=ED=91=9C=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20UpdateWorkspaceRequestStatusTests=EB=A1=9C?= =?UTF-8?q?=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ApproveWorkspaceRequest→UpdateWorkspaceRequestStatus 리네임에 따라 옛 ApproveWorkspaceRequestTests의 대표이미지 마이그레이션 케이스를 UpdateWorkspaceRequestStatusTests로 이전하고, ACTIVATED 케이스에 누락된 stub을 보강. --- .../usecase/ApproveWorkspaceRequestTests.java | 153 ------------------ .../UpdateWorkspaceRequestStatusTests.java | 102 ++++++++++++ 2 files changed, 102 insertions(+), 153 deletions(-) delete mode 100644 src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java deleted file mode 100644 index 2daacfca..00000000 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/ApproveWorkspaceRequestTests.java +++ /dev/null @@ -1,153 +0,0 @@ -package com.dreamteam.alter.application.workspace.usecase; - -import com.dreamteam.alter.application.file.FileDeleteService; -import com.dreamteam.alter.domain.file.entity.File; -import com.dreamteam.alter.domain.file.port.outbound.FileQueryRepository; -import com.dreamteam.alter.domain.user.entity.ManagerUser; -import com.dreamteam.alter.domain.user.entity.User; -import com.dreamteam.alter.domain.user.port.outbound.ManagerUserQueryRepository; -import com.dreamteam.alter.domain.user.port.outbound.ManagerUserRepository; -import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; -import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequest; -import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; -import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; -import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRepository; -import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageQueryRepository; -import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestQueryRepository; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; - -@ExtendWith(MockitoExtension.class) -@DisplayName("ApproveWorkspaceRequest 테스트") -class ApproveWorkspaceRequestTests { - - @Mock - private WorkspaceRepository workspaceRepository; - - @Mock - private ManagerUserQueryRepository managerUserQueryRepository; - - @Mock - private ManagerUserRepository managerUserRepository; - - @Mock - private WorkspaceRequestQueryRepository workspaceRequestQueryRepository; - - @Mock - private WorkspaceImageRepository workspaceImageRepository; - - @Mock - private WorkspaceRequestImageQueryRepository workspaceRequestImageQueryRepository; - - @Mock - private FileQueryRepository fileQueryRepository; - - @Mock - private FileDeleteService fileDeleteService; - - @InjectMocks - private ApproveWorkspaceRequest approveWorkspaceRequest; - - @Captor - private ArgumentCaptor> imagesCaptor; - - private WorkspaceRequest mockApprovedRequest() { - WorkspaceRequest request = mock(WorkspaceRequest.class); - User user = mock(User.class); - given(user.getId()).willReturn(1L); - given(request.getUser()).willReturn(user); - given(workspaceRequestQueryRepository.findByIdWithUser(10L)).willReturn(Optional.of(request)); - given(managerUserQueryRepository.findByUserId(1L)).willReturn(Optional.of(mock(ManagerUser.class))); - return request; - } - - private WorkspaceRequestImage requestImage(String fileId) { - WorkspaceRequestImage image = mock(WorkspaceRequestImage.class); - given(image.getFileId()).willReturn(fileId); - return image; - } - - @Test - @DisplayName("대표이미지가 없으면 WorkspaceImage 저장을 호출하지 않는다") - void execute_대표이미지없음() { - // given - mockApprovedRequest(); - given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)).willReturn(List.of()); - given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); - - // when - approveWorkspaceRequest.execute(10L); - - // then - then(workspaceImageRepository).should(never()).saveAll(any()); - } - - @Test - @DisplayName("신청 단계 파일을 업장으로 재첨부하고 순서대로 WorkspaceImage를 생성한다") - void execute_대표이미지마이그레이션() { - // given - mockApprovedRequest(); - WorkspaceRequestImage riA = requestImage("fileA"); - WorkspaceRequestImage riB = requestImage("fileB"); - given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)) - .willReturn(List.of(riA, riB)); - - File fileA = mock(File.class); - File fileB = mock(File.class); - given(fileA.getId()).willReturn("fileA"); - given(fileB.getId()).willReturn("fileB"); - given(fileQueryRepository.findAllByIdIn(List.of("fileA", "fileB"))).willReturn(List.of(fileA, fileB)); - given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); - - // when - approveWorkspaceRequest.execute(10L); - - // then - then(fileA).should().attach(anyString()); - then(fileB).should().attach(anyString()); - then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); - assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("fileA", "fileB"); - assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getSortOrder).containsExactly(0, 1); - } - - @Test - @DisplayName("신청~승인 사이 삭제된 파일은 건너뛰고 존재하는 파일만 마이그레이션한다") - void execute_일부파일누락() { - // given - mockApprovedRequest(); - WorkspaceRequestImage riA = requestImage("fileA"); - WorkspaceRequestImage riB = requestImage("fileB"); - given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)) - .willReturn(List.of(riA, riB)); - - File fileA = mock(File.class); - given(fileA.getId()).willReturn("fileA"); - given(fileQueryRepository.findAllByIdIn(List.of("fileA", "fileB"))).willReturn(List.of(fileA)); - given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); - - // when - approveWorkspaceRequest.execute(10L); - - // then - then(fileA).should().attach(anyString()); - then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); - assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("fileA"); - } -} diff --git a/src/test/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatusTests.java b/src/test/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatusTests.java index f665bfb8..0f3b294a 100644 --- a/src/test/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatusTests.java +++ b/src/test/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatusTests.java @@ -11,23 +11,31 @@ import com.dreamteam.alter.domain.user.port.outbound.ManagerUserQueryRepository; import com.dreamteam.alter.domain.user.port.outbound.ManagerUserRepository; import com.dreamteam.alter.domain.workspace.entity.Workspace; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceImage; import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequest; +import com.dreamteam.alter.domain.workspace.entity.WorkspaceRequestImage; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceImageRepository; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRepository; +import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestImageQueryRepository; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceRequestQueryRepository; import com.dreamteam.alter.domain.workspace.type.WorkspaceRequestStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -49,6 +57,12 @@ class UpdateWorkspaceRequestStatusTests { @Mock private ManagerUserRepository managerUserRepository; + @Mock + private WorkspaceImageRepository workspaceImageRepository; + + @Mock + private WorkspaceRequestImageQueryRepository workspaceRequestImageQueryRepository; + @Mock private FileQueryRepository fileQueryRepository; @@ -58,6 +72,9 @@ class UpdateWorkspaceRequestStatusTests { @InjectMocks private UpdateWorkspaceRequestStatus updateWorkspaceRequestStatus; + @Captor + private ArgumentCaptor> imagesCaptor; + @Nested @DisplayName("execute") class ExecuteTests { @@ -106,6 +123,7 @@ class ExecuteTests { given(user.getId()).willReturn(1L); given(workspaceRequestQueryRepository.findByIdWithUser(1L)).willReturn(Optional.of(request)); given(managerUserQueryRepository.findByUserId(1L)).willReturn(Optional.of(managerUser)); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(1L)).willReturn(List.of()); given(fileQueryRepository.findByTargetTypeAndTargetId(FileTargetType.WORKSPACE_OWN_IDENTITY, "1")) .willReturn(Optional.of(identityFile)); @@ -129,6 +147,7 @@ class ExecuteTests { given(user.getId()).willReturn(1L); given(workspaceRequestQueryRepository.findByIdWithUser(1L)).willReturn(Optional.of(request)); given(managerUserQueryRepository.findByUserId(1L)).willReturn(Optional.empty()); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(1L)).willReturn(List.of()); given(fileQueryRepository.findByTargetTypeAndTargetId(FileTargetType.WORKSPACE_OWN_IDENTITY, "1")) .willReturn(Optional.empty()); @@ -158,5 +177,88 @@ class ExecuteTests { then(managerUserRepository).should(never()).save(any()); then(fileDeleteService).should(never()).delete(any()); } + + @Test + @DisplayName("대표이미지가 없으면 WorkspaceImage 저장을 호출하지 않는다") + void execute_대표이미지없음() { + // given + mockApprovedRequest(); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)).willReturn(List.of()); + given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); + + // when + updateWorkspaceRequestStatus.execute(10L, WorkspaceRequestStatus.ACTIVATED); + + // then + then(workspaceImageRepository).should(never()).saveAll(any()); + } + + @Test + @DisplayName("신청 단계 파일을 업장으로 재첨부하고 순서대로 WorkspaceImage를 생성한다") + void execute_대표이미지마이그레이션() { + // given + mockApprovedRequest(); + WorkspaceRequestImage riA = requestImage("fileA"); + WorkspaceRequestImage riB = requestImage("fileB"); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)) + .willReturn(List.of(riA, riB)); + + File fileA = mock(File.class); + File fileB = mock(File.class); + given(fileA.getId()).willReturn("fileA"); + given(fileB.getId()).willReturn("fileB"); + given(fileQueryRepository.findAllByIdIn(List.of("fileA", "fileB"))).willReturn(List.of(fileA, fileB)); + given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); + + // when + updateWorkspaceRequestStatus.execute(10L, WorkspaceRequestStatus.ACTIVATED); + + // then + then(fileA).should().attach(anyString()); + then(fileB).should().attach(anyString()); + then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("fileA", "fileB"); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getSortOrder).containsExactly(0, 1); + } + + @Test + @DisplayName("신청~승인 사이 삭제된 파일은 건너뛰고 존재하는 파일만 마이그레이션한다") + void execute_일부파일누락() { + // given + mockApprovedRequest(); + WorkspaceRequestImage riA = requestImage("fileA"); + WorkspaceRequestImage riB = requestImage("fileB"); + given(workspaceRequestImageQueryRepository.findAllByWorkspaceRequestId(10L)) + .willReturn(List.of(riA, riB)); + + File fileA = mock(File.class); + given(fileA.getId()).willReturn("fileA"); + given(fileQueryRepository.findAllByIdIn(List.of("fileA", "fileB"))).willReturn(List.of(fileA)); + given(fileQueryRepository.findByTargetTypeAndTargetId(any(), any())).willReturn(Optional.empty()); + + // when + updateWorkspaceRequestStatus.execute(10L, WorkspaceRequestStatus.ACTIVATED); + + // then + then(fileA).should().attach(anyString()); + then(workspaceImageRepository).should().saveAll(imagesCaptor.capture()); + assertThat(imagesCaptor.getValue()).extracting(WorkspaceImage::getFileId).containsExactly("fileA"); + } + } + + private WorkspaceRequest mockApprovedRequest() { + WorkspaceRequest request = mock(WorkspaceRequest.class); + User user = mock(User.class); + given(user.getId()).willReturn(1L); + given(request.getUser()).willReturn(user); + given(workspaceRequestQueryRepository.findByIdWithUser(10L)).willReturn(Optional.of(request)); + given(managerUserQueryRepository.findByUserId(1L)).willReturn(Optional.of(mock(ManagerUser.class))); + return request; + } + + private WorkspaceRequestImage requestImage(String fileId) { + WorkspaceRequestImage image = mock(WorkspaceRequestImage.class); + given(image.getFileId()).willReturn(fileId); + return image; } }