diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/BiddingProcessPort.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/BiddingProcessPort.java new file mode 100644 index 00000000..6245fb3e --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/BiddingProcessPort.java @@ -0,0 +1,10 @@ +package com.peauty.customer.business.bidding; + +import com.peauty.domain.bidding.BiddingProcess; + +public interface BiddingProcessPort { + + BiddingProcess getProcessById(Long processId); + BiddingProcess save(BiddingProcess process); + BiddingProcess initProcess(BiddingProcess process); +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/BiddingThreadPort.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/BiddingThreadPort.java new file mode 100644 index 00000000..f0442e24 --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/BiddingThreadPort.java @@ -0,0 +1,4 @@ +package com.peauty.customer.business.bidding; + +public interface BiddingThreadPort { +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/CustomerBiddingService.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/CustomerBiddingService.java new file mode 100644 index 00000000..00b2cc0b --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/CustomerBiddingService.java @@ -0,0 +1,21 @@ +package com.peauty.customer.business.bidding; + +import com.peauty.customer.business.bidding.dto.AcceptEstimateResult; +import com.peauty.customer.business.bidding.dto.SendEstimateProposalCommand; +import com.peauty.customer.business.bidding.dto.SendEstimateProposalResult; + +public interface CustomerBiddingService { + + SendEstimateProposalResult sendEstimateProposal( + Long userId, + Long puppyId, + SendEstimateProposalCommand command + ); + + AcceptEstimateResult acceptEstimate( + Long userId, + Long puppyId, + Long processId, + Long threadId + ); +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/CustomerBiddingServiceImpl.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/CustomerBiddingServiceImpl.java new file mode 100644 index 00000000..7c95ef03 --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/CustomerBiddingServiceImpl.java @@ -0,0 +1,46 @@ +package com.peauty.customer.business.bidding; + +import com.peauty.customer.business.bidding.dto.AcceptEstimateResult; +import com.peauty.customer.business.bidding.dto.SendEstimateProposalCommand; +import com.peauty.customer.business.bidding.dto.SendEstimateProposalResult; +import com.peauty.domain.bidding.BiddingProcess; +import com.peauty.domain.bidding.DesignerId; +import com.peauty.domain.bidding.PuppyId; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CustomerBiddingServiceImpl implements CustomerBiddingService { + + private final BiddingProcessPort biddingProcessPort; + + // TODO 프로세스 접근 검증 ex) 올바른 유저인지... + // TODO 견적요청서 저장 + @Override + @Transactional + public SendEstimateProposalResult sendEstimateProposal( + Long userId, + Long puppyId, + SendEstimateProposalCommand command + ) { + BiddingProcess process = biddingProcessPort.initProcess(BiddingProcess.createNewProcess(new PuppyId(puppyId))); + command.designerIds() + .forEach(id -> process.addNewThread(new DesignerId(id))); + BiddingProcess savedProcess = biddingProcessPort.save(process); + return SendEstimateProposalResult.from(savedProcess); + } + + @Override + @Transactional + public AcceptEstimateResult acceptEstimate( + Long userId, + Long puppyId, + Long processId, + Long threadId + ) { + return null; + } +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/AcceptEstimateResult.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/AcceptEstimateResult.java new file mode 100644 index 00000000..57436f8f --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/AcceptEstimateResult.java @@ -0,0 +1,4 @@ +package com.peauty.customer.business.bidding.dto; + +public record AcceptEstimateResult() { +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/SendEstimateProposalCommand.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/SendEstimateProposalCommand.java new file mode 100644 index 00000000..5f72225d --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/SendEstimateProposalCommand.java @@ -0,0 +1,8 @@ +package com.peauty.customer.business.bidding.dto; + +import java.util.List; + +public record SendEstimateProposalCommand( + List designerIds +) { +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/SendEstimateProposalResult.java b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/SendEstimateProposalResult.java new file mode 100644 index 00000000..1f8e81ad --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/business/bidding/dto/SendEstimateProposalResult.java @@ -0,0 +1,29 @@ +package com.peauty.customer.business.bidding.dto; + +import com.peauty.domain.bidding.BiddingProcess; +import com.peauty.domain.bidding.BiddingProcessStatus; +import com.peauty.domain.bidding.BiddingProcessTimeInfo; +import com.peauty.domain.bidding.BiddingThread; + +import java.util.List; + +public record SendEstimateProposalResult( + Long processId, + Long puppyId, + List designerIds, + BiddingProcessStatus processStatus, + BiddingProcessTimeInfo processTimeInfo +) { + + public static SendEstimateProposalResult from(BiddingProcess process) { + return new SendEstimateProposalResult( + process.getId().orElse(new BiddingProcess.ID(0L)).value(), + process.getPuppyId().value(), + process.getThreads().stream() + .map(thread -> thread.getDesignerId().value()) + .toList(), + process.getStatus(), + process.getTimeInfo() + ); + } +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/implementaion/bidding/BiddingMapper.java b/peauty-customer-api/src/main/java/com/peauty/customer/implementaion/bidding/BiddingMapper.java new file mode 100644 index 00000000..13d2c60c --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/implementaion/bidding/BiddingMapper.java @@ -0,0 +1,71 @@ +package com.peauty.customer.implementaion.bidding; + +import com.peauty.domain.bidding.*; +import com.peauty.persistence.bidding.BiddingProcessEntity; +import com.peauty.persistence.bidding.BiddingThreadEntity; + +import java.util.List; + +public class BiddingMapper { + + public static BiddingProcess toProcessDomain( + BiddingProcessEntity processEntity, + List threadEntities + ) { + List threads = threadEntities.stream() + .map(BiddingMapper::toThreadDomain) + .toList(); + + return BiddingProcess.loadProcess( + new BiddingProcess.ID(processEntity.getId()), + new PuppyId(processEntity.getPuppyId()), + processEntity.getStatus(), + new BiddingProcessTimeInfo(processEntity.getCreatedAt(), processEntity.getStatusModifiedAt()), + threads + ); + } + + public static BiddingProcessEntity toProcessEntity(BiddingProcess process) { + return BiddingProcessEntity.builder() + .id(process.getId().map(BiddingProcess.ID::value).orElse(null)) + .puppyId(process.getPuppyId().value()) + .status(process.getStatus()) + .createdAt(process.getTimeInfo().getCreatedAt()) + .statusModifiedAt(process.getTimeInfo().getStatusModifiedAt()) + .build(); + } + + public static BiddingThread toThreadDomain(BiddingThreadEntity threadEntity) { + return BiddingThread.loadThread( + new BiddingThread.ID(threadEntity.getId()), + new BiddingProcess.ID(threadEntity.getBiddingProcessId()), + new DesignerId(threadEntity.getDesignerId()), + threadEntity.getStep(), + threadEntity.getStatus(), + new BiddingThreadTimeInfo( + threadEntity.getCreatedAt(), + threadEntity.getStepModifiedAt(), + threadEntity.getStatusModifiedAt() + ) + ); + } + + public static BiddingThreadEntity toThreadEntity(BiddingThread thread) { + return BiddingThreadEntity.builder() + .id(thread.getId().map(BiddingThread.ID::value).orElse(null)) + .biddingProcessId(thread.getProcessId().value()) + .designerId(thread.getDesignerId().value()) + .step(thread.getStep()) + .status(thread.getStatus()) + .createdAt(thread.getTimeInfo().getCreatedAt()) + .stepModifiedAt(thread.getTimeInfo().getStepModifiedAt()) + .statusModifiedAt(thread.getTimeInfo().getStatusModifiedAt()) + .build(); + } + + public static List toThreadEntities(List threads) { + return threads.stream() + .map(BiddingMapper::toThreadEntity) + .toList(); + } +} \ No newline at end of file diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/implementaion/bidding/BiddingProcessAdapter.java b/peauty-customer-api/src/main/java/com/peauty/customer/implementaion/bidding/BiddingProcessAdapter.java new file mode 100644 index 00000000..073147db --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/implementaion/bidding/BiddingProcessAdapter.java @@ -0,0 +1,52 @@ +package com.peauty.customer.implementaion.bidding; + +import com.peauty.customer.business.bidding.BiddingProcessPort; +import com.peauty.domain.bidding.BiddingProcess; +import com.peauty.domain.exception.PeautyException; +import com.peauty.domain.response.PeautyResponseCode; +import com.peauty.persistence.bidding.BiddingProcessEntity; +import com.peauty.persistence.bidding.BiddingProcessRepository; +import com.peauty.persistence.bidding.BiddingThreadEntity; +import com.peauty.persistence.bidding.BiddingThreadRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class BiddingProcessAdapter implements BiddingProcessPort { + + private final BiddingProcessRepository biddingProcessRepository; + private final BiddingThreadRepository biddingThreadRepository; + + @Override + public BiddingProcess getProcessById(Long processId) { + BiddingProcessEntity foundProcessEntity = biddingProcessRepository.findById(processId) + .orElseThrow(() -> new PeautyException(PeautyResponseCode.NOT_FOUND_BIDDING_PROCESS)); + List foundBiddingThreadEntities = biddingThreadRepository.findByBiddingProcessId(processId); + return BiddingMapper.toProcessDomain(foundProcessEntity, foundBiddingThreadEntities); + } + + @Override + public BiddingProcess save(BiddingProcess process) { + checkInitProcess(process); + BiddingProcessEntity processEntityToSave = BiddingMapper.toProcessEntity(process); + List threadEntitiesToSave = BiddingMapper.toThreadEntities(process.getThreads()); + BiddingProcessEntity savedProcessEntity = biddingProcessRepository.save(processEntityToSave); + List savedThreadEntities = biddingThreadRepository.saveAll(threadEntitiesToSave); + return BiddingMapper.toProcessDomain(savedProcessEntity, savedThreadEntities); + } + + @Override + public BiddingProcess initProcess(BiddingProcess process) { + BiddingProcessEntity processEntityToSave = BiddingMapper.toProcessEntity(process); + BiddingProcessEntity savedProcessEntity = biddingProcessRepository.save(processEntityToSave); + return BiddingMapper.toProcessDomain(savedProcessEntity, List.of()); + } + + // TODO init 을 강제하기 위함인데.. 다른 방법이 있나 알아보기 + private void checkInitProcess(BiddingProcess process) { + process.getId().orElseThrow(() -> new PeautyException(PeautyResponseCode.NOT_INITIALIZED_PROCESS)); + } +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/CustomerBiddingController.java b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/CustomerBiddingController.java new file mode 100644 index 00000000..81b52b8b --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/CustomerBiddingController.java @@ -0,0 +1,49 @@ +package com.peauty.customer.presentation.controller.bidding; + +import com.peauty.customer.business.bidding.CustomerBiddingService; +import com.peauty.customer.business.bidding.dto.AcceptEstimateResult; +import com.peauty.customer.business.bidding.dto.SendEstimateProposalResult; +import com.peauty.customer.presentation.controller.bidding.dto.AcceptEstimateRequest; +import com.peauty.customer.presentation.controller.bidding.dto.AcceptEstimateResponse; +import com.peauty.customer.presentation.controller.bidding.dto.SendEstimateProposalRequest; +import com.peauty.customer.presentation.controller.bidding.dto.SendEstimateProposalResponse; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/v1/users") +@RequiredArgsConstructor +public class CustomerBiddingController { + + private final CustomerBiddingService customerBiddingService; + + @PostMapping("/{userId}/puppies/{puppyId}/biddings") + @Operation(summary = "입찰 프로세스 시작", description = "디자이너들에게 견적 제안을 전송하면서 입찰 프로세스를 시작합니다.") + public SendEstimateProposalResponse initProcessWithSendEstimateProposal( + @PathVariable Long userId, + @PathVariable Long puppyId, + @RequestBody SendEstimateProposalRequest request + ) { + SendEstimateProposalResult result = customerBiddingService.sendEstimateProposal(userId, puppyId, request.toCommand()); + return SendEstimateProposalResponse.from(result); + } + + @PostMapping("/{userId}/puppies/{puppyId}/biddings/{processId}/threads/{threadId}/accept") + @Operation(summary = "견적 수락", description = "디자이너가 제안한 견적을 수락합니다.") + public AcceptEstimateResponse acceptEstimate( + @PathVariable Long userId, + @PathVariable Long puppyId, + @PathVariable Long processId, + @PathVariable Long threadId, + @RequestBody AcceptEstimateRequest request + ) { + AcceptEstimateResult result = customerBiddingService.acceptEstimate( + userId, + puppyId, + processId, + threadId + ); + return AcceptEstimateResponse.from(result); + } +} \ No newline at end of file diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/AcceptEstimateRequest.java b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/AcceptEstimateRequest.java new file mode 100644 index 00000000..c05e2a1d --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/AcceptEstimateRequest.java @@ -0,0 +1,4 @@ +package com.peauty.customer.presentation.controller.bidding.dto; + +public record AcceptEstimateRequest() { +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/AcceptEstimateResponse.java b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/AcceptEstimateResponse.java new file mode 100644 index 00000000..424b1b33 --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/AcceptEstimateResponse.java @@ -0,0 +1,13 @@ +package com.peauty.customer.presentation.controller.bidding.dto; + +import com.peauty.customer.business.bidding.dto.AcceptEstimateResult; + +public record AcceptEstimateResponse( +) { + + public static AcceptEstimateResponse from(AcceptEstimateResult result) { + return new AcceptEstimateResponse( + + ); + } +} \ No newline at end of file diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/SendEstimateProposalRequest.java b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/SendEstimateProposalRequest.java new file mode 100644 index 00000000..8a2c577c --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/SendEstimateProposalRequest.java @@ -0,0 +1,13 @@ +package com.peauty.customer.presentation.controller.bidding.dto; + +import com.peauty.customer.business.bidding.dto.SendEstimateProposalCommand; + +import java.util.List; + +public record SendEstimateProposalRequest( + List designerIds +) { + public SendEstimateProposalCommand toCommand() { + return new SendEstimateProposalCommand(designerIds); + } +} diff --git a/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/SendEstimateProposalResponse.java b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/SendEstimateProposalResponse.java new file mode 100644 index 00000000..c23fc6b0 --- /dev/null +++ b/peauty-customer-api/src/main/java/com/peauty/customer/presentation/controller/bidding/dto/SendEstimateProposalResponse.java @@ -0,0 +1,25 @@ +package com.peauty.customer.presentation.controller.bidding.dto; + +import com.peauty.customer.business.bidding.dto.SendEstimateProposalResult; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +public record SendEstimateProposalResponse( + Long processId, + Long puppyId, + List designerIds, + String processStatus, + String createdAt +) { + + public static SendEstimateProposalResponse from(SendEstimateProposalResult result) { + return new SendEstimateProposalResponse( + result.processId(), + result.puppyId(), + result.designerIds(), + result.processStatus().getDescription(), + result.processTimeInfo().getCreatedAt().format(DateTimeFormatter.BASIC_ISO_DATE) + ); + } +} diff --git a/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/DesignerBiddingService.java b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/DesignerBiddingService.java new file mode 100644 index 00000000..46eae6c2 --- /dev/null +++ b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/DesignerBiddingService.java @@ -0,0 +1,21 @@ +package com.peauty.designer.business.bidding; + +import com.peauty.designer.business.bidding.dto.CompleteGroomingResult; +import com.peauty.designer.business.bidding.dto.SendEstimateCommand; +import com.peauty.designer.business.bidding.dto.SendEstimateResult; + +public interface DesignerBiddingService { + + SendEstimateResult sendEstimate( + Long userId, + Long processId, + Long threadId, + SendEstimateCommand command + ); + + CompleteGroomingResult completeGrooming( + Long userId, + Long processId, + Long threadId + ); +} diff --git a/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/DesignerBiddingServiceImpl.java b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/DesignerBiddingServiceImpl.java new file mode 100644 index 00000000..b0933f42 --- /dev/null +++ b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/DesignerBiddingServiceImpl.java @@ -0,0 +1,31 @@ +package com.peauty.designer.business.bidding; + +import com.peauty.designer.business.bidding.dto.CompleteGroomingResult; +import com.peauty.designer.business.bidding.dto.SendEstimateCommand; +import com.peauty.designer.business.bidding.dto.SendEstimateResult; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class DesignerBiddingServiceImpl implements DesignerBiddingService { + + @Override + public SendEstimateResult sendEstimate( + Long userId, + Long processId, + Long threadId, + SendEstimateCommand command + ) { + return null; + } + + @Override + public CompleteGroomingResult completeGrooming( + Long userId, + Long processId, + Long threadId + ) { + return null; + } +} diff --git a/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/CompleteGroomingResult.java b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/CompleteGroomingResult.java new file mode 100644 index 00000000..60ae52c3 --- /dev/null +++ b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/CompleteGroomingResult.java @@ -0,0 +1,4 @@ +package com.peauty.designer.business.bidding.dto; + +public record CompleteGroomingResult() { +} diff --git a/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/SendEstimateCommand.java b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/SendEstimateCommand.java new file mode 100644 index 00000000..f8b4e6ca --- /dev/null +++ b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/SendEstimateCommand.java @@ -0,0 +1,4 @@ +package com.peauty.designer.business.bidding.dto; + +public record SendEstimateCommand() { +} diff --git a/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/SendEstimateResult.java b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/SendEstimateResult.java new file mode 100644 index 00000000..c13ba18b --- /dev/null +++ b/peauty-designer-api/src/main/java/com/peauty/designer/business/bidding/dto/SendEstimateResult.java @@ -0,0 +1,4 @@ +package com.peauty.designer.business.bidding.dto; + +public record SendEstimateResult() { +} diff --git a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcess.java b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcess.java index 9fa96c73..c0cfb888 100644 --- a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcess.java +++ b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcess.java @@ -10,7 +10,7 @@ public class BiddingProcess { - @Getter private final ID id; // TODO Optional Getter 적용하기 + private final ID id; @Getter private final PuppyId puppyId; @Getter private BiddingProcessStatus status; @Getter private final BiddingProcessTimeInfo timeInfo; @@ -28,7 +28,7 @@ private BiddingProcess( this.status = status; this.timeInfo = timeInfo; this.threads = new ArrayList<>(threads); - threads.forEach(thread -> thread.registerProcessObserver(this)); + threads.forEach(thread -> thread.registerBelongingProcessObserver(this)); } public static BiddingProcess loadProcess( @@ -51,20 +51,15 @@ public static BiddingProcess createNewProcess(PuppyId puppyId) { ); } - public static BiddingProcess createNewProcess( - PuppyId puppyId, - DesignerId designerId - ) { - BiddingProcess newProcess = createNewProcess(puppyId); - newProcess.addNewThread(designerId); - return newProcess; + public Optional getId() { + return Optional.ofNullable(this.id); } public void addNewThread(DesignerId targetDesignerId) { validateProcessStatus(); checkThreadAlreadyInProcess(targetDesignerId); - BiddingThread newThread = BiddingThread.createNewThread(this.puppyId, targetDesignerId); - newThread.registerProcessObserver(this); + BiddingThread newThread = BiddingThread.createNewThread(this.id, targetDesignerId); + newThread.registerBelongingProcessObserver(this); this.threads.add(newThread); } @@ -105,31 +100,33 @@ public void cancelThread(DesignerId targetThreadDesignerId) { getThreadForChangeState(targetThreadDesignerId).cancel(); } - public BiddingThread getThread(BiddingThread.ID threadThreadId) { + public BiddingThread getThread(BiddingThread.ID targetThreadId) { return threads.stream() - .filter(thread -> thread.getId().value().equals(threadThreadId.value())) + .filter(thread -> thread.getId() + .map(id -> id.value().equals(targetThreadId.value())) + .orElse(false)) .findFirst() .orElseThrow(() -> new PeautyException(PeautyResponseCode.NOT_FOUND_BIDDING_THREAD_IN_PROCESS)); } - public BiddingThread getThread(DesignerId threadThreadDesignerId) { + public BiddingThread getThread(DesignerId targetThreadDesignerId) { return threads.stream() - .filter(thread -> thread.getDesignerId().value().equals(threadThreadDesignerId.value())) + .filter(thread -> thread.getDesignerId().value().equals(targetThreadDesignerId.value())) .findFirst() .orElseThrow(() -> new PeautyException(PeautyResponseCode.NOT_FOUND_BIDDING_THREAD_IN_PROCESS)); } - public void onReservedThreadCancel() { + protected void onReservedThreadCancel() { threads.forEach(BiddingThread::release); changeStatus(BiddingProcessStatus.RESERVED_YET); } - public void onThreadReserved() { + protected void onThreadReserved() { threads.forEach(BiddingThread::waiting); changeStatus(BiddingProcessStatus.RESERVED); } - public void onThreadCompleted() { + protected void onThreadCompleted() { changeStatus(BiddingProcessStatus.COMPLETED); } @@ -165,10 +162,6 @@ private void changeStatus(BiddingProcessStatus status) { timeInfo.onStatusChange(); } - public Optional getId() { - return Optional.ofNullable(this.id); - } - public record ID(Long value) { public boolean isSameId(Long id) { return value.equals(id); diff --git a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcessTimeInfo.java b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcessTimeInfo.java index 1e048db1..81e6930e 100644 --- a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcessTimeInfo.java +++ b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingProcessTimeInfo.java @@ -12,7 +12,7 @@ public class BiddingProcessTimeInfo { private LocalDateTime createdAt; private LocalDateTime statusModifiedAt; - public void onStatusChange() { + protected void onStatusChange() { this.statusModifiedAt = LocalDateTime.now(); } diff --git a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThread.java b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThread.java index 21395ea2..1fa9be62 100644 --- a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThread.java +++ b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThread.java @@ -4,32 +4,34 @@ import com.peauty.domain.response.PeautyResponseCode; import lombok.*; +import java.util.Optional; + @AllArgsConstructor(access = AccessLevel.PRIVATE) public class BiddingThread { - @Getter private final ID id; // TODO Optional Getter 적용하기 - @Getter private final PuppyId puppyId; + private final ID id; + @Getter private final BiddingProcess.ID processId; @Getter private final DesignerId designerId; @Getter private BiddingThreadStep step; @Getter private BiddingThreadStatus status; @Getter private BiddingThreadTimeInfo timeInfo; - private BiddingProcess processObserver; + private BiddingProcess belongingProcessObserver; public static BiddingThread loadThread( ID id, - PuppyId puppyId, + BiddingProcess.ID processId, DesignerId designerId, BiddingThreadStep step, BiddingThreadStatus status, BiddingThreadTimeInfo timeInfo ) { - return new BiddingThread(id, puppyId, designerId, step, status, timeInfo, null); + return new BiddingThread(id, processId, designerId, step, status, timeInfo, null); } - public static BiddingThread createNewThread(PuppyId puppyId, DesignerId designerId) { + protected static BiddingThread createNewThread(BiddingProcess.ID processId, DesignerId designerId) { return new BiddingThread( null, - puppyId, + processId, designerId, BiddingThreadStep.ESTIMATE_REQUEST, BiddingThreadStatus.NORMAL, @@ -38,53 +40,72 @@ public static BiddingThread createNewThread(PuppyId puppyId, DesignerId designer ); } - public void registerProcessObserver(BiddingProcess process) { - this.processObserver = process; + public Optional getId() { + return Optional.ofNullable(id); + } + + protected void registerBelongingProcessObserver(BiddingProcess belongingProcess) { + Long processId = belongingProcess.getId() + .orElseThrow(() -> new PeautyException(PeautyResponseCode.PROCESS_NOT_REGISTERED)) + .value(); + if (!this.processId.isSameId(processId)) { + throw new PeautyException(PeautyResponseCode.ONLY_BELONGING_PROCESS_CAN_BE_OBSERVER); + } + this.belongingProcessObserver = belongingProcess; } - public void responseEstimate() { + protected void responseEstimate() { validateStatusForStepProgressing(); validateProgressTo(BiddingThreadStep.ESTIMATE_RESPONSE); changeToNextStep(); } - public void reserve() { + protected void reserve() { validateStatusForStepProgressing(); validateProgressTo(BiddingThreadStep.RESERVED); changeToNextStep(); - processObserver.onThreadReserved(); + validateObserverRegistered(); + belongingProcessObserver.onThreadReserved(); } - public void complete() { + protected void complete() { validateStatusForStepProgressing(); validateProgressTo(BiddingThreadStep.COMPLETED); changeToNextStep(); - processObserver.onThreadCompleted(); + validateObserverRegistered(); + belongingProcessObserver.onThreadCompleted(); } - public void cancel() { + protected void cancel() { validateCancellation(); if (step.isReserved()) { // TODO 결제 환불 changeStatus(BiddingThreadStatus.CANCELED); - processObserver.onReservedThreadCancel(); + validateObserverRegistered(); + belongingProcessObserver.onReservedThreadCancel(); return; } changeStatus(BiddingThreadStatus.CANCELED); } - public void waiting() { + protected void waiting() { if (status.isNormal() & step.isBefore(BiddingThreadStep.RESERVED)) { changeStatus(BiddingThreadStatus.WAITING); } } - public void release() { + protected void release() { if (status.isWaiting()) { changeStatus(BiddingThreadStatus.NORMAL); } } + private void validateObserverRegistered() { + if (belongingProcessObserver == null) { + throw new PeautyException(PeautyResponseCode.PROCESS_OBSERVER_NOT_REGISTERED); + } + } + private void validateProgressTo(BiddingThreadStep nextStep) { if (step.isCompleted()) { throw new PeautyException(PeautyResponseCode.ALREADY_COMPLETED_BIDDING_THREAD); diff --git a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThreadTimeInfo.java b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThreadTimeInfo.java index d89a548f..08ff78f0 100644 --- a/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThreadTimeInfo.java +++ b/peauty-domain/src/main/java/com/peauty/domain/bidding/BiddingThreadTimeInfo.java @@ -13,11 +13,11 @@ public class BiddingThreadTimeInfo { private LocalDateTime stepModifiedAt; private LocalDateTime statusModifiedAt; - public void onStepChange() { + protected void onStepChange() { this.stepModifiedAt = LocalDateTime.now(); } - public void onStatusChange() { + protected void onStatusChange() { this.statusModifiedAt = LocalDateTime.now(); } diff --git a/peauty-domain/src/main/java/com/peauty/domain/response/PeautyResponseCode.java b/peauty-domain/src/main/java/com/peauty/domain/response/PeautyResponseCode.java index 1b755fe0..57dde1c7 100644 --- a/peauty-domain/src/main/java/com/peauty/domain/response/PeautyResponseCode.java +++ b/peauty-domain/src/main/java/com/peauty/domain/response/PeautyResponseCode.java @@ -53,6 +53,11 @@ public enum PeautyResponseCode { CANNOT_PROGRESS_CANCELED_THREAD_STEP("1314", "Cannot Progress Canceled Thread Step", "취소된 스레드의 단계는 변경할 수 없습니다."), CANNOT_PROGRESS_WAITING_THREAD_STEP("1315", "Cannot Progress Waiting Thread Step", "대기 중인 스레드의 단계는 변경할 수 없습니다."), INVALID_STEP_PROGRESSING("1316", "Invalid Step Progressing", "해당 단계로 올릴 수 있는 단계가 아닙니다."), + ONLY_BELONGING_PROCESS_CAN_BE_OBSERVER("1317", "Invalid Process Observer", "속한 프로세스가 아닌 다른 프로세스의 상태는 변경할 수 없습니다."), + PROCESS_OBSERVER_NOT_REGISTERED("1318", "Observer Not Registered", "스레드의 상태가 변경되면 속한 프로세스의 상태도 변경될 수 있어야합니다."), + PROCESS_NOT_REGISTERED("1319", "Process Not Registered", "스레드는 등록되지 않은 프로세스에 속할 수 없습니다."), + NOT_INITIALIZED_PROCESS("1320", "Not Initialized Process", "초기화되지 않은 프로세스입니다."), + NOT_FOUND_BIDDING_PROCESS("1321", "Not Found Bidding Process", "해당 입찰 프로세스를 찾을 수 없습니다."), // AWS 관련 (7000 ~ 8000) IMAGE_UPLOAD_FAIl("7001", "Fail To Upload Image To S3", "S3 에 이미지를 업로드하는 것을 실패했습니다."), diff --git a/peauty-domain/src/test/java/bidding/BiddingProcessTest.java b/peauty-domain/src/test/java/bidding/BiddingProcessTest.java deleted file mode 100644 index cc186dc9..00000000 --- a/peauty-domain/src/test/java/bidding/BiddingProcessTest.java +++ /dev/null @@ -1,426 +0,0 @@ -package bidding; - -import com.peauty.domain.exception.PeautyException; -import com.peauty.domain.bidding.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -class BiddingProcessTest { - - private PuppyId puppyId; - private DesignerId designerId; - - @BeforeEach - void setUp() { - puppyId = new PuppyId(1L); - designerId = new DesignerId(1L); - } - - @Nested - @DisplayName("프로세스 생성 테스트") - class CreateTest { - - @Test - @DisplayName("새로운 프로세스를 생성할 수 있다") - void createNewProcess() { - // when - BiddingProcess process = BiddingProcess.createNewProcess(puppyId); - - // then - assertEquals(puppyId, process.getPuppyId()); - assertNull(process.getId().orElse(null)); - assertEquals(BiddingProcessStatus.RESERVED_YET, process.getStatus()); - assertTrue(process.getThreads().isEmpty()); - } - - @Test - @DisplayName("디자이너 ID와 함께 새로운 프로세스를 생성할 수 있다") - void createNewProcessWithDesigner() { - // when - BiddingProcess process = BiddingProcess.createNewProcess(puppyId, designerId); - - // then - assertEquals(puppyId, process.getPuppyId()); - assertEquals(1, process.getThreads().size()); - assertEquals(designerId, process.getThreads().get(0).getDesignerId()); - } - - @Test - @DisplayName("기존 프로세스를 로드할 수 있다") - void loadProcess() { - // given - BiddingProcess.ID processId = new BiddingProcess.ID(1L); - List threads = new ArrayList<>(); - - // when - BiddingProcess process = BiddingProcess.loadProcess( - processId, - puppyId, - BiddingProcessStatus.RESERVED_YET, - BiddingProcessTimeInfo.createNewTimeInfo(), - threads - ); - - // then - assertEquals(processId, process.getId().orElse(null)); - assertEquals(puppyId, process.getPuppyId()); - } - } - - @Nested - @DisplayName("스레드 추가 테스트") - class AddThreadTest { - private BiddingProcess process; - - @BeforeEach - void setUp() { - process = BiddingProcess.createNewProcess(puppyId); - } - - @Test - @DisplayName("프로세스에 새로운 스레드를 추가할 수 있다") - void addNewThread() { - // when - process.addNewThread(designerId); - - // then - assertEquals(1, process.getThreads().size()); - BiddingThread addedThread = process.getThread(designerId); - assertEquals(designerId, addedThread.getDesignerId()); - } - - @Test - @DisplayName("이미 취소된 프로세스에는 스레드를 추가할 수 없다") - void cannotAddThreadToCanceledProcess() { - // given - process = BiddingProcess.loadProcess( - null, - puppyId, - BiddingProcessStatus.CANCELED, - BiddingProcessTimeInfo.createNewTimeInfo(), - new ArrayList<>() - ); - - // when & then - assertThrows(PeautyException.class, () -> process.addNewThread(designerId)); - } - - @Test - @DisplayName("이미 완료된 프로세스에는 스레드를 추가할 수 없다") - void cannotAddThreadToCompletedProcess() { - // given - process = BiddingProcess.loadProcess( - null, - puppyId, - BiddingProcessStatus.COMPLETED, - BiddingProcessTimeInfo.createNewTimeInfo(), - new ArrayList<>() - ); - - // when & then - assertThrows(PeautyException.class, () -> process.addNewThread(designerId)); - } - - @Test - @DisplayName("같은 디자이너의 스레드는 중복해서 추가할 수 없다") - void cannotAddDuplicateDesignerThread() { - // given - process.addNewThread(designerId); - - // when & then - assertThrows(PeautyException.class, () -> process.addNewThread(designerId)); - } - } - - @Nested - @DisplayName("스레드 조회 테스트") - class GetThreadTest { - private BiddingProcess process; - private BiddingThread thread; - - @BeforeEach - void setUp() { - // 스레드 생성 - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - BiddingThreadTimeInfo.createNewTimeInfo() - ); - - List threads = List.of(thread); - - // 프로세스 생성 - process = BiddingProcess.loadProcess( - new BiddingProcess.ID(1L), - puppyId, - BiddingProcessStatus.RESERVED_YET, - BiddingProcessTimeInfo.createNewTimeInfo(), - threads - ); - } - - @Test - @DisplayName("ID로 스레드를 조회할 수 있다") - void getThreadById() { - // when - BiddingThread found = process.getThread(thread.getId()); - - // then - assertEquals(thread.getId(), found.getId()); - } - - @Test - @DisplayName("디자이너 ID로 스레드를 조회할 수 있다") - void getThreadByDesignerId() { - // when - BiddingThread found = process.getThread(designerId); - - // then - assertEquals(designerId, found.getDesignerId()); - } - - @Test - @DisplayName("존재하지 않는 스레드 ID로 조회시 예외가 발생한다") - void throwExceptionWhenThreadNotFound() { - // when & then - assertThrows(PeautyException.class, () -> process.getThread(new BiddingThread.ID(999L))); - } - - @Test - @DisplayName("존재하지 않는 디자이너 ID로 조회시 예외가 발생한다") - void throwExceptionWhenDesignerNotFound() { - // when & then - assertThrows(PeautyException.class, - () -> process.getThread(new DesignerId(999L))); - } - } - - @Nested - @DisplayName("프로세스 취소 테스트") - class ProcessCancelTest { - private BiddingProcess process; - private BiddingThreadTimeInfo threadTimeInfo; - private BiddingProcessTimeInfo processTimeInfo; - - @BeforeEach - void setUp() { - // TimeInfo mocking - threadTimeInfo = mock(BiddingThreadTimeInfo.class); - processTimeInfo = mock(BiddingProcessTimeInfo.class); - - // 스레드 준비 - BiddingThread thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - new DesignerId(1L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - List threads = List.of(thread); - - // 프로세스 생성 - process = BiddingProcess.loadProcess( - new BiddingProcess.ID(1L), - puppyId, - BiddingProcessStatus.RESERVED_YET, - processTimeInfo, - threads - ); - } - - @Test - @DisplayName("프로세스를 취소할 수 있다") - void cancelProcess() { - // when - process.cancel(); - - // then - assertEquals(BiddingProcessStatus.CANCELED, process.getStatus()); - verify(processTimeInfo).onStatusChange(); - } - - @Test - @DisplayName("이미 취소된 프로세스는 다시 취소할 수 없다") - void cannotCancelCanceledProcess() { - // given - process.cancel(); - - // when & then - assertThrows(PeautyException.class, () -> process.cancel()); - } - - @Test - @DisplayName("취소된 프로세스의 스레드는 취소할 수 없다") - void cannotCancelThreadInCanceledProcess() { - // given - process.cancel(); - - // when & then - assertThrows(PeautyException.class, () -> process.cancelThread(new BiddingThread.ID(1L))); - } - } - - @Nested - @DisplayName("스레드 진행 테스트") - class ThreadProgressTest { - private BiddingProcess process; - private BiddingThread.ID thread1Id; - private BiddingThread.ID thread2Id; - private BiddingThread.ID thread3Id; - private BiddingThreadTimeInfo threadTimeInfo; - private BiddingProcessTimeInfo processTimeInfo; - - @BeforeEach - void setUp() { - // TimeInfo mocking - threadTimeInfo = mock(BiddingThreadTimeInfo.class); - processTimeInfo = mock(BiddingProcessTimeInfo.class); - - // 세 개의 스레드 준비 - BiddingThread thread1 = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - new DesignerId(1L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - BiddingThread thread2 = BiddingThread.loadThread( - new BiddingThread.ID(2L), - puppyId, - new DesignerId(2L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - BiddingThread thread3 = BiddingThread.loadThread( - new BiddingThread.ID(3L), - puppyId, - new DesignerId(3L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - thread1Id = thread1.getId(); - thread2Id = thread2.getId(); - thread3Id = thread3.getId(); - - List threads = List.of(thread1, thread2, thread3); - - // 프로세스 생성 - process = BiddingProcess.loadProcess( - new BiddingProcess.ID(1L), - puppyId, - BiddingProcessStatus.RESERVED_YET, - processTimeInfo, - threads - ); - } - - - @Test - @DisplayName("견적 응답으로 진행할 수 있다") - void responseEstimate() { - // when - process.responseEstimateThread(thread1Id); - - // then - BiddingThread thread = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.ESTIMATE_RESPONSE, thread.getStep()); - assertEquals(BiddingProcessStatus.RESERVED_YET, process.getStatus()); - verify(threadTimeInfo).onStepChange(); - } - - @Test - @DisplayName("예약 단계로 진행할 수 있다") - void reserve() { - // given - process.responseEstimateThread(thread1Id); - - // when - process.reserveThread(thread1Id); - - // then - BiddingThread thread = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.RESERVED, thread.getStep()); - assertEquals(BiddingProcessStatus.RESERVED, process.getStatus()); - verify(threadTimeInfo, times(2)).onStepChange(); - verify(processTimeInfo).onStatusChange(); - } - - @Test - @DisplayName("완료 단계로 진행할 수 있다") - void complete() { - // given - process.responseEstimateThread(thread1Id); - process.reserveThread(thread1Id); - - // when - process.completeThread(thread1Id); - - // then - BiddingThread thread = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.COMPLETED, thread.getStep()); - assertEquals(BiddingProcessStatus.COMPLETED, process.getStatus()); - verify(threadTimeInfo, times(3)).onStepChange(); - verify(processTimeInfo, times(2)).onStatusChange(); - } - - @Test - @DisplayName("취소된 프로세스는 스레드를 진행할 수 없다") - void cannotProgressThreadInCanceledProcess() { - // given - process.cancel(); - - // when & then - assertThrows(PeautyException.class, () -> process.responseEstimateThread(thread1Id)); - } - - @Test - @DisplayName("완료된 프로세스는 스레드를 진행할 수 없다") - void cannotProgressThreadInCompletedProcess() { - // given - process.responseEstimateThread(thread1Id); - process.reserveThread(thread1Id); - process.completeThread(thread1Id); - - // when & then - assertThrows(PeautyException.class, () -> process.responseEstimateThread(thread2Id)); - } - - @Test - @DisplayName("잘못된 순서로 스레드를 진행할 수 없다") - void cannotProgressThreadInWrongOrder() { - // when & then - assertThrows(PeautyException.class, () -> process.reserveThread(thread1Id)); - } - - @Test - @DisplayName("디자이너 ID로도 스레드를 진행할 수 있다") - void progressThreadByDesignerId() { - // when - process.responseEstimateThread(new DesignerId(1L)); - - // then - BiddingThread thread = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.ESTIMATE_RESPONSE, thread.getStep()); - verify(threadTimeInfo).onStepChange(); - } - } -} \ No newline at end of file diff --git a/peauty-domain/src/test/java/bidding/BiddingScenarioTest.java b/peauty-domain/src/test/java/bidding/BiddingScenarioTest.java index d5e899a3..bd617af8 100644 --- a/peauty-domain/src/test/java/bidding/BiddingScenarioTest.java +++ b/peauty-domain/src/test/java/bidding/BiddingScenarioTest.java @@ -1,196 +1,338 @@ package bidding; import com.peauty.domain.bidding.*; +import com.peauty.domain.exception.PeautyException; +import com.peauty.domain.response.PeautyResponseCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.List; +import java.util.ArrayList; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @DisplayName("입찰 시나리오 테스트") class BiddingScenarioTest { + private static final Long PROCESS_ID = 1L; + private static final Long PUPPY_ID = 1L; + private static final Long DESIGNER_ID_1 = 1L; + private static final Long DESIGNER_ID_2 = 2L; - private PuppyId puppyId; - private DesignerId designerId; private BiddingProcess process; - private BiddingThread.ID thread1Id; - private BiddingThread.ID thread2Id; - private BiddingThread.ID thread3Id; - private BiddingThreadTimeInfo threadTimeInfo; - private BiddingProcessTimeInfo processTimeInfo; + private DesignerId designerId1; + private DesignerId designerId2; @BeforeEach void setUp() { - puppyId = new PuppyId(1L); - designerId = new DesignerId(1L); - threadTimeInfo = mock(BiddingThreadTimeInfo.class); - processTimeInfo = mock(BiddingProcessTimeInfo.class); - // 세 개의 스레드 준비 - BiddingThread thread1 = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - new DesignerId(1L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - BiddingThread thread2 = BiddingThread.loadThread( - new BiddingThread.ID(2L), - puppyId, - new DesignerId(2L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - BiddingThread thread3 = BiddingThread.loadThread( - new BiddingThread.ID(3L), - puppyId, - new DesignerId(3L), - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - threadTimeInfo - ); - - thread1Id = thread1.getId(); - thread2Id = thread2.getId(); - thread3Id = thread3.getId(); - - List threads = List.of(thread1, thread2, thread3); - - // 프로세스 생성 + designerId1 = new DesignerId(DESIGNER_ID_1); + designerId2 = new DesignerId(DESIGNER_ID_2); process = BiddingProcess.loadProcess( - new BiddingProcess.ID(1L), - puppyId, + new BiddingProcess.ID(PROCESS_ID), + new PuppyId(PUPPY_ID), BiddingProcessStatus.RESERVED_YET, - processTimeInfo, - threads + BiddingProcessTimeInfo.createNewTimeInfo(), + new ArrayList<>() ); } - @Test - @DisplayName("한 스레드가 예약 상태가 되면 다른 스레드들이 대기 상태로 변경된다") - void changeOtherThreadsToWaitingWhenOneThreadReserved() { - // when - process.responseEstimateThread(thread1Id); // ESTIMATE_REQUEST -> ESTIMATE_RESPONSE - process.reserveThread(thread1Id); // ESTIMATE_RESPONSE -> RESERVED - - // then - // 첫 번째 스레드는 RESERVED 상태 - BiddingThread firstThread = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.RESERVED, firstThread.getStep()); - - // 나머지 스레드들은 WAITING 상태로 변경 - BiddingThread secondThread = process.getThread(thread2Id); - BiddingThread thirdThread = process.getThread(thread3Id); - - assertEquals(BiddingThreadStatus.WAITING, secondThread.getStatus()); - assertEquals(BiddingThreadStatus.WAITING, thirdThread.getStatus()); - - // 프로세스는 RESERVED 상태 - assertEquals(BiddingProcessStatus.RESERVED, process.getStatus()); + @Nested + @DisplayName("프로세스 생성 테스트") + class CreationTest { + @Test + @DisplayName("새로운 프로세스가 정상적으로 생성된다") + void createNewProcess() { + process = BiddingProcess.createNewProcess(new PuppyId(PUPPY_ID)); + assertThat(process.getId()).isEmpty(); + assertThat(process.getPuppyId().value()).isEqualTo(PUPPY_ID); + assertThat(process.getStatus()).isEqualTo(BiddingProcessStatus.RESERVED_YET); + assertThat(process.getThreads()).isEmpty(); + } + + @Test + @DisplayName("기존 프로세스가 정상적으로 로드된다") + void loadProcess() { + BiddingProcess loadedProcess = BiddingProcess.loadProcess( + new BiddingProcess.ID(PROCESS_ID), + new PuppyId(PUPPY_ID), + BiddingProcessStatus.RESERVED_YET, + BiddingProcessTimeInfo.createNewTimeInfo(), + new ArrayList<>() + ); + + assertThat(loadedProcess.getId()).hasValue(new BiddingProcess.ID(PROCESS_ID)); + assertThat(loadedProcess.getPuppyId().value()).isEqualTo(PUPPY_ID); + assertThat(loadedProcess.getStatus()).isEqualTo(BiddingProcessStatus.RESERVED_YET); + } } - @Test - @DisplayName("예약된 스레드가 취소되면 다른 스레드들이 진행중 상태로 변경된다") - void changeWaitingThreadsToOngoingWhenReservedThreadCanceled() { - // given - process.responseEstimateThread(thread1Id); // ESTIMATE_REQUEST -> ESTIMATE_RESPONSE - process.reserveThread(thread1Id); // ESTIMATE_RESPONSE -> RESERVED - - // when - process.cancelThread(thread1Id); - - // then - // 예약됐던 스레드는 취소 상태 - BiddingThread firstThread = process.getThread(thread1Id); - assertEquals(BiddingThreadStatus.CANCELED, firstThread.getStatus()); - - // 나머지 스레드들은 다시 NORMAL 상태로 변경 - BiddingThread secondThread = process.getThread(thread2Id); - BiddingThread thirdThread = process.getThread(thread3Id); - - assertEquals(BiddingThreadStatus.NORMAL, secondThread.getStatus()); - assertEquals(BiddingThreadStatus.NORMAL, thirdThread.getStatus()); - - // 프로세스는 RESERVED_YET 상태로 변경 - assertEquals(BiddingProcessStatus.RESERVED_YET, process.getStatus()); + @Nested + @DisplayName("스레드 관리 테스트") + class ThreadManagementTest { + + @BeforeEach + void setUp() { + process = BiddingProcess.loadProcess( + new BiddingProcess.ID(PROCESS_ID), + new PuppyId(PUPPY_ID), + BiddingProcessStatus.RESERVED_YET, + BiddingProcessTimeInfo.createNewTimeInfo(), + new ArrayList<>() + ); + } + + @Test + @DisplayName("새로운 스레드를 추가할 수 있다") + void addNewThread() { + process.addNewThread(designerId1); + + assertThat(process.getThreads()).hasSize(1); + BiddingThread thread = process.getThread(designerId1); + assertThat(thread.getDesignerId()).isEqualTo(designerId1); + assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.ESTIMATE_REQUEST); + } + + @Test + @DisplayName("이미 존재하는 디자이너의 스레드는 추가할 수 없다") + void cannotAddDuplicateThread() { + process.addNewThread(designerId1); + + assertThatThrownBy(() -> process.addNewThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.THREAD_ALREADY_IN_PROCESS); + } + + @Test + @DisplayName("취소된 프로세스에는 스레드를 추가할 수 없다") + void cannotAddThreadToCanceledProcess() { + process.cancel(); + + assertThatThrownBy(() -> process.addNewThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_PROCESS); + } + + @Test + @DisplayName("디자이너의 ID로 스레드를 조회할 수 있다") + void getThreadById() { + process.addNewThread(designerId1); + BiddingThread foundThread = process.getThread(designerId1); + + assertThat(foundThread.getDesignerId().value()).isEqualTo(designerId1.value()); + } + + @Test + @DisplayName("존재하지 않는 스레드 조회 시 예외가 발생한다") + void throwExceptionWhenThreadNotFound() { + assertThatThrownBy(() -> process.getThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.NOT_FOUND_BIDDING_THREAD_IN_PROCESS); + } } - @Test - @DisplayName("예약 단계로 진행 시 취소된 스레드를 제외한 나머지 스레드들만 대기 상태로 변경된다") - void changeOnlyActiveThreadsToWaitingWhenReserved() { - // given - process.cancelThread(thread3Id); // 3번 스레드 취소 - process.responseEstimateThread(thread1Id); // 1번 스레드 견적 응답 - - // when - process.reserveThread(thread1Id); // 1번 스레드 예약 - - // then - // 1번 스레드는 예약 상태 - BiddingThread thread1 = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.RESERVED, thread1.getStep()); - assertEquals(BiddingThreadStatus.NORMAL, thread1.getStatus()); - - // 2번 스레드는 대기 상태 - BiddingThread thread2 = process.getThread(thread2Id); - assertEquals(BiddingThreadStep.ESTIMATE_REQUEST, thread2.getStep()); - assertEquals(BiddingThreadStatus.WAITING, thread2.getStatus()); - - // 3번 스레드는 여전히 취소 상태 - BiddingThread thread3 = process.getThread(thread3Id); - assertEquals(BiddingThreadStep.ESTIMATE_REQUEST, thread3.getStep()); - assertEquals(BiddingThreadStatus.CANCELED, thread3.getStatus()); - - // 프로세스는 예약 상태 - assertEquals(BiddingProcessStatus.RESERVED, process.getStatus()); - - // TimeInfo 검증 - verify(threadTimeInfo, times(2)).onStepChange(); // 1번 스레드의 견적응답, 예약 - verify(threadTimeInfo, times(2)).onStatusChange(); // 2번 스레드의 대기, 3번 스레드의 취소 - verify(processTimeInfo).onStatusChange(); // 프로세스의 예약 + @Nested + @DisplayName("스레드 상태 변경 테스트") + class ThreadStateChangeTest { + @BeforeEach + void addThread() { + process.addNewThread(designerId1); + } + + @Test + @DisplayName("스레드가 견적 응답 상태로 변경된다") + void responseEstimate() { + process.responseEstimateThread(designerId1); + + BiddingThread thread = process.getThread(designerId1); + assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.ESTIMATE_RESPONSE); + } + + @Test + @DisplayName("스레드가 예약 상태로 변경되면 다른 스레드들은 대기 상태가 된다") + void reserve() { + process.addNewThread(designerId2); + process.responseEstimateThread(designerId1); + process.reserveThread(designerId1); + + BiddingThread reservedThread = process.getThread(designerId1); + BiddingThread waitingThread = process.getThread(designerId2); + + assertThat(reservedThread.getStep()).isEqualTo(BiddingThreadStep.RESERVED); + assertThat(waitingThread.getStatus()).isEqualTo(BiddingThreadStatus.WAITING); + assertThat(process.getStatus()).isEqualTo(BiddingProcessStatus.RESERVED); + } + + @Test + @DisplayName("예약된 스레드가 취소되면 다른 스레드들이 정상 상태로 돌아온다") + void cancelReservedThread() { + process.addNewThread(designerId2); + process.responseEstimateThread(designerId1); + process.reserveThread(designerId1); + process.cancelThread(designerId1); + + BiddingThread canceledThread = process.getThread(designerId1); + BiddingThread normalThread = process.getThread(designerId2); + + assertThat(canceledThread.getStatus()).isEqualTo(BiddingThreadStatus.CANCELED); + assertThat(normalThread.getStatus()).isEqualTo(BiddingThreadStatus.NORMAL); + assertThat(process.getStatus()).isEqualTo(BiddingProcessStatus.RESERVED_YET); + } + + @Test + @DisplayName("스레드가 완료되면 프로세스도 완료 상태가 된다") + void complete() { + process.responseEstimateThread(designerId1); + process.reserveThread(designerId1); + process.completeThread(designerId1); + + BiddingThread thread = process.getThread(designerId1); + assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.COMPLETED); + assertThat(process.getStatus()).isEqualTo(BiddingProcessStatus.COMPLETED); + } + + @Test + @DisplayName("취소된 스레드는 견적 응답으로 진행할 수 없다") + void cannotProgressCanceledThread() { + process.cancelThread(designerId1); + + assertThatThrownBy(() -> process.responseEstimateThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.CANNOT_PROGRESS_CANCELED_THREAD_STEP); + } + + @Test + @DisplayName("대기 상태의 스레드는 견적 응답으로 진행할 수 없다") + void cannotProgressWaitingThread() { + process.addNewThread(designerId2); + process.responseEstimateThread(designerId1); + process.reserveThread(designerId1); // 이때 designerId2 스레드는 대기상태가 됨 + + assertThatThrownBy(() -> process.responseEstimateThread(designerId2)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.CANNOT_PROGRESS_WAITING_THREAD_STEP); + } + + @Test + @DisplayName("견적 응답 없이 예약 상태로 진행할 수 없다") + void cannotReserveWithoutEstimateResponse() { + assertThatThrownBy(() -> process.reserveThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.INVALID_STEP_PROGRESSING); + } + + @Test + @DisplayName("예약 없이 완료 상태로 진행할 수 없다") + void cannotCompleteWithoutReservation() { + process.responseEstimateThread(designerId1); + + assertThatThrownBy(() -> process.completeThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.INVALID_STEP_PROGRESSING); + } + + + + @Test + @DisplayName("이미 취소된 스레드는 다시 취소할 수 없다") + void cannotCancelAlreadyCanceledThread() { + process.cancelThread(designerId1); + + assertThatThrownBy(() -> process.cancelThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_THREAD); + } + + + @Test + @DisplayName("예약된 스레드가 취소되면 대기 중이던 스레드가 다시 예약 가능해진다") + void canReserveAfterReservedThreadCanceled() { + process.addNewThread(designerId2); + process.responseEstimateThread(designerId2); + process.responseEstimateThread(designerId1); + process.reserveThread(designerId1); + + assertThatThrownBy(() -> process.reserveThread(designerId2)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.CANNOT_PROGRESS_WAITING_THREAD_STEP); + + process.cancelThread(designerId1); + + // designerId2 스레드가 다시 예약 가능해짐 + assertDoesNotThrow(() -> { + process.reserveThread(designerId2); + BiddingThread reservedThread = process.getThread(designerId2); + assertThat(reservedThread.getStep()).isEqualTo(BiddingThreadStep.RESERVED); + }); + } } - @Test - @DisplayName("예약된 스레드 취소 시 취소된 스레드를 제외한 나머지 스레드들만 진행중 상태로 변경된다") - void changeOnlyWaitingThreadsToOngoingWhenReservedThreadCanceled() { - // given - process.cancelThread(thread3Id); // 3번 스레드 취소 - process.responseEstimateThread(thread1Id); // 1번 스레드 견적 응답 - process.reserveThread(thread1Id); // 1번 스레드 예약 - - // 이 시점에서 2번만 WAITING 상태 - - // when - process.cancelThread(thread1Id); // 예약된 1번 스레드 취소 - - // then - // 1번 스레드는 취소 상태 - BiddingThread thread1 = process.getThread(thread1Id); - assertEquals(BiddingThreadStep.RESERVED, thread1.getStep()); - assertEquals(BiddingThreadStatus.CANCELED, thread1.getStatus()); - - // 2번 스레드는 다시 진행중 상태 - BiddingThread thread2 = process.getThread(thread2Id); - assertEquals(BiddingThreadStep.ESTIMATE_REQUEST, thread2.getStep()); - assertEquals(BiddingThreadStatus.NORMAL, thread2.getStatus()); - - // 3번 스레드는 여전히 취소 상태 - BiddingThread thread3 = process.getThread(thread3Id); - assertEquals(BiddingThreadStep.ESTIMATE_REQUEST, thread3.getStep()); - assertEquals(BiddingThreadStatus.CANCELED, thread3.getStatus()); - - // 프로세스는 예약 전 상태 - assertEquals(BiddingProcessStatus.RESERVED_YET, process.getStatus()); - - // TimeInfo 검증 - verify(threadTimeInfo, times(2)).onStepChange(); // 1번 스레드의 견적응답, 예약 - verify(threadTimeInfo, times(4)).onStatusChange(); // 2번의 대기->진행중, 1번과 3번의 취소 - verify(processTimeInfo, times(2)).onStatusChange(); // 프로세스의 예약->예약전 + @Nested + @DisplayName("프로세스 상태 제약 테스트") + class ProcessStatusConstraintTest { + @BeforeEach + void addThread() { + process.addNewThread(designerId1); + } + + @Test + @DisplayName("취소된 프로세스에서는 어떠한 스레드 동작도 불가능하다") + void cannotDoAnyOperationInCanceledProcess() { + // 프로세스를 취소 상태로 만듦 + process.cancel(); + assertThat(process.getStatus()).isEqualTo(BiddingProcessStatus.CANCELED); + + // 새로운 스레드 추가 시도 + assertThatThrownBy(() -> process.addNewThread(designerId2)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_PROCESS); + + // 기존 스레드에 대한 모든 동작 시도 + assertThatThrownBy(() -> process.responseEstimateThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_PROCESS); + + assertThatThrownBy(() -> process.reserveThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_PROCESS); + + assertThatThrownBy(() -> process.completeThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_PROCESS); + + assertThatThrownBy(() -> process.cancelThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_CANCELED_BIDDING_PROCESS); + } + + @Test + @DisplayName("완료된 프로세스에서는 어떠한 스레드 동작도 불가능하다") + void cannotDoAnyOperationInCompletedProcess() { + // 첫 번째 스레드를 완료 상태로 만듦 + process.responseEstimateThread(designerId1); + process.reserveThread(designerId1); + process.completeThread(designerId1); + + assertThat(process.getStatus()).isEqualTo(BiddingProcessStatus.COMPLETED); + + // 새로운 스레드 추가 시도 + assertThatThrownBy(() -> process.addNewThread(designerId2)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_COMPLETED_BIDDING_PROCESS); + + // 기존 스레드에 대한 모든 동작 시도 + assertThatThrownBy(() -> process.responseEstimateThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_COMPLETED_BIDDING_PROCESS); + + assertThatThrownBy(() -> process.reserveThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_COMPLETED_BIDDING_PROCESS); + + assertThatThrownBy(() -> process.cancelThread(designerId1)) + .isInstanceOf(PeautyException.class) + .hasFieldOrPropertyWithValue("peautyResponseCode", PeautyResponseCode.ALREADY_COMPLETED_BIDDING_PROCESS); + } } -} +} \ No newline at end of file diff --git a/peauty-domain/src/test/java/bidding/BiddingThreadTest.java b/peauty-domain/src/test/java/bidding/BiddingThreadTest.java deleted file mode 100644 index 7f9aebf1..00000000 --- a/peauty-domain/src/test/java/bidding/BiddingThreadTest.java +++ /dev/null @@ -1,295 +0,0 @@ -package bidding; - -import com.peauty.domain.exception.PeautyException; -import com.peauty.domain.bidding.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -class BiddingThreadTest { - - private BiddingProcess mockProcess; - private PuppyId puppyId; - private DesignerId designerId; - - @BeforeEach - void setUp() { - mockProcess = mock(BiddingProcess.class); - puppyId = new PuppyId(1L); - designerId = new DesignerId(1L); - } - - @Nested - @DisplayName("스레드 생성 테스트") - class CreateTest { - @Test - @DisplayName("새로운 스레드를 생성할 수 있다") - void createNewThread() { - // when - BiddingThread thread = BiddingThread.createNewThread(puppyId, designerId); - - // then - assertThat(thread.getId()).isNull(); - assertThat(thread.getPuppyId()).isEqualTo(puppyId); - assertThat(thread.getDesignerId()).isEqualTo(designerId); - assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.ESTIMATE_REQUEST); - } - - @Test - @DisplayName("기존 스레드를 로드할 수 있다") - void loadThread() { - // given - BiddingThread.ID threadId = new BiddingThread.ID(1L); - BiddingThreadStep step = BiddingThreadStep.ESTIMATE_RESPONSE; - BiddingThreadStatus status = BiddingThreadStatus.NORMAL; - BiddingThreadTimeInfo timeInfo = mock(BiddingThreadTimeInfo.class); - - // when - BiddingThread thread = BiddingThread.loadThread( - threadId, puppyId, designerId, step, status, timeInfo); - - // then - assertThat(thread.getId()).isEqualTo(threadId); - assertThat(thread.getStep()).isEqualTo(step); - } - } - - @Nested - @DisplayName("스레드 단계 진행 테스트") - class ProgressStepTest { - private BiddingThread thread; - private BiddingThreadTimeInfo mockTimeInfo; - - @BeforeEach - void setUp() { - mockTimeInfo = mock(BiddingThreadTimeInfo.class); - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - mockTimeInfo - ); - thread.registerProcessObserver(mockProcess); - } - - @Test - @DisplayName("견적 응답 단계로 진행할 수 있다") - void responseEstimate() { - // when - thread.responseEstimate(); - - // then - assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.ESTIMATE_RESPONSE); - verify(mockTimeInfo).onStepChange(); - } - - @Test - @DisplayName("예약 단계로 진행할 수 있다") - void reserve() { - // given - thread.responseEstimate(); - - // when - thread.reserve(); - - // then - assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.RESERVED); - verify(mockProcess).onThreadReserved(); - verify(mockTimeInfo, times(2)).onStepChange(); - } - - @Test - @DisplayName("완료 단계로 진행할 수 있다") - void complete() { - // given - thread.responseEstimate(); - thread.reserve(); - - // when - thread.complete(); - - // then - assertThat(thread.getStep()).isEqualTo(BiddingThreadStep.COMPLETED); - verify(mockProcess).onThreadReserved(); - verify(mockProcess).onThreadCompleted(); - verify(mockTimeInfo, times(3)).onStepChange(); - } - - @Test - @DisplayName("잘못된 순서로 단계를 진행할 수 없다") - void cannotProgressInvalidOrder() { - // given - thread.responseEstimate(); - thread.reserve(); - - // when & then - assertThrows(PeautyException.class, () -> thread.reserve()); - } - - @Test - @DisplayName("취소된 스레드는 단계를 진행할 수 없다") - void cannotProgressCanceledThread() { - // given - thread.cancel(); - - // when & then - assertThrows(PeautyException.class, () -> thread.responseEstimate()); - } - - @Test - @DisplayName("대기 중인 스레드는 단계를 진행할 수 없다") - void cannotProgressWaitingThread() { - // given - thread.waiting(); - - // when & then - assertThrows(PeautyException.class, () -> thread.responseEstimate()); - } - } - - @Nested - @DisplayName("스레드 취소 테스트") - class CancelTest { - private BiddingThread thread; - private BiddingThreadTimeInfo mockTimeInfo; - - @BeforeEach - void setUp() { - mockTimeInfo = mock(BiddingThreadTimeInfo.class); - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - mockTimeInfo - ); - thread.registerProcessObserver(mockProcess); - } - - @Test - @DisplayName("스레드를 취소할 수 있다") - void cancelThread() { - // when - thread.cancel(); - - // then - assertThat(thread.getStatus()).isEqualTo(BiddingThreadStatus.CANCELED); - verify(mockTimeInfo).onStatusChange(); - } - - @Test - @DisplayName("예약된 스레드를 취소하면 프로세스에 통지한다") - void notifyProcessWhenReservedThreadCanceled() { - // given - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.RESERVED, - BiddingThreadStatus.NORMAL, - mockTimeInfo - ); - thread.registerProcessObserver(mockProcess); - - // when - thread.cancel(); - - // then - verify(mockProcess).onReservedThreadCancel(); - } - - @Test - @DisplayName("이미 취소된 스레드는 다시 취소할 수 없다") - void cannotCancelCanceledThread() { - // given - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.CANCELED, - mockTimeInfo - ); - - // when & then - assertThrows(PeautyException.class, () -> thread.cancel()); - } - - @Test - @DisplayName("완료된 스레드는 취소할 수 없다") - void cannotCancelCompletedThread() { - // given - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.COMPLETED, - BiddingThreadStatus.NORMAL, - mockTimeInfo - ); - - // when & then - assertThrows(PeautyException.class, () -> thread.cancel()); - } - } - - @Nested - @DisplayName("스레드 상태 변경 테스트") - class StatusChangeTest { - private BiddingThread thread; - private BiddingThreadTimeInfo mockTimeInfo; - - @BeforeEach - void setUp() { - mockTimeInfo = mock(BiddingThreadTimeInfo.class); - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.NORMAL, - mockTimeInfo - ); - } - - @Test - @DisplayName("진행 중인 스레드를 대기 상태로 변경할 수 있다") - void canChangeToWaiting() { - // when - thread.waiting(); - - // then - assertThat(thread.getStatus()).isEqualTo(BiddingThreadStatus.WAITING); - verify(mockTimeInfo).onStatusChange(); - } - - @Test - @DisplayName("대기 중인 스레드를 진행 중 상태로 변경할 수 있다") - void canRelease() { - // given - thread = BiddingThread.loadThread( - new BiddingThread.ID(1L), - puppyId, - designerId, - BiddingThreadStep.ESTIMATE_REQUEST, - BiddingThreadStatus.WAITING, - mockTimeInfo - ); - - // when - thread.release(); - - // then - assertThat(thread.getStatus()).isEqualTo(BiddingThreadStatus.NORMAL); - verify(mockTimeInfo).onStatusChange(); - } - } -} \ No newline at end of file diff --git a/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingProcessEntity.java b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingProcessEntity.java new file mode 100644 index 00000000..a8c5dee2 --- /dev/null +++ b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingProcessEntity.java @@ -0,0 +1,34 @@ +package com.peauty.persistence.bidding; + +import com.peauty.domain.bidding.BiddingProcessStatus; +import com.peauty.domain.bidding.BiddingProcessTimeInfo; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "bidding_process") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class BiddingProcessEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "puppy_id", nullable = false) + private Long puppyId; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private BiddingProcessStatus status; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "status_modified_at", nullable = false) + private LocalDateTime statusModifiedAt; +} \ No newline at end of file diff --git a/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingProcessRepository.java b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingProcessRepository.java new file mode 100644 index 00000000..67767254 --- /dev/null +++ b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingProcessRepository.java @@ -0,0 +1,31 @@ +package com.peauty.persistence.bidding; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface BiddingProcessRepository extends JpaRepository { + + List findByPuppyId(Long puppyId); + + @Query(""" + SELECT p\s + FROM BiddingProcessEntity p, BiddingThreadEntity t\s + WHERE t.biddingProcessId = p.id\s + AND t.id = :threadId + """) + Optional findByThreadId(@Param("threadId") Long threadId); + + @Query(""" + SELECT DISTINCT p\s + FROM BiddingProcessEntity p, BiddingThreadEntity t\s + WHERE t.biddingProcessId = p.id\s + AND t.designerId = :designerId + """) + List findAllByDesignerId(@Param("designerId") Long designerId); +} \ No newline at end of file diff --git a/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingThreadEntity.java b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingThreadEntity.java new file mode 100644 index 00000000..5a7ae1f1 --- /dev/null +++ b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingThreadEntity.java @@ -0,0 +1,45 @@ +package com.peauty.persistence.bidding; + +import com.peauty.domain.bidding.BiddingThreadStatus; +import com.peauty.domain.bidding.BiddingThreadStep; +import com.peauty.domain.bidding.BiddingThreadTimeInfo; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "bidding_thread") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class BiddingThreadEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "bidding_process_id", nullable = false) + private Long biddingProcessId; + + @Column(name = "designer_id", nullable = false) + private Long designerId; + + @Enumerated(EnumType.STRING) + @Column(name = "step", nullable = false) + private BiddingThreadStep step; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private BiddingThreadStatus status; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "step_modified_at", nullable = false) + private LocalDateTime stepModifiedAt; + + @Column(name = "status_modified_at", nullable = false) + private LocalDateTime statusModifiedAt; +} \ No newline at end of file diff --git a/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingThreadRepository.java b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingThreadRepository.java new file mode 100644 index 00000000..f8b35d06 --- /dev/null +++ b/peauty-persistence/src/main/java/com/peauty/persistence/bidding/BiddingThreadRepository.java @@ -0,0 +1,12 @@ +package com.peauty.persistence.bidding; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BiddingThreadRepository extends JpaRepository { + + List findByBiddingProcessId(Long biddingProcessId); +}