diff --git a/src/main/java/com/retrip/crew/application/in/CrewService.java b/src/main/java/com/retrip/crew/application/in/CrewService.java index 33bae4b..8958dd1 100644 --- a/src/main/java/com/retrip/crew/application/in/CrewService.java +++ b/src/main/java/com/retrip/crew/application/in/CrewService.java @@ -1,35 +1,55 @@ package com.retrip.crew.application.in; +import com.retrip.crew.application.in.request.IntroductionCreateRequest; +import com.retrip.crew.application.in.request.IntroductionDeleteRequest; +import com.retrip.crew.application.in.request.IntroductionUpdateRequest; import com.retrip.crew.application.in.request.crew.CrewCreateRequest; import com.retrip.crew.application.in.request.crew.CrewOrder; import com.retrip.crew.application.in.request.crew.CrewUpdateRequest; import com.retrip.crew.application.in.request.demand.CreateDemandRequest; import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.IntroductionCreateResponse; +import com.retrip.crew.application.in.response.IntroductionDetailResponse; +import com.retrip.crew.application.in.response.IntroductionListResponse; +import com.retrip.crew.application.in.response.IntroductionUpdateResponse; import com.retrip.crew.application.in.response.crew.CrewCreateResponse; import com.retrip.crew.application.in.response.crew.CrewDetailResponse; import com.retrip.crew.application.in.response.crew.CrewListResponse; import com.retrip.crew.application.in.response.crew.CrewUpdateResponse; -import com.retrip.crew.application.in.response.demand.*; +import com.retrip.crew.application.in.response.demand.ApproveDemandResponse; +import com.retrip.crew.application.in.response.demand.ChangeRecruitmentStatusResponse; +import com.retrip.crew.application.in.response.demand.CreateDemandResponse; +import com.retrip.crew.application.in.response.demand.CrewsOfDemandResponse; +import com.retrip.crew.application.in.response.demand.DemandsResponse; +import com.retrip.crew.application.in.response.demand.RejectDemandResponse; import com.retrip.crew.application.in.usecase.GetCrewUseCase; import com.retrip.crew.application.in.usecase.ManageCrewUseCase; import com.retrip.crew.application.in.usecase.ManageDemandUseCase; +import com.retrip.crew.application.in.usecase.ManageIntroductionUseCase; import com.retrip.crew.application.in.usecase.UpdateRecruitmentUseCase; import com.retrip.crew.application.out.repository.CrewDemandRepository; import com.retrip.crew.application.out.repository.CrewMemberRepository; import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; +import com.retrip.crew.application.out.repository.IntroductionQueryRepository; +import com.retrip.crew.application.out.repository.IntroductionRepository; import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.entity.Demand; +import com.retrip.crew.domain.entity.Introduction; import com.retrip.crew.domain.entity.Recruitment; import com.retrip.crew.domain.exception.CrewNotFoundException; +import com.retrip.crew.domain.exception.IntroductionNotFoundException; import com.retrip.crew.domain.exception.NotCrewLeaderException; import com.retrip.crew.domain.exception.common.BusinessException; import com.retrip.crew.domain.exception.common.EntityNotFoundException; import com.retrip.crew.domain.vo.CrewDescription; import com.retrip.crew.domain.vo.CrewTitle; import com.retrip.crew.domain.vo.DemandStatus; +import com.retrip.crew.domain.vo.IntroductionContent; +import com.retrip.crew.domain.vo.IntroductionTitle; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; import com.retrip.crew.infra.util.PaginationUtils; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -37,16 +57,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.UUID; - @Service @Transactional @RequiredArgsConstructor -public class CrewService implements ManageCrewUseCase, UpdateRecruitmentUseCase, ManageDemandUseCase, GetCrewUseCase { +public class CrewService implements ManageCrewUseCase, UpdateRecruitmentUseCase, ManageDemandUseCase, GetCrewUseCase, ManageIntroductionUseCase{ + private final CrewRepository crewRepository; private final CrewMemberRepository crewMemberRepository; private final CrewQueryRepository crewQueryRepository; private final CrewDemandRepository demandRepository; + private final IntroductionRepository introductionRepository; + private final IntroductionQueryRepository introductionQueryRepository; @Override public CrewCreateResponse createCrew(CrewCreateRequest request) { @@ -56,7 +77,7 @@ public CrewCreateResponse createCrew(CrewCreateRequest request) { @Override public CrewUpdateResponse updateCrew(UUID crewId, CrewUpdateRequest request) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); CrewTitle title = new CrewTitle(request.title()); CrewDescription description = new CrewDescription(request.description()); Recruitment recruitment = crew.getRecruitment(); @@ -68,21 +89,21 @@ public CrewUpdateResponse updateCrew(UUID crewId, CrewUpdateRequest request) { @Override public ChangeRecruitmentStatusResponse startRecruitment(UUID crewId) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); crew.startRecruitment(); return ChangeRecruitmentStatusResponse.of(crew); } @Override public ChangeRecruitmentStatusResponse stopRecruitment(UUID crewId) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); crew.stopRecruitment(); return ChangeRecruitmentStatusResponse.of(crew); } @Override public CreateDemandResponse createDemand(UUID crewId, CreateDemandRequest request) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); Demand demand = crew.demand(request.memberId()); return CreateDemandResponse.of(crew.getId(), demand); } @@ -90,7 +111,7 @@ public CreateDemandResponse createDemand(UUID crewId, CreateDemandRequest reques @Override public Page getDemands( UUID crewId, UUID memberId, String status, Pageable pageable, DemandOrder order, String sort) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); throwIfNotLeader(crew, memberId, new NotCrewLeaderException()); Page demands = demandRepository.findByCrewIdAndStatus( crewId, DemandStatus.valueOf(status), PaginationUtils.createPageRequest(pageable, order.getField(), sort)); @@ -100,7 +121,7 @@ public Page getDemands( @Override public Page getCrewsOfDemand( UUID crewId, UUID demandId, UUID memberId, Pageable pageable, CrewOrder order, String sort) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); throwIfNotLeader(crew, memberId, new NotCrewLeaderException()); Demand demand = findDemandByIdAndCrewId(demandId, crewId); return crewQueryRepository.findAllContainsMember(pageable, demand.getMemberId()); @@ -152,13 +173,63 @@ public ScrollPageResponse getCrews(Pageable pageable, String k @Override @Transactional(readOnly = true) public CrewDetailResponse getCrewDetail(UUID crewId) { - Crew crew = findById(crewId); + Crew crew = findCrewById(crewId); int memberCount = crewMemberRepository.countByCrewId(crewId); return CrewDetailResponse.of(crew, memberCount); } - private Crew findById(UUID crewId){ + @Override + public IntroductionCreateResponse createIntroduction(UUID crewId, IntroductionCreateRequest request) { + Crew crew = findCrewById(crewId); + Introduction introduction = request.to(crew); + crew.addIntroduction(introduction); + return IntroductionCreateResponse.of(introduction); + } + + @Override + public IntroductionUpdateResponse updateIntroduction(UUID crewId, UUID introductionId, IntroductionUpdateRequest request) { + Introduction introduction = findIntroductionByIdAndCrewId(introductionId, crewId); + introduction.update( + new IntroductionTitle(request.title()), + new IntroductionContent(request.content()), + request.loginMemberId() + ); + return IntroductionUpdateResponse.from(introduction); + } + + @Override + public void deleteIntroduction(UUID crewId, UUID introductionId, IntroductionDeleteRequest request) { + Crew crew = findCrewById(crewId); + Introduction introduction = findIntroductionById(introductionId); + introduction.delete(request.loginMemberId(), crew); + } + + @Override + public IntroductionDetailResponse getIntroduction(UUID crewId, UUID introductionId) { + Introduction introduction = findIntroductionByIdAndCrewId(introductionId, crewId); + return IntroductionDetailResponse.of(introduction); + } + + @Override + public ScrollPageResponse getIntroductions(UUID crewId, Pageable pageable) { + findCrewById(crewId); + Slice result = introductionQueryRepository.getIntroductions(crewId, pageable); + Long totalCount = introductionQueryRepository.getIntroductionCount(crewId); + return ScrollPageResponse.of(totalCount, result.hasNext(), result.getContent()); + } + + public Crew findCrewById(UUID crewId){ return crewRepository.findById(crewId) .orElseThrow(CrewNotFoundException::new); } + + public Introduction findIntroductionById(UUID introductionId){ + return introductionRepository.findById(introductionId) + .orElseThrow(IntroductionNotFoundException::new); + } + + public Introduction findIntroductionByIdAndCrewId(UUID introductionId, UUID crewId) { + return introductionRepository.findByIdAndCrewId(introductionId, crewId) + .orElseThrow(IntroductionNotFoundException::new); + } } diff --git a/src/main/java/com/retrip/crew/application/in/request/IntroductionCreateRequest.java b/src/main/java/com/retrip/crew/application/in/request/IntroductionCreateRequest.java new file mode 100644 index 0000000..2df92e5 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/IntroductionCreateRequest.java @@ -0,0 +1,21 @@ +package com.retrip.crew.application.in.request; + +import com.retrip.crew.domain.entity.Crew; +import com.retrip.crew.domain.entity.Introduction; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "크루 자기소개 등록 Request") +public record IntroductionCreateRequest( + + @Schema(description = "로그인한 사용자 - 임시 방편") + UUID loginMemberId, + @Schema(description = "자기소개 제목") + String title, + @Schema(description = "자기소개 본문") + String content +) { + public Introduction to(Crew crew){ + return Introduction.create(this.loginMemberId, this.title, this.content, crew); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/request/IntroductionDeleteRequest.java b/src/main/java/com/retrip/crew/application/in/request/IntroductionDeleteRequest.java new file mode 100644 index 0000000..30937f9 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/IntroductionDeleteRequest.java @@ -0,0 +1,12 @@ +package com.retrip.crew.application.in.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "크루 자기소개 삭제 Request") +public record IntroductionDeleteRequest( + + @Schema(description = "로그인한 사용자 - 임시 방편") + UUID loginMemberId +) { +} diff --git a/src/main/java/com/retrip/crew/application/in/request/IntroductionUpdateRequest.java b/src/main/java/com/retrip/crew/application/in/request/IntroductionUpdateRequest.java new file mode 100644 index 0000000..e374a44 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/request/IntroductionUpdateRequest.java @@ -0,0 +1,16 @@ +package com.retrip.crew.application.in.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "크루 자기소개 수정 Request") +public record IntroductionUpdateRequest( + + @Schema(description = "로그인한 사용자 - 임시 방편") + UUID loginMemberId, + @Schema(description = "자기소개 제목") + String title, + @Schema(description = "자기소개 본문") + String content +) { +} diff --git a/src/main/java/com/retrip/crew/application/in/response/IntroductionCreateResponse.java b/src/main/java/com/retrip/crew/application/in/response/IntroductionCreateResponse.java new file mode 100644 index 0000000..fb3a265 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/IntroductionCreateResponse.java @@ -0,0 +1,27 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Introduction; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "크루 자기소개 등록 Response") +public record IntroductionCreateResponse( + + @Schema(description = "크루 ID") + UUID crewId, + @Schema(description = "자기소개 ID") + UUID introductionId, + @Schema(description = "자기소개 제목") + String title, + @Schema(description = "자기소개 본문") + String content +) { + public static IntroductionCreateResponse of(Introduction introduction){ + return new IntroductionCreateResponse( + introduction.getCrew().getId(), + introduction.getId(), + introduction.getTitle(), + introduction.getContent() + ); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/IntroductionDetailResponse.java b/src/main/java/com/retrip/crew/application/in/response/IntroductionDetailResponse.java new file mode 100644 index 0000000..1f7a951 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/IntroductionDetailResponse.java @@ -0,0 +1,32 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Introduction; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; +import lombok.Builder; + +@Builder +@Schema(description = "크루 자기소개 상세 Response") +public record IntroductionDetailResponse( + + @Schema(description = "크루 ID") + UUID crewId, + @Schema(description = "자기소개 ID") + UUID introductionId, + @Schema(description = "자기소개 등록한 사용자 ID") + UUID memberId, + @Schema(description = "자기소개 제목") + String title, + @Schema(description = "자기소개 본문") + String content +) { + public static IntroductionDetailResponse of(Introduction introduction){ + return IntroductionDetailResponse.builder() + .crewId(introduction.getCrew().getId()) + .introductionId(introduction.getId()) + .memberId(introduction.getMemberId()) + .title(introduction.getTitle()) + .content(introduction.getContent()) + .build(); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/response/IntroductionListResponse.java b/src/main/java/com/retrip/crew/application/in/response/IntroductionListResponse.java new file mode 100644 index 0000000..f625f4e --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/IntroductionListResponse.java @@ -0,0 +1,21 @@ +package com.retrip.crew.application.in.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; +import java.util.UUID; + +@Schema(description = "크루 자기소개 리스트 Response") +public record IntroductionListResponse( + + @Schema(description = "크루 ID") + UUID crewId, + @Schema(description = "자기소개 ID") + UUID introductionId, + @Schema(description = "자기소개 제목") + String title, + @Schema(description = "자기소개 등록일자") + LocalDateTime createdAt, + @Schema(description = "자기소개 수정일자") + LocalDateTime editedAt +) { +} diff --git a/src/main/java/com/retrip/crew/application/in/response/IntroductionUpdateResponse.java b/src/main/java/com/retrip/crew/application/in/response/IntroductionUpdateResponse.java new file mode 100644 index 0000000..c3f92c4 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/response/IntroductionUpdateResponse.java @@ -0,0 +1,27 @@ +package com.retrip.crew.application.in.response; + +import com.retrip.crew.domain.entity.Introduction; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.UUID; + +@Schema(description = "크루 자기소개 수정 Response") +public record IntroductionUpdateResponse( + + @Schema(description = "크루 ID") + UUID crewId, + @Schema(description = "자기소개 ID") + UUID introductionId, + @Schema(description = "자기소개 제목") + String title, + @Schema(description = "자기소개 본문") + String content +) { + public static IntroductionUpdateResponse from(Introduction introduction){ + return new IntroductionUpdateResponse( + introduction.getCrew().getId(), + introduction.getId(), + introduction.getTitle(), + introduction.getContent() + ); + } +} diff --git a/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java index 61df32d..6131c7f 100644 --- a/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageCrewUseCase.java @@ -11,4 +11,5 @@ public interface ManageCrewUseCase { CrewCreateResponse createCrew(CrewCreateRequest request); CrewUpdateResponse updateCrew(UUID crewId, CrewUpdateRequest request); + } diff --git a/src/main/java/com/retrip/crew/application/in/usecase/ManageIntroductionUseCase.java b/src/main/java/com/retrip/crew/application/in/usecase/ManageIntroductionUseCase.java new file mode 100644 index 0000000..6f0c4db --- /dev/null +++ b/src/main/java/com/retrip/crew/application/in/usecase/ManageIntroductionUseCase.java @@ -0,0 +1,24 @@ +package com.retrip.crew.application.in.usecase; + +import com.retrip.crew.application.in.request.IntroductionCreateRequest; +import com.retrip.crew.application.in.request.IntroductionDeleteRequest; +import com.retrip.crew.application.in.request.IntroductionUpdateRequest; +import com.retrip.crew.application.in.response.IntroductionCreateResponse; +import com.retrip.crew.application.in.response.IntroductionDetailResponse; +import com.retrip.crew.application.in.response.IntroductionListResponse; +import com.retrip.crew.application.in.response.IntroductionUpdateResponse; +import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import java.util.UUID; +import org.springframework.data.domain.Pageable; + +public interface ManageIntroductionUseCase { + IntroductionCreateResponse createIntroduction(UUID crewId, IntroductionCreateRequest request); + + IntroductionUpdateResponse updateIntroduction(UUID crewId, UUID introductionId, IntroductionUpdateRequest request); + + void deleteIntroduction(UUID crewId, UUID introductionId, IntroductionDeleteRequest request); + + IntroductionDetailResponse getIntroduction(UUID crewId, UUID introductionId); + + ScrollPageResponse getIntroductions(UUID crewId, Pageable pageable); +} diff --git a/src/main/java/com/retrip/crew/application/out/repository/IntroductionQueryRepository.java b/src/main/java/com/retrip/crew/application/out/repository/IntroductionQueryRepository.java new file mode 100644 index 0000000..c7f5d36 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/out/repository/IntroductionQueryRepository.java @@ -0,0 +1,13 @@ +package com.retrip.crew.application.out.repository; + +import com.retrip.crew.application.in.response.IntroductionListResponse; +import java.util.UUID; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +public interface IntroductionQueryRepository { + + Slice getIntroductions(UUID crewId, Pageable pageable); + + Long getIntroductionCount(UUID crewId); +} diff --git a/src/main/java/com/retrip/crew/application/out/repository/IntroductionRepository.java b/src/main/java/com/retrip/crew/application/out/repository/IntroductionRepository.java new file mode 100644 index 0000000..8060c27 --- /dev/null +++ b/src/main/java/com/retrip/crew/application/out/repository/IntroductionRepository.java @@ -0,0 +1,11 @@ +package com.retrip.crew.application.out.repository; + +import com.retrip.crew.domain.entity.Introduction; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface IntroductionRepository extends JpaRepository { + + Optional findByIdAndCrewId(UUID introductionId, UUID crewId); +} diff --git a/src/main/java/com/retrip/crew/domain/entity/Crew.java b/src/main/java/com/retrip/crew/domain/entity/Crew.java index b3898d0..114f580 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Crew.java +++ b/src/main/java/com/retrip/crew/domain/entity/Crew.java @@ -2,13 +2,16 @@ import com.retrip.crew.domain.vo.CrewDescription; import com.retrip.crew.domain.vo.CrewTitle; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import java.util.UUID; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -69,6 +72,10 @@ public void stopRecruitment() { this.recruitment.stop(); } + public void addIntroduction(Introduction introduction){ + this.introductions.addIntroduction(introduction); + } + public void update(CrewTitle title, CrewDescription description) { this.title = title; this.description = description; diff --git a/src/main/java/com/retrip/crew/domain/entity/Introduction.java b/src/main/java/com/retrip/crew/domain/entity/Introduction.java index 1753df1..0a10ce2 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Introduction.java +++ b/src/main/java/com/retrip/crew/domain/entity/Introduction.java @@ -1,14 +1,21 @@ package com.retrip.crew.domain.entity; +import com.retrip.crew.domain.exception.common.InvalidAccessException; import com.retrip.crew.domain.vo.IntroductionContent; import com.retrip.crew.domain.vo.IntroductionTitle; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.util.UUID; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -17,6 +24,8 @@ public class Introduction extends BaseEntity { @Column(columnDefinition = "varbinary(16)") private UUID id; + private UUID memberId; + @Embedded private IntroductionTitle title; @@ -28,7 +37,58 @@ public class Introduction extends BaseEntity { name = "crew_id", nullable = false, columnDefinition = "varbinary(16)", - foreignKey = @ForeignKey(name = "fk_self_introduce_board_to_crew") + foreignKey = @ForeignKey(name = "fk_introduction_to_crew") ) private Crew crew; + + public Introduction(UUID loginMemberId, String title, String content, Crew crew){ + this.id = UUID.randomUUID(); + this.memberId = loginMemberId; + this.title = new IntroductionTitle(title); + this.content = new IntroductionContent(content); + this.crew = crew; + } + + public static Introduction create(UUID loginMemberId, String title, String content, Crew crew) { + return new Introduction(loginMemberId, title, content, crew); + } + + public String getTitle(){ + return title.getValue(); + } + + public String getContent(){ + return content.getValue(); + } + + public void update(IntroductionTitle introductionTitle, IntroductionContent introductionContent, UUID loginMemberId) { + validateIntroductionOwner(loginMemberId); + this.title = introductionTitle; + this.content = introductionContent; + } + + public void validateIntroductionOwner(UUID loginMemberId){ + if(!isOwner(loginMemberId)){ + throw new InvalidAccessException(); + } + } + + public void validateIntroductionOwnerAndLeader(UUID loginMemberId){ + if(!isOwner(loginMemberId) || !isLeader(loginMemberId)){ + throw new InvalidAccessException(); + } + } + + public boolean isOwner(UUID loginMemberId){ + return this.memberId.equals(loginMemberId); + } + + public boolean isLeader(UUID loginMemberId){ + return this.getCrew().getLeader().getMemberId().equals(loginMemberId); + } + + public void delete(UUID loginMemberId, Crew crew) { + validateIntroductionOwnerAndLeader(loginMemberId); + crew.getIntroductions().deleteIntroduction(this); + } } diff --git a/src/main/java/com/retrip/crew/domain/entity/Introductions.java b/src/main/java/com/retrip/crew/domain/entity/Introductions.java index 6d269aa..7128b03 100644 --- a/src/main/java/com/retrip/crew/domain/entity/Introductions.java +++ b/src/main/java/com/retrip/crew/domain/entity/Introductions.java @@ -3,14 +3,14 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Embeddable; import jakarta.persistence.OneToMany; -import lombok.Getter; - import java.util.ArrayList; import java.util.List; +import lombok.Getter; @Getter @Embeddable public class Introductions { + @OneToMany(mappedBy = "crew", cascade = CascadeType.ALL, orphanRemoval = true) private List values = new ArrayList<>(); @@ -21,4 +21,12 @@ public Introductions() { private List createEmptyValues() { return new ArrayList<>(); } + + public void addIntroduction(Introduction introduction){ + this.values.add(introduction); + } + + public void deleteIntroduction(Introduction introduction) { + this.values.remove(introduction); + } } diff --git a/src/main/java/com/retrip/crew/domain/exception/IntroductionNotFoundException.java b/src/main/java/com/retrip/crew/domain/exception/IntroductionNotFoundException.java new file mode 100644 index 0000000..bd9eb75 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/IntroductionNotFoundException.java @@ -0,0 +1,12 @@ +package com.retrip.crew.domain.exception; + +import com.retrip.crew.domain.exception.common.EntityNotFoundException; +import com.retrip.crew.domain.exception.common.ErrorCode; + +public class IntroductionNotFoundException extends EntityNotFoundException { + private static final ErrorCode errorCode = ErrorCode.INTRODUCTION_NOT_FOUND; + + public IntroductionNotFoundException() { + super(errorCode); + } +} diff --git a/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java b/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java index 4f2ae0b..bea0b8c 100644 --- a/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java +++ b/src/main/java/com/retrip/crew/domain/exception/common/ErrorCode.java @@ -17,7 +17,9 @@ public enum ErrorCode { NOT_CREW_LEADER(BAD_REQUEST, "Crew-002", "크루 리더가 아니면 접근할 수 없습니다."), UNABLE_TO_START_RECRUITMENT(BAD_REQUEST, "Crew-003", "크루 멤버를 모집할 수 없습니다."), DUPLICATE_DEMAND(BAD_REQUEST, "Crew-004", "이미 크루 참여를 요청한 사용자는 다시 요청할 수 없습니다."), - ILLEGAL_DEMAND_STATE(BAD_REQUEST, "Crew-004", "이미 크루 참여를 요청한 사용자는 다시 요청할 수 없습니다.") + ILLEGAL_DEMAND_STATE(BAD_REQUEST, "Crew-004", "이미 크루 참여를 요청한 사용자는 다시 요청할 수 없습니다."), + INTRODUCTION_NOT_FOUND(BAD_REQUEST, "Crew-005", "크루 자기소개 엔티티를 찾을 수 없습니다."), + INVALID_ACCESS(FORBIDDEN, "Crew-006","접근 권한이 존재하지 않습니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/retrip/crew/domain/exception/common/InvalidAccessException.java b/src/main/java/com/retrip/crew/domain/exception/common/InvalidAccessException.java new file mode 100644 index 0000000..be1c306 --- /dev/null +++ b/src/main/java/com/retrip/crew/domain/exception/common/InvalidAccessException.java @@ -0,0 +1,21 @@ +package com.retrip.crew.domain.exception.common; + +public class InvalidAccessException extends BusinessException { + private static final ErrorCode errorCode = ErrorCode.INVALID_ACCESS; + + public InvalidAccessException() { + super(errorCode); + } + public InvalidAccessException(ErrorCode errorCode) { + super(errorCode); + } + + public InvalidAccessException(String message) { + super(errorCode, message); + } + + public InvalidAccessException(ErrorCode errorCode, String message) { + super(errorCode, message); + } + +} diff --git a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java index 5dcfa2d..cc3c6b4 100644 --- a/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java +++ b/src/main/java/com/retrip/crew/infra/adapter/in/presentation/rest/CrewController.java @@ -1,31 +1,40 @@ package com.retrip.crew.infra.adapter.in.presentation.rest; +import com.retrip.crew.application.in.request.IntroductionCreateRequest; +import com.retrip.crew.application.in.request.IntroductionDeleteRequest; +import com.retrip.crew.application.in.request.IntroductionUpdateRequest; import com.retrip.crew.application.in.request.crew.CrewCreateRequest; import com.retrip.crew.application.in.request.crew.CrewOrder; import com.retrip.crew.application.in.request.crew.CrewUpdateRequest; -import com.retrip.crew.application.in.request.demand.CreateDemandRequest; -import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.IntroductionCreateResponse; +import com.retrip.crew.application.in.response.IntroductionDetailResponse; +import com.retrip.crew.application.in.response.IntroductionListResponse; +import com.retrip.crew.application.in.response.IntroductionUpdateResponse; import com.retrip.crew.application.in.response.crew.CrewCreateResponse; import com.retrip.crew.application.in.response.crew.CrewDetailResponse; import com.retrip.crew.application.in.response.crew.CrewListResponse; import com.retrip.crew.application.in.response.crew.CrewUpdateResponse; -import com.retrip.crew.application.in.response.demand.*; import com.retrip.crew.application.in.usecase.GetCrewUseCase; import com.retrip.crew.application.in.usecase.ManageCrewUseCase; -import com.retrip.crew.application.in.usecase.ManageDemandUseCase; -import com.retrip.crew.application.in.usecase.UpdateRecruitmentUseCase; +import com.retrip.crew.application.in.usecase.ManageIntroductionUseCase; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ApiResponse; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.UUID; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.UUID; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor @RequestMapping("/crews") @@ -34,24 +43,25 @@ public class CrewController { private final ManageCrewUseCase manageCrewUseCase; private final GetCrewUseCase getCrewUseCase; + private final ManageIntroductionUseCase manageIntroductionUseCase; - @PostMapping @Schema(description = "크루 생성") + @PostMapping public ApiResponse createCrew(@RequestBody CrewCreateRequest request) { CrewCreateResponse crew = manageCrewUseCase.createCrew(request); return ApiResponse.created(crew); } - @PutMapping("/{crewId}") @Schema(description = "크루 정보 수정") + @PutMapping("/{crewId}") public ApiResponse updateCrew( @PathVariable UUID crewId, @RequestBody CrewUpdateRequest request) { CrewUpdateResponse crew = manageCrewUseCase.updateCrew(crewId, request); return ApiResponse.ok(crew); } - @GetMapping @Schema(description = "크루 리스트 조회") + @GetMapping public ResponseEntity>> getCrews( @RequestParam(name = "keyword", required = false) String keyword, @RequestParam(name = "order", defaultValue = "DATE") CrewOrder order, @@ -62,12 +72,61 @@ public ResponseEntity>> getCrew return ResponseEntity.ok().body(ApiResponse.ok(response)); } - @GetMapping("/{crewId}") @Schema(description = "크루 상세 조회") + @GetMapping("/{crewId}") public ResponseEntity> getCrewDetail( @PathVariable("crewId") UUID crewId ) { CrewDetailResponse response = getCrewUseCase.getCrewDetail(crewId); return ResponseEntity.ok().body(ApiResponse.ok(response)); } + + @Schema(description = "크루 자기소개 등록") + @PostMapping("/{crewId}/introductions") + public ApiResponse createIntroduction( + @PathVariable final UUID crewId, + @RequestBody IntroductionCreateRequest request) { + IntroductionCreateResponse response = manageIntroductionUseCase.createIntroduction(crewId, request); + return ApiResponse.created(response); + } + + @Schema(description = "크루 자기소개 수정") + @PutMapping("/{crewId}/introductions/{introductionId}") + public ApiResponse updateIntroduction( + @PathVariable("crewId") final UUID crewId, + @PathVariable("introductionId") final UUID introductionId, + @RequestBody IntroductionUpdateRequest request) { + IntroductionUpdateResponse response = manageIntroductionUseCase.updateIntroduction(crewId, introductionId, request); + return ApiResponse.created(response); + } + + @Schema(description = "크루 자기소개 삭제") + @DeleteMapping("/{crewId}/introductions/{introductionId}") + public ApiResponse deleteIntroduction( + @PathVariable("crewId") final UUID crewId, + @PathVariable("introductionId") final UUID introductionId, + @RequestBody IntroductionDeleteRequest request) { + manageIntroductionUseCase.deleteIntroduction(crewId, introductionId, request); + return ApiResponse.created(null); + } + + @Schema(description = "크루 자기소개 상세 조회") + @GetMapping("/{crewId}/introductions/{introductionId}") + public ApiResponse getIntroduction( + @PathVariable("crewId") final UUID crewId, + @PathVariable("introductionId") final UUID introductionId + ) { + IntroductionDetailResponse response = manageIntroductionUseCase.getIntroduction(crewId, introductionId); + return ApiResponse.ok(response); + } + + @Schema(description = "크루 자기소개 리스트 조회") + @GetMapping("/{crewId}/introductions") + public ApiResponse> getIntroductions( + @PathVariable("crewId") final UUID crewId, + @PageableDefault(size = 10) Pageable pageable + ) { + ScrollPageResponse response = manageIntroductionUseCase.getIntroductions(crewId, pageable); + return ApiResponse.ok(response); + } } diff --git a/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/IntroductionQuerydslRepository.java b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/IntroductionQuerydslRepository.java new file mode 100644 index 0000000..8795921 --- /dev/null +++ b/src/main/java/com/retrip/crew/infra/adapter/out/persistence/mysql/query/IntroductionQuerydslRepository.java @@ -0,0 +1,51 @@ +package com.retrip.crew.infra.adapter.out.persistence.mysql.query; + +import static com.retrip.crew.domain.entity.QCrew.crew; +import static com.retrip.crew.domain.entity.QIntroduction.introduction; +import static com.retrip.crew.infra.util.PaginationUtils.checkEndPage; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.retrip.crew.application.in.response.IntroductionListResponse; +import com.retrip.crew.application.out.repository.IntroductionQueryRepository; +import java.util.List; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class IntroductionQuerydslRepository implements IntroductionQueryRepository { + + private final JPAQueryFactory query; + + @Override + public Slice getIntroductions(UUID crewId, Pageable pageable) { + List fetch = query.select(Projections.constructor(IntroductionListResponse.class, + introduction.crew.id.as("crewId"), + introduction.id.as("introductionId"), + introduction.title.value.as("title"), + introduction.createdAt.as("createdAt"), + introduction.editedAt.as("editedAt") + ) + ) + .from(introduction) + .join(crew).on(crew.id.eq(introduction.crew.id)) + .where(introduction.crew.id.eq(crewId)) + .limit(pageable.getPageSize() + 1) + .offset(pageable.getOffset()) + .orderBy(introduction.editedAt.desc()) + .fetch(); + return checkEndPage(pageable, fetch); + } + + @Override + public Long getIntroductionCount(UUID crewId) { + return query.select(introduction.count()) + .from(introduction) + .where(introduction.crew.id.eq(crewId)) + .fetchOne(); + } +} diff --git a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java index 42c5e2a..8ef1a5e 100644 --- a/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java +++ b/src/test/java/com/retrip/crew/application/in/CrewServiceTest.java @@ -1,10 +1,31 @@ package com.retrip.crew.application.in; +import static com.retrip.crew.common.fixture.CrewFixture.LEADER_ID; +import static com.retrip.crew.common.fixture.CrewFixture.MEMBER_ID; +import static com.retrip.crew.common.fixture.CrewFixture.createCrew; +import static com.retrip.crew.common.fixture.CrewFixture.createCrewRequest; +import static com.retrip.crew.common.fixture.CrewFixture.createCrewWithMembers; +import static com.retrip.crew.common.fixture.CrewFixture.createMultipleCrews; +import static com.retrip.crew.common.fixture.CrewFixture.정수_ID; +import static com.retrip.crew.common.fixture.CrewFixture.준호_ID; +import static com.retrip.crew.common.fixture.CrewFixture.지수_ID; +import static com.retrip.crew.common.fixture.CrewFixture.혁진_ID; +import static com.retrip.crew.common.fixture.CrewFixture.홍석_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.retrip.crew.application.in.request.IntroductionCreateRequest; +import com.retrip.crew.application.in.request.IntroductionDeleteRequest; +import com.retrip.crew.application.in.request.IntroductionUpdateRequest; import com.retrip.crew.application.in.request.crew.CrewCreateRequest; import com.retrip.crew.application.in.request.crew.CrewOrder; import com.retrip.crew.application.in.request.crew.CrewUpdateRequest; import com.retrip.crew.application.in.request.demand.CreateDemandRequest; import com.retrip.crew.application.in.request.demand.DemandOrder; +import com.retrip.crew.application.in.response.IntroductionCreateResponse; +import com.retrip.crew.application.in.response.IntroductionDetailResponse; import com.retrip.crew.application.in.response.crew.CrewCreateResponse; import com.retrip.crew.application.in.response.crew.CrewDetailResponse; import com.retrip.crew.application.in.response.crew.CrewListResponse; @@ -16,9 +37,14 @@ import com.retrip.crew.domain.entity.Crew; import com.retrip.crew.domain.entity.CrewMemberRole; import com.retrip.crew.domain.entity.Demand; +import com.retrip.crew.domain.entity.Introduction; import com.retrip.crew.domain.exception.DuplicateDemandException; +import com.retrip.crew.domain.exception.common.InvalidAccessException; import com.retrip.crew.domain.vo.DemandStatus; import com.retrip.crew.infra.adapter.in.presentation.rest.common.ScrollPageResponse; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -27,14 +53,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.test.util.ReflectionTestUtils; -import java.util.List; -import java.util.UUID; - -import static com.retrip.crew.common.fixture.CrewFixture.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - class CrewServiceTest extends ServiceTest { @Test void 크루를_생성_한다() { @@ -222,4 +240,130 @@ class CrewServiceTest extends ServiceTest { () -> assertThat(response.members().getFirst().memberId()).isEqualTo(MEMBER_ID) ); } + + @Test + void 자기소개를_등록한다() { + // given + Crew crew = crewRepository.save(Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + )); + IntroductionCreateRequest request = new IntroductionCreateRequest(MEMBER_ID, "정수의 자기소개!", "안녕하세요!"); + + // when + IntroductionCreateResponse response = crewService.createIntroduction(crew.getId(), request); + + // then + assertThat(response.crewId()).isNotNull(); + } + + @Test + void 내가_작성한_자기소개를_수정할_수_있다() { + // given + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + ); + Introduction introduction = Introduction.create( + MEMBER_ID, + "정수의 자기소개!", + "안녕하세요!", + crew + ); + crew.addIntroduction(introduction); + Crew savedCrew = crewRepository.save(crew); + + IntroductionUpdateRequest request = new IntroductionUpdateRequest(MEMBER_ID, "변경된 자기소개!", "안녕!"); + + // when + crewService.updateIntroduction(savedCrew.getId(), introduction.getId(), request); + Introduction result = crewService.findIntroductionByIdAndCrewId(introduction.getId(), savedCrew.getId()); + + // then + assertThat(result.getTitle()).isEqualTo("변경된 자기소개!"); + assertThat(result.getContent()).isEqualTo("안녕!"); + } + + @Test + void 일반_멤버는_다른_멤버의_자기소개를_수정할_수_없다() { + // given + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + ); + Introduction introduction = Introduction.create( + MEMBER_ID, + "정수의 자기소개!", + "안녕하세요!", + crew + ); + crew.addIntroduction(introduction); + Crew savedCrew = crewRepository.save(crew); + + UUID otherMemberId = UUID.randomUUID(); + IntroductionUpdateRequest request = new IntroductionUpdateRequest(otherMemberId, "수정된 소개", "안"); + + // when & then + assertThrows(InvalidAccessException.class, () -> crewService.updateIntroduction(savedCrew.getId(), introduction.getId(), request)); + } + + @Test + void 리더가_자기소개를_삭제할_수_있다() { + // given + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + ); + Introduction introduction = Introduction.create( + MEMBER_ID, + "정수의 자기소개!", + "안녕하세요!", + crew + ); + crew.addIntroduction(introduction); + Crew savedCrew = crewRepository.save(crew); + + IntroductionDeleteRequest request = new IntroductionDeleteRequest(MEMBER_ID); + + // when + crewService.deleteIntroduction(savedCrew.getId(), introduction.getId(), request); + + // then + assertThat(savedCrew.getIntroductions().getValues().isEmpty()).isTrue(); + } + + @Test + void 자기소개를_조회한다() { + // given + Crew crew = Crew.create( + "속초 크루원 구함", + "속초 친구 구합니다! 나이는 20~40.. 많은 가입 부탁드립니다.", + 100, + MEMBER_ID + ); + Introduction introduction = Introduction.create( + MEMBER_ID, + "정수의 자기소개!", + "안녕하세요!", + crew + ); + crew.addIntroduction(introduction); + Crew savedCrew = crewRepository.save(crew); + + // when + IntroductionDetailResponse response = crewService.getIntroduction(savedCrew.getId(), introduction.getId()); + + // then + assertThat(response.introductionId()).isEqualTo(introduction.getId()); + assertThat(response.content()).isEqualTo("안녕하세요!"); + assertThat(response.title()).isEqualTo("정수의 자기소개!"); + } } diff --git a/src/test/java/com/retrip/crew/common/ServiceTest.java b/src/test/java/com/retrip/crew/common/ServiceTest.java index cce29ed..0a7d9b3 100644 --- a/src/test/java/com/retrip/crew/common/ServiceTest.java +++ b/src/test/java/com/retrip/crew/common/ServiceTest.java @@ -5,11 +5,15 @@ import com.retrip.crew.application.out.repository.CrewMemberRepository; import com.retrip.crew.application.out.repository.CrewQueryRepository; import com.retrip.crew.application.out.repository.CrewRepository; +import com.retrip.crew.application.out.repository.IntroductionQueryRepository; +import com.retrip.crew.application.out.repository.IntroductionRepository; import com.retrip.crew.common.config.QuerydslConfig; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; import org.springframework.context.annotation.Import; @DataJpaTest @@ -29,10 +33,19 @@ public class ServiceTest { @Autowired protected CrewDemandRepository demandRepository; + @Autowired + protected IntroductionRepository introductionRepository; + + @Autowired + protected IntroductionQueryRepository introductionQueryRepository; + + @Autowired + protected TestEntityManager entityManager; + protected CrewService crewService; @BeforeEach void setUp() { - crewService = new CrewService(crewRepository, crewMemberRepository, crewQueryRepository, demandRepository); + crewService = new CrewService(crewRepository, crewMemberRepository, crewQueryRepository, demandRepository, introductionRepository, introductionQueryRepository); } } diff --git a/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java b/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java index 7331886..d5c1b22 100644 --- a/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java +++ b/src/test/java/com/retrip/crew/common/config/QuerydslConfig.java @@ -2,6 +2,7 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import com.retrip.crew.infra.adapter.out.persistence.mysql.query.CrewQuerydslRepository; +import com.retrip.crew.infra.adapter.out.persistence.mysql.query.IntroductionQuerydslRepository; import jakarta.persistence.EntityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.TestConfiguration; @@ -24,4 +25,9 @@ public JPAQueryFactory jpaQueryFactory() { public CrewQuerydslRepository crewQuerydslRepository(JPAQueryFactory jpaQueryFactory) { return new CrewQuerydslRepository(jpaQueryFactory); } + + @Bean + public IntroductionQuerydslRepository introductionQuerydslRepository(JPAQueryFactory jpaQueryFactory) { + return new IntroductionQuerydslRepository(jpaQueryFactory); + } }