Skip to content

Commit 4b7c7ad

Browse files
authored
[FEAT]: 게시글 CRUD 기능 구현 (#12)
* Post 엔티티 수정 1. category - CHAT(잡담), SCENARIO(시나리오), POLL(투표) 2. voteContent - JSON 데이터를 단순 문자열로 저장 * Post 요청, 응답 DTO 생성 * [Refactor]: 응답 DTO createdAt -> createdDate 필드명 수정 * [Feat]: 엔티티 - DTO 변환 담당 매퍼 클래스 생성 * [Refactor]: 요청 DTO 검증 추가 * [Feat]: 게시글 생성 기능 구현 * [Test]: 게시글 생성 테스트 구현 * [Feat]: 게시글 목록, 단건 조회 기능 구현 * [Test]: 게시글 조회 테스트 구현 * [Feat]: 게시글 수정 기능 구현 * [Feat]: 게시글 삭제 기능 구현 * [Feat]: API 요청 성공 응답 클래스 구현 * [Feat]: 응답 형태 변경 * [Feat]: 페이지 설정 빈 등록 - 요청 시작 1, 최대 사이즈 50, 기본 사이즈 5 * [Feat]: 게시글 목록 페이징 기능 추가 * [Feat]: 응답 형태 변경에 따른 PostControllerTest 수정
1 parent c462b84 commit 4b7c7ad

File tree

11 files changed

+608
-13
lines changed

11 files changed

+608
-13
lines changed

back/src/main/java/com/back/domain/post/controller/PostController.java

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package com.back.domain.post.controller;
22

3+
import com.back.domain.post.dto.PostRequest;
4+
import com.back.domain.post.dto.PostResponse;
35
import com.back.domain.post.service.PostService;
6+
import com.back.global.common.ApiResponse;
7+
import com.back.global.common.PageResponse;
8+
import jakarta.validation.Valid;
49
import lombok.RequiredArgsConstructor;
5-
import org.springframework.web.bind.annotation.RequestMapping;
6-
import org.springframework.web.bind.annotation.RestController;
10+
import org.springframework.data.domain.Page;
11+
import org.springframework.data.domain.Pageable;
12+
import org.springframework.http.HttpStatus;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.bind.annotation.*;
15+
16+
import java.util.List;
717

818
/**
919
* 게시글 관련 API 요청을 처리하는 컨트롤러.
@@ -15,4 +25,40 @@ public class PostController {
1525

1626
private final PostService postService;
1727

28+
// 게시글 생성
29+
@PostMapping
30+
public ApiResponse<PostResponse> createPost(
31+
@RequestBody @Valid PostRequest request) {
32+
Long userId = 1L; // fixme 임시 사용자 ID
33+
PostResponse response = postService.createPost(userId, request);
34+
return ApiResponse.success(response, "성공적으로 생성되었습니다.", HttpStatus.OK);
35+
}
36+
37+
// 게시글 목록 조회
38+
@GetMapping
39+
public ApiResponse<PageResponse<PostResponse>> getPosts(Pageable pageable) {
40+
Page<PostResponse> responses = postService.getPosts(pageable);
41+
return ApiResponse.success(PageResponse.of(responses), "성공적으로 조회되었습니다.", HttpStatus.OK);
42+
}
43+
44+
// 게시글 단건 조회
45+
@GetMapping("/{postId}")
46+
public ApiResponse<PostResponse> getPost(@PathVariable Long postId) {
47+
return ApiResponse.success(postService.getPost(postId), "성공적으로 조회되었습니다.", HttpStatus.OK);
48+
}
49+
50+
@PutMapping("/{postId}")
51+
public ApiResponse<PostResponse> updatePost(
52+
@PathVariable Long postId,
53+
@RequestBody @Valid PostRequest request) {
54+
Long userId = 1L; // fixme 임시 사용자 ID
55+
return ApiResponse.success(postService.updatePost(userId, postId, request), "성공적으로 수정되었습니다.", HttpStatus.OK);
56+
}
57+
58+
@DeleteMapping("/{postId}")
59+
public ApiResponse<Void> deletePost(@PathVariable Long postId) {
60+
Long userId = 1L; // fixme 임시 사용자 ID
61+
postService.deletePost(userId, postId);
62+
return ApiResponse.success(null, "성공적으로 삭제되었습니다.", HttpStatus.OK);
63+
}
1864
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.back.domain.post.dto;
2+
3+
import com.back.domain.post.enums.PostCategory;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.NotNull;
6+
import jakarta.validation.constraints.Size;
7+
8+
public record PostRequest(
9+
@NotBlank(message = "제목은 필수입니다")
10+
@Size(max = 200, message = "제목은 200자를 초과할 수 없습니다")
11+
String title,
12+
13+
@NotBlank(message = "내용은 필수입니다")
14+
String content,
15+
16+
@NotNull(message = "카테고리는 필수입니다")
17+
PostCategory category
18+
) { }
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.back.domain.post.dto;
2+
3+
import com.back.domain.post.enums.PostCategory;
4+
5+
import java.time.LocalDateTime;
6+
7+
/**
8+
* @param id
9+
* @param title
10+
* @param content
11+
* @param category
12+
* @param hide
13+
* @param likeCount
14+
* @param createdDate
15+
* fixme @param createdBy 추가 예정
16+
*/
17+
public record PostResponse(
18+
Long id,
19+
String title,
20+
String content,
21+
PostCategory category,
22+
boolean hide,
23+
int likeCount,
24+
LocalDateTime createdDate
25+
) { }
Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package com.back.domain.post.entity;
22

3+
import com.back.domain.post.enums.PostCategory;
34
import com.back.domain.user.entity.User;
45
import com.back.global.baseentity.BaseEntity;
6+
import com.back.global.exception.ApiException;
7+
import com.back.global.exception.ErrorCode;
58
import jakarta.persistence.*;
6-
import lombok.AllArgsConstructor;
7-
import lombok.Builder;
8-
import lombok.Getter;
9-
import lombok.NoArgsConstructor;
10-
import lombok.Setter;
9+
import lombok.*;
1110
import org.hibernate.annotations.ColumnDefault;
11+
import org.hibernate.service.spi.ServiceException;
1212
import org.springframework.data.annotation.LastModifiedDate;
1313

1414
import java.time.LocalDateTime;
@@ -19,9 +19,8 @@
1919
*/
2020
@Entity
2121
@Getter
22-
@Setter
23-
@NoArgsConstructor
24-
@AllArgsConstructor
22+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
23+
@AllArgsConstructor(access = AccessLevel.PROTECTED)
2524
@Builder
2625
public class Post extends BaseEntity {
2726

@@ -32,13 +31,16 @@ public class Post extends BaseEntity {
3231
@Column(length = 200)
3332
private String title;
3433

35-
@Column(length = 200)
36-
private String category;
34+
@Column(length = 20)
35+
@Enumerated(EnumType.STRING)
36+
private PostCategory category;
3737

3838
@Column(columnDefinition = "TEXT")
3939
private String content;
4040

41-
@Column(columnDefinition = "jsonb")
41+
/**
42+
* JSON 데이터를 단순 문자열로 저장 (예: {"option1": 10, "option2": 5})
43+
*/
4244
private String voteContent;
4345

4446
@Column(nullable = false)
@@ -49,4 +51,19 @@ public class Post extends BaseEntity {
4951

5052
@LastModifiedDate
5153
private LocalDateTime updatedAt;
54+
55+
public void assignUser(User user) {
56+
this.user = user;
57+
}
58+
59+
public void updatePost(String title, String content, PostCategory category) {
60+
this.title = title;
61+
this.content = content;
62+
this.category = category;
63+
}
64+
65+
public void checkUser(User targetUser) {
66+
if (!user.equals(targetUser))
67+
throw new ApiException(ErrorCode.UNAUTHORIZED_USER);
68+
}
5269
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.back.domain.post.enums;
2+
3+
public enum PostCategory {
4+
CHAT, // 잡담
5+
SCENARIO, // 시나리오 첨부
6+
POLL // 투표
7+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.back.domain.post.mapper;
2+
3+
import com.back.domain.post.dto.PostRequest;
4+
import com.back.domain.post.dto.PostResponse;
5+
import com.back.domain.post.entity.Post;
6+
7+
import java.util.List;
8+
9+
/**
10+
* PostMapper
11+
* 엔티티와 PostRequest, PostResponse 간의 변환을 담당하는 매퍼 클래스
12+
*/
13+
public abstract class PostMapper {
14+
public static Post toEntity(PostRequest request) {
15+
return Post.builder()
16+
.title(request.title())
17+
.content(request.content())
18+
.category(request.category())
19+
.hide(false)
20+
.likeCount(0)
21+
.build();
22+
}
23+
24+
public static PostResponse toResponse(Post post) {
25+
return new PostResponse(
26+
post.getId(),
27+
post.getTitle(),
28+
post.getContent(),
29+
post.getCategory(),
30+
post.isHide(),
31+
post.getLikeCount(),
32+
post.getCreatedDate()
33+
);
34+
}
35+
36+
public static List<PostResponse> toResponseList(List<Post> posts) {
37+
return posts.stream()
38+
.map(PostMapper::toResponse)
39+
.toList();
40+
}
41+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,82 @@
11
package com.back.domain.post.service;
22

3+
import com.back.domain.post.dto.PostRequest;
4+
import com.back.domain.post.dto.PostResponse;
5+
import com.back.domain.post.entity.Post;
6+
import com.back.domain.post.mapper.PostMapper;
37
import com.back.domain.post.repository.PostRepository;
8+
import com.back.domain.user.entity.User;
9+
import com.back.domain.user.repository.UserRepository;
10+
import com.back.global.exception.ApiException;
11+
import com.back.global.exception.ErrorCode;
412
import lombok.RequiredArgsConstructor;
13+
import org.springframework.data.domain.Page;
14+
import org.springframework.data.domain.Pageable;
515
import org.springframework.stereotype.Service;
16+
import org.springframework.transaction.annotation.Transactional;
17+
18+
import java.util.List;
19+
import java.util.Optional;
620

721
/**
822
* 게시글 관련 비즈니스 로직을 처리하는 서비스.
923
*/
1024
@Service
1125
@RequiredArgsConstructor
26+
@Transactional(readOnly = true)
1227
public class PostService {
1328

29+
private final UserRepository userRepository;
1430
private final PostRepository postRepository;
1531

32+
@Transactional
33+
public PostResponse createPost(Long userId, PostRequest request) {
34+
User user = userRepository.findById(userId)
35+
.orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND));
36+
37+
Post post = PostMapper.toEntity(request);
38+
post.assignUser(user);
39+
Post savedPost = postRepository.save(post);
40+
41+
return PostMapper.toResponse(savedPost);
42+
}
43+
44+
public PostResponse getPost(Long postId) {
45+
return postRepository.findById(postId)
46+
.filter(post -> !post.isHide())
47+
.map(PostMapper::toResponse)
48+
.orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND));
49+
}
50+
51+
public Page<PostResponse> getPosts(Pageable pageable) {
52+
return postRepository.findAll(pageable)
53+
.map(PostMapper::toResponse);
54+
}
55+
56+
@Transactional
57+
public PostResponse updatePost(Long userId, Long postId, PostRequest request) {
58+
Post post = validatePostOwnership(userId, postId);
59+
60+
post.updatePost(request.title(), request.content(), request.category());
61+
62+
return PostMapper.toResponse(post);
63+
}
64+
65+
@Transactional
66+
public void deletePost(Long userId, Long postId) {
67+
Post post = validatePostOwnership(userId, postId);
68+
postRepository.delete(post);
69+
}
70+
71+
private Post validatePostOwnership(Long userId, Long postId) {
72+
Post post = postRepository.findById(postId)
73+
.orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND));
74+
75+
User requestUser = userRepository.findById(userId)
76+
.orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND));
77+
78+
post.checkUser(requestUser);
79+
80+
return post;
81+
}
1682
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.global.common;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import org.springframework.http.HttpStatus;
7+
8+
/**
9+
* 공통 응답 형태
10+
* {
11+
* "data": { ... },
12+
* "message": "성공적으로 조회되었습니다.",
13+
* "status": 200
14+
* }
15+
*/
16+
@Getter
17+
@JsonInclude(JsonInclude.Include.NON_NULL)
18+
public class ApiResponse<T> {
19+
20+
private final T data;
21+
private final String message;
22+
private final int status;
23+
24+
private ApiResponse(T data, String message, int status) {
25+
this.data = data;
26+
this.message = message;
27+
this.status = status;
28+
}
29+
30+
public static <T> ApiResponse<T> success(T data, String message) {
31+
return new ApiResponse<>(data, message, HttpStatus.OK.value());
32+
}
33+
34+
public static <T> ApiResponse<T> success(T data, String message, HttpStatus status) {
35+
return new ApiResponse<>(data, message, status.value());
36+
}
37+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.back.global.common;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import org.springframework.data.domain.Page;
6+
7+
import java.util.List;
8+
9+
@Getter
10+
@AllArgsConstructor
11+
public class PageResponse<T> {
12+
private List<T> items;
13+
private int page;
14+
private int size;
15+
private long totalElements;
16+
private int totalPages;
17+
private boolean last;
18+
19+
public static <T> PageResponse<T> of(Page<T> page) {
20+
return new PageResponse<>(
21+
page.getContent(),
22+
page.getNumber() + 1, // 응답도 1페이지 시작으로 반환, Page 객체는 0페이지부터 시작
23+
page.getSize(),
24+
page.getTotalElements(),
25+
page.getTotalPages(),
26+
page.isLast()
27+
);
28+
}
29+
}

0 commit comments

Comments
 (0)