Skip to content

Commit 969f537

Browse files
authored
Merge pull request #58 from prgrms-web-devcourse-final-project/feature/EA3-78-study-board-api
[EA3-78] feature: 스터디 커뮤니티 게시글 엔티티 및 스웨거 작성(+pageresponse 생성)
2 parents 310f83d + 4ce750b commit 969f537

File tree

8 files changed

+301
-0
lines changed

8 files changed

+301
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package grep.neogul_coder.domain.studypost;
2+
3+
import com.fasterxml.jackson.annotation.JsonValue;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
6+
@Schema(description = "카테고리: NOTICE(공지), FREE(자유)")
7+
public enum Category {
8+
NOTICE("공지"),
9+
FREE("자유");
10+
11+
private final String korean;
12+
13+
Category(String korean) { this.korean = korean; }
14+
15+
@JsonValue
16+
public String toJson() {
17+
return korean;
18+
}
19+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package grep.neogul_coder.domain.studypost;
2+
3+
import grep.neogul_coder.domain.study.Study;
4+
import grep.neogul_coder.global.entity.BaseEntity;
5+
import jakarta.persistence.Column;
6+
import jakarta.persistence.Entity;
7+
import jakarta.persistence.EnumType;
8+
import jakarta.persistence.Enumerated;
9+
import jakarta.persistence.FetchType;
10+
import jakarta.persistence.GeneratedValue;
11+
import jakarta.persistence.GenerationType;
12+
import jakarta.persistence.Id;
13+
import jakarta.persistence.JoinColumn;
14+
import jakarta.persistence.ManyToOne;
15+
import jakarta.validation.constraints.NotBlank;
16+
17+
@Entity
18+
public class StudyPost extends BaseEntity {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.AUTO)
22+
private Long id;
23+
24+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
25+
@JoinColumn(name = "study_id", nullable = false)
26+
private Study study;
27+
28+
@Column(nullable = false)
29+
private Long userId;
30+
31+
@NotBlank(message = "제목은 필수입니다.")
32+
@Column(nullable = false)
33+
private String title;
34+
35+
@Enumerated(EnumType.STRING)
36+
@Column(nullable = false)
37+
private Category category;
38+
39+
@NotBlank(message = "내용은 필수입니다.")
40+
@Column(nullable = false)
41+
private String content;
42+
43+
@Column(nullable = false)
44+
private boolean isDeleted = false;
45+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package grep.neogul_coder.domain.studypost.controller;
2+
3+
import grep.neogul_coder.domain.studypost.dto.StudyPostDetailResponse;
4+
import grep.neogul_coder.domain.studypost.dto.StudyPostListResponse;
5+
import grep.neogul_coder.domain.studypost.dto.StudyPostRequest;
6+
import grep.neogul_coder.global.response.ApiResponse;
7+
import jakarta.validation.Valid;
8+
import java.util.List;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
@RestController
12+
@RequestMapping("/api/studies/{studyId}/posts")
13+
public class StudyPostController implements StudyPostSpecification {
14+
15+
@PostMapping
16+
public ApiResponse<Void> create(
17+
@PathVariable("studyId") Long studyId,
18+
@RequestBody @Valid StudyPostRequest request
19+
) {
20+
return ApiResponse.noContent();
21+
}
22+
23+
@GetMapping("/all")
24+
public ApiResponse<List<StudyPostListResponse>> findAllWithoutPagination(
25+
@PathVariable("studyId") Long studyId
26+
) {
27+
List<StudyPostListResponse> content = List.of(new StudyPostListResponse());
28+
return ApiResponse.success(content);
29+
}
30+
31+
@GetMapping("/{postId}")
32+
public ApiResponse<StudyPostDetailResponse> findOne(
33+
@PathVariable("studyId") Long studyId,
34+
@PathVariable("postId") Long postId
35+
) {
36+
return ApiResponse.success(new StudyPostDetailResponse());
37+
}
38+
39+
@PutMapping("/{postId}")
40+
public ApiResponse<Void> update(
41+
@PathVariable("studyId") Long studyId,
42+
@PathVariable("postId") Long postId,
43+
@RequestBody @Valid StudyPostRequest request
44+
) {
45+
return ApiResponse.noContent();
46+
}
47+
48+
@DeleteMapping("/{postId}")
49+
public ApiResponse<Void> delete(
50+
@PathVariable("studyId") Long studyId,
51+
@PathVariable("postId") Long postId
52+
) {
53+
return ApiResponse.noContent();
54+
}
55+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package grep.neogul_coder.domain.studypost.controller;
2+
3+
import grep.neogul_coder.domain.studypost.dto.StudyPostDetailResponse;
4+
import grep.neogul_coder.domain.studypost.dto.StudyPostListResponse;
5+
import grep.neogul_coder.domain.studypost.dto.StudyPostRequest;
6+
import grep.neogul_coder.global.response.ApiResponse;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.Parameter;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import jakarta.validation.Valid;
11+
import java.util.List;
12+
13+
@Tag(name = "Study-Post", description = "스터디 게시판 API")
14+
public interface StudyPostSpecification {
15+
16+
@Operation(summary = "게시글 생성", description = "스터디에 새로운 게시글을 작성합니다.")
17+
ApiResponse<Void> create(
18+
@Parameter(description = "스터디 ID", example = "1") Long studyId,
19+
@Valid StudyPostRequest request
20+
);
21+
22+
@Operation(summary = "게시글 목록 전체 조회", description = "스터디의 게시글 전체 목록을 조회합니다.")
23+
ApiResponse<List<StudyPostListResponse>> findAllWithoutPagination(
24+
@Parameter(description = "스터디 ID", example = "1") Long studyId
25+
);
26+
27+
@Operation(summary = "게시글 상세 조회", description = "특정 게시글의 상세 정보를 조회합니다.")
28+
ApiResponse<StudyPostDetailResponse> findOne(
29+
@Parameter(description = "스터디 ID", example = "1") Long studyId,
30+
@Parameter(description = "게시글 ID", example = "15") Long postId
31+
);
32+
33+
@Operation(summary = "게시글 수정", description = "기존 게시글을 수정합니다.")
34+
ApiResponse<Void> update(
35+
@Parameter(description = "스터디 ID", example = "1") Long studyId,
36+
@Parameter(description = "게시글 ID", example = "15") Long postId,
37+
@Valid StudyPostRequest request
38+
);
39+
40+
@Operation(summary = "게시글 삭제", description = "특정 게시글을 삭제합니다.")
41+
ApiResponse<Void> delete(
42+
@Parameter(description = "스터디 ID", example = "1") Long studyId,
43+
@Parameter(description = "게시글 ID", example = "15") Long postId
44+
);
45+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package grep.neogul_coder.domain.studypost.dto;
2+
3+
import grep.neogul_coder.domain.comment.dto.CommentResponse;
4+
import grep.neogul_coder.domain.studypost.Category;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import java.time.LocalDateTime;
7+
import java.util.List;
8+
import lombok.Getter;
9+
10+
@Getter
11+
@Schema(description = "스터디 게시글 상세 응답 DTO")
12+
public class StudyPostDetailResponse {
13+
14+
@Schema(description = "게시글 ID", example = "10")
15+
private Long id;
16+
17+
@Schema(description = "제목", example = "모든 국민은 직업선택의 자유를 가진다.")
18+
private String title;
19+
20+
@Schema(description = "카테고리: NOTICE(공지), FREE(자유)", example = "NOTICE")
21+
private Category category;
22+
23+
@Schema(description = "본문", example = "국회는 의원의 자격을 심사하며, 의원을 징계할 있다.")
24+
private String content;
25+
26+
@Schema(description = "작성일", example = "2025-07-10T14:00:00")
27+
private LocalDateTime createdDate;
28+
29+
@Schema(description = "작성자 닉네임", example = "너굴코더")
30+
private String nickname;
31+
32+
@Schema(description = "작성자 프로필 이미지 URL", example = "https://cdn.example.com/profile.jpg")
33+
private String profileImageUrl;
34+
35+
@Schema(description = "댓글 수", example = "3")
36+
private int commentCount;
37+
38+
@Schema(description = "댓글 목록")
39+
private List<CommentResponse> comments;
40+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package grep.neogul_coder.domain.studypost.dto;
2+
3+
import grep.neogul_coder.domain.studypost.Category;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import java.time.LocalDateTime;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@Schema(description = "스터디 게시글 목록 응답 DTO")
10+
public class StudyPostListResponse {
11+
12+
@Schema(description = "게시글 ID", example = "12")
13+
private Long id;
14+
15+
@Schema(description = "제목", example = "모든 국민은 직업선택의 자유를 가진다.")
16+
private String title;
17+
18+
@Schema(description = "카테고리: NOTICE(공지), FREE(자유)", example = "NOTICE")
19+
private Category category;
20+
21+
@Schema(description = "본문", example = "국회는 의원의 자격을 심사하며, 의원을 징계할 있다.")
22+
private String content;
23+
24+
@Schema(description = "작성일", example = "2025-07-10T14:32:00")
25+
private LocalDateTime createdDate;
26+
27+
@Schema(description = "댓글 수", example = "3")
28+
private int commentCount;
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package grep.neogul_coder.domain.studypost.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotBlank;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Schema(description = "스터디 게시글 저장/수정 요청 DTO")
9+
public class StudyPostRequest {
10+
11+
@Schema(description = "제목", example = "스터디 공지")
12+
@NotBlank
13+
private String title;
14+
15+
@Schema(description = "카테고리: NOTICE(공지), FREE(자유)", example = "NOTICE")
16+
@NotBlank
17+
private String category;
18+
19+
@Schema(description = "내용", example = "오늘은 각자 공부한 내용에 대해 발표가 있는 날 입니다!")
20+
@NotBlank
21+
private String content;
22+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package grep.neogul_coder.global.response;
2+
3+
import java.util.List;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.data.domain.Page;
6+
7+
@RequiredArgsConstructor
8+
public class PageResponse<T> {
9+
10+
private final String url;
11+
private final Page<T> page;
12+
private final int pageButtonCnt;
13+
14+
public String url(){
15+
return url;
16+
}
17+
18+
public int currentNumber(){
19+
return page.getNumber() + 1;
20+
}
21+
22+
public int prevPage(){
23+
return Math.max(currentNumber() - 1, 1);
24+
}
25+
26+
public int nextPage(){
27+
return Math.min(currentNumber() + 1, calcTotalPage());
28+
}
29+
30+
public int startNumber(){
31+
return Math.floorDiv(page.getNumber(), pageButtonCnt) * pageButtonCnt + 1;
32+
}
33+
34+
public int endNumber(){
35+
return Math.min(startNumber() + pageButtonCnt - 1, calcTotalPage());
36+
}
37+
38+
public List<T> content(){
39+
return page.getContent();
40+
}
41+
42+
private int calcTotalPage(){
43+
int totalPage = page.getTotalPages();
44+
return totalPage == 0 ? 1 : totalPage;
45+
}
46+
}

0 commit comments

Comments
 (0)