Skip to content

Commit 07a6197

Browse files
authored
Merge pull request #195 from prgrms-web-devcourse-final-project/feature/EA3-168-study-application
[EA3-168] feature: 모집글 신청 목록 조회 기능 및 경로 수정
2 parents b18990b + 5007f21 commit 07a6197

File tree

10 files changed

+156
-80
lines changed

10 files changed

+156
-80
lines changed

src/main/java/grep/neogulcoder/domain/recruitment/post/controller/RecruitmentPostController.java

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

33
import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusUpdateRequest;
44
import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostUpdateRequest;
5-
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentApplicationPagingInfo;
65
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostInfo;
76
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostPagingInfo;
87
import grep.neogulcoder.domain.recruitment.post.service.RecruitmentPostService;
@@ -76,11 +75,4 @@ public ApiResponse<Long> changeStatus(@PathVariable("recruitment-post-id") long
7675
long postId = recruitmentPostService.updateStatus(request.toServiceRequest(), recruitmentPostId, userDetails.getUserId());
7776
return ApiResponse.success(postId);
7877
}
79-
80-
@GetMapping("{recruitment-post-id}/applications")
81-
public ApiResponse<RecruitmentApplicationPagingInfo> getApplications(@PageableDefault(size = 5) Pageable pageable,
82-
@PathVariable("recruitment-post-id") long recruitmentPostId) {
83-
return ApiResponse.success(new RecruitmentApplicationPagingInfo());
84-
}
85-
8678
}

src/main/java/grep/neogulcoder/domain/recruitment/post/controller/RecruitmentPostSpecification.java

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

33
import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusUpdateRequest;
44
import grep.neogulcoder.domain.recruitment.post.controller.dto.request.RecruitmentPostUpdateRequest;
5-
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentApplicationPagingInfo;
65
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostInfo;
76
import grep.neogulcoder.domain.recruitment.post.controller.dto.response.RecruitmentPostPagingInfo;
87
import grep.neogulcoder.domain.study.enums.Category;
@@ -144,32 +143,4 @@ ApiResponse<RecruitmentPostPagingInfo> getMyPostPagingInfo(Pageable pageable, Ca
144143
"""
145144
)
146145
ApiResponse<RecruitmentPostPagingInfo> getPagingInfo(Pageable pageable, Category category, StudyType studyType, String keyword);
147-
148-
@Operation(
149-
summary = "스터디 신청한 회원 목록 페이징 조회",
150-
description = """
151-
특정 모집글에 신청한 회원 목록을 페이징하여 조회합니다.
152-
153-
✅ 요청 형식:
154-
`GET /recruitment-posts/{id}/applications?page=0&size=5`
155-
156-
✅ 응답 예시:
157-
```json
158-
{
159-
"applicationInfos": [
160-
{
161-
"nickname": "테스터",
162-
"buddyEnergy": 30,
163-
"createdDate": "2025-07-13T15:30:00",
164-
"applicationReason": "자바를 더 공부 하고싶어요!"
165-
}
166-
],
167-
"totalPage": 3,
168-
"totalElementCount": 20
169-
}
170-
```
171-
"""
172-
)
173-
ApiResponse<RecruitmentApplicationPagingInfo> getApplications(Pageable pageable, long recruitmentPostId);
174-
175146
}

src/main/java/grep/neogulcoder/domain/recruitment/post/controller/dto/response/RecruitmentApplicationPagingInfo.java

Lines changed: 0 additions & 36 deletions
This file was deleted.

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import grep.neogulcoder.domain.studyapplication.ApplicationStatus;
44
import grep.neogulcoder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest;
55
import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationPagingResponse;
6+
import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationPagingResponse;
67
import grep.neogulcoder.domain.studyapplication.service.ApplicationService;
78
import grep.neogulcoder.global.auth.Principal;
89
import grep.neogulcoder.global.response.ApiResponse;
@@ -21,8 +22,14 @@ public class ApplicationController implements ApplicationSpecification {
2122
private final ApplicationService applicationService;
2223

2324
@GetMapping("/{recruitment-post-id}/applications")
24-
public ApiResponse<MyApplicationPagingResponse> getMyStudyApplications(@PathVariable("recruitment-post-id") Long recruitmentPostId,
25-
@PageableDefault(size = 12) Pageable pageable,
25+
public ApiResponse<ReceivedApplicationPagingResponse> getReceivedApplications(@PathVariable("recruitment-post-id") Long recruitmentPostId,
26+
@PageableDefault(size = 5) Pageable pageable,
27+
@AuthenticationPrincipal Principal userDetails) {
28+
return ApiResponse.success(applicationService.getReceivedApplicationsPaging(recruitmentPostId, pageable, userDetails.getUserId()));
29+
}
30+
31+
@GetMapping("/applications")
32+
public ApiResponse<MyApplicationPagingResponse> getMyStudyApplications(@PageableDefault(size = 12) Pageable pageable,
2633
@RequestParam(required = false) ApplicationStatus status,
2734
@AuthenticationPrincipal Principal userDetails) {
2835
return ApiResponse.success(applicationService.getMyStudyApplicationsPaging(pageable, userDetails.getUserId(), status));

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import grep.neogulcoder.domain.studyapplication.ApplicationStatus;
44
import grep.neogulcoder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest;
55
import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationPagingResponse;
6+
import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationPagingResponse;
67
import grep.neogulcoder.global.auth.Principal;
78
import grep.neogulcoder.global.response.ApiResponse;
89
import io.swagger.v3.oas.annotations.Operation;
@@ -12,8 +13,11 @@
1213
@Tag(name = "StudyApplication", description = "스터디 신청 API")
1314
public interface ApplicationSpecification {
1415

16+
@Operation(summary = "모집글 신청 목록 조회", description = "스터디장(모집글 작성자)이 신청 목록을 조회합니다.")
17+
public ApiResponse<ReceivedApplicationPagingResponse> getReceivedApplications(Long recruitmentPostId, Pageable pageable, Principal userDetails);
18+
1519
@Operation(summary = "내 스터디 신청 목록 조회", description = "내가 신청한 스터디의 목록을 조회합니다.")
16-
ApiResponse<MyApplicationPagingResponse> getMyStudyApplications(Long recruitmentPostId, Pageable pageable, ApplicationStatus status, Principal userDetails);
20+
ApiResponse<MyApplicationPagingResponse> getMyStudyApplications(Pageable pageable, ApplicationStatus status, Principal userDetails);
1721

1822
@Operation(summary = "스터디 신청 생성", description = "스터디를 신청합니다.")
1923
ApiResponse<Long> createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Principal userDetails);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package grep.neogulcoder.domain.studyapplication.controller.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import org.springframework.data.domain.Page;
7+
8+
import java.util.List;
9+
10+
@Getter
11+
public class ReceivedApplicationPagingResponse {
12+
13+
@Schema(
14+
description = "내 모집글에 지원한 신청 목록",
15+
example = "[{" +
16+
"\"applicationId\": 1," +
17+
"\"nickname\": \"너굴\"," +
18+
"\"buddyEnergy\": 30," +
19+
"\"createdDate\": \"2025-07-10T15:30:00\"," +
20+
"\"applicationReason\": \"자바를 더 공부하고 싶어 지원합니다.\"" +
21+
"}]"
22+
)
23+
private List<ReceivedApplicationResponse> receivedApplications;
24+
25+
@Schema(description = "총 페이지 수", example = "2")
26+
private int totalPage;
27+
28+
@Schema(description = "총 요소 개수", example = "10")
29+
private int totalElementCount;
30+
31+
@Schema(description = "다음 페이지 여부", example = "false")
32+
private boolean hasNext;
33+
34+
@Builder
35+
private ReceivedApplicationPagingResponse(Page<ReceivedApplicationResponse> page) {
36+
this.receivedApplications = page.getContent();
37+
this.totalPage = page.getTotalPages();
38+
this.totalElementCount = (int) page.getTotalElements();
39+
this.hasNext = page.hasNext();
40+
}
41+
42+
public static ReceivedApplicationPagingResponse of(Page<ReceivedApplicationResponse> page) {
43+
return new ReceivedApplicationPagingResponse(page);
44+
}
45+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package grep.neogulcoder.domain.studyapplication.controller.dto.response;
2+
3+
import com.querydsl.core.annotations.QueryProjection;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.Getter;
6+
7+
import java.time.LocalDateTime;
8+
9+
@Getter
10+
public class ReceivedApplicationResponse {
11+
12+
@Schema(description = "신청 번호", example = "1")
13+
private Long applicationId;
14+
15+
@Schema(description = "신청자 닉네임", example = "너굴")
16+
private String nickname;
17+
18+
@Schema(description = "신청자 버디에너지", example = "30")
19+
private int buddyEnergy;
20+
21+
@Schema(description = "신청 날짜", example = "2025-07-10T15:30:00")
22+
private LocalDateTime createdDate;
23+
24+
@Schema(description = "스터디 신청 지원 동기", example = "자바를 더 공부하고 싶어 지원합니다.")
25+
private String applicationReason;
26+
27+
@QueryProjection
28+
public ReceivedApplicationResponse(Long applicationId, String nickname, int buddyEnergy, LocalDateTime createdDate, String applicationReason) {
29+
this.applicationId = applicationId;
30+
this.nickname = nickname;
31+
this.buddyEnergy = buddyEnergy;
32+
this.createdDate = createdDate;
33+
this.applicationReason = applicationReason;
34+
}
35+
}

src/main/java/grep/neogulcoder/domain/studyapplication/repository/ApplicationQueryRepository.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import grep.neogulcoder.domain.studyapplication.ApplicationStatus;
77
import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationResponse;
88
import grep.neogulcoder.domain.studyapplication.controller.dto.response.QMyApplicationResponse;
9+
import grep.neogulcoder.domain.studyapplication.controller.dto.response.QReceivedApplicationResponse;
10+
import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationResponse;
911
import jakarta.persistence.EntityManager;
1012
import org.springframework.data.domain.Page;
1113
import org.springframework.data.domain.PageImpl;
@@ -21,6 +23,7 @@
2123
import static grep.neogulcoder.domain.study.enums.StudyMemberRole.LEADER;
2224
import static grep.neogulcoder.domain.studyapplication.QStudyApplication.studyApplication;
2325
import static grep.neogulcoder.domain.users.entity.QUser.user;
26+
import static grep.neogulcoder.domain.buddy.entity.QBuddyEnergy.buddyEnergy;
2427

2528
@Repository
2629
public class ApplicationQueryRepository {
@@ -31,6 +34,34 @@ public ApplicationQueryRepository(EntityManager em) {
3134
this.queryFactory = new JPAQueryFactory(em);
3235
}
3336

37+
public Page<ReceivedApplicationResponse> findReceivedApplicationsPaging(Long recruitmentPostId, Pageable pageable) {
38+
List<ReceivedApplicationResponse> receivedApplications = queryFactory
39+
.select(new QReceivedApplicationResponse(
40+
studyApplication.id,
41+
user.nickname,
42+
buddyEnergy.level,
43+
studyApplication.createdDate,
44+
studyApplication.applicationReason
45+
))
46+
.from(studyApplication)
47+
.join(user).on(studyApplication.userId.eq(user.id))
48+
.join(buddyEnergy).on(user.id.eq(buddyEnergy.userId))
49+
.where(studyApplication.recruitmentPostId.eq(recruitmentPostId))
50+
.offset(pageable.getOffset())
51+
.limit(pageable.getPageSize())
52+
.fetch();
53+
54+
Long total = queryFactory
55+
.select(studyApplication.count())
56+
.from(studyApplication)
57+
.join(user).on(studyApplication.userId.eq(user.id))
58+
.join(buddyEnergy).on(user.id.eq(buddyEnergy.userId))
59+
.where(studyApplication.recruitmentPostId.eq(recruitmentPostId))
60+
.fetchOne();
61+
62+
return new PageImpl<>(receivedApplications, pageable, total == null ? 0 : total);
63+
}
64+
3465
public Page<MyApplicationResponse> findMyStudyApplicationsPaging(Pageable pageable, Long userId, ApplicationStatus status) {
3566
List<MyApplicationResponse> applications = queryFactory
3667
.select(new QMyApplicationResponse(

src/main/java/grep/neogulcoder/domain/studyapplication/repository/ApplicationRepository.java

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

33
import grep.neogulcoder.domain.studyapplication.StudyApplication;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58

69
import java.util.List;
710
import java.util.Optional;
@@ -12,4 +15,8 @@ public interface ApplicationRepository extends JpaRepository<StudyApplication, L
1215
boolean existsByRecruitmentPostIdAndUserId(Long recruitmentPostId, Long userId);
1316

1417
Optional<StudyApplication> findByIdAndActivatedTrue(Long applicationId);
18+
19+
@Modifying(clearAutomatically = true)
20+
@Query("update StudyApplication sa set sa.isRead = true where sa.recruitmentPostId = :recruitmentPostId and sa.isRead = false")
21+
void markAllAsReadByRecruitmentPostId(@Param("recruitmentPostId") Long recruitmentPostId);
1522
}

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import grep.neogulcoder.domain.studyapplication.controller.dto.request.ApplicationCreateRequest;
1313
import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationPagingResponse;
1414
import grep.neogulcoder.domain.studyapplication.controller.dto.response.MyApplicationResponse;
15+
import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationPagingResponse;
16+
import grep.neogulcoder.domain.studyapplication.controller.dto.response.ReceivedApplicationResponse;
1517
import grep.neogulcoder.domain.studyapplication.repository.ApplicationQueryRepository;
1618
import grep.neogulcoder.domain.studyapplication.repository.ApplicationRepository;
1719
import grep.neogulcoder.global.exception.business.BusinessException;
@@ -23,6 +25,7 @@
2325
import org.springframework.transaction.annotation.Transactional;
2426

2527
import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.NOT_FOUND;
28+
import static grep.neogulcoder.domain.recruitment.RecruitmentErrorCode.NOT_OWNER;
2629
import static grep.neogulcoder.domain.study.exception.code.StudyErrorCode.STUDY_NOT_FOUND;
2730
import static grep.neogulcoder.domain.studyapplication.exception.code.ApplicationErrorCode.*;
2831

@@ -37,14 +40,25 @@ public class ApplicationService {
3740
private final StudyMemberRepository studyMemberRepository;
3841
private final StudyRepository studyRepository;
3942

43+
@Transactional
44+
public ReceivedApplicationPagingResponse getReceivedApplicationsPaging(Long recruitmentPostId, Pageable pageable, Long userId) {
45+
RecruitmentPost recruitmentPost = findValidRecruitmentPost(recruitmentPostId);
46+
47+
validateOwner(userId, recruitmentPost);
48+
applicationRepository.markAllAsReadByRecruitmentPostId(recruitmentPostId);
49+
50+
Page<ReceivedApplicationResponse> page = applicationQueryRepository.findReceivedApplicationsPaging(recruitmentPostId, pageable);
51+
return ReceivedApplicationPagingResponse.of(page);
52+
}
53+
4054
public MyApplicationPagingResponse getMyStudyApplicationsPaging(Pageable pageable, Long userId, ApplicationStatus status) {
4155
Page<MyApplicationResponse> page = applicationQueryRepository.findMyStudyApplicationsPaging(pageable, userId, status);
4256
return MyApplicationPagingResponse.of(page);
4357
}
4458

4559
@Transactional
4660
public Long createApplication(Long recruitmentPostId, ApplicationCreateRequest request, Long userId) {
47-
RecruitmentPost recruitmentPost = findValidRecruimentPost(recruitmentPostId);
61+
RecruitmentPost recruitmentPost = findValidRecruitmentPost(recruitmentPostId);
4862

4963
validateNotLeaderApply(recruitmentPost, userId);
5064
validateNotAlreadyApplied(recruitmentPostId, userId);
@@ -58,7 +72,7 @@ public Long createApplication(Long recruitmentPostId, ApplicationCreateRequest r
5872
@Transactional
5973
public void approveApplication(Long applicationId, Long userId) {
6074
StudyApplication application = findValidApplication(applicationId);
61-
RecruitmentPost post = findValidRecruimentPost(application.getRecruitmentPostId());
75+
RecruitmentPost post = findValidRecruitmentPost(application.getRecruitmentPostId());
6276
Study study = findValidStudy(post);
6377

6478
validateOnlyLeaderCanApprove(study, userId);
@@ -74,7 +88,7 @@ public void approveApplication(Long applicationId, Long userId) {
7488
@Transactional
7589
public void rejectApplication(Long applicationId, Long userId) {
7690
StudyApplication application = findValidApplication(applicationId);
77-
RecruitmentPost post = findValidRecruimentPost(application.getRecruitmentPostId());
91+
RecruitmentPost post = findValidRecruitmentPost(application.getRecruitmentPostId());
7892
Study study = findValidStudy(post);
7993

8094
validateOnlyLeaderCanReject(study, userId);
@@ -95,12 +109,18 @@ private StudyApplication findValidApplication(Long applicationId) {
95109
return application;
96110
}
97111

98-
private RecruitmentPost findValidRecruimentPost(Long recruitmentPostId) {
112+
private RecruitmentPost findValidRecruitmentPost(Long recruitmentPostId) {
99113
RecruitmentPost post = recruitmentPostRepository.findByIdAndActivatedTrue(recruitmentPostId)
100114
.orElseThrow(() -> new NotFoundException(NOT_FOUND));
101115
return post;
102116
}
103117

118+
private static void validateOwner(Long userId, RecruitmentPost recruitmentPost) {
119+
if (recruitmentPost.isNotOwnedBy(userId)) {
120+
throw new BusinessException(NOT_OWNER);
121+
}
122+
}
123+
104124
private void validateNotLeaderApply(RecruitmentPost recruitmentPost, Long userId) {
105125
boolean isLeader = studyMemberRepository.existsByStudyIdAndUserIdAndRole(recruitmentPost.getStudyId(), userId, StudyMemberRole.LEADER);
106126
if (isLeader) {

0 commit comments

Comments
 (0)