From 79e12d7d077086e5474a1500bd818ad9e4b2a9af Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 09:33:33 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/PostController.java | 35 ++++++++++++ .../back/domain/board/dto/AuthorResponse.java | 21 +++++++ .../domain/board/dto/CategoryResponse.java | 21 +++++++ .../back/domain/board/dto/PostRequest.java | 18 ++++++ .../back/domain/board/dto/PostResponse.java | 40 +++++++++++++ .../com/back/domain/board/entity/Post.java | 24 ++++++++ .../board/entity/PostCategoryMapping.java | 6 ++ .../repository/PostCategoryRepository.java | 9 +++ .../board/repository/PostRepository.java | 9 +++ .../domain/board/service/PostService.java | 56 +++++++++++++++++++ .../com/back/global/exception/ErrorCode.java | 5 ++ 11 files changed, 244 insertions(+) create mode 100644 src/main/java/com/back/domain/board/controller/PostController.java create mode 100644 src/main/java/com/back/domain/board/dto/AuthorResponse.java create mode 100644 src/main/java/com/back/domain/board/dto/CategoryResponse.java create mode 100644 src/main/java/com/back/domain/board/dto/PostRequest.java create mode 100644 src/main/java/com/back/domain/board/dto/PostResponse.java create mode 100644 src/main/java/com/back/domain/board/repository/PostCategoryRepository.java create mode 100644 src/main/java/com/back/domain/board/repository/PostRepository.java create mode 100644 src/main/java/com/back/domain/board/service/PostService.java diff --git a/src/main/java/com/back/domain/board/controller/PostController.java b/src/main/java/com/back/domain/board/controller/PostController.java new file mode 100644 index 00000000..74cc3d5d --- /dev/null +++ b/src/main/java/com/back/domain/board/controller/PostController.java @@ -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 { + private final PostService postService; + + // 게시글 생성 + @PostMapping + public ResponseEntity> createPost( + @RequestBody @Valid PostRequest request, + @AuthenticationPrincipal CustomUserDetails user + ) { + PostResponse response = postService.createPost(request, user.getUserId()); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(RsData.success( + "게시글이 생성되었습니다.", + response + )); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/dto/AuthorResponse.java b/src/main/java/com/back/domain/board/dto/AuthorResponse.java new file mode 100644 index 00000000..e5e0a874 --- /dev/null +++ b/src/main/java/com/back/domain/board/dto/AuthorResponse.java @@ -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() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/dto/CategoryResponse.java b/src/main/java/com/back/domain/board/dto/CategoryResponse.java new file mode 100644 index 00000000..50b3fe4f --- /dev/null +++ b/src/main/java/com/back/domain/board/dto/CategoryResponse.java @@ -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() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/dto/PostRequest.java b/src/main/java/com/back/domain/board/dto/PostRequest.java new file mode 100644 index 00000000..1b8f655a --- /dev/null +++ b/src/main/java/com/back/domain/board/dto/PostRequest.java @@ -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 categoryIds +) {} \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/dto/PostResponse.java b/src/main/java/com/back/domain/board/dto/PostResponse.java new file mode 100644 index 00000000..a253bb4f --- /dev/null +++ b/src/main/java/com/back/domain/board/dto/PostResponse.java @@ -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 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() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/entity/Post.java b/src/main/java/com/back/domain/board/entity/Post.java index 9f79ca63..dac6f47a 100644 --- a/src/main/java/com/back/domain/board/entity/Post.java +++ b/src/main/java/com/back/domain/board/entity/Post.java @@ -32,4 +32,28 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) private List comments = new ArrayList<>(); + + // -------------------- 생성자 -------------------- + public Post(User user, String title, String content) { + this.user = user; + this.title = title; + this.content = content; + } + + // -------------------- 비즈니스 메서드 -------------------- + // 카테고리 업데이트 + public void updateCategories(List categories) { + this.postCategoryMappings.clear(); + categories.forEach(category -> + this.postCategoryMappings.add(new PostCategoryMapping(this, category)) + ); + } + + // -------------------- 헬퍼 메서드 -------------------- + // 게시글에 연결된 카테고리 목록 조회 + public List getCategories() { + return postCategoryMappings.stream() + .map(PostCategoryMapping::getCategory) + .toList(); + } } diff --git a/src/main/java/com/back/domain/board/entity/PostCategoryMapping.java b/src/main/java/com/back/domain/board/entity/PostCategoryMapping.java index 6f1dce8d..5524e924 100644 --- a/src/main/java/com/back/domain/board/entity/PostCategoryMapping.java +++ b/src/main/java/com/back/domain/board/entity/PostCategoryMapping.java @@ -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; + } } diff --git a/src/main/java/com/back/domain/board/repository/PostCategoryRepository.java b/src/main/java/com/back/domain/board/repository/PostCategoryRepository.java new file mode 100644 index 00000000..70e8a935 --- /dev/null +++ b/src/main/java/com/back/domain/board/repository/PostCategoryRepository.java @@ -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 { +} diff --git a/src/main/java/com/back/domain/board/repository/PostRepository.java b/src/main/java/com/back/domain/board/repository/PostRepository.java new file mode 100644 index 00000000..50294f94 --- /dev/null +++ b/src/main/java/com/back/domain/board/repository/PostRepository.java @@ -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 { +} diff --git a/src/main/java/com/back/domain/board/service/PostService.java b/src/main/java/com/back/domain/board/service/PostService.java new file mode 100644 index 00000000..906d48ec --- /dev/null +++ b/src/main/java/com/back/domain/board/service/PostService.java @@ -0,0 +1,56 @@ +package com.back.domain.board.service; + +import com.back.domain.board.dto.PostRequest; +import com.back.domain.board.dto.PostResponse; +import com.back.domain.board.entity.Post; +import com.back.domain.board.entity.PostCategory; +import com.back.domain.board.repository.PostCategoryRepository; +import com.back.domain.board.repository.PostRepository; +import com.back.domain.user.entity.User; +import com.back.domain.user.repository.UserRepository; +import com.back.global.exception.CustomException; +import com.back.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class PostService { + private final PostRepository postRepository; + private final UserRepository userRepository; + private final PostCategoryRepository postCategoryRepository; + + /** + * 게시글 생성 서비스 + * 1. User 조회 + * 2. Post 생성 + * 3. Category 매핑 + * 4. Post 저장 및 PostResponse 반환 + */ + public PostResponse createPost(PostRequest request, Long userId) { + + // User 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); + + // Post 생성 + Post post = new Post(user, request.title(), request.content()); + + // Category 매핑 + if (request.categoryIds() != null) { + List categories = postCategoryRepository.findAllById(request.categoryIds()); + if (categories.size() != request.categoryIds().size()) { + throw new CustomException(ErrorCode.CATEGORY_NOT_FOUND); + } + post.updateCategories(categories); + } + + // Post 저장 및 응답 반환 + Post saved = postRepository.save(post); + return PostResponse.from(saved); + } +} \ No newline at end of file diff --git a/src/main/java/com/back/global/exception/ErrorCode.java b/src/main/java/com/back/global/exception/ErrorCode.java index a948a8b6..8f16e7e7 100644 --- a/src/main/java/com/back/global/exception/ErrorCode.java +++ b/src/main/java/com/back/global/exception/ErrorCode.java @@ -77,6 +77,11 @@ public enum ErrorCode { WS_INTERNAL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "WS_015", "WebSocket 내부 오류가 발생했습니다."), WS_CHAT_DELETE_FORBIDDEN(HttpStatus.FORBIDDEN, "WS_016", "채팅 삭제 권한이 없습니다. 방장 또는 부방장만 가능합니다."), + // ======================== 커뮤니티 관련 ======================== + POST_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_001", "존재하지 않는 게시글입니다."), + POST_NO_PERMISSION(HttpStatus.FORBIDDEN, "POST_002", "게시글 작성자만 수정/삭제할 수 있습니다."), + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "POST_003", "존재하지 않는 카테고리입니다."), + // ======================== 공통 에러 ======================== BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON_400", "잘못된 요청입니다."), FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON_403", "접근 권한이 없습니다."), From a5111985a1f8d886a9c33d1c23e97e077401a971 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:01:22 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/board/entity/PostCategory.java | 7 + .../board/controller/PostControllerTest.java | 171 ++++++++++++++++++ .../domain/board/service/PostServiceTest.java | 98 ++++++++++ 3 files changed, 276 insertions(+) create mode 100644 src/test/java/com/back/domain/board/controller/PostControllerTest.java create mode 100644 src/test/java/com/back/domain/board/service/PostServiceTest.java diff --git a/src/main/java/com/back/domain/board/entity/PostCategory.java b/src/main/java/com/back/domain/board/entity/PostCategory.java index a99cef4f..b47332df 100644 --- a/src/main/java/com/back/domain/board/entity/PostCategory.java +++ b/src/main/java/com/back/domain/board/entity/PostCategory.java @@ -7,6 +7,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; import java.util.List; @Entity @@ -17,4 +18,10 @@ public class PostCategory extends BaseEntity { @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true) private List postCategoryMappings; + + // -------------------- 생성자 -------------------- + public PostCategory(String name) { + this.name = name; + this.postCategoryMappings = new ArrayList<>(); + } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/board/controller/PostControllerTest.java b/src/test/java/com/back/domain/board/controller/PostControllerTest.java new file mode 100644 index 00000000..736903ab --- /dev/null +++ b/src/test/java/com/back/domain/board/controller/PostControllerTest.java @@ -0,0 +1,171 @@ +package com.back.domain.board.controller; + +import com.back.domain.board.dto.PostRequest; +import com.back.domain.board.entity.PostCategory; +import com.back.domain.board.repository.PostCategoryRepository; +import com.back.domain.user.entity.User; +import com.back.domain.user.entity.UserProfile; +import com.back.domain.user.entity.UserStatus; +import com.back.domain.user.repository.UserRepository; +import com.back.fixture.TestJwtTokenProvider; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +@Transactional +class PostControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostCategoryRepository postCategoryRepository; + + @Autowired + private TestJwtTokenProvider testJwtTokenProvider; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private ObjectMapper objectMapper; + + private String generateAccessToken(User user) { + return testJwtTokenProvider.createAccessToken(user.getId(), user.getUsername(), user.getRole().name()); + } + + // ====================== 게시글 생성 테스트 ====================== + + @Test + @DisplayName("게시글 생성 성공 → 201 Created") + void createPost_success() throws Exception { + // given: 정상 유저 생성 + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + // 카테고리 등록 + PostCategory c1 = new PostCategory("공지사항"); + postCategoryRepository.save(c1); + + PostCategory c2 = new PostCategory("자유게시판"); + postCategoryRepository.save(c2); + + PostRequest request = new PostRequest("첫 번째 게시글", "안녕하세요, 첫 글입니다!", List.of(c1.getId(), c2.getId())); + + // when + ResultActions resultActions = mvc.perform( + post("/api/posts") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andDo(print()); + + // then + resultActions + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.data.title").value("첫 번째 게시글")) + .andExpect(jsonPath("$.data.author.nickname").value("홍길동")) + .andExpect(jsonPath("$.data.categories.length()").value(2)); + } + + @Test + @DisplayName("존재하지 않는 사용자 → 404 Not Found") + void createPost_userNotFound() throws Exception { + // given: 토큰만 발급(실제 DB엔 없음) + String fakeToken = testJwtTokenProvider.createAccessToken(999L, "ghost", "USER"); + + PostRequest request = new PostRequest("제목", "내용", null); + + // when & then + mvc.perform(post("/api/posts") + .header("Authorization", "Bearer " + fakeToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("USER_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 사용자입니다.")); + } + + @Test + @DisplayName("존재하지 않는 카테고리 → 404 Not Found") + void createPost_categoryNotFound() throws Exception { + // given: 정상 유저 + User user = User.createUser("writer2", "writer2@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + // 존재하지 않는 카테고리 ID + PostRequest request = new PostRequest("제목", "내용", List.of(999L)); + + // when & then + mvc.perform(post("/api/posts") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("POST_003")) + .andExpect(jsonPath("$.message").value("존재하지 않는 카테고리입니다.")); + } + + @Test + @DisplayName("잘못된 요청(필드 누락) → 400 Bad Request") + void createPost_badRequest() throws Exception { + // given: 정상 유저 생성 + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "홍길동", null, "소개글", LocalDate.of(2000, 1, 1), 1000)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + // given: title 누락 + String invalidJson = """ + { + "content": "본문만 있음" + } + """; + + // when & then + mvc.perform(post("/api/posts") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(invalidJson)) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value("COMMON_400")) + .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); + } +} diff --git a/src/test/java/com/back/domain/board/service/PostServiceTest.java b/src/test/java/com/back/domain/board/service/PostServiceTest.java new file mode 100644 index 00000000..deb96ae4 --- /dev/null +++ b/src/test/java/com/back/domain/board/service/PostServiceTest.java @@ -0,0 +1,98 @@ +package com.back.domain.board.service; + +import com.back.domain.board.dto.PostRequest; +import com.back.domain.board.dto.PostResponse; +import com.back.domain.board.entity.PostCategory; +import com.back.domain.board.repository.PostCategoryRepository; +import com.back.domain.board.repository.PostRepository; +import com.back.domain.user.entity.User; +import com.back.domain.user.entity.UserProfile; +import com.back.domain.user.entity.UserStatus; +import com.back.domain.user.repository.UserRepository; +import com.back.global.exception.CustomException; +import com.back.global.exception.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +class PostServiceTest { + + @Autowired + private PostService postService; + + @Autowired + private PostRepository postRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private PostCategoryRepository postCategoryRepository; + + // ====================== 게시글 생성 테스트 ====================== + + @Test + @DisplayName("게시글 생성 성공 - 카테고리 포함") + void createPost_success_withCategories() { + // given: 유저 + 카테고리 저장 + User user = User.createUser("writer", "writer@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + PostCategory category = new PostCategory("공지"); + postCategoryRepository.save(category); + + PostRequest request = new PostRequest("제목", "내용", List.of(category.getId())); + + // when + PostResponse response = postService.createPost(request, user.getId()); + + // then + assertThat(response.title()).isEqualTo("제목"); + assertThat(response.content()).isEqualTo("내용"); + assertThat(response.author().nickname()).isEqualTo("작성자"); + assertThat(response.categories()).hasSize(1); + assertThat(response.categories().get(0).name()).isEqualTo("공지"); + } + + @Test + @DisplayName("게시글 생성 실패 - 존재하지 않는 유저") + void createPost_fail_userNotFound() { + // given + PostRequest request = new PostRequest("제목", "내용", null); + + // when & then + assertThatThrownBy(() -> postService.createPost(request, 999L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("게시글 생성 실패 - 존재하지 않는 카테고리 ID 포함") + void createPost_fail_categoryNotFound() { + // given: 유저는 정상 + User user = User.createUser("writer2", "writer2@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자2", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + // 실제 저장 안 된 카테고리 ID 요청 + PostRequest request = new PostRequest("제목", "내용", List.of(100L, 200L)); + + // when & then + assertThatThrownBy(() -> postService.createPost(request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.CATEGORY_NOT_FOUND.getMessage()); + } +} From c972452fe619867ab239eefe7a3e17498516ae7d Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:04:25 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Docs:=20Swagger=20=EB=AC=B8=EC=84=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/controller/PostController.java | 2 +- .../board/controller/PostControllerDocs.java | 147 ++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/back/domain/board/controller/PostControllerDocs.java diff --git a/src/main/java/com/back/domain/board/controller/PostController.java b/src/main/java/com/back/domain/board/controller/PostController.java index 74cc3d5d..0e7400d4 100644 --- a/src/main/java/com/back/domain/board/controller/PostController.java +++ b/src/main/java/com/back/domain/board/controller/PostController.java @@ -15,7 +15,7 @@ @RestController @RequestMapping("/api/posts") @RequiredArgsConstructor -public class PostController { +public class PostController implements PostControllerDocs { private final PostService postService; // 게시글 생성 diff --git a/src/main/java/com/back/domain/board/controller/PostControllerDocs.java b/src/main/java/com/back/domain/board/controller/PostControllerDocs.java new file mode 100644 index 00000000..4ce1eb44 --- /dev/null +++ b/src/main/java/com/back/domain/board/controller/PostControllerDocs.java @@ -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> createPost( + @RequestBody PostRequest request, + @AuthenticationPrincipal CustomUserDetails user + ); +}