Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/main/java/com/back/domain/board/controller/PostController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.back.domain.board.controller;

import com.back.domain.board.dto.PostRequest;
import com.back.domain.board.dto.PostResponse;
import com.back.domain.board.service.PostService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostController implements PostControllerDocs {
private final PostService postService;

// 게시글 생성
@PostMapping
public ResponseEntity<RsData<PostResponse>> createPost(
@RequestBody @Valid PostRequest request,
@AuthenticationPrincipal CustomUserDetails user
) {
PostResponse response = postService.createPost(request, user.getUserId());
return ResponseEntity
.status(HttpStatus.CREATED)
.body(RsData.success(
"게시글이 생성되었습니다.",
response
));
}
}
147 changes: 147 additions & 0 deletions src/main/java/com/back/domain/board/controller/PostControllerDocs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.back.domain.board.controller;

import com.back.domain.board.dto.PostRequest;
import com.back.domain.board.dto.PostResponse;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.RequestBody;

@Tag(name = "Post API", description = "게시글 관련 API")
public interface PostControllerDocs {

@Operation(
summary = "게시글 생성",
description = "로그인한 사용자가 새 게시글을 작성합니다."
)
@ApiResponses({
@ApiResponse(
responseCode = "201",
description = "게시글 생성 성공",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": true,
"code": "SUCCESS_200",
"message": "게시글이 생성되었습니다.",
"data": {
"postId": 101,
"author": {
"id": 5,
"nickname": "홍길동"
},
"title": "첫 번째 게시글",
"content": "안녕하세요, 첫 글입니다!",
"categories": [
{ "id": 1, "name": "공지사항" },
{ "id": 2, "name": "자유게시판" }
],
"createdAt": "2025-09-22T10:30:00",
"updatedAt": "2025-09-22T10:30:00"
}
}
""")
)
),
@ApiResponse(
responseCode = "400",
description = "잘못된 요청 (필드 누락 등)",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_400",
"message": "잘못된 요청입니다.",
"data": null
}
""")
)
),
@ApiResponse(
responseCode = "401",
description = "인증 실패 (토큰 없음/잘못됨/만료)",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "토큰 없음", value = """
{
"success": false,
"code": "AUTH_001",
"message": "인증이 필요합니다.",
"data": null
}
"""),
@ExampleObject(name = "잘못된 토큰", value = """
{
"success": false,
"code": "AUTH_002",
"message": "유효하지 않은 액세스 토큰입니다.",
"data": null
}
"""),
@ExampleObject(name = "만료된 토큰", value = """
{
"success": false,
"code": "AUTH_004",
"message": "만료된 액세스 토큰입니다.",
"data": null
}
""")
}
)
),
@ApiResponse(
responseCode = "404",
description = "존재하지 않는 사용자 또는 카테고리",
content = @Content(
mediaType = "application/json",
examples = {
@ExampleObject(name = "존재하지 않는 사용자", value = """
{
"success": false,
"code": "USER_001",
"message": "존재하지 않는 사용자입니다.",
"data": null
}
"""),
@ExampleObject(name = "존재하지 않는 카테고리", value = """
{
"success": false,
"code": "POST_003",
"message": "존재하지 않는 카테고리입니다.",
"data": null
}
""")
}
)
),
@ApiResponse(
responseCode = "500",
description = "서버 내부 오류",
content = @Content(
mediaType = "application/json",
examples = @ExampleObject(value = """
{
"success": false,
"code": "COMMON_500",
"message": "서버 오류가 발생했습니다.",
"data": null
}
""")
)
)
})
ResponseEntity<RsData<PostResponse>> createPost(
@RequestBody PostRequest request,
@AuthenticationPrincipal CustomUserDetails user
);
}
21 changes: 21 additions & 0 deletions src/main/java/com/back/domain/board/dto/AuthorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.board.dto;

import com.back.domain.user.entity.User;

/**
* 작성자 응답 DTO
*
* @param id 작성자 ID
* @param nickname 작성자 닉네임
*/
public record AuthorResponse(
Long id,
String nickname
) {
public static AuthorResponse from(User user) {
return new AuthorResponse(
user.getId(),
user.getUserProfile().getNickname()
);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/back/domain/board/dto/CategoryResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.board.dto;

import com.back.domain.board.entity.PostCategory;

/**
* 카테고리 응답 DTO
*
* @param id 카테고리 ID
* @param name 카테고리 이름
*/
public record CategoryResponse(
Long id,
String name
) {
public static CategoryResponse from(PostCategory category) {
return new CategoryResponse(
category.getId(),
category.getName()
);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/back/domain/board/dto/PostRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.back.domain.board.dto;

import jakarta.validation.constraints.NotBlank;

import java.util.List;

/**
* 게시글 생성 및 수정을 위한 요청 DTO
*
* @param title 게시글 제목
* @param content 게시글 내용
* @param categoryIds 카테고리 ID 리스트
*/
public record PostRequest(
@NotBlank String title,
@NotBlank String content,
List<Long> categoryIds
) {}
40 changes: 40 additions & 0 deletions src/main/java/com/back/domain/board/dto/PostResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.back.domain.board.dto;

import com.back.domain.board.entity.Post;
import java.time.LocalDateTime;
import java.util.List;

/**
* 게시글 응답 DTO
*
* @param postId 게시글 ID
* @param author 작성자 정보
* @param title 게시글 제목
* @param content 게시글 내용
* @param categories 게시글 카테고리 목록
* @param createdAt 게시글 생성 일시
* @param updatedAt 게시글 수정 일시
*/
public record PostResponse(
Long postId,
AuthorResponse author,
String title,
String content,
List<CategoryResponse> categories,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
public static PostResponse from(Post post) {
return new PostResponse(
post.getId(),
AuthorResponse.from(post.getUser()),
post.getTitle(),
post.getContent(),
post.getCategories().stream()
.map(CategoryResponse::from)
.toList(),
post.getCreatedAt(),
post.getUpdatedAt()
);
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/back/domain/board/entity/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,28 @@ public class Post extends BaseEntity {

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();

// -------------------- 생성자 --------------------
public Post(User user, String title, String content) {
this.user = user;
this.title = title;
this.content = content;
}

// -------------------- 비즈니스 메서드 --------------------
// 카테고리 업데이트
public void updateCategories(List<PostCategory> categories) {
this.postCategoryMappings.clear();
categories.forEach(category ->
this.postCategoryMappings.add(new PostCategoryMapping(this, category))
);
}

// -------------------- 헬퍼 메서드 --------------------
// 게시글에 연결된 카테고리 목록 조회
public List<PostCategory> getCategories() {
return postCategoryMappings.stream()
.map(PostCategoryMapping::getCategory)
.toList();
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/back/domain/board/entity/PostCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Entity
Expand All @@ -17,4 +18,10 @@ public class PostCategory extends BaseEntity {

@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostCategoryMapping> postCategoryMappings;

// -------------------- 생성자 --------------------
public PostCategory(String name) {
this.name = name;
this.postCategoryMappings = new ArrayList<>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ public class PostCategoryMapping {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private PostCategory category;

// -------------------- 생성자 --------------------
public PostCategoryMapping(Post post, PostCategory category) {
this.post = post;
this.category = category;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.back.domain.board.repository;

import com.back.domain.board.entity.PostCategory;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostCategoryRepository extends JpaRepository<PostCategory, Long> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.back.domain.board.repository;

import com.back.domain.board.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}
Loading