Skip to content

Commit 6b37630

Browse files
authored
Merge pull request #215 from prgrms-web-devcourse-final-project/feature/EA3-168-study-application
[EA3-168] feature: 스터디 신청, 신청 승인 예외 추가
2 parents 1588bd7 + 2828deb commit 6b37630

File tree

5 files changed

+95
-8
lines changed

5 files changed

+95
-8
lines changed

src/main/java/grep/neogulcoder/domain/study/repository/StudyMemberQueryRepository.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package grep.neogulcoder.domain.study.repository;
22

33
import com.querydsl.core.types.Projections;
4+
import com.querydsl.core.types.dsl.BooleanExpression;
45
import com.querydsl.jpa.impl.JPAQueryFactory;
56
import grep.neogulcoder.domain.study.StudyMember;
67
import grep.neogulcoder.domain.study.controller.dto.response.ExtendParticipationResponse;
@@ -81,4 +82,23 @@ public List<StudyMember> findByIdIn(List<Long> studyIds) {
8182
.join(studyMember.study, study).fetchJoin()
8283
.fetch();
8384
}
85+
86+
public int countActiveUnfinishedStudies(Long userId) {
87+
BooleanExpression notExtendedAndParticipate = study.extended.isFalse().and(studyMember.activated.isTrue());
88+
BooleanExpression extendedAndNotParticipate = study.extended.isTrue().and(studyMember.participated.isFalse());
89+
90+
Long result = queryFactory
91+
.select(studyMember.count())
92+
.from(studyMember)
93+
.join(studyMember.study, study)
94+
.where(
95+
studyMember.userId.eq(userId),
96+
study.activated.isTrue(),
97+
study.finished.isFalse(),
98+
notExtendedAndParticipate.or(extendedAndNotParticipate)
99+
)
100+
.fetchOne();
101+
102+
return result != null ? result.intValue() : 0;
103+
}
84104
}

src/main/java/grep/neogulcoder/domain/study/service/StudyService.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ public class StudyService {
6161
private final TeamCalendarService teamCalendarService;
6262
private final GroupChatRoomRepository groupChatRoomRepository;
6363

64-
6564
public StudyItemPagingResponse getMyStudiesPaging(Pageable pageable, Long userId, Boolean finished) {
6665
Page<StudyItemResponse> page = studyQueryRepository.findMyStudiesPaging(pageable, userId, finished);
6766
return StudyItemPagingResponse.of(page);
@@ -259,4 +258,7 @@ private boolean isImgExists(MultipartFile image) {
259258
return image != null && !image.isEmpty();
260259
}
261260

261+
private int getActiveUnfinishedStudiesCount(Long userId) {
262+
return studyMemberQueryRepository.countActiveUnfinishedStudies(userId);
263+
}
262264
}

src/main/java/grep/neogulcoder/domain/studyapplication/exception/code/ApplicationErrorCode.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ public enum ApplicationErrorCode implements ErrorCode {
1313

1414
LEADER_CANNOT_APPLY("SA004", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."),
1515
LEADER_ONLY_APPROVED("SA005", HttpStatus.BAD_REQUEST, "스터디장만 승인이 가능합니다."),
16-
LEADER_ONLY_REJECTED("SA006", HttpStatus.BAD_REQUEST, "스터디장만 거절이 가능합니다.");
16+
LEADER_ONLY_REJECTED("SA006", HttpStatus.BAD_REQUEST, "스터디장만 거절이 가능합니다."),
17+
18+
APPLICATION_PARTICIPATION_LIMIT_EXCEEDED("SA005", HttpStatus.BAD_REQUEST, "종료되지 않은 스터디는 최대 10개까지만 참여할 수 있습니다."),
19+
APPLICATION_PARTICIPANT_LIMIT_EXCEEDED("SA006", HttpStatus.BAD_REQUEST, "해당 사용자가 이미 10개의 스터디에 참여 중입니다.");
1720

1821
private final String code;
1922
private final HttpStatus status;

src/main/java/grep/neogulcoder/domain/studyapplication/service/ApplicationService.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import grep.neogulcoder.domain.study.Study;
66
import grep.neogulcoder.domain.study.StudyMember;
77
import grep.neogulcoder.domain.study.enums.StudyMemberRole;
8+
import grep.neogulcoder.domain.study.repository.StudyMemberQueryRepository;
89
import grep.neogulcoder.domain.study.repository.StudyMemberRepository;
910
import grep.neogulcoder.domain.study.repository.StudyRepository;
1011
import grep.neogulcoder.domain.studyapplication.ApplicationStatus;
@@ -24,9 +25,8 @@
2425
import org.springframework.stereotype.Service;
2526
import org.springframework.transaction.annotation.Transactional;
2627

27-
import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.NOT_FOUND;
28-
import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.NOT_OWNER;
29-
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND;
28+
import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.*;
29+
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.*;
3030
import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*;
3131

3232
@Transactional(readOnly = true)
@@ -39,6 +39,7 @@ public class ApplicationService {
3939
private final RecruitmentPostRepository recruitmentPostRepository;
4040
private final StudyMemberRepository studyMemberRepository;
4141
private final StudyRepository studyRepository;
42+
private final StudyMemberQueryRepository studyMemberQueryRepository;
4243

4344
@Transactional
4445
public ReceivedApplicationPagingResponse getReceivedApplicationsPaging(Long recruitmentPostId, Pageable pageable, Long userId) {
@@ -62,6 +63,7 @@ public Long createApplication(Long recruitmentPostId, ApplicationCreateRequest r
6263

6364
validateNotLeaderApply(recruitmentPost, userId);
6465
validateNotAlreadyApplied(recruitmentPostId, userId);
66+
validateApplicantStudyLimit(userId);
6567

6668
StudyApplication application = request.toEntity(recruitmentPostId, userId);
6769
applicationRepository.save(application);
@@ -77,6 +79,7 @@ public void approveApplication(Long applicationId, Long userId) {
7779

7880
validateOnlyLeaderCanApprove(study, userId);
7981
validateStatusIsApplying(application);
82+
validateParticipantStudyLimit(application.getUserId());
8083

8184
application.approve();
8285

@@ -154,4 +157,18 @@ private void validateOnlyLeaderCanReject(Study study, Long userId) {
154157
throw new BusinessException(LEADER_ONLY_REJECTED);
155158
}
156159
}
160+
161+
private void validateApplicantStudyLimit(Long userId) {
162+
int count = studyMemberQueryRepository.countActiveUnfinishedStudies(userId);
163+
if (count >= 10) {
164+
throw new BusinessException(APPLICATION_PARTICIPATION_LIMIT_EXCEEDED);
165+
}
166+
}
167+
168+
private void validateParticipantStudyLimit(Long userId) {
169+
int count = studyMemberQueryRepository.countActiveUnfinishedStudies(userId);
170+
if (count >= 10) {
171+
throw new BusinessException(APPLICATION_PARTICIPANT_LIMIT_EXCEEDED);
172+
}
173+
}
157174
}

src/test/java/grep/neogulcoder/domain/studyapplication/service/ApplicationServiceTest.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@
2222
import java.time.LocalDateTime;
2323
import java.util.List;
2424

25-
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.LEADER;
26-
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.MEMBER;
25+
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.*;
2726
import static grep.neogulcoder.domain.studyapplication.ApplicationStatus.*;
28-
import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.APPLICATION_NOT_APPLYING;
27+
import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*;
2928
import static org.assertj.core.api.Assertions.assertThat;
3029
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3130

@@ -96,6 +95,29 @@ void createApplication() {
9695
assertThat(application.getApplicationReason()).isEqualTo("자바를 더 공부하고 싶어 지원합니다.");
9796
}
9897

98+
@DisplayName("종료되지 않은 스터디를 10개를 진행중일 때 신청서 생성 시 예외가 발생합니다.")
99+
@Test
100+
void createApplicationFail() {
101+
// given
102+
ApplicationCreateRequest request = ApplicationCreateRequest.builder()
103+
.applicationReason("자바를 더 공부하고 싶어 지원합니다.")
104+
.build();
105+
106+
for (int i = 0; i < 9; i++) {
107+
Study study = createStudy("스터디", LocalDateTime.parse("2025-07-25T20:20:20"), LocalDateTime.parse("2026-07-28T20:20:20"));
108+
studyRepository.save(study);
109+
StudyMember studyLeader = createStudyLeader(study, userId2);
110+
studyMemberRepository.save(studyLeader);
111+
em.flush();
112+
em.clear();
113+
}
114+
115+
// when then
116+
assertThatThrownBy(() ->
117+
applicationService.createApplication(recruitmentPostId, request, userId2))
118+
.isInstanceOf(BusinessException.class).hasMessage(APPLICATION_PARTICIPATION_LIMIT_EXCEEDED.getMessage());
119+
}
120+
99121
@DisplayName("스터디장이 신청서를 승인합니다.")
100122
@Test
101123
void approveApplication() {
@@ -113,6 +135,29 @@ void approveApplication() {
113135
assertThat(application.getStatus()).isEqualTo(APPROVED);
114136
}
115137

138+
@DisplayName("참여 중인 스터디가 10개인 사용자의 신청서 승인 시 예외가 발생합니다.")
139+
@Test
140+
void approveApplicationFail() {
141+
// given
142+
StudyApplication application = createApplication(recruitmentPostId, userId2);
143+
applicationRepository.save(application);
144+
Long id = application.getId();
145+
146+
for (int i = 0; i < 9; i++) {
147+
Study study = createStudy("스터디", LocalDateTime.parse("2025-07-25T20:20:20"), LocalDateTime.parse("2026-07-28T20:20:20"));
148+
studyRepository.save(study);
149+
StudyMember studyLeader = createStudyLeader(study, userId2);
150+
studyMemberRepository.save(studyLeader);
151+
em.flush();
152+
em.clear();
153+
}
154+
155+
// when then
156+
assertThatThrownBy(() ->
157+
applicationService.approveApplication(id, userId1))
158+
.isInstanceOf(BusinessException.class).hasMessage(APPLICATION_PARTICIPANT_LIMIT_EXCEEDED.getMessage());
159+
}
160+
116161
@DisplayName("스터디장이 신청서를 거절합니다.")
117162
@Test
118163
void rejectApplication() {

0 commit comments

Comments
 (0)