Skip to content

Commit 493fe8d

Browse files
authored
Merge pull request #172 from prgrms-web-devcourse-final-project/feature/EA3-168-study-application
[EA3-168] feature: 스터디 신청 기능 및 테스트
2 parents ca8a599 + 92d1bb2 commit 493fe8d

File tree

16 files changed

+522
-28
lines changed

16 files changed

+522
-28
lines changed

src/main/java/grep/neogul_coder/domain/recruitment/post/service/RecruitmentPostService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import grep.neogul_coder.domain.study.Study;
1616
import grep.neogul_coder.domain.study.repository.StudyRepository;
1717
import grep.neogul_coder.domain.studyapplication.StudyApplication;
18-
import grep.neogul_coder.domain.studyapplication.repository.StudyApplicationRepository;
18+
import grep.neogul_coder.domain.studyapplication.repository.ApplicationRepository;
1919
import grep.neogul_coder.global.exception.business.BusinessException;
2020
import grep.neogul_coder.global.exception.business.NotFoundException;
2121
import lombok.RequiredArgsConstructor;
@@ -40,7 +40,7 @@ public class RecruitmentPostService {
4040
private final RecruitmentPostRepository postRepository;
4141
private final RecruitmentPostQueryRepository postQueryRepository;
4242

43-
private final StudyApplicationRepository studyApplicationRepository;
43+
private final ApplicationRepository applicationRepository;
4444
private final RecruitmentPostCommentQueryRepository commentQueryRepository;
4545

4646
public RecruitmentPostInfo get(long recruitmentPostId) {
@@ -49,7 +49,7 @@ public RecruitmentPostInfo get(long recruitmentPostId) {
4949

5050
RecruitmentPostWithStudyInfo postInfo = postQueryRepository.findPostWithStudyInfo(post.getId());
5151
List<CommentsWithWriterInfo> comments = findCommentsWithWriterInfo(post);
52-
List<StudyApplication> applications = studyApplicationRepository.findByRecruitmentPostId(post.getId());
52+
List<StudyApplication> applications = applicationRepository.findByRecruitmentPostId(post.getId());
5353

5454
return new RecruitmentPostInfo(postInfo, comments, applications.size());
5555
}

src/main/java/grep/neogul_coder/domain/study/Study.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public long calculateRemainSlots(long currentCount) {
8686
return this.capacity - currentCount;
8787
}
8888

89+
public void increaseMemberCount() {
90+
currentCount++;
91+
}
92+
8993
public void decreaseMemberCount() {
9094
currentCount--;
9195
}

src/main/java/grep/neogul_coder/domain/study/StudyMember.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ public StudyMember(Study study, Long userId, StudyMemberRole role) {
3636
this.participated = false;
3737
}
3838

39+
public static StudyMember createMember(Study study, Long userId) {
40+
return StudyMember.builder()
41+
.study(study)
42+
.userId(userId)
43+
.role(StudyMemberRole.MEMBER)
44+
.build();
45+
}
46+
3947
public void delete() {
4048
this.activated = false;
4149
}

src/main/java/grep/neogul_coder/domain/study/repository/StudyMemberRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import grep.neogul_coder.domain.study.Study;
44
import grep.neogul_coder.domain.study.StudyMember;
5+
import grep.neogul_coder.domain.study.enums.StudyMemberRole;
56
import org.springframework.data.jpa.repository.JpaRepository;
67
import org.springframework.data.jpa.repository.Modifying;
78
import org.springframework.data.jpa.repository.Query;
@@ -37,4 +38,6 @@ public interface StudyMemberRepository extends JpaRepository<StudyMember, Long>
3738
LocalDateTime findCreatedDateByStudyIdAndUserId(@Param("studyId") Long studyId, @Param("userId") Long userId);
3839

3940
boolean existsByStudyIdAndUserId(Long studyId, Long id);
41+
42+
boolean existsByStudyIdAndUserIdAndRole(Long studyId, Long userId, StudyMemberRole role);
4043
}

src/main/java/grep/neogul_coder/domain/studyapplication/StudyApplication.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
import grep.neogul_coder.global.entity.BaseEntity;
44
import jakarta.persistence.*;
55
import lombok.Builder;
6+
import lombok.Getter;
67

8+
@Getter
79
@Entity
810
public class StudyApplication extends BaseEntity {
911

1012
@Id
1113
@GeneratedValue(strategy = GenerationType.IDENTITY)
12-
private Long studyApplicationId;
14+
private Long id;
1315

1416
@Column(nullable = false)
1517
private Long recruitmentPostId;
@@ -35,4 +37,12 @@ private StudyApplication(Long recruitmentPostId, String applicationReason, Appli
3537
}
3638

3739
protected StudyApplication() {}
40+
41+
public void approve() {
42+
this.status = ApplicationStatus.APPROVED;
43+
}
44+
45+
public void reject() {
46+
this.status = ApplicationStatus.REJECTED;
47+
}
3848
}

src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationController.java

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,48 @@
22

33
import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest;
44
import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse;
5+
import grep.neogul_coder.domain.studyapplication.service.ApplicationService;
6+
import grep.neogul_coder.global.auth.Principal;
57
import grep.neogul_coder.global.response.ApiResponse;
68
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
711
import org.springframework.web.bind.annotation.*;
812

913
import java.util.List;
1014

11-
@RequestMapping("/api/applications")
15+
@RequestMapping("/api/recruitment-posts")
16+
@RequiredArgsConstructor
1217
@RestController
1318
public class ApplicationController implements ApplicationSpecification {
1419

15-
@GetMapping("/me")
16-
public ApiResponse<List<MyApplicationResponse>> getMyStudyApplications() {
17-
return ApiResponse.success(List.of(new MyApplicationResponse()));
20+
private final ApplicationService applicationService;
21+
22+
@GetMapping("/{recruitment-post-id}/applications")
23+
public ApiResponse<List<MyApplicationResponse>> getMyStudyApplications(@PathVariable("recruitment-post-id") Long recruitmentPostId,
24+
@AuthenticationPrincipal Principal userDetails) {
25+
return ApiResponse.success(applicationService.getMyStudyApplications(userDetails.getUserId()));
1826
}
1927

20-
@PostMapping
21-
public ApiResponse<Void> createApplication(@RequestBody @Valid ApplicationCreateRequest request) {
22-
return ApiResponse.noContent();
28+
@PostMapping("/{recruitment-post-id}/applications")
29+
public ApiResponse<Long> createApplication(@PathVariable("recruitment-post-id") Long recruitmentPostId,
30+
@RequestBody @Valid ApplicationCreateRequest request,
31+
@AuthenticationPrincipal Principal userDetails) {
32+
Long id = applicationService.createApplication(recruitmentPostId, request, userDetails.getUserId());
33+
return ApiResponse.success(id);
2334
}
2435

25-
@PostMapping("/{applicationId}/approve")
26-
public ApiResponse<Void> approveApplication(@PathVariable("applicationId") Long applicationId) {
36+
@PostMapping("/applications/{applicationId}/approve")
37+
public ApiResponse<Void> approveApplication(@PathVariable("applicationId") Long applicationId,
38+
@AuthenticationPrincipal Principal userDetails) {
39+
applicationService.approveApplication(applicationId, userDetails.getUserId());
2740
return ApiResponse.noContent();
2841
}
2942

30-
@PostMapping("/{applicationId}/reject")
31-
public ApiResponse<Void> rejectApplication(@PathVariable("applicationId") Long applicationId) {
43+
@PostMapping("/applications/{applicationId}/reject")
44+
public ApiResponse<Void> rejectApplication(@PathVariable("applicationId") Long applicationId,
45+
@AuthenticationPrincipal Principal userDetails) {
46+
applicationService.rejectApplication(applicationId, userDetails.getUserId());
3247
return ApiResponse.noContent();
3348
}
3449
}

src/main/java/grep/neogul_coder/domain/studyapplication/controller/ApplicationSpecification.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import grep.neogul_coder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest;
44
import grep.neogul_coder.domain.studyapplication.controller.dto.response.MyApplicationResponse;
5+
import grep.neogul_coder.global.auth.Principal;
56
import grep.neogul_coder.global.response.ApiResponse;
67
import io.swagger.v3.oas.annotations.Operation;
78
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -12,14 +13,14 @@
1213
public interface ApplicationSpecification {
1314

1415
@Operation(summary = "내 스터디 신청 목록 조회", description = "내가 신청한 스터디의 목록을 조회합니다.")
15-
ApiResponse<List<MyApplicationResponse>> getMyStudyApplications();
16+
ApiResponse<List<MyApplicationResponse>> getMyStudyApplications(Long recruitmentPostId, Principal userDetails);
1617

1718
@Operation(summary = "스터디 신청 생성", description = "스터디를 신청합니다.")
18-
ApiResponse<Void> createApplication(ApplicationCreateRequest request);
19+
ApiResponse<Long> createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Principal userDetails);
1920

2021
@Operation(summary = "스터디 신청 승인", description = "스터디장이 스터디 신청을 승인합니다.")
21-
ApiResponse<Void> approveApplication(Long applicationId);
22+
ApiResponse<Void> approveApplication(Long applicationId, Principal userDetails);
2223

2324
@Operation(summary = "스터디 신청 거절", description = "스터디장이 스터디 신청을 거절합니다.")
24-
ApiResponse<Void> rejectApplication(Long applicationId);
25+
ApiResponse<Void> rejectApplication(Long applicationId, Principal userDetails);
2526
}

src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/request/ApplicationCreateRequest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package grep.neogul_coder.domain.studyapplication.controller.dto.request;
22

3+
import grep.neogul_coder.domain.studyapplication.ApplicationStatus;
4+
import grep.neogul_coder.domain.studyapplication.StudyApplication;
35
import io.swagger.v3.oas.annotations.media.Schema;
46
import jakarta.validation.constraints.NotBlank;
7+
import lombok.Builder;
58
import lombok.Getter;
69

710
@Getter
@@ -10,4 +13,22 @@ public class ApplicationCreateRequest {
1013
@NotBlank
1114
@Schema(description = "스터디 신청 지원 동기", example = "자바를 더 공부하고싶어 지원하였습니다.")
1215
private String applicationReason;
16+
17+
private ApplicationCreateRequest() {
18+
}
19+
20+
@Builder
21+
private ApplicationCreateRequest(String applicationReason) {
22+
this.applicationReason = applicationReason;
23+
}
24+
25+
public StudyApplication toEntity(Long recruitmentPostId, Long userId) {
26+
return StudyApplication.builder()
27+
.recruitmentPostId(recruitmentPostId)
28+
.userId(userId)
29+
.applicationReason(this.applicationReason)
30+
.isRead(false)
31+
.status(ApplicationStatus.APPLYING)
32+
.build();
33+
}
1334
}

src/main/java/grep/neogul_coder/domain/studyapplication/controller/dto/response/MyApplicationResponse.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
package grep.neogul_coder.domain.studyapplication.controller.dto.response;
22

3+
import com.querydsl.core.annotations.QueryProjection;
34
import grep.neogul_coder.domain.study.enums.Category;
45
import grep.neogul_coder.domain.study.enums.StudyType;
56
import grep.neogul_coder.domain.studyapplication.ApplicationStatus;
67
import io.swagger.v3.oas.annotations.media.Schema;
78
import lombok.Getter;
89

9-
import java.time.LocalDate;
10+
import java.time.LocalDateTime;
1011

1112
@Getter
1213
public class MyApplicationResponse {
1314

15+
@Schema(description = "신청 번호", example = "1")
16+
private Long applicationId;
17+
1418
@Schema(description = "스터디 이름", example = "자바 스터디")
1519
private String name;
1620

@@ -24,7 +28,7 @@ public class MyApplicationResponse {
2428
private int currentCount;
2529

2630
@Schema(description = "시작일", example = "2025-07-15")
27-
private LocalDate startDate;
31+
private LocalDateTime startDate;
2832

2933
@Schema(description = "대표 이미지", example = "http://localhost:8083/image.jpg")
3034
private String imageUrl;
@@ -43,4 +47,21 @@ public class MyApplicationResponse {
4347

4448
@Schema(description = "신청 상태", example = "PENDING")
4549
private ApplicationStatus status;
50+
51+
@QueryProjection
52+
public MyApplicationResponse(Long applicationId, String name, String leaderNickname, int capacity, int currentCount, LocalDateTime startDate,
53+
String imageUrl,String introduction, Category category, StudyType studyType, boolean isRead, ApplicationStatus status) {
54+
this.applicationId = applicationId;
55+
this.name = name;
56+
this.leaderNickname = leaderNickname;
57+
this.capacity = capacity;
58+
this.currentCount = currentCount;
59+
this.startDate = startDate;
60+
this.imageUrl = imageUrl;
61+
this.introduction = introduction;
62+
this.category = category;
63+
this.studyType = studyType;
64+
this.isRead = isRead;
65+
this.status = status;
66+
}
4667
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package grep.neogul_coder.domain.studyapplication.exception.code;
2+
3+
import grep.neogul_coder.global.response.code.ErrorCode;
4+
import lombok.Getter;
5+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
public enum ApplicationErrorCode implements ErrorCode {
9+
APPLICATION_NOT_FOUND("SA001",HttpStatus.NOT_FOUND,"신청서를 찾을 수 없습니다."),
10+
11+
ALREADY_APPLICATION("SA002", HttpStatus.BAD_REQUEST, "이미 지원한 모집글입니다."),
12+
APPLICATION_NOT_APPLYING("SA003", HttpStatus.BAD_REQUEST, "신청 상태가 APPLYING이 아닙니다."),
13+
14+
LEADER_CANNOT_APPLY("SA004", HttpStatus.BAD_REQUEST, "스터디장은 스터디를 신청할 수 없습니다."),
15+
LEADER_ONLY_APPROVED("SA005", HttpStatus.BAD_REQUEST, "스터디장만 승인이 가능합니다."),
16+
LEADER_ONLY_REJECTED("SA006", HttpStatus.BAD_REQUEST, "스터디장만 거절이 가능합니다.");
17+
18+
private final String code;
19+
private final HttpStatus status;
20+
private final String message;
21+
22+
ApplicationErrorCode(String code, HttpStatus status, String message) {
23+
this.code = code;
24+
this.status = status;
25+
this.message = message;
26+
}
27+
}

0 commit comments

Comments
 (0)