Skip to content

Commit 17581d7

Browse files
authored
Merge pull request #62 from prgrms-web-devcourse-final-project/feature/EA3-108-recruitment-post-crud
[EA3-108] Feature: 모집글 수정 삭제 구현 및 테스트 추가
2 parents 12c41dc + fe040a0 commit 17581d7

27 files changed

+677
-110
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# === Gradle ===
2+
src/main/generated/**
23
.gradle
34
build/
45
!gradle/wrapper/gradle-wrapper.jar
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package grep.neogul_coder.domain.recruitment;
2+
3+
import grep.neogul_coder.global.response.ErrorCode;
4+
import lombok.Getter;
5+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
public enum RecruitmentErrorCode implements ErrorCode {
9+
NOT_FOUND(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.name(), "모집글을 찾지 못했습니다."),
10+
NOT_OWNER(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.name(), "모집글을 등록한 당사자가 아닙니다.");
11+
12+
private final HttpStatus status;
13+
private final String code;
14+
private final String message;
15+
16+
RecruitmentErrorCode(HttpStatus status, String code, String message) {
17+
this.status = status;
18+
this.code = code;
19+
this.message = message;
20+
}
21+
22+
}
Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,62 @@
11
package grep.neogul_coder.domain.recruitment.post;
22

33
import grep.neogul_coder.domain.recruitment.RecruitmentPostStatus;
4+
import grep.neogul_coder.global.entity.BaseEntity;
45
import jakarta.persistence.Entity;
56
import jakarta.persistence.GeneratedValue;
67
import jakarta.persistence.GenerationType;
78
import jakarta.persistence.Id;
9+
import lombok.Builder;
10+
import lombok.Getter;
811

912
import java.time.LocalDate;
1013

14+
@Getter
1115
@Entity
12-
public class RecruitmentPost {
16+
public class RecruitmentPost extends BaseEntity {
1317

1418
@Id
1519
@GeneratedValue(strategy = GenerationType.IDENTITY)
1620
private Long id;
21+
private long userId;
1722
private long studyId;
1823

1924
private String subject;
2025
private String content;
2126
private int recruitmentCount;
2227
private LocalDate expiredDate;
2328
private RecruitmentPostStatus status;
29+
private boolean isDeleted;
30+
31+
@Builder
32+
private RecruitmentPost(long studyId, String subject, String content, long userId,
33+
int recruitmentCount, LocalDate expiredDate, RecruitmentPostStatus status) {
34+
this.studyId = studyId;
35+
this.userId = userId;
36+
this.subject = subject;
37+
this.content = content;
38+
this.recruitmentCount = recruitmentCount;
39+
this.expiredDate = expiredDate;
40+
this.status = status;
41+
}
42+
43+
protected RecruitmentPost() {}
44+
45+
public void update(String subject, String content, int recruitmentCount) {
46+
this.subject = subject;
47+
this.content = content;
48+
this.recruitmentCount = recruitmentCount;
49+
}
50+
51+
public boolean isNotOwnedBy(long userId) {
52+
return this.userId != userId;
53+
}
54+
55+
public void delete() {
56+
this.isDeleted = true;
57+
}
58+
59+
public void updateStatus(RecruitmentPostStatus status) {
60+
this.status = status;
61+
}
2462
}
Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,81 @@
11
package grep.neogul_coder.domain.recruitment.post.controller;
22

3-
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostEditRequest;
4-
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostSaveRequest;
5-
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusEditRequest;
6-
import grep.neogul_coder.domain.recruitment.post.controller.dto.response.RecruitmentPostInfo;
7-
import grep.neogul_coder.domain.studyapplication.controller.dto.response.ApplicationResponse;
3+
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostCreateRequest;
4+
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusUpdateRequest;
5+
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostUpdateRequest;
6+
import grep.neogul_coder.domain.recruitment.post.controller.dto.response.*;
7+
import grep.neogul_coder.domain.recruitment.post.service.RecruitmentPostService;
8+
import grep.neogul_coder.global.auth.Principal;
89
import grep.neogul_coder.global.response.ApiResponse;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.data.domain.Pageable;
13+
import org.springframework.data.web.PageableDefault;
14+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
915
import org.springframework.web.bind.annotation.*;
1016

11-
import java.util.List;
12-
1317
@RequestMapping("/recruitment-posts")
18+
@RequiredArgsConstructor
1419
@RestController
1520
public class RecruitmentPostController implements RecruitmentPostSpecification {
1621

22+
private final RecruitmentPostService recruitmentPostService;
23+
1724
@GetMapping("/{recruitment-post-id}")
1825
public ApiResponse<RecruitmentPostInfo> get(@PathVariable("recruitment-post-id") long recruitmentPostId) {
1926
return ApiResponse.success(new RecruitmentPostInfo());
2027
}
2128

29+
@GetMapping("/studies")
30+
public ApiResponse<ParticipatedStudyNamesInfo> getParticipatedStudyNames(@AuthenticationPrincipal Principal userDetails) {
31+
return ApiResponse.success(new ParticipatedStudyNamesInfo());
32+
}
33+
34+
@GetMapping("/studies/{study-id}")
35+
public ApiResponse<ParticipatedStudyInfo> getParticipatedStudy(@PathVariable("study-id") long StudyId,
36+
@AuthenticationPrincipal Principal userDetails) {
37+
return ApiResponse.success(new ParticipatedStudyInfo());
38+
}
39+
2240
@PostMapping
23-
public ApiResponse<Void> save(@RequestBody RecruitmentPostSaveRequest request) {
41+
public ApiResponse<Void> save(@Valid @RequestBody RecruitmentPostCreateRequest request,
42+
@AuthenticationPrincipal Principal userDetails) {
43+
recruitmentPostService.create(request.toServiceRequest(), userDetails.getUserId());
2444
return ApiResponse.noContent();
2545
}
2646

2747
@PutMapping("/{recruitment-post-id}")
28-
public ApiResponse<Void> edit(@PathVariable("recruitment-post-id") long recruitmentPostId,
29-
RecruitmentPostEditRequest request) {
48+
public ApiResponse<Void> update(@PathVariable("recruitment-post-id") long recruitmentPostId,
49+
@Valid @RequestBody RecruitmentPostUpdateRequest request,
50+
@AuthenticationPrincipal Principal userDetails) {
51+
recruitmentPostService.update(request.toServiceRequest(), recruitmentPostId, userDetails.getUserId());
3052
return ApiResponse.noContent();
3153
}
3254

3355
@DeleteMapping("/{recruitment-post-id}")
34-
public ApiResponse<Void> delete(@PathVariable("recruitment-post-id") long recruitmentPostId) {
56+
public ApiResponse<Void> delete(@PathVariable("recruitment-post-id") long recruitmentPostId,
57+
@AuthenticationPrincipal Principal userDetails) {
58+
recruitmentPostService.delete(recruitmentPostId, userDetails.getUserId());
3559
return ApiResponse.noContent();
3660
}
3761

3862
@PostMapping("/{recruitment-post-id}/status")
3963
public ApiResponse<Void> changeStatus(@PathVariable("recruitment-post-id") long recruitmentPostId,
40-
@RequestBody RecruitmentPostStatusEditRequest request) {
64+
@RequestBody RecruitmentPostStatusUpdateRequest request,
65+
@AuthenticationPrincipal Principal userDetails) {
66+
recruitmentPostService.updateStatus(request.toServiceRequest(), recruitmentPostId, userDetails.getUserId());
4167
return ApiResponse.noContent();
4268
}
4369

4470
@GetMapping("{recruitment-post-id}/applications")
45-
public ApiResponse<List<ApplicationResponse>> getApplications(@PathVariable("recruitment-post-id") long recruitmentPostId) {
46-
return ApiResponse.success(List.of(new ApplicationResponse()));
71+
public ApiResponse<RecruitmentApplicationPagingInfo> getApplications(@PageableDefault(size = 5) Pageable pageable,
72+
@PathVariable("recruitment-post-id") long recruitmentPostId) {
73+
return ApiResponse.success(new RecruitmentApplicationPagingInfo());
74+
}
75+
76+
@GetMapping("{recruitment-post-id}/comments")
77+
public ApiResponse<RecruitmentPostCommentPagingInfo> getComments(@PageableDefault(size = 5) Pageable pageable,
78+
@PathVariable("recruitment-post-id") long recruitmentPostId) {
79+
return ApiResponse.success(new RecruitmentPostCommentPagingInfo());
4780
}
4881
}
Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package grep.neogul_coder.domain.recruitment.post.controller;
22

3-
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostEditRequest;
4-
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostSaveRequest;
5-
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusEditRequest;
6-
import grep.neogul_coder.domain.recruitment.post.controller.dto.response.RecruitmentPostInfo;
7-
import grep.neogul_coder.domain.studyapplication.controller.dto.response.ApplicationResponse;
3+
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostCreateRequest;
4+
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostStatusUpdateRequest;
5+
import grep.neogul_coder.domain.recruitment.post.controller.dto.request.RecruitmentPostUpdateRequest;
6+
import grep.neogul_coder.domain.recruitment.post.controller.dto.response.*;
7+
import grep.neogul_coder.global.auth.Principal;
88
import grep.neogul_coder.global.response.ApiResponse;
99
import io.swagger.v3.oas.annotations.Operation;
1010
import io.swagger.v3.oas.annotations.tags.Tag;
11-
12-
import java.util.List;
11+
import org.springframework.data.domain.Pageable;
1312

1413
@Tag(name = "Recruitment-Post", description = "모집 글 API")
1514
public interface RecruitmentPostSpecification {
@@ -18,17 +17,29 @@ public interface RecruitmentPostSpecification {
1817
ApiResponse<RecruitmentPostInfo> get(long id);
1918

2019
@Operation(summary = "모집 글 저장", description = "스터디 모집 글을 저장 합니다.")
21-
ApiResponse<Void> save(RecruitmentPostSaveRequest request);
20+
ApiResponse<Void> save(RecruitmentPostCreateRequest request, Principal userDetails);
21+
22+
@Operation(summary = "참여 중인 스터디 이름들 조회", description = "스터디 모집글 작성시 내가 참여한 스터디 이름들을 가져올수 있습니다.")
23+
ApiResponse<ParticipatedStudyNamesInfo> getParticipatedStudyNames(Principal userDetails);
24+
25+
@Operation(summary = "스터디 불러오기", description = "스터디 모집글 작성시 스터디 불러오기를 통해 스터디 정보를 조회할 수 있습니다.")
26+
ApiResponse<ParticipatedStudyInfo> getParticipatedStudy(long studyId, Principal userDetails);
2227

2328
@Operation(summary = "모집 글 수정", description = "모집글을 수정 합니다.")
24-
ApiResponse<Void> edit(long recruitmentPostId, RecruitmentPostEditRequest request);
29+
ApiResponse<Void> update(long recruitmentPostId, RecruitmentPostUpdateRequest request, Principal userDetails);
2530

2631
@Operation(summary = "모집 글 삭제", description = "모집글을 삭제 합니다.")
27-
ApiResponse<Void> delete(long recruitmentPostId);
32+
ApiResponse<Void> delete(long recruitmentPostId, Principal userDetails);
2833

2934
@Operation(summary = "모집 상태 변경", description = "모집글의 상태를 변경 합니다.")
30-
ApiResponse<Void> changeStatus(long recruitmentPostId, RecruitmentPostStatusEditRequest request);
35+
ApiResponse<Void> changeStatus(long recruitmentPostId, RecruitmentPostStatusUpdateRequest request, Principal userDetails);
36+
37+
@Operation(summary = "스터디 신청 목록 페이징 조회", description =
38+
"스터디 신청 목록들을 페이징 조회합니다. <br>" +
39+
"사용법 : /recruitment-post/{id}/applications?page=0&size=5")
40+
ApiResponse<RecruitmentApplicationPagingInfo> getApplications(Pageable pageable, long recruitmentPostId);
3141

32-
@Operation(summary = "스터디 신청 목록 조회", description = "스터디장이 스터디 신청 목록을 조회합니다.")
33-
ApiResponse<List<ApplicationResponse>> getApplications(long recruitmentPostId);
42+
@Operation(summary = "모집글 댓글 페이징 조회", description = "모집글의 댓글들을 페이징 조회 합니다. <br>" +
43+
"사용법 : /recruitment-post/{id}/comments?page=0&size=5")
44+
ApiResponse<RecruitmentPostCommentPagingInfo> getComments(Pageable pageable, long recruitmentPostId);
3445
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package grep.neogul_coder.domain.recruitment.post.controller.dto.request;
2+
3+
import grep.neogul_coder.domain.recruitment.post.service.request.RecruitmentPostCreateServiceRequest;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import jakarta.validation.Valid;
6+
import jakarta.validation.constraints.FutureOrPresent;
7+
import jakarta.validation.constraints.NotBlank;
8+
import jakarta.validation.constraints.NotNull;
9+
import jakarta.validation.constraints.Positive;
10+
import lombok.Builder;
11+
import lombok.Getter;
12+
13+
import java.time.LocalDate;
14+
15+
@Getter
16+
public class RecruitmentPostCreateRequest {
17+
18+
@Schema(example = "2", description = "스터디 ID")
19+
@NotNull(message = "스터디 ID 값은 필수 입니다.")
20+
private long studyId;
21+
22+
@Schema(example = "모각코 1일 스터디 모집 합니다.", description = "제목")
23+
@NotBlank(message = "제목은 필수 입니다.")
24+
private String subject;
25+
26+
@Schema(example = "모각코 1일 스터디에 참여하실분들은 신청 해주시면 감사합니다!", description = "소개 글 내용")
27+
@NotBlank(message = "내용은 필수 입니다.")
28+
private String content;
29+
30+
@Schema(example = "3", description = "모집글 인원 수")
31+
@Positive(message = "모집인원은 최소 1명을 입력해 주세요")
32+
private int recruitmentCount;
33+
34+
@Schema(example = "2025-07-10", description = "모집 마감일")
35+
@FutureOrPresent(message = "모집 마감일은 현재 날짜 이전으로 설정이 불가능 합니다.")
36+
private LocalDate expiredDate;
37+
38+
@Builder
39+
private RecruitmentPostCreateRequest(long studyId, String subject, String content,
40+
int recruitmentCount, LocalDate expiredDate) {
41+
this.studyId = studyId;
42+
this.subject = subject;
43+
this.content = content;
44+
this.recruitmentCount = recruitmentCount;
45+
this.expiredDate = expiredDate;
46+
}
47+
48+
public RecruitmentPostCreateServiceRequest toServiceRequest() {
49+
return RecruitmentPostCreateServiceRequest.builder()
50+
.studyId(this.studyId)
51+
.subject(this.subject)
52+
.content(this.content)
53+
.recruitmentCount(this.recruitmentCount)
54+
.expiredDate(this.expiredDate)
55+
.build();
56+
}
57+
}

src/main/java/grep/neogul_coder/domain/recruitment/post/controller/dto/request/RecruitmentPostEditRequest.java

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

src/main/java/grep/neogul_coder/domain/recruitment/post/controller/dto/request/RecruitmentPostSaveRequest.java

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

src/main/java/grep/neogul_coder/domain/recruitment/post/controller/dto/request/RecruitmentPostStatusEditRequest.java

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package grep.neogul_coder.domain.recruitment.post.controller.dto.request;
2+
3+
import grep.neogul_coder.domain.recruitment.RecruitmentPostStatus;
4+
import grep.neogul_coder.domain.recruitment.post.service.request.RecruitmentPostStatusUpdateServiceRequest;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotNull;
7+
import lombok.Getter;
8+
9+
@Getter
10+
public class RecruitmentPostStatusUpdateRequest {
11+
12+
@Schema(example = "IN_PROGRESS", description = "모집 글 상태")
13+
@NotNull
14+
private RecruitmentPostStatus status;
15+
16+
public RecruitmentPostStatusUpdateServiceRequest toServiceRequest() {
17+
return new RecruitmentPostStatusUpdateServiceRequest(status);
18+
}
19+
}

0 commit comments

Comments
 (0)