Skip to content

Commit

Permalink
feat: 참여 시 물품 개수 필드 추가 (#703)
Browse files Browse the repository at this point in the history
* feat: 참여 물품 개수 필드 추가

* feat: 참여 시 개수 함께 요청

* feat: 참여 취소 시 요청 개수 전체 취소

* feat: 참여자 목록 조회 시 참여 개수 및 가격 필드 추가

* feat: 공모 생성 시 총대의 구매 개수 필드 추가 (이전 버전 고려)

* feat: 이전 버전을 고려하여 요청 DTO에서 null 값 관리

* test: 네이밍 구체화 및 문서화 작업

* test: 참여 기능 동시성 테스트 추가
  • Loading branch information
helenason authored Mar 2, 2025
1 parent 99ba0dd commit f2fe520
Show file tree
Hide file tree
Showing 18 changed files with 707 additions and 498 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum OfferingErrorCode implements ErrorResponse {
CANNOT_UPDATE_LESS_THAN_CURRENT_COUNT(BAD_REQUEST, "총 인원은 참여 인원수 미만으로 수정할 수 없습니다."),
CANNOT_UPDATE_BEFORE_NOW_MEETING_DATE(BAD_REQUEST, "거래 날짜는 오늘보다 이전일 수 없습니다."),
CANNOT_MEETING_DATE_BEFORE_THAN_TODAY(BAD_REQUEST, "거래 날짜는 오늘부터 설정할 수 있습니다."),
INVALID_MY_COUNT(BAD_REQUEST, "총대의 구매 개수는 총 개수보다 적어야 합니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ public OfferingEntity(MemberEntity member, String title, String description, Str
originPrice, discountRate, offeringStatus, roomStatus, false);
}

public void participate() {
currentCount++;
public void participate(int participationCount) {
currentCount += participationCount;
OfferingStatus offeringStatus = toOfferingJoinedCount().decideOfferingStatus();
updateOfferingStatus(offeringStatus);
}

public void leave() {
currentCount--;
public void leave(int participationCount) {
currentCount -= participationCount;
OfferingStatus offeringStatus = toOfferingJoinedCount().decideOfferingStatus();
updateOfferingStatus(offeringStatus);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,22 @@ private void validateIsProposer(OfferingEntity offering, MemberEntity member) {
@WriterDatabase
public Long saveOffering(OfferingSaveRequest request, MemberEntity member) {
OfferingEntity offering = request.toEntity(member);
validateMeetingDate(offering.getMeetingDate());
validateOffering(request, offering);
OfferingEntity saved = offeringRepository.save(offering);

OfferingMemberEntity offeringMember = new OfferingMemberEntity(member, saved, OfferingMemberRole.PROPOSER);
OfferingMemberEntity offeringMember = new OfferingMemberEntity(member, saved, OfferingMemberRole.PROPOSER,
request.myCount());
offeringMemberRepository.save(offeringMember);

eventPublisher.publishEvent(new SaveOfferingEvent(this, saved));
return saved.getId();
}

private void validateOffering(OfferingSaveRequest request, OfferingEntity offering) {
validateMeetingDate(offering.getMeetingDate());
validateCount(request.myCount(), request.totalCount());
}

private void validateMeetingDate(LocalDateTime offeringMeetingDateTime) {
LocalDate thresholdDate = LocalDate.now(clock);
LocalDate targetDate = offeringMeetingDateTime.toLocalDate();
Expand All @@ -149,6 +155,12 @@ private void validateMeetingDate(LocalDateTime offeringMeetingDateTime) {
}
}

private void validateCount(Integer myCount, Integer totalCount) {
if (myCount != null && myCount >= totalCount) {
throw new MarketException(OfferingErrorCode.INVALID_MY_COUNT);
}
}

public OfferingProductImageResponse uploadProductImage(MultipartFile image) {
String imageUrl = storageService.uploadFile(image);
return new OfferingProductImageResponse(imageUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Objects;

public record OfferingSaveRequest(@NotBlank
String title,
Expand Down Expand Up @@ -37,14 +38,21 @@ public record OfferingSaveRequest(@NotBlank
LocalDateTime meetingDate,

@NotNull
String description) {
String description,

Integer myCount) {

public OfferingEntity toEntity(MemberEntity member) {
OfferingPrice offeringPrice = new OfferingPrice(totalCount, totalPrice, originPrice);
Double discountRate = offeringPrice.calculateDiscountRate();

return new OfferingEntity(member, title, description, thumbnailUrl, productUrl, meetingDate, meetingAddress,
meetingAddressDetail, meetingAddressDong, totalCount, 1,
meetingAddressDetail, meetingAddressDong, totalCount, Objects.requireNonNullElse(myCount, 1),
totalPrice, originPrice, discountRate, OfferingStatus.AVAILABLE, CommentRoomStatus.GROUPING);
}

@Override
public Integer myCount() {
return Objects.requireNonNullElse(myCount, 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.zzang.chongdae.offeringmember.domain;

import com.zzang.chongdae.global.exception.MarketException;
import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.offeringmember.exception.OfferingMemberErrorCode;
import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity;
import java.util.List;
Expand All @@ -14,18 +13,16 @@ public class OfferingMembers {

private final List<OfferingMemberEntity> offeringMembers;

public MemberEntity getProposer() {
public OfferingMemberEntity getProposer() {
return offeringMembers.stream()
.filter(OfferingMemberEntity::isProposer)
.findFirst()
.orElseThrow(() -> new MarketException(OfferingMemberErrorCode.PROPOSER_NOT_FOUND))
.getMember();
.orElseThrow(() -> new MarketException(OfferingMemberErrorCode.PROPOSER_NOT_FOUND));
}

public List<MemberEntity> getParticipants() {
public List<OfferingMemberEntity> getParticipants() {
return offeringMembers.stream()
.filter(OfferingMemberEntity::isParticipant)
.map(OfferingMemberEntity::getMember)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public enum OfferingMemberErrorCode implements ErrorResponse {
PROPOSER_NOT_FOUND(BAD_REQUEST, "총대를 찾을 수 없습니다."),
NOT_FOUND(BAD_REQUEST, "해당 공모의 총대 혹은 참여자가 아닙니다."),
CANNOT_CANCEL_PROPOSER(BAD_REQUEST, "총대는 참여를 취소할 수 없습니다."),
CANNOT_CANCEL_IN_PROGRESS(BAD_REQUEST, "진행중인 공모는 참여를 취소할 수 없습니다.");
CANNOT_CANCEL_IN_PROGRESS(BAD_REQUEST, "진행중인 공모는 참여를 취소할 수 없습니다."),
INVALID_PARTICIPATION_COUNT(BAD_REQUEST, "총 수량을 넘어서 참여할 수 없습니다."),
;

private final HttpStatus status;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
Expand All @@ -28,6 +29,8 @@
@Entity
public class OfferingMemberEntity extends BaseTimeEntity {

private static final int DEFAULT_PARTICIPATION_COUNT = 1;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Expand All @@ -43,8 +46,12 @@ public class OfferingMemberEntity extends BaseTimeEntity {
@Enumerated(EnumType.STRING)
private OfferingMemberRole role;

public OfferingMemberEntity(MemberEntity member, OfferingEntity offering, OfferingMemberRole role) {
this(null, member, offering, role);
@NotNull
@Positive
private Integer participationCount = DEFAULT_PARTICIPATION_COUNT;

public OfferingMemberEntity(MemberEntity member, OfferingEntity offering, OfferingMemberRole role, Integer count) {
this(null, member, offering, role, count);
}

public boolean isProposer() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,21 @@ public class OfferingMemberService {
public Long participate(ParticipationRequest request, MemberEntity member) {
OfferingEntity offering = offeringRepository.findByIdWithLock(request.offeringId())
.orElseThrow(() -> new MarketException(OfferingErrorCode.NOT_FOUND));
validateParticipate(offering, member);
validateParticipate(offering, member, request.participationCount());

OfferingMemberEntity offeringMember = new OfferingMemberEntity(
member, offering, OfferingMemberRole.PARTICIPANT);
member, offering, OfferingMemberRole.PARTICIPANT, request.participationCount());
OfferingMemberEntity saved = offeringMemberRepository.save(offeringMember);
offering.participate();
offering.participate(request.participationCount());

eventPublisher.publishEvent(new ParticipateEvent(this, saved));
return saved.getId();
}

private void validateParticipate(OfferingEntity offering, MemberEntity member) {
private void validateParticipate(OfferingEntity offering, MemberEntity member, int participationCount) {
validateClosed(offering);
validateDuplicate(offering, member);
validateParticipationCount(offering, participationCount);
}

private void validateClosed(OfferingEntity offering) {
Expand All @@ -63,6 +64,12 @@ private void validateDuplicate(OfferingEntity offering, MemberEntity member) {
}
}

private void validateParticipationCount(OfferingEntity offering, int participationCount) {
if (offering.getCurrentCount() + participationCount > offering.getTotalCount()) {
throw new MarketException(OfferingMemberErrorCode.INVALID_PARTICIPATION_COUNT);
}
}

@WriterDatabase
@Transactional
public void cancelParticipate(Long offeringId, MemberEntity member) {
Expand All @@ -73,7 +80,7 @@ public void cancelParticipate(Long offeringId, MemberEntity member) {
validateCancel(offeringMember);

offeringMemberRepository.delete(offeringMember);
offering.leave();
offering.leave(offeringMember.getParticipationCount());

eventPublisher.publishEvent(new CancelParticipateEvent(this, offeringMember));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.zzang.chongdae.offeringmember.service.dto;

import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.offering.repository.entity.OfferingEntity;
import com.zzang.chongdae.offeringmember.domain.OfferingMembers;
import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity;
import java.util.List;

public record ParticipantResponse(ProposerResponseItem proposer,
Expand All @@ -12,14 +12,16 @@ public record ParticipantResponse(ProposerResponseItem proposer,
) {

public static ParticipantResponse from(OfferingEntity offering, OfferingMembers offeringMembers) {
MemberEntity proposer = offeringMembers.getProposer();
List<MemberEntity> participants = offeringMembers.getParticipants();
ProposerResponseItem proposerResponse = new ProposerResponseItem(proposer);
int pricePerOne = offering.toOfferingPrice().calculateDividedPrice();

OfferingMemberEntity proposer = offeringMembers.getProposer();
List<OfferingMemberEntity> participants = offeringMembers.getParticipants();
ProposerResponseItem proposerResponse = new ProposerResponseItem(proposer, pricePerOne);
List<ParticipantResponseItem> participantsResponse = participants.stream()
.map(ParticipantResponseItem::new)
.map(participant -> new ParticipantResponseItem(participant, pricePerOne))
.toList();
ParticipantCountResponseItem countResponse = new ParticipantCountResponseItem(offering);
Integer priceResponse = offering.toOfferingPrice().calculateDividedPrice();
return new ParticipantResponse(proposerResponse, participantsResponse, countResponse, priceResponse);

return new ParticipantResponse(proposerResponse, participantsResponse, countResponse, pricePerOne);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.zzang.chongdae.offeringmember.service.dto;

import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity;

public record ParticipantResponseItem(String nickname) {
public record ParticipantResponseItem(String nickname,
Integer count,
Integer price) {

public ParticipantResponseItem(MemberEntity offeringMember) {
this(offeringMember.getNickname());
public ParticipantResponseItem(OfferingMemberEntity offeringMember, int pricePerOne) {
this(offeringMember.getMember().getNickname(),
offeringMember.getParticipationCount(),
offeringMember.getParticipationCount() * pricePerOne);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package com.zzang.chongdae.offeringmember.service.dto;

import jakarta.validation.constraints.NotNull;
import java.util.Objects;

public record ParticipationRequest(@NotNull
Long offeringId) {
Long offeringId,

Integer participationCount) {

@Override
public Integer participationCount() {
return Objects.requireNonNullElse(participationCount, 1);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.zzang.chongdae.offeringmember.service.dto;

import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity;

public record ProposerResponseItem(String nickname) {
public record ProposerResponseItem(String nickname,
Integer count,
Integer price) {

public ProposerResponseItem(MemberEntity member) {
this(member.getNickname());
public ProposerResponseItem(OfferingMemberEntity offeringMember, int pricePerOne) {
this(offeringMember.getMember().getNickname(),
offeringMember.getParticipationCount(),
offeringMember.getParticipationCount() * pricePerOne);
}
}
Loading

0 comments on commit f2fe520

Please sign in to comment.