Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.flytrap.venusplanner.api.join_request.business.service;

import static com.flytrap.venusplanner.api.join_request.exception.JoinRequestExceptionType.DuplicateJoinRequestException;
import static com.flytrap.venusplanner.api.join_request.exception.JoinRequestExceptionType.JoinRequestAlreadyHandledException;
import static com.flytrap.venusplanner.api.study.exception.StudyExceptionType.StudyAlreadyJoinedException;
import static com.flytrap.venusplanner.api.study.exception.StudyExceptionType.StudyMismatchException;

import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;
import com.flytrap.venusplanner.api.member_study.business.service.MemberStudyValidator;
import com.flytrap.venusplanner.api.study.business.service.StudyValidator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class JoinRequestCurdFacadeService {

private final JoinRequestUpdater joinRequestUpdater;
private final JoinRequestValidator joinRequestValidator;
private final StudyValidator studyValidator;
private final MemberStudyValidator memberStudyValidator;

@Transactional
public JoinRequest createJoinRequest(Long studyId, Long memberId) {
studyValidator.validateStudyExists(studyId);

if (memberStudyValidator.validateMemberBelongsToStudy(memberId, studyId)) {
throw StudyAlreadyJoinedException();
}

if (joinRequestValidator.validateWaitingJoinRequestExists(studyId, memberId)) {
throw DuplicateJoinRequestException();
}

return joinRequestUpdater.saveJoinRequest(studyId, memberId);
}

@Transactional
public void acceptJoinRequest(Long studyId, Long requestId, Long memberId) {
studyValidator.validateStudyExists(studyId);
memberStudyValidator.validateMemberCanAcceptJoinRequest(memberId, studyId);

JoinRequest joinRequest = joinRequestValidator.findById(requestId);

if (!joinRequest.validateStudyIdMatch(studyId)) {
throw StudyMismatchException("요청한 스터디 ID와 가입 요청의 스터디 ID가 일치하지 않습니다.");
}

if (!joinRequest.isWaiting()) {
throw JoinRequestAlreadyHandledException();
}

joinRequest.accept();
}

@Transactional
public void rejectJoinRequest(Long studyId, Long requestId, Long memberId) {
studyValidator.validateStudyExists(studyId);
memberStudyValidator.validateMemberCanRejectJoinRequest(memberId, studyId);

JoinRequest joinRequest = joinRequestValidator.findById(requestId);

if (!joinRequest.validateStudyIdMatch(studyId)) {
throw StudyMismatchException("요청한 스터디 ID와 가입 요청의 스터디 ID가 일치하지 않습니다.");
}

if (!joinRequest.isWaiting()) {
throw JoinRequestAlreadyHandledException();
}

joinRequest.reject();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.flytrap.venusplanner.api.join_request.business.service;

import static com.flytrap.venusplanner.api.join_request.exception.JoinRequestExceptionType.JoinRequestNotFoundException;

import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;
import com.flytrap.venusplanner.api.join_request.domain.JoinRequestState;
import com.flytrap.venusplanner.api.join_request.infrastructure.repository.JoinRequestRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class JoinRequestService implements JoinRequestUpdater, JoinRequestValidator {

private final JoinRequestRepository joinRequestRepository;

@Override
@Transactional
public JoinRequest saveJoinRequest(Long studyId, Long memberId) {
return joinRequestRepository.save(JoinRequest.create(studyId, memberId));
}

@Override
public boolean validateWaitingJoinRequestExists(Long studyId, Long memberId) {
return joinRequestRepository.existsByMemberIdAndStudyIdAndState(
memberId, studyId, JoinRequestState.WAIT);
}

@Override
public JoinRequest findById(Long joinRequestId) {
return joinRequestRepository.findById(joinRequestId)
.orElseThrow(() -> JoinRequestNotFoundException(joinRequestId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.flytrap.venusplanner.api.join_request.business.service;

import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;

public interface JoinRequestUpdater {

JoinRequest saveJoinRequest(Long studyId, Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.flytrap.venusplanner.api.join_request.business.service;

import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;

public interface JoinRequestValidator {
boolean validateWaitingJoinRequestExists(Long studyId, Long memberId);
JoinRequest findById(Long joinRequestId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.flytrap.venusplanner.api.join_request.domain;

import com.flytrap.venusplanner.global.entity.TimeAuditingBaseEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class JoinRequest extends TimeAuditingBaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotNull
private Long memberId;

@NotNull
private Long studyId;

@Enumerated(EnumType.STRING)
private JoinRequestState state;

@Builder
private JoinRequest(Long memberId, Long studyId, JoinRequestState state) {
this.memberId = memberId;
this.studyId = studyId;
this.state = state;
}

public static JoinRequest create(Long studyId, Long memberId) {
return JoinRequest.builder()
.studyId(studyId).memberId(memberId)
.state(JoinRequestState.WAIT)
.build();
}

public boolean isWaiting() {
return this.state == JoinRequestState.WAIT;
}

public boolean validateStudyIdMatch(Long studyId) {
return Objects.equals(this.studyId, studyId);
}

public void accept() {
this.state = JoinRequestState.ACCEPT;
}

public void reject() {
this.state = JoinRequestState.REJECT;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.flytrap.venusplanner.api.join_request.domain;

public enum JoinRequestState {
WAIT, ACCEPT, REJECT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.flytrap.venusplanner.api.join_request.exception;

import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;
import com.flytrap.venusplanner.global.exception.CustomException;
import com.flytrap.venusplanner.global.exception.GeneralExceptionType;

public class JoinRequestExceptionType extends GeneralExceptionType {

public static CustomException JoinRequestNotFoundException(Long joinRequestId) {
return DomainNotFoundException(JoinRequest.class, joinRequestId);
}

public static CustomException DuplicateJoinRequestException() {
return DuplicateDomainException("이미 가입 요청된 상태입니다.");
}

public static CustomException JoinRequestAlreadyHandledException() {
return DuplicateDomainException("이미 처리된 가입 요청입니다");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.flytrap.venusplanner.api.join_request.infrastructure.repository;

import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;
import com.flytrap.venusplanner.api.join_request.domain.JoinRequestState;
import org.springframework.data.jpa.repository.JpaRepository;

public interface JoinRequestRepository extends JpaRepository<JoinRequest, Long> {

boolean existsByMemberIdAndStudyIdAndState(Long memberId, Long studyId, JoinRequestState state);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.flytrap.venusplanner.api.join_request.presentation.controller;

import com.flytrap.venusplanner.api.join_request.business.service.JoinRequestCurdFacadeService;
import com.flytrap.venusplanner.api.join_request.domain.JoinRequest;
import com.flytrap.venusplanner.api.join_request.presentation.dto.response.JoinRequestCreateResponse;
import com.flytrap.venusplanner.global.auth.annotation.SignIn;
import com.flytrap.venusplanner.global.auth.dto.SessionMember;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class JoinRequestController {

private final JoinRequestCurdFacadeService joinRequestCrudService;

@PostMapping("/api/v1/studies/{studyId}/join-requests")
public ResponseEntity<JoinRequestCreateResponse> requestToJoinStudy(
@PathVariable Long studyId,
@SignIn SessionMember sessionMember
) {
JoinRequest joinRequest = joinRequestCrudService
.createJoinRequest(studyId, sessionMember.id());

return ResponseEntity.status(HttpStatus.CREATED)
.body(new JoinRequestCreateResponse(joinRequest.getId()));
}

@PatchMapping("/api/v1/studies/{studyId}/join-requests/{requestId}/accept")
public ResponseEntity<Void> acceptJoinRequest(
@PathVariable Long studyId,
@PathVariable Long requestId,
@SignIn SessionMember sessionMember
) {
joinRequestCrudService.acceptJoinRequest(studyId, requestId, sessionMember.id());

return ResponseEntity.ok(null);
}

@PatchMapping("/api/v1/studies/{studyId}/join-requests/{requestId}/reject")
public ResponseEntity<Void> rejectJoinRequest(
@PathVariable Long studyId,
@PathVariable Long requestId,
@SignIn SessionMember sessionMember
) {
joinRequestCrudService.rejectJoinRequest(studyId, requestId, sessionMember.id());

return ResponseEntity.ok(null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바디가 없으면 .ok().build() 하면 됩니당!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다. 고쳤어요!

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.flytrap.venusplanner.api.join_request.presentation.dto.response;

public record JoinRequestCreateResponse(
Long id
) {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.flytrap.venusplanner.api.member_study.business.service;

import static com.flytrap.venusplanner.global.exception.GeneralExceptionType.ForbiddenException;

import com.flytrap.venusplanner.api.member_study.domain.MemberStudy;
import com.flytrap.venusplanner.api.member_study.infrastructure.repository.MemberStudyRepository;
import com.flytrap.venusplanner.api.study.domain.Study;
Expand All @@ -11,7 +13,7 @@
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MemberStudyService {
public class MemberStudyService implements MemberStudyValidator {

private final MemberStudyRepository memberStudyRepository;

Expand All @@ -22,4 +24,29 @@ public void saveLeaderStudy(SessionMember leader, Study study) {
);
memberStudyRepository.save(memberStudy);
}

@Override
public boolean validateMemberBelongsToStudy(Long memberId, Long studyId) {
return memberStudyRepository.existsByStudyIdAndMemberId(studyId, memberId);
}

@Override
public void validateMemberCanAcceptJoinRequest(Long memberId, Long studyId) {
MemberStudy memberStudy = memberStudyRepository.findByStudyIdAndMemberId(studyId, memberId)
.orElseThrow(() -> ForbiddenException("스터디 멤버가 아닙니다."));

if (!memberStudy.canAcceptStudyJoinRequest()) {
throw ForbiddenException("수락 권한이 없습니다.");
}
}

@Override
public void validateMemberCanRejectJoinRequest(Long memberId, Long studyId) {
MemberStudy memberStudy = memberStudyRepository.findByStudyIdAndMemberId(studyId, memberId)
.orElseThrow(() -> ForbiddenException("스터디 멤버가 아닙니다."));

if (!memberStudy.canAcceptStudyJoinRequest()) {
throw ForbiddenException("거절 권한이 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.flytrap.venusplanner.api.member_study.business.service;

public interface MemberStudyValidator {

boolean validateMemberBelongsToStudy(Long memberId, Long studyId);
void validateMemberCanAcceptJoinRequest(Long memberId, Long studyId);
void validateMemberCanRejectJoinRequest(Long memberId, Long studyId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -47,4 +48,16 @@ public static MemberStudy fromLeader(Long leaderId, Study study) {
.permissionId(Permission.EDIT.getId())
.build();
}

public boolean isLeader() {
return Objects.equals(rollId, Roll.LEADER.getId());
}

public boolean canAcceptStudyJoinRequest() {
return isLeader();
}

public boolean canRejectStudyJoinRequest() {
return isLeader();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.flytrap.venusplanner.api.member_study.infrastructure.repository;

import com.flytrap.venusplanner.api.member_study.domain.MemberStudy;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface MemberStudyRepository extends JpaRepository<MemberStudy, Long> {

boolean existsByStudyIdAndMemberId(Long studyId, Long memberId);
Optional<MemberStudy> findByStudyIdAndMemberId(Long studyId, Long memberId);
}
Loading