From e1511419b4adbccd323e4ca4bf5447a6a4e147ec Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:15 +0900 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20=EA=B3=B5=EA=B3=A0=20=EB=8F=84?= =?UTF-8?q?=EB=A9=94=EC=9D=B8=20Command=20=ED=83=80=EC=9E=85=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 --- .../posting/command/CreatePostingCommand.java | 14 ++++++++++++++ .../posting/command/PostingScheduleCommand.java | 13 +++++++++++++ .../posting/command/UpdatePostingCommand.java | 15 +++++++++++++++ .../command/UpdatePostingScheduleCommand.java | 14 ++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java create mode 100644 src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java create mode 100644 src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java create mode 100644 src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java diff --git a/src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java b/src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java new file mode 100644 index 000000000..6a87ebfb1 --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java @@ -0,0 +1,14 @@ +package com.dreamteam.alter.domain.posting.command; + +import com.dreamteam.alter.domain.posting.type.PaymentType; + +import java.util.List; + +public record CreatePostingCommand( + Long workspaceId, + String title, + String description, + int payAmount, + PaymentType paymentType, + List schedules +) {} diff --git a/src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java b/src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java new file mode 100644 index 000000000..beeead2dd --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java @@ -0,0 +1,13 @@ +package com.dreamteam.alter.domain.posting.command; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.List; + +public record PostingScheduleCommand( + List workingDays, + LocalTime startTime, + LocalTime endTime, + int positionsNeeded, + String position +) {} diff --git a/src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java b/src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java new file mode 100644 index 000000000..cb4ecdb6e --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java @@ -0,0 +1,15 @@ +package com.dreamteam.alter.domain.posting.command; + +import com.dreamteam.alter.domain.posting.type.PaymentType; + +import java.util.List; + +public record UpdatePostingCommand( + String title, + String description, + int payAmount, + PaymentType paymentType, + List createSchedules, + List updateSchedules, + List deleteScheduleIds +) {} diff --git a/src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java b/src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java new file mode 100644 index 000000000..73d85affb --- /dev/null +++ b/src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java @@ -0,0 +1,14 @@ +package com.dreamteam.alter.domain.posting.command; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.List; + +public record UpdatePostingScheduleCommand( + Long id, + List workingDays, + LocalTime startTime, + LocalTime endTime, + int positionsNeeded, + String position +) {} From e1bf5641be09fbc1e8cc78b65733019f64684e1c Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:19 +0900 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EC=8A=A4=EC=BD=94=ED=94=84=20=EC=97=85=EC=9E=A5=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/WorkspaceQueryRepositoryImpl.java | 14 ++++++++++++++ .../port/outbound/WorkspaceQueryRepository.java | 1 + 2 files changed, 15 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java index 900b7cf25..062fb5a5e 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java @@ -64,6 +64,20 @@ public Optional findById(Long id) { ); } + @Override + public Optional findByIdAndManagerUser(Long id, ManagerUser managerUser) { + QWorkspace qWorkspace = QWorkspace.workspace; + return Optional.ofNullable( + queryFactory + .selectFrom(qWorkspace) + .where( + qWorkspace.id.eq(id), + qWorkspace.managerUser.eq(managerUser) + ) + .fetchOne() + ); + } + @Override public List getManagerWorkspaceList(ManagerUser managerUser) { QWorkspace qWorkspace = QWorkspace.workspace; diff --git a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java index e65676357..a8a36bf2a 100644 --- a/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java +++ b/src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java @@ -20,6 +20,7 @@ public interface WorkspaceQueryRepository { Optional findById(Long id); + Optional findByIdAndManagerUser(Long id, ManagerUser managerUser); List getManagerWorkspaceList(ManagerUser managerUser); ManagerWorkspaceResponse getByManagerUserAndId(ManagerUser managerUser, Long workspaceId); From 3cc06bd8c78e61ffcd9c5e873c94fa49f42eaf3c Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:32 +0900 Subject: [PATCH 03/22] =?UTF-8?q?refactor:=20=EA=B3=B5=EA=B3=A0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=C2=B7=EC=88=98=EC=A0=95=20UseCase=EB=A5=BC?= =?UTF-8?q?=20Command=20=EC=9E=85=EB=A0=A5=EC=9C=BC=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 포트 시그니처에서 어댑터 DTO를 제거하고 도메인 Command를 받도록 변경 - Posting 엔티티의 adapter.inbound 의존 제거, 요일·시각 파싱을 어댑터 경계로 이동 - 공고 등록 시 매니저 스코프 업장 조회로 소유권 검증 추가 - 삭제된 공고의 내용 수정 차단 - 필수값 검증 보강: workspaceId·paymentType의 jakarta 어노테이션 교정, schedules NotEmpty, 수정 DTO의 description NotBlank, 수정 스케줄의 workingDays NotEmpty·positionsNeeded Positive 소유권 검증과 Command 전환이 동일한 UseCase 시그니처를 건드려 한 커밋으로 묶음. --- .../posting/dto/CreatePostingRequestDto.java | 17 ++- .../dto/CreatePostingScheduleRequestDto.java | 5 + .../controller/ManagerPostingController.java | 6 +- .../posting/dto/UpdatePostingRequestDto.java | 21 ++++ .../posting/dto/UpdatePostingScheduleDto.java | 20 +++- .../posting/usecase/CreatePosting.java | 12 +- .../posting/usecase/ManagerUpdatePosting.java | 20 ++-- .../alter/domain/posting/entity/Posting.java | 110 +++++++++--------- .../port/inbound/CreatePostingUseCase.java | 5 +- .../inbound/ManagerUpdatePostingUseCase.java | 4 +- 10 files changed, 136 insertions(+), 84 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java index 0fe9f6a8f..78a89b3d2 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java @@ -1,16 +1,17 @@ package com.dreamteam.alter.adapter.inbound.general.posting.dto; +import com.dreamteam.alter.domain.posting.command.CreatePostingCommand; import com.dreamteam.alter.domain.posting.type.PaymentType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Positive; import lombok.*; import java.util.List; -import javax.validation.constraints.NotNull; - @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor(access = AccessLevel.PRIVATE) @@ -47,6 +48,18 @@ public class CreatePostingRequestDto { "}" + "]") @Valid + @NotEmpty private List schedules; + public CreatePostingCommand toCommand() { + return new CreatePostingCommand( + workspaceId, + title, + description, + payAmount, + paymentType, + schedules.stream().map(CreatePostingScheduleRequestDto::toCommand).toList() + ); + } + } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java index 5fd697d7e..26fbf9941 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java @@ -1,5 +1,6 @@ package com.dreamteam.alter.adapter.inbound.general.posting.dto; +import com.dreamteam.alter.domain.posting.command.PostingScheduleCommand; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; @@ -38,4 +39,8 @@ public class CreatePostingScheduleRequestDto { @NotBlank private String position; + public PostingScheduleCommand toCommand() { + return new PostingScheduleCommand(workingDays, startTime, endTime, positionsNeeded, position); + } + } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java index 6d34852c7..5f4e68fae 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java @@ -77,7 +77,9 @@ public class ManagerPostingController implements ManagerPostingControllerSpec { public ResponseEntity> createPosting( @Valid CreatePostingRequestDto request ) { - createPosting.execute(request); + ManagerActor actor = ManagerActionContext.getInstance().getActor(); + + createPosting.execute(request.toCommand(), actor); return ResponseEntity.ok(CommonApiResponse.empty()); } @@ -155,7 +157,7 @@ public ResponseEntity> updatePosting( ) { ManagerActor actor = ManagerActionContext.getInstance().getActor(); - managerUpdatePosting.execute(postingId, request, actor); + managerUpdatePosting.execute(postingId, request.toCommand(), actor); return ResponseEntity.ok(CommonApiResponse.empty()); } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java index 5c1af20a8..4365984ca 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java @@ -1,8 +1,12 @@ package com.dreamteam.alter.adapter.inbound.manager.posting.dto; import java.util.List; +import java.util.function.Function; + +import org.apache.commons.lang3.ObjectUtils; import com.dreamteam.alter.adapter.inbound.general.posting.dto.CreatePostingScheduleRequestDto; +import com.dreamteam.alter.domain.posting.command.UpdatePostingCommand; import com.dreamteam.alter.domain.posting.type.PaymentType; import io.swagger.v3.oas.annotations.media.Schema; @@ -27,6 +31,7 @@ public class UpdatePostingRequestDto { @Schema(description = "공고 제목", example = "홀서빙 구합니다") private String title; + @NotBlank @Schema(description = "공고 설명", example = "홀서빙 구합니다. 주말 근무 가능하신 분 우대합니다.") private String description; @@ -48,4 +53,20 @@ public class UpdatePostingRequestDto { @Schema(description = "삭제할 스케줄 ID", example = "[2, 3]") private List deleteScheduleIds; + + public UpdatePostingCommand toCommand() { + return new UpdatePostingCommand( + title, + description, + payAmount, + paymentType, + toCommands(createSchedules, CreatePostingScheduleRequestDto::toCommand), + toCommands(updateSchedules, UpdatePostingScheduleDto::toCommand), + deleteScheduleIds + ); + } + + private static List toCommands(List source, Function mapper) { + return ObjectUtils.isEmpty(source) ? List.of() : source.stream().map(mapper).toList(); + } } diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java index 12b748c95..2f279b300 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java @@ -1,10 +1,15 @@ package com.dreamteam.alter.adapter.inbound.manager.posting.dto; +import com.dreamteam.alter.domain.posting.command.UpdatePostingScheduleCommand; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import lombok.*; +import java.time.DayOfWeek; +import java.time.LocalTime; import java.util.List; @Getter @@ -18,7 +23,7 @@ public class UpdatePostingScheduleDto { @Schema(description = "스케줄 ID", example = "1") private Long id; - @NotNull + @NotEmpty @Schema(description = "근무일", example = "[\"MONDAY\", \"WEDNESDAY\"]") private List workingDays; @@ -30,11 +35,22 @@ public class UpdatePostingScheduleDto { @Schema(description = "종료 시간", example = "18:00") private String endTime; - @NotNull + @Positive @Schema(description = "필요 인원", example = "3") private int positionsNeeded; @NotBlank @Schema(description = "포지션", example = "홀서빙") private String position; + + public UpdatePostingScheduleCommand toCommand() { + return new UpdatePostingScheduleCommand( + id, + workingDays.stream().map(DayOfWeek::valueOf).toList(), + LocalTime.parse(startTime), + LocalTime.parse(endTime), + positionsNeeded, + position + ); + } } diff --git a/src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java b/src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java index c477d58d5..0ab8649cc 100644 --- a/src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java +++ b/src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java @@ -3,12 +3,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.dreamteam.alter.adapter.inbound.general.posting.dto.CreatePostingRequestDto; import com.dreamteam.alter.common.exception.CustomException; import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.CreatePostingCommand; import com.dreamteam.alter.domain.posting.entity.Posting; import com.dreamteam.alter.domain.posting.port.inbound.CreatePostingUseCase; import com.dreamteam.alter.domain.posting.port.outbound.PostingRepository; +import com.dreamteam.alter.domain.user.context.ManagerActor; import com.dreamteam.alter.domain.workspace.entity.Workspace; import com.dreamteam.alter.domain.workspace.port.outbound.WorkspaceQueryRepository; @@ -23,11 +24,12 @@ public class CreatePosting implements CreatePostingUseCase { private final WorkspaceQueryRepository workspaceQueryRepository; @Override - public void execute(CreatePostingRequestDto request) { - Workspace workspace = workspaceQueryRepository.findById(request.getWorkspaceId()) - .orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND)); + public void execute(CreatePostingCommand command, ManagerActor actor) { + Workspace workspace = + workspaceQueryRepository.findByIdAndManagerUser(command.workspaceId(), actor.getManagerUser()) + .orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND)); - Posting posting = Posting.create(request, workspace); + Posting posting = Posting.create(command, workspace); postingRepository.save(posting); } diff --git a/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java b/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java index fcc24cee8..98664629c 100644 --- a/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java +++ b/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java @@ -3,12 +3,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.dreamteam.alter.adapter.inbound.manager.posting.dto.UpdatePostingRequestDto; import com.dreamteam.alter.common.exception.CustomException; import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.UpdatePostingCommand; import com.dreamteam.alter.domain.posting.entity.Posting; import com.dreamteam.alter.domain.posting.port.inbound.ManagerUpdatePostingUseCase; import com.dreamteam.alter.domain.posting.port.outbound.PostingQueryRepository; +import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.dreamteam.alter.domain.user.context.ManagerActor; import com.dreamteam.alter.domain.user.entity.ManagerUser; @@ -22,20 +23,17 @@ public class ManagerUpdatePosting implements ManagerUpdatePostingUseCase { private final PostingQueryRepository postingQueryRepository; @Override - public void execute(Long postingId, UpdatePostingRequestDto request, ManagerActor actor) { + public void execute(Long postingId, UpdatePostingCommand command, ManagerActor actor) { ManagerUser managerUser = actor.getManagerUser(); Posting posting = postingQueryRepository.findByManagerAndId(postingId, managerUser) .orElseThrow(() -> new CustomException(ErrorCode.POSTING_NOT_FOUND)); - posting.updateContent( - request.getTitle(), - request.getDescription(), - request.getPayAmount(), - request.getPaymentType(), - request.getCreateSchedules(), - request.getUpdateSchedules(), - request.getDeleteScheduleIds() - ); + // 삭제된 공고는 내용 수정 불가 + if (PostingStatus.DELETED.equals(posting.getStatus())) { + throw new CustomException(ErrorCode.CONFLICT); + } + + posting.updateContent(command); } } diff --git a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java index 62ab54710..bb04db790 100644 --- a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java +++ b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java @@ -1,16 +1,15 @@ package com.dreamteam.alter.domain.posting.entity; -import com.dreamteam.alter.adapter.inbound.general.posting.dto.CreatePostingRequestDto; -import com.dreamteam.alter.adapter.inbound.general.posting.dto.CreatePostingScheduleRequestDto; -import com.dreamteam.alter.adapter.inbound.manager.posting.dto.UpdatePostingScheduleDto; import com.dreamteam.alter.common.exception.CustomException; import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.CreatePostingCommand; +import com.dreamteam.alter.domain.posting.command.PostingScheduleCommand; +import com.dreamteam.alter.domain.posting.command.UpdatePostingCommand; +import com.dreamteam.alter.domain.posting.command.UpdatePostingScheduleCommand; import com.dreamteam.alter.domain.posting.type.PaymentType; import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.dreamteam.alter.domain.workspace.entity.Workspace; -import java.time.DayOfWeek; -import java.time.LocalTime; import jakarta.persistence.*; import lombok.*; import org.apache.commons.lang3.ObjectUtils; @@ -19,6 +18,7 @@ import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; @Entity @@ -67,28 +67,19 @@ public class Posting { @OneToMany(mappedBy = "posting", cascade = CascadeType.ALL, orphanRemoval = true) private List schedules; - public static Posting create(CreatePostingRequestDto request, Workspace workspace) { + public static Posting create(CreatePostingCommand command, Workspace workspace) { Posting posting = Posting.builder() .workspace(workspace) - .title(request.getTitle()) - .description(request.getDescription()) - .payAmount(request.getPayAmount()) - .paymentType(request.getPaymentType()) + .title(command.title()) + .description(command.description()) + .payAmount(command.payAmount()) + .paymentType(command.paymentType()) .status(PostingStatus.OPEN) .build(); - if (ObjectUtils.isNotEmpty(request.getSchedules())) { - posting.schedules = request.getSchedules() - .stream() - .map(scheduleDto -> PostingSchedule.create( - scheduleDto.getWorkingDays(), - scheduleDto.getStartTime(), - scheduleDto.getEndTime(), - scheduleDto.getPositionsNeeded(), - scheduleDto.getPosition(), - posting - )) - .toList(); + posting.schedules = new ArrayList<>(); + if (ObjectUtils.isNotEmpty(command.schedules())) { + posting.addSchedules(command.schedules()); } return posting; @@ -98,45 +89,50 @@ public void updateStatus(PostingStatus status) { this.status = status; } - public void updateContent( - String title, - String description, - int payAmount, - PaymentType paymentType, - List createSchedules, - List updateSchedules, - List deleteScheduleIds - ) { - this.title = title; - this.description = description; - this.payAmount = payAmount; - this.paymentType = paymentType; + /** + * 삭제되지 않은 근무일정만 반환한다. (응답 노출 기준) + */ + public List getActiveSchedules() { + if (ObjectUtils.isEmpty(this.schedules)) { + return List.of(); + } + + return this.schedules.stream() + .filter(schedule -> !PostingStatus.DELETED.equals(schedule.getStatus())) + .toList(); + } + + public void updateContent(UpdatePostingCommand command) { + this.title = command.title(); + this.description = command.description(); + this.payAmount = command.payAmount(); + this.paymentType = command.paymentType(); // 스케줄 삭제 처리 - if (ObjectUtils.isNotEmpty(deleteScheduleIds)) - deleteSchedules(deleteScheduleIds); + if (ObjectUtils.isNotEmpty(command.deleteScheduleIds())) + deleteSchedules(command.deleteScheduleIds()); // 스케줄 수정 처리 - if (ObjectUtils.isNotEmpty(updateSchedules)) - updateSchedules(updateSchedules); + if (ObjectUtils.isNotEmpty(command.updateSchedules())) + updateSchedules(command.updateSchedules()); // 스케줄 추가 처리 - if (ObjectUtils.isNotEmpty(createSchedules)) - addSchedules(createSchedules); + if (ObjectUtils.isNotEmpty(command.createSchedules())) + addSchedules(command.createSchedules()); } /** * 스케줄 추가 * @param createSchedules 스케줄 추가 정보 List */ - public void addSchedules(List createSchedules) { - for (CreatePostingScheduleRequestDto createDto : createSchedules) { + public void addSchedules(List createSchedules) { + for (PostingScheduleCommand createCommand : createSchedules) { PostingSchedule newSchedule = PostingSchedule.create( - createDto.getWorkingDays(), - createDto.getStartTime(), - createDto.getEndTime(), - createDto.getPositionsNeeded(), - createDto.getPosition(), + createCommand.workingDays(), + createCommand.startTime(), + createCommand.endTime(), + createCommand.positionsNeeded(), + createCommand.position(), this ); this.schedules.add(newSchedule); @@ -147,21 +143,19 @@ public void addSchedules(List createSchedules) * 스케줄 수정 * @param updateSchedules 스케줄 수정 정보 List */ - public void updateSchedules(List updateSchedules) { - for (UpdatePostingScheduleDto updateDto : updateSchedules) { + public void updateSchedules(List updateSchedules) { + for (UpdatePostingScheduleCommand updateCommand : updateSchedules) { PostingSchedule existingSchedule = this.schedules.stream() - .filter(schedule -> schedule.getId().equals(updateDto.getId())) + .filter(schedule -> schedule.getId().equals(updateCommand.id())) .findFirst() .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND, "수정할 스케줄을 찾을 수 없습니다.")); existingSchedule.update( - updateDto.getWorkingDays().stream() - .map(DayOfWeek::valueOf) - .toList(), - LocalTime.parse(updateDto.getStartTime()), - LocalTime.parse(updateDto.getEndTime()), - updateDto.getPositionsNeeded(), - updateDto.getPosition() + updateCommand.workingDays(), + updateCommand.startTime(), + updateCommand.endTime(), + updateCommand.positionsNeeded(), + updateCommand.position() ); } } diff --git a/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java b/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java index e47a8abdb..88deed4ff 100644 --- a/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java +++ b/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java @@ -1,7 +1,8 @@ package com.dreamteam.alter.domain.posting.port.inbound; -import com.dreamteam.alter.adapter.inbound.general.posting.dto.CreatePostingRequestDto; +import com.dreamteam.alter.domain.posting.command.CreatePostingCommand; +import com.dreamteam.alter.domain.user.context.ManagerActor; public interface CreatePostingUseCase { - void execute(CreatePostingRequestDto request); + void execute(CreatePostingCommand command, ManagerActor actor); } diff --git a/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java b/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java index 147f7d285..005084a3d 100644 --- a/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java +++ b/src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java @@ -1,8 +1,8 @@ package com.dreamteam.alter.domain.posting.port.inbound; -import com.dreamteam.alter.adapter.inbound.manager.posting.dto.UpdatePostingRequestDto; +import com.dreamteam.alter.domain.posting.command.UpdatePostingCommand; import com.dreamteam.alter.domain.user.context.ManagerActor; public interface ManagerUpdatePostingUseCase { - void execute(Long postingId, UpdatePostingRequestDto request, ManagerActor actor); + void execute(Long postingId, UpdatePostingCommand command, ManagerActor actor); } From 71d33dfe4f8b90d4955e886c794f900a4565c8f2 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:36 +0900 Subject: [PATCH 04/22] =?UTF-8?q?fix:=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=EA=B7=BC=EB=AC=B4=EC=9D=BC=EC=A0=95=EC=9D=84=20=EA=B3=B5?= =?UTF-8?q?=EA=B3=A0=20=EC=9D=91=EB=8B=B5=EC=97=90=EC=84=9C=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/readonly/ManagerPostingDetailResponse.java | 2 +- .../persistence/readonly/ManagerPostingListResponse.java | 6 +++++- .../posting/persistence/readonly/PostingDetailResponse.java | 2 +- .../posting/persistence/readonly/PostingListResponse.java | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java index d4e6052fc..d5eee5b07 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java @@ -48,7 +48,7 @@ public static ManagerPostingDetailResponse of( posting.getStatus(), posting.getCreatedAt(), posting.getUpdatedAt(), - posting.getSchedules() + posting.getActiveSchedules() ); } } diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java index 6c06539a9..fe8727e32 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java @@ -3,6 +3,7 @@ import com.dreamteam.alter.domain.posting.entity.Posting; import com.dreamteam.alter.domain.posting.entity.PostingSchedule; import com.dreamteam.alter.domain.posting.type.PaymentType; +import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.dreamteam.alter.domain.workspace.entity.Workspace; import lombok.*; @@ -23,6 +24,8 @@ public class ManagerPostingListResponse { private PaymentType paymentType; + private PostingStatus status; + private LocalDateTime createdAt; private List schedules; @@ -35,8 +38,9 @@ public static ManagerPostingListResponse of(Posting posting) { .title(posting.getTitle()) .payAmount(posting.getPayAmount()) .paymentType(posting.getPaymentType()) + .status(posting.getStatus()) .createdAt(posting.getCreatedAt()) - .schedules(posting.getSchedules()) + .schedules(posting.getActiveSchedules()) .workspace(posting.getWorkspace()) .build(); } diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java index a1f4f4d69..eb6cf3143 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java @@ -44,7 +44,7 @@ public static PostingDetailResponse of( posting.getPayAmount(), posting.getPaymentType(), posting.getCreatedAt(), - posting.getSchedules(), + posting.getActiveSchedules(), scrapped ); } diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java index fd4303948..cea8e7c6f 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java @@ -38,7 +38,7 @@ public static PostingListResponse of(Posting posting, boolean scrapped) { .payAmount(posting.getPayAmount()) .paymentType(posting.getPaymentType()) .createdAt(posting.getCreatedAt()) - .schedules(posting.getSchedules()) + .schedules(posting.getActiveSchedules()) .workspace(posting.getWorkspace()) .scrapped(scrapped) .build(); From 4b20c0d4a8454c5350f488fd57e156e25342c23e Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:43 +0900 Subject: [PATCH 05/22] =?UTF-8?q?fix:=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=EA=B7=BC=EB=AC=B4=EC=9D=BC=EC=A0=95=EC=97=90=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=ED=95=A0=20=EC=88=98=20=EC=97=86=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/PostingScheduleQueryRepositoryImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java index 3d4ad4e11..b3a74d14a 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java @@ -3,6 +3,7 @@ import com.dreamteam.alter.domain.posting.entity.PostingSchedule; import com.dreamteam.alter.domain.posting.entity.QPostingSchedule; import com.dreamteam.alter.domain.posting.port.outbound.PostingScheduleQueryRepository; +import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.ObjectUtils; @@ -24,7 +25,8 @@ public Optional findByIdAndPostingId(Long postingId, Long posti .from(qPostingSchedule) .where( qPostingSchedule.posting.id.eq(postingId), - qPostingSchedule.id.eq(postingScheduleId) + qPostingSchedule.id.eq(postingScheduleId), + qPostingSchedule.status.ne(PostingStatus.DELETED) ) .fetchOne(); From 62cd4f0c869efbb5e8c61bc7782649b486ab27b6 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:43 +0900 Subject: [PATCH 06/22] =?UTF-8?q?fix:=20=EA=B3=B5=EA=B3=A0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=EC=9D=98=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EB=88=84=EB=9D=BD=20=EB=B3=B4=EC=99=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알바생 목록의 근무시간 필터가 삭제된 일정으로 매칭되던 문제 수정 (건수·목록 쿼리 모두 스케줄 조인 시 DELETED 제외) - 삭제된 공고의 매니저 상세 조회 차단 --- .../posting/persistence/PostingQueryRepositoryImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java index 8df55887f..debf56edc 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java @@ -56,6 +56,7 @@ public long getCountOfPostings(PostingListFilterDto filter) { .select(qPosting.countDistinct()) .from(qPosting) .leftJoin(qPosting.schedules, qPostingSchedule) + .on(qPostingSchedule.status.ne(PostingStatus.DELETED)) .leftJoin(qPosting.workspace, qWorkspace) .where( qPosting.status.eq(PostingStatus.OPEN), @@ -103,6 +104,7 @@ public List getPostingsWithCursor(CursorPageRequest getManagerPostingDetail(Long posti .leftJoin(qWorkspace.businessType, QBusinessType.businessType).fetchJoin() .where( qPosting.id.eq(postingId), - qWorkspace.managerUser.eq(managerUser) + qWorkspace.managerUser.eq(managerUser), + qPosting.status.ne(PostingStatus.DELETED) ) .fetchOne(); From 7903d671a950856ba0b5145dcc6c591ea0a0ad53 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:48 +0900 Subject: [PATCH 07/22] =?UTF-8?q?feat:=20=EB=A7=A4=EB=8B=88=EC=A0=80=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=20=EB=AA=A9=EB=A1=9D=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=97=90=20=EA=B3=B5=EA=B3=A0=20=EC=83=81=ED=83=9C=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 --- .../manager/posting/dto/ManagerPostingListResponseDto.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java index 4500f7498..5d169acb5 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java @@ -1,9 +1,11 @@ package com.dreamteam.alter.adapter.inbound.manager.posting.dto; +import com.dreamteam.alter.adapter.inbound.common.dto.DescribedEnumDto; import com.dreamteam.alter.adapter.inbound.general.posting.dto.ManagerPostingListWorkspaceResponseDto; import com.dreamteam.alter.adapter.inbound.general.posting.dto.PostingScheduleResponseDto; import com.dreamteam.alter.adapter.outbound.posting.persistence.readonly.ManagerPostingListResponse; import com.dreamteam.alter.domain.posting.type.PaymentType; +import com.dreamteam.alter.domain.posting.type.PostingStatus; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -35,6 +37,10 @@ public class ManagerPostingListResponseDto { @Schema(description = "급여 타입", example = "HOURLY") private PaymentType paymentType; + @NotNull + @Schema(description = "공고 상태") + private DescribedEnumDto status; + @NotNull @Schema(description = "생성일", example = "2023-10-01T12:00:00") private LocalDateTime createdAt; @@ -60,6 +66,7 @@ public static ManagerPostingListResponseDto from(ManagerPostingListResponse resp .title(response.getTitle()) .payAmount(response.getPayAmount()) .paymentType(response.getPaymentType()) + .status(DescribedEnumDto.of(response.getStatus(), PostingStatus.describe())) .createdAt(response.getCreatedAt()) .schedules(response.getSchedules().stream() .map(PostingScheduleResponseDto::from) From a05bf5e2db0bed6f8bff792bf172e7ca642e5637 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:48 +0900 Subject: [PATCH 08/22] =?UTF-8?q?docs:=20=EA=B3=B5=EA=B3=A0=20API=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=98=88=EC=8B=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=9E=94=EC=9E=AC=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 --- .../controller/ManagerPostingControllerSpec.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java index f7437d7e3..151aa42ca 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java @@ -39,6 +39,10 @@ public interface ManagerPostingControllerSpec { mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = { + @ExampleObject( + name = "존재하지 않거나 자신이 관리하지 않는 업장", + value = "{\"code\" : \"B008\"}" + ), @ExampleObject( name = "서버 내부 오류", value = "{\"code\" : \"C001\"}" @@ -167,14 +171,6 @@ ResponseEntity> updatePostingStatus( name = "존재하지 않는 공고", value = "{\"code\" : \"B007\"}" ), - @ExampleObject( - name = "등록되지 않은 키워드로 요청", - value = "{\"code\" : \"B006\"}" - ), - @ExampleObject( - name = "요청에 키워드가 포함되지 않은 경우", - value = "{\"code\" : \"B001\"}" - ), })), @ApiResponse(responseCode = "404", description = "404 Error 실패 케이스", content = @Content( From 7104cd6e6779a2314febffdde52de48fa82127fc Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Wed, 29 Jul 2026 17:57:48 +0900 Subject: [PATCH 09/22] =?UTF-8?q?test:=20=EA=B3=B5=EA=B3=A0=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=C2=B7=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B7=BC?= =?UTF-8?q?=EB=AC=B4=EC=9D=BC=EC=A0=95=20=ED=95=84=ED=84=B0=EB=A7=81=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 --- .../posting/usecase/CreatePostingTests.java | 110 ++++++++++++++++++ .../usecase/ManagerUpdatePostingTests.java | 104 +++++++++++++++++ .../domain/posting/entity/PostingTests.java | 54 +++++++++ 3 files changed, 268 insertions(+) create mode 100644 src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java create mode 100644 src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java create mode 100644 src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java diff --git a/src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java b/src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java new file mode 100644 index 000000000..32dd21f97 --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java @@ -0,0 +1,110 @@ +package com.dreamteam.alter.application.posting.usecase; + +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.CreatePostingCommand; +import com.dreamteam.alter.domain.posting.command.PostingScheduleCommand; +import com.dreamteam.alter.domain.posting.entity.Posting; +import com.dreamteam.alter.domain.posting.port.outbound.PostingRepository; +import com.dreamteam.alter.domain.posting.type.PaymentType; +import com.dreamteam.alter.domain.posting.type.PostingStatus; +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.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.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.DayOfWeek; +import java.time.LocalTime; +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.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +@DisplayName("CreatePosting 테스트") +class CreatePostingTests { + + @Mock + private PostingRepository postingRepository; + + @Mock + private WorkspaceQueryRepository workspaceQueryRepository; + + @InjectMocks + private CreatePosting createPosting; + + @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.findByIdAndManagerUser(1L, managerUser)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> createPosting.execute(command(), actor)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.WORKSPACE_NOT_FOUND)); + then(postingRepository).should(never()).save(any()); + } + + @Test + @DisplayName("자신이 관리하는 업장이면 공고가 OPEN 상태로 근무일정과 함께 저장된다") + void execute_소유업장_공고저장() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + Workspace workspace = mock(Workspace.class); + + given(actor.getManagerUser()).willReturn(managerUser); + given(workspaceQueryRepository.findByIdAndManagerUser(1L, managerUser)).willReturn(Optional.of(workspace)); + + // when + createPosting.execute(command(), actor); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(Posting.class); + then(postingRepository).should().save(captor.capture()); + + Posting saved = captor.getValue(); + assertThat(saved.getTitle()).isEqualTo("홀서빙 구합니다"); + assertThat(saved.getWorkspace()).isSameAs(workspace); + assertThat(saved.getStatus()).isEqualTo(PostingStatus.OPEN); + assertThat(saved.getPaymentType()).isEqualTo(PaymentType.HOURLY); + assertThat(saved.getSchedules()).hasSize(1); + assertThat(saved.getSchedules().getFirst().getPosition()).isEqualTo("홀서빙"); + assertThat(saved.getSchedules().getFirst().getPositionsAvailable()).isEqualTo(3); + } + + private CreatePostingCommand command() { + return new CreatePostingCommand( + 1L, + "홀서빙 구합니다", + "주말 근무 가능하신 분", + 12000, + PaymentType.HOURLY, + List.of(new PostingScheduleCommand( + List.of(DayOfWeek.MONDAY), + LocalTime.of(9, 0), + LocalTime.of(18, 0), + 3, + "홀서빙" + )) + ); + } +} diff --git a/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java b/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java new file mode 100644 index 000000000..a9fda18e3 --- /dev/null +++ b/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java @@ -0,0 +1,104 @@ +package com.dreamteam.alter.application.posting.usecase; + +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.UpdatePostingCommand; +import com.dreamteam.alter.domain.posting.entity.Posting; +import com.dreamteam.alter.domain.posting.port.outbound.PostingQueryRepository; +import com.dreamteam.alter.domain.posting.type.PaymentType; +import com.dreamteam.alter.domain.posting.type.PostingStatus; +import com.dreamteam.alter.domain.user.context.ManagerActor; +import com.dreamteam.alter.domain.user.entity.ManagerUser; +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 java.util.Optional; + +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.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ManagerUpdatePosting 테스트") +class ManagerUpdatePostingTests { + + @Mock + private PostingQueryRepository postingQueryRepository; + + @InjectMocks + private ManagerUpdatePosting managerUpdatePosting; + + @Test + @DisplayName("존재하지 않거나 자신의 공고가 아니면 POSTING_NOT_FOUND 예외가 발생한다") + void execute_공고없음_예외발생() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + given(actor.getManagerUser()).willReturn(managerUser); + given(postingQueryRepository.findByManagerAndId(1L, managerUser)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> managerUpdatePosting.execute(1L, command(), actor)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.POSTING_NOT_FOUND)); + } + + @Test + @DisplayName("삭제된 공고는 내용을 수정할 수 없다") + void execute_삭제된공고_예외발생() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + Posting posting = mock(Posting.class); + + given(actor.getManagerUser()).willReturn(managerUser); + given(postingQueryRepository.findByManagerAndId(1L, managerUser)).willReturn(Optional.of(posting)); + given(posting.getStatus()).willReturn(PostingStatus.DELETED); + + // when & then + assertThatThrownBy(() -> managerUpdatePosting.execute(1L, command(), actor)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.CONFLICT)); + then(posting).should(never()).updateContent(command()); + } + + @Test + @DisplayName("모집 중인 공고는 내용이 수정된다") + void execute_정상공고_수정() { + // given + ManagerActor actor = mock(ManagerActor.class); + ManagerUser managerUser = mock(ManagerUser.class); + Posting posting = mock(Posting.class); + UpdatePostingCommand command = command(); + + given(actor.getManagerUser()).willReturn(managerUser); + given(postingQueryRepository.findByManagerAndId(1L, managerUser)).willReturn(Optional.of(posting)); + given(posting.getStatus()).willReturn(PostingStatus.OPEN); + + // when + managerUpdatePosting.execute(1L, command, actor); + + // then + then(posting).should().updateContent(command); + } + + private UpdatePostingCommand command() { + return new UpdatePostingCommand( + "수정된 제목", + "수정된 설명", + 13000, + PaymentType.HOURLY, + List.of(), + List.of(), + List.of() + ); + } +} diff --git a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java new file mode 100644 index 000000000..1f8d3453d --- /dev/null +++ b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java @@ -0,0 +1,54 @@ +package com.dreamteam.alter.domain.posting.entity; + +import com.dreamteam.alter.domain.posting.type.PostingStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Posting 테스트") +class PostingTests { + + @Test + @DisplayName("삭제된 근무일정은 활성 일정 목록에서 제외된다") + void getActiveSchedules_삭제일정제외() { + // given + Posting posting = new Posting(); + PostingSchedule open = createSchedule(posting, "홀서빙"); + PostingSchedule deleted = createSchedule(posting, "주방보조"); + deleted.updateStatus(PostingStatus.DELETED); + ReflectionTestUtils.setField(posting, "schedules", List.of(open, deleted)); + + // when + List result = posting.getActiveSchedules(); + + // then + assertThat(result).containsExactly(open); + } + + @Test + @DisplayName("근무일정이 없으면 빈 목록을 반환한다") + void getActiveSchedules_일정없음_빈목록() { + // given + Posting posting = new Posting(); + + // when & then + assertThat(posting.getActiveSchedules()).isEmpty(); + } + + private PostingSchedule createSchedule(Posting posting, String position) { + return PostingSchedule.create( + List.of(DayOfWeek.MONDAY), + LocalTime.of(9, 0), + LocalTime.of(18, 0), + 1, + position, + posting + ); + } +} From ede2c0ca74e54f03c082e47a904c5a8de211ad8b Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Fri, 31 Jul 2026 15:51:03 +0900 Subject: [PATCH 10/22] =?UTF-8?q?fix:=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=EA=B7=BC=EB=AC=B4=EC=9D=BC=EC=A0=95=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=C2=B7=EC=82=AD=EC=A0=9C=20=EB=8C=80=EC=83=81=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dreamteam/alter/domain/posting/entity/Posting.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java index bb04db790..410d3afa6 100644 --- a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java +++ b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java @@ -145,7 +145,7 @@ public void addSchedules(List createSchedules) { */ public void updateSchedules(List updateSchedules) { for (UpdatePostingScheduleCommand updateCommand : updateSchedules) { - PostingSchedule existingSchedule = this.schedules.stream() + PostingSchedule existingSchedule = getActiveSchedules().stream() .filter(schedule -> schedule.getId().equals(updateCommand.id())) .findFirst() .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND, "수정할 스케줄을 찾을 수 없습니다.")); @@ -166,7 +166,7 @@ public void updateSchedules(List updateSchedules) */ public void deleteSchedules(List deleteScheduleIds) { for (Long scheduleId : deleteScheduleIds) { - PostingSchedule existingSchedule = this.schedules.stream() + PostingSchedule existingSchedule = getActiveSchedules().stream() .filter(schedule -> schedule.getId().equals(scheduleId)) .findFirst() .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND, "삭제할 스케줄을 찾을 수 없습니다.")); From ad0d6567ac642c8defcbf7e23ad790988de2579e Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Fri, 31 Jul 2026 15:51:06 +0900 Subject: [PATCH 11/22] =?UTF-8?q?fix:=20=EC=8A=A4=EC=BC=80=EC=A4=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=9A=94=EC=B2=AD=EC=9D=98=20=EC=9A=94?= =?UTF-8?q?=EC=9D=BC=C2=B7=EC=8B=9C=EA=B0=81=EC=9D=84=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=20=EB=B0=94=EC=9D=B8=EB=94=A9=EC=9C=BC=EB=A1=9C=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../posting/dto/UpdatePostingScheduleDto.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java index 2f279b300..4e53c1e7e 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java @@ -25,15 +25,15 @@ public class UpdatePostingScheduleDto { @NotEmpty @Schema(description = "근무일", example = "[\"MONDAY\", \"WEDNESDAY\"]") - private List workingDays; + private List workingDays; @NotNull @Schema(description = "시작 시간", example = "09:00") - private String startTime; + private LocalTime startTime; @NotNull @Schema(description = "종료 시간", example = "18:00") - private String endTime; + private LocalTime endTime; @Positive @Schema(description = "필요 인원", example = "3") @@ -44,13 +44,6 @@ public class UpdatePostingScheduleDto { private String position; public UpdatePostingScheduleCommand toCommand() { - return new UpdatePostingScheduleCommand( - id, - workingDays.stream().map(DayOfWeek::valueOf).toList(), - LocalTime.parse(startTime), - LocalTime.parse(endTime), - positionsNeeded, - position - ); + return new UpdatePostingScheduleCommand(id, workingDays, startTime, endTime, positionsNeeded, position); } } From ca6fa40ea441780eb1fbda20da66cf60a257e63c Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Fri, 31 Jul 2026 15:51:18 +0900 Subject: [PATCH 12/22] =?UTF-8?q?fix:=20=ED=99=9C=EC=84=B1=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=97=85=EC=9E=A5=EB=A7=8C=20=EA=B3=B5=EA=B3=A0?= =?UTF-8?q?=EB=A5=BC=20=EB=93=B1=EB=A1=9D=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=A1=B0=EA=B1=B4=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 --- .../posting/controller/ManagerPostingControllerSpec.java | 2 +- .../workspace/persistence/WorkspaceQueryRepositoryImpl.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java index 151aa42ca..9bb4a603a 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java @@ -40,7 +40,7 @@ public interface ManagerPostingControllerSpec { schema = @Schema(implementation = ErrorResponse.class), examples = { @ExampleObject( - name = "존재하지 않거나 자신이 관리하지 않는 업장", + name = "존재하지 않거나, 자신이 관리하지 않거나, 활성화되지 않은 업장", value = "{\"code\" : \"B008\"}" ), @ExampleObject( diff --git a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java index 062fb5a5e..95449f670 100644 --- a/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java +++ b/src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java @@ -72,7 +72,8 @@ public Optional findByIdAndManagerUser(Long id, ManagerUser managerUs .selectFrom(qWorkspace) .where( qWorkspace.id.eq(id), - qWorkspace.managerUser.eq(managerUser) + qWorkspace.managerUser.eq(managerUser), + qWorkspace.status.eq(WorkspaceStatus.ACTIVATED) ) .fetchOne() ); From e5c6cbd32ed651ea99f76abcdf34c8fb2cff0c94 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Fri, 31 Jul 2026 15:51:22 +0900 Subject: [PATCH 13/22] =?UTF-8?q?docs:=20=EA=B3=B5=EA=B3=A0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20=EC=8A=A4=ED=8E=99=EC=97=90=20409=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ManagerPostingControllerSpec.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java index 9bb4a603a..8a72912e2 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java @@ -186,6 +186,16 @@ ResponseEntity> updatePostingStatus( value = "{\"code\" : \"B019\"}" ), })), + @ApiResponse(responseCode = "409", description = "409 Error 실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "DELETED 상태의 공고는 내용 수정 불가", + value = "{\"code\" : \"B020\"}" + ), + })), }) ResponseEntity> updatePosting( @PathVariable Long postingId, From f28c8db52d5e1cffa4330fe26981bc6c0b22040b Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Fri, 31 Jul 2026 15:51:25 +0900 Subject: [PATCH 14/22] =?UTF-8?q?test:=20=EC=82=AD=EC=A0=9C=EB=90=9C=20?= =?UTF-8?q?=EA=B7=BC=EB=AC=B4=EC=9D=BC=EC=A0=95=20=EC=88=98=EC=A0=95=C2=B7?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=B0=A8=EB=8B=A8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=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 --- .../domain/posting/entity/PostingTests.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java index 1f8d3453d..41445a6f6 100644 --- a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java +++ b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java @@ -1,5 +1,8 @@ package com.dreamteam.alter.domain.posting.entity; +import com.dreamteam.alter.common.exception.CustomException; +import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.UpdatePostingScheduleCommand; import com.dreamteam.alter.domain.posting.type.PostingStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,6 +13,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; @DisplayName("Posting 테스트") class PostingTests { @@ -41,6 +45,47 @@ class PostingTests { assertThat(posting.getActiveSchedules()).isEmpty(); } + @Test + @DisplayName("삭제된 근무일정은 수정할 수 없다") + void updateSchedules_삭제일정_예외발생() { + // given + Posting posting = new Posting(); + PostingSchedule deleted = createSchedule(posting, 1L, "주방보조"); + deleted.updateStatus(PostingStatus.DELETED); + ReflectionTestUtils.setField(posting, "schedules", List.of(deleted)); + + UpdatePostingScheduleCommand command = new UpdatePostingScheduleCommand( + 1L, + List.of(DayOfWeek.TUESDAY), + LocalTime.of(10, 0), + LocalTime.of(19, 0), + 5, + "홀서빙" + ); + + // when & then + assertThatThrownBy(() -> posting.updateSchedules(List.of(command))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND)); + assertThat(deleted.getPosition()).isEqualTo("주방보조"); + assertThat(deleted.getPositionsAvailable()).isEqualTo(1); + } + + @Test + @DisplayName("이미 삭제된 근무일정은 다시 삭제할 수 없다") + void deleteSchedules_삭제일정_예외발생() { + // given + Posting posting = new Posting(); + PostingSchedule deleted = createSchedule(posting, 1L, "주방보조"); + deleted.updateStatus(PostingStatus.DELETED); + ReflectionTestUtils.setField(posting, "schedules", List.of(deleted)); + + // when & then + assertThatThrownBy(() -> posting.deleteSchedules(List.of(1L))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND)); + } + private PostingSchedule createSchedule(Posting posting, String position) { return PostingSchedule.create( List.of(DayOfWeek.MONDAY), @@ -51,4 +96,10 @@ private PostingSchedule createSchedule(Posting posting, String position) { posting ); } + + private PostingSchedule createSchedule(Posting posting, Long id, String position) { + PostingSchedule schedule = createSchedule(posting, position); + ReflectionTestUtils.setField(schedule, "id", id); + return schedule; + } } From 51416acc67cdb5cbd34aeecd47b075714af9ce0b Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Fri, 31 Jul 2026 15:51:25 +0900 Subject: [PATCH 15/22] =?UTF-8?q?test:=20=EA=B3=B5=EA=B3=A0=20=EB=AF=B8?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EA=B2=80=EC=A6=9D=20=EB=A7=A4=EC=B2=98?= =?UTF-8?q?=EB=A5=BC=20any=EB=A1=9C=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/posting/usecase/ManagerUpdatePostingTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java b/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java index a9fda18e3..aacc880cd 100644 --- a/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java +++ b/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java @@ -21,6 +21,7 @@ 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.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -67,7 +68,7 @@ class ManagerUpdatePostingTests { assertThatThrownBy(() -> managerUpdatePosting.execute(1L, command(), actor)) .isInstanceOf(CustomException.class) .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.CONFLICT)); - then(posting).should(never()).updateContent(command()); + then(posting).should(never()).updateContent(any()); } @Test From 5d011b726ac505501b63169da3950662c3dded40 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 15:07:23 +0900 Subject: [PATCH 16/22] =?UTF-8?q?fix:=20=EC=B1=84=ED=8C=85=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=20=EB=B2=88=ED=98=B8=20=EC=A4=91=EB=B3=B5=20=ED=95=B4?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V4·V5·V6가 업종(#97)과 채팅(#95)에서 각각 중복돼 Flyway가 기동 단계에서 실패했다. 채팅 계열을 V11~V14로 옮긴다. 네 파일은 스키마 생성 → 백필 → 인덱스 순서로 서로 의존하므로 상대 순서를 유지한 채 함께 이동한다. --- .../{V4__unify_chat_schema.sql => V11__unify_chat_schema.sql} | 0 ...ontent_nullable.sql => V12__chat_message_content_nullable.sql} | 0 ...pace_group_chat.sql => V13__backfill_workspace_group_chat.sql} | 0 ...up_chat_room.sql => V14__unique_workspace_group_chat_room.sql} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V4__unify_chat_schema.sql => V11__unify_chat_schema.sql} (100%) rename src/main/resources/db/migration/{V5__chat_message_content_nullable.sql => V12__chat_message_content_nullable.sql} (100%) rename src/main/resources/db/migration/{V6__backfill_workspace_group_chat.sql => V13__backfill_workspace_group_chat.sql} (100%) rename src/main/resources/db/migration/{V7__unique_workspace_group_chat_room.sql => V14__unique_workspace_group_chat_room.sql} (100%) diff --git a/src/main/resources/db/migration/V4__unify_chat_schema.sql b/src/main/resources/db/migration/V11__unify_chat_schema.sql similarity index 100% rename from src/main/resources/db/migration/V4__unify_chat_schema.sql rename to src/main/resources/db/migration/V11__unify_chat_schema.sql diff --git a/src/main/resources/db/migration/V5__chat_message_content_nullable.sql b/src/main/resources/db/migration/V12__chat_message_content_nullable.sql similarity index 100% rename from src/main/resources/db/migration/V5__chat_message_content_nullable.sql rename to src/main/resources/db/migration/V12__chat_message_content_nullable.sql diff --git a/src/main/resources/db/migration/V6__backfill_workspace_group_chat.sql b/src/main/resources/db/migration/V13__backfill_workspace_group_chat.sql similarity index 100% rename from src/main/resources/db/migration/V6__backfill_workspace_group_chat.sql rename to src/main/resources/db/migration/V13__backfill_workspace_group_chat.sql diff --git a/src/main/resources/db/migration/V7__unique_workspace_group_chat_room.sql b/src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql similarity index 100% rename from src/main/resources/db/migration/V7__unique_workspace_group_chat_room.sql rename to src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql From 59a1bb0819aa0ef1881d75b508ef610fc2302fe4 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 15:07:29 +0900 Subject: [PATCH 17/22] =?UTF-8?q?fix:=20=EC=9A=94=EC=B2=AD=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EC=97=AD=EC=A7=81=EB=A0=AC=ED=99=94=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EB=A5=BC=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=ED=8F=AC=EB=A7=B7=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HttpMessageNotReadableException 핸들러가 없어 잘못된 요일·시각 값이 오면 상태는 400이지만 body가 Spring 기본 형식으로 나갔다. 앱의 오류 파싱이 깨진다. --- .../handler/GlobalExceptionHandler.java | 12 ++++ .../handler/GlobalExceptionHandlerTests.java | 61 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java diff --git a/src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java index cb61ef9ca..c2fbe0d38 100644 --- a/src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java @@ -10,6 +10,7 @@ import jakarta.validation.ConstraintViolationException; import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -83,6 +84,17 @@ public ResponseEntity> handleMethodArgumentTypeMismatchExcep .body(ErrorResponse.of(errorCode)); } + /** + * 요청 본문 역직렬화 실패. 잘못된 JSON 이나 enum · 시각 등 타입 변환 실패가 여기로 온다. + * 핸들러가 없으면 Spring 기본 응답이 나가 프로젝트 오류 포맷과 어긋난다. + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + ErrorCode errorCode = ErrorCode.ILLEGAL_ARGUMENT; + return ResponseEntity.status(errorCode.getStatus()) + .body(ErrorResponse.of(errorCode, "요청 본문을 해석할 수 없습니다.")); + } + @ExceptionHandler(MissingServletRequestPartException.class) public ResponseEntity> handleMissingServletRequestPartException(MissingServletRequestPartException e) { ErrorCode errorCode = ErrorCode.ILLEGAL_ARGUMENT; diff --git a/src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java b/src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java new file mode 100644 index 000000000..9a616df93 --- /dev/null +++ b/src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java @@ -0,0 +1,61 @@ +package com.dreamteam.alter.common.exception.handler; + +import com.dreamteam.alter.adapter.inbound.manager.posting.dto.UpdatePostingScheduleDto; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("GlobalExceptionHandler 테스트") +class GlobalExceptionHandlerTests { + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(new TestController()) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + } + + @Test + @DisplayName("요일 값이 잘못되면 프로젝트 오류 포맷으로 400을 반환한다") + void 잘못된_요일값_400() throws Exception { + String body = """ + {"id": 1, "workingDays": ["monday"], "startTime": "09:00", "endTime": "18:00", "positionsNeeded": 1, "position": "홀서빙"} + """; + + mockMvc.perform(post("/test").contentType(MediaType.APPLICATION_JSON).content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("B001")); + } + + @Test + @DisplayName("시각 값이 잘못되면 프로젝트 오류 포맷으로 400을 반환한다") + void 잘못된_시각값_400() throws Exception { + String body = """ + {"id": 1, "workingDays": ["MONDAY"], "startTime": "25:00", "endTime": "18:00", "positionsNeeded": 1, "position": "홀서빙"} + """; + + mockMvc.perform(post("/test").contentType(MediaType.APPLICATION_JSON).content(body)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("B001")); + } + + @RestController + static class TestController { + + @PostMapping("/test") + void receive(@RequestBody UpdatePostingScheduleDto request) { + } + } +} From 6537d58e2345f2fdd29ab366444b8766b70ee021 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 15:08:14 +0900 Subject: [PATCH 18/22] =?UTF-8?q?refactor:=20=EC=82=AD=EC=A0=9C=EB=90=9C?= =?UTF-8?q?=20=EA=B3=B5=EA=B3=A0=20=EB=B3=80=EA=B2=BD=20=EC=B0=A8=EB=8B=A8?= =?UTF-8?q?=EC=9D=84=20=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 같은 가드가 ManagerUpdatePosting과 ManagerUpdatePostingStatus에 중복돼 있었고, Posting.updateContent 자체는 상태를 검사하지 않아 다른 호출부가 규칙을 우회할 수 있었다. validateModifiable()로 엔티티가 불변식을 지키게 한다. UseCase 테스트의 삭제 공고 케이스는 mock으로는 검증할 게 없어져 PostingTests의 실제 엔티티 검증으로 대체한다. --- .../posting/usecase/ManagerUpdatePosting.java | 6 --- .../usecase/ManagerUpdatePostingStatus.java | 6 --- .../alter/domain/posting/entity/Posting.java | 13 +++++ .../usecase/ManagerUpdatePostingTests.java | 25 +--------- .../domain/posting/entity/PostingTests.java | 48 +++++++++++++++++++ 5 files changed, 62 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java b/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java index 98664629c..b935051ea 100644 --- a/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java +++ b/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java @@ -9,7 +9,6 @@ import com.dreamteam.alter.domain.posting.entity.Posting; import com.dreamteam.alter.domain.posting.port.inbound.ManagerUpdatePostingUseCase; import com.dreamteam.alter.domain.posting.port.outbound.PostingQueryRepository; -import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.dreamteam.alter.domain.user.context.ManagerActor; import com.dreamteam.alter.domain.user.entity.ManagerUser; @@ -29,11 +28,6 @@ public void execute(Long postingId, UpdatePostingCommand command, ManagerActor a Posting posting = postingQueryRepository.findByManagerAndId(postingId, managerUser) .orElseThrow(() -> new CustomException(ErrorCode.POSTING_NOT_FOUND)); - // 삭제된 공고는 내용 수정 불가 - if (PostingStatus.DELETED.equals(posting.getStatus())) { - throw new CustomException(ErrorCode.CONFLICT); - } - posting.updateContent(command); } } diff --git a/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java b/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java index 30eee0c54..a4a01c3e8 100644 --- a/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java +++ b/src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java @@ -6,7 +6,6 @@ import com.dreamteam.alter.domain.posting.entity.Posting; import com.dreamteam.alter.domain.posting.port.inbound.ManagerUpdatePostingStatusUseCase; import com.dreamteam.alter.domain.posting.port.outbound.PostingQueryRepository; -import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.dreamteam.alter.domain.user.context.ManagerActor; import com.dreamteam.alter.domain.user.entity.ManagerUser; import lombok.RequiredArgsConstructor; @@ -27,11 +26,6 @@ public void execute(Long postingId, UpdatePostingStatusRequestDto request, Manag Posting posting = postingQueryRepository.findByManagerAndId(postingId, managerUser) .orElseThrow(() -> new CustomException(ErrorCode.POSTING_NOT_FOUND)); - // DELETED 상태인 경우 상태 변경 불가 - if (PostingStatus.DELETED.equals(posting.getStatus())) { - throw new CustomException(ErrorCode.CONFLICT); - } - posting.updateStatus(request.getStatus()); } } diff --git a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java index 410d3afa6..f6f5eb9ca 100644 --- a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java +++ b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java @@ -86,6 +86,8 @@ public static Posting create(CreatePostingCommand command, Workspace workspace) } public void updateStatus(PostingStatus status) { + validateModifiable(); + this.status = status; } @@ -103,6 +105,8 @@ public List getActiveSchedules() { } public void updateContent(UpdatePostingCommand command) { + validateModifiable(); + this.title = command.title(); this.description = command.description(); this.payAmount = command.payAmount(); @@ -174,4 +178,13 @@ public void deleteSchedules(List deleteScheduleIds) { existingSchedule.updateStatus(PostingStatus.DELETED); } } + + /** + * 삭제된 공고는 더 이상 변경할 수 없다. + */ + private void validateModifiable() { + if (PostingStatus.DELETED.equals(this.status)) { + throw new CustomException(ErrorCode.CONFLICT); + } + } } diff --git a/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java b/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java index aacc880cd..15443d33c 100644 --- a/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java +++ b/src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java @@ -6,7 +6,6 @@ import com.dreamteam.alter.domain.posting.entity.Posting; import com.dreamteam.alter.domain.posting.port.outbound.PostingQueryRepository; import com.dreamteam.alter.domain.posting.type.PaymentType; -import com.dreamteam.alter.domain.posting.type.PostingStatus; import com.dreamteam.alter.domain.user.context.ManagerActor; import com.dreamteam.alter.domain.user.entity.ManagerUser; import org.junit.jupiter.api.DisplayName; @@ -21,11 +20,9 @@ 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.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; @ExtendWith(MockitoExtension.class) @DisplayName("ManagerUpdatePosting 테스트") @@ -53,26 +50,7 @@ class ManagerUpdatePostingTests { } @Test - @DisplayName("삭제된 공고는 내용을 수정할 수 없다") - void execute_삭제된공고_예외발생() { - // given - ManagerActor actor = mock(ManagerActor.class); - ManagerUser managerUser = mock(ManagerUser.class); - Posting posting = mock(Posting.class); - - given(actor.getManagerUser()).willReturn(managerUser); - given(postingQueryRepository.findByManagerAndId(1L, managerUser)).willReturn(Optional.of(posting)); - given(posting.getStatus()).willReturn(PostingStatus.DELETED); - - // when & then - assertThatThrownBy(() -> managerUpdatePosting.execute(1L, command(), actor)) - .isInstanceOf(CustomException.class) - .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.CONFLICT)); - then(posting).should(never()).updateContent(any()); - } - - @Test - @DisplayName("모집 중인 공고는 내용이 수정된다") + @DisplayName("조회한 공고에 수정 명령을 그대로 위임한다") void execute_정상공고_수정() { // given ManagerActor actor = mock(ManagerActor.class); @@ -82,7 +60,6 @@ class ManagerUpdatePostingTests { given(actor.getManagerUser()).willReturn(managerUser); given(postingQueryRepository.findByManagerAndId(1L, managerUser)).willReturn(Optional.of(posting)); - given(posting.getStatus()).willReturn(PostingStatus.OPEN); // when managerUpdatePosting.execute(1L, command, actor); diff --git a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java index 41445a6f6..930118b72 100644 --- a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java +++ b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java @@ -2,7 +2,10 @@ import com.dreamteam.alter.common.exception.CustomException; import com.dreamteam.alter.common.exception.ErrorCode; +import com.dreamteam.alter.domain.posting.command.PostingScheduleCommand; +import com.dreamteam.alter.domain.posting.command.UpdatePostingCommand; import com.dreamteam.alter.domain.posting.command.UpdatePostingScheduleCommand; +import com.dreamteam.alter.domain.posting.type.PaymentType; import com.dreamteam.alter.domain.posting.type.PostingStatus; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -86,6 +89,51 @@ class PostingTests { .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND)); } + @Test + @DisplayName("삭제된 공고는 내용을 수정할 수 없다") + void updateContent_삭제공고_예외발생() { + // given + Posting posting = new Posting(); + ReflectionTestUtils.setField(posting, "status", PostingStatus.DELETED); + ReflectionTestUtils.setField(posting, "title", "원래 제목"); + + // when & then + assertThatThrownBy(() -> posting.updateContent(updateCommand(null, null, null))) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.CONFLICT)); + assertThat(posting.getTitle()).isEqualTo("원래 제목"); + } + + @Test + @DisplayName("삭제된 공고는 상태를 변경할 수 없다") + void updateStatus_삭제공고_예외발생() { + // given + Posting posting = new Posting(); + ReflectionTestUtils.setField(posting, "status", PostingStatus.DELETED); + + // when & then + assertThatThrownBy(() -> posting.updateStatus(PostingStatus.OPEN)) + .isInstanceOf(CustomException.class) + .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.CONFLICT)); + assertThat(posting.getStatus()).isEqualTo(PostingStatus.DELETED); + } + + private UpdatePostingCommand updateCommand( + List createSchedules, + List updateSchedules, + List deleteScheduleIds + ) { + return new UpdatePostingCommand( + "제목", + "설명", + 12000, + PaymentType.HOURLY, + createSchedules, + updateSchedules, + deleteScheduleIds + ); + } + private PostingSchedule createSchedule(Posting posting, String position) { return PostingSchedule.create( List.of(DayOfWeek.MONDAY), From ee15e43b57ca5a850b1c8597b63fa28ef317039f Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 15:08:28 +0900 Subject: [PATCH 19/22] =?UTF-8?q?fix:=20=ED=99=9C=EC=84=B1=20=EA=B7=BC?= =?UTF-8?q?=EB=AC=B4=EC=9D=BC=EC=A0=95=EC=9D=B4=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B3=B5=EA=B3=A0=EB=A5=BC=20=EB=AA=A8=EC=A7=91=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일정을 전부 삭제해도 공고가 OPEN으로 남아 목록에 schedules 빈 배열로 노출됐다. 모집할 일정이 없으면 마감된 공고이므로 CLOSED로 넘긴다. 목록 쿼리가 모두 status=OPEN을 걸고 있어 자연히 빠지고, 지원도 함께 닫힌다. 판정은 삭제·수정·추가를 모두 마친 뒤에 한다. 전부 지우고 새로 추가하는 요청을 마감으로 잘못 처리하지 않기 위해서다. --- .../ManagerPostingControllerSpec.java | 5 +- .../alter/domain/posting/entity/Posting.java | 12 ++++ .../domain/posting/entity/PostingTests.java | 64 +++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java index 8a72912e2..b834c7b5a 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java @@ -159,7 +159,10 @@ ResponseEntity> updatePostingStatus( @Valid @RequestBody UpdatePostingStatusRequestDto request ); - @Operation(summary = "매니저 - 내가 등록한 공고 내용 수정", description = "") + @Operation( + summary = "매니저 - 내가 등록한 공고 내용 수정", + description = "수정을 마친 뒤 남은 근무일정이 하나도 없으면 공고가 모집 완료(CLOSED)로 바뀌며 더 이상 지원을 받지 않습니다." + ) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "공고 내용 수정 성공"), @ApiResponse(responseCode = "400", description = "실패 케이스", diff --git a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java index f6f5eb9ca..c278b3895 100644 --- a/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java +++ b/src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java @@ -123,6 +123,8 @@ public void updateContent(UpdatePostingCommand command) { // 스케줄 추가 처리 if (ObjectUtils.isNotEmpty(command.createSchedules())) addSchedules(command.createSchedules()); + + closeIfNoActiveSchedules(); } /** @@ -187,4 +189,14 @@ private void validateModifiable() { throw new CustomException(ErrorCode.CONFLICT); } } + + /** + * 모집할 근무일정이 하나도 남지 않으면 마감된 공고로 본다. + * 삭제·수정·추가를 모두 마친 뒤에 판정해야 전부 지우고 새로 추가하는 요청을 마감으로 잘못 처리하지 않는다. + */ + private void closeIfNoActiveSchedules() { + if (PostingStatus.OPEN.equals(this.status) && getActiveSchedules().isEmpty()) { + this.status = PostingStatus.CLOSED; + } + } } diff --git a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java index 930118b72..0e98aa458 100644 --- a/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java +++ b/src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java @@ -13,6 +13,7 @@ import java.time.DayOfWeek; import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -89,6 +90,63 @@ class PostingTests { .satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.NOT_FOUND)); } + @Test + @DisplayName("마지막 근무일정을 삭제하면 공고가 모집 완료로 바뀐다") + void updateContent_마지막일정삭제_자동종료() { + // given + Posting posting = openPosting(); + PostingSchedule only = createSchedule(posting, 1L, "홀서빙"); + ReflectionTestUtils.setField(posting, "schedules", new ArrayList<>(List.of(only))); + + // when + posting.updateContent(updateCommand(null, null, List.of(1L))); + + // then + assertThat(posting.getActiveSchedules()).isEmpty(); + assertThat(posting.getStatus()).isEqualTo(PostingStatus.CLOSED); + } + + @Test + @DisplayName("근무일정을 전부 삭제하고 새로 추가하면 모집 중을 유지한다") + void updateContent_전부삭제후추가_모집중유지() { + // given + Posting posting = openPosting(); + PostingSchedule only = createSchedule(posting, 1L, "홀서빙"); + ReflectionTestUtils.setField(posting, "schedules", new ArrayList<>(List.of(only))); + + PostingScheduleCommand created = new PostingScheduleCommand( + List.of(DayOfWeek.FRIDAY), + LocalTime.of(13, 0), + LocalTime.of(21, 0), + 2, + "주방보조" + ); + + // when + posting.updateContent(updateCommand(List.of(created), null, List.of(1L))); + + // then + assertThat(posting.getActiveSchedules()).hasSize(1); + assertThat(posting.getStatus()).isEqualTo(PostingStatus.OPEN); + } + + @Test + @DisplayName("근무일정이 남아 있으면 모집 중을 유지한다") + void updateContent_일부일정삭제_모집중유지() { + // given + Posting posting = openPosting(); + PostingSchedule first = createSchedule(posting, 1L, "홀서빙"); + PostingSchedule second = createSchedule(posting, 2L, "주방보조"); + ReflectionTestUtils.setField(posting, "schedules", new ArrayList<>(List.of(first, second))); + + // when + posting.updateContent(updateCommand(null, null, List.of(1L))); + + // then + assertThat(posting.getActiveSchedules()).containsExactly(second); + assertThat(posting.getStatus()).isEqualTo(PostingStatus.OPEN); + } + @Test @DisplayName("삭제된 공고는 내용을 수정할 수 없다") void updateContent_삭제공고_예외발생() { @@ -118,6 +176,12 @@ class PostingTests { assertThat(posting.getStatus()).isEqualTo(PostingStatus.DELETED); } + private Posting openPosting() { + Posting posting = new Posting(); + ReflectionTestUtils.setField(posting, "status", PostingStatus.OPEN); + return posting; + } + private UpdatePostingCommand updateCommand( List createSchedules, List updateSchedules, From 294451b95eab8b478c2d6d0284167ae9b686dd49 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 15:08:35 +0900 Subject: [PATCH 20/22] =?UTF-8?q?fix:=20=EA=B3=B5=EA=B3=A0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=EC=9A=94=EC=B2=AD=EC=9D=98=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=9D=BC=EC=A0=95=20ID=20=EB=AA=A9=EB=A1=9D=20=EC=A0=95?= =?UTF-8?q?=EA=B7=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createSchedules·updateSchedules만 null을 빈 목록으로 바꿔 넘기고 deleteScheduleIds는 그대로 넘어갔다. Command를 받는 쪽이 세 목록 중 둘만 non-null이라는 규칙을 알아야 했다. --- .../inbound/manager/posting/dto/UpdatePostingRequestDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java index 4365984ca..d67a2d282 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java @@ -62,7 +62,7 @@ public UpdatePostingCommand toCommand() { paymentType, toCommands(createSchedules, CreatePostingScheduleRequestDto::toCommand), toCommands(updateSchedules, UpdatePostingScheduleDto::toCommand), - deleteScheduleIds + ObjectUtils.isEmpty(deleteScheduleIds) ? List.of() : deleteScheduleIds ); } From a3700075d8c1628842432a0c61cd2b977386f156 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 15:08:35 +0900 Subject: [PATCH 21/22] =?UTF-8?q?docs:=20=EA=B3=B5=EA=B3=A0=20=EC=A7=80?= =?UTF-8?q?=EC=9B=90=20API=20=EC=8A=A4=ED=8E=99=EC=97=90=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=EC=9D=91=EB=8B=B5=20=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 존재하지 않는 공고(B007), 모집 종료·중복 지원(B001), 잠금 실패(429/E001)가 스펙에 빠져 있었다. B001 두 건은 코드가 같아 예시에 message를 함께 적는다. --- .../controller/PostingControllerSpec.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/controller/PostingControllerSpec.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/controller/PostingControllerSpec.java index 1538717d1..b11e2105e 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/controller/PostingControllerSpec.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/controller/PostingControllerSpec.java @@ -71,6 +71,10 @@ ResponseEntity> getPostingsWi mediaType = "application/json", schema = @Schema(implementation = ErrorResponse.class), examples = { + @ExampleObject( + name = "존재하지 않는 공고", + value = "{\"code\" : \"B007\"}" + ), @ExampleObject( name = "지원하고자 하는 공고 일정 찾을 수 없음", value = "{\"code\" : \"B010\"}" @@ -78,7 +82,25 @@ ResponseEntity> getPostingsWi @ExampleObject( name = "이미 근무중인 사용자입니다.", value = "{\"code\" : \"B018\"}" + ), + @ExampleObject( + name = "모집이 종료된 공고 (OPEN 이 아닌 상태)", + value = "{\"code\" : \"B001\", \"message\" : \"모집이 종료된 공고입니다.\"}" + ), + @ExampleObject( + name = "이미 지원한 공고 (같은 공고의 다른 근무일정 포함)", + value = "{\"code\" : \"B001\", \"message\" : \"이미 지원한 공고입니다.\"}" ) + })), + @ApiResponse(responseCode = "429", description = "429 Error 실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "동시 지원 요청이 몰려 잠금 획득에 실패", + value = "{\"code\" : \"E001\"}" + ), })) }) ResponseEntity> applyIntoPosting( From 50200874fc4c505be3fe1c8fc4cba9273402fe87 Mon Sep 17 00:00:00 2001 From: Seungwan Yoo Date: Mon, 3 Aug 2026 16:11:05 +0900 Subject: [PATCH 22/22] =?UTF-8?q?fix:=20=EA=B3=B5=EA=B3=A0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EC=8A=A4=EC=BC=80=EC=A4=84=20=EC=9B=90=EC=86=8C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inbound/general/posting/dto/CreatePostingRequestDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java index 78a89b3d2..4dd6cafcf 100644 --- a/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java +++ b/src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java @@ -49,7 +49,7 @@ public class CreatePostingRequestDto { "]") @Valid @NotEmpty - private List schedules; + private List<@NotNull @Valid CreatePostingScheduleRequestDto> schedules; public CreatePostingCommand toCommand() { return new CreatePostingCommand(