From c3f2572251863282ee0d57ec67757b861739d1ea Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:06:02 +0900 Subject: [PATCH 1/6] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=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 | 16 ++++++++++ .../com/back/domain/board/entity/Post.java | 6 ++++ .../domain/board/service/PostService.java | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+) 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 c4afc0bc..f5c2d440 100644 --- a/src/main/java/com/back/domain/board/controller/PostController.java +++ b/src/main/java/com/back/domain/board/controller/PostController.java @@ -65,4 +65,20 @@ public ResponseEntity> getPost( response )); } + + // 게시글 수정 + @PutMapping("/{postId}") + public ResponseEntity> updatePost( + @PathVariable Long postId, + @RequestBody @Valid PostRequest request, + @AuthenticationPrincipal CustomUserDetails user + ) { + PostResponse response = postService.updatePost(postId, request, user.getUserId()); + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success( + "게시글이 수정되었습니다.", + response + )); + } } \ 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 dac6f47a..a64297d5 100644 --- a/src/main/java/com/back/domain/board/entity/Post.java +++ b/src/main/java/com/back/domain/board/entity/Post.java @@ -41,6 +41,12 @@ public Post(User user, String title, String content) { } // -------------------- 비즈니스 메서드 -------------------- + // 게시글 업데이트 + public void update(String title, String content) { + this.title = title; + this.content = content; + } + // 카테고리 업데이트 public void updateCategories(List categories) { this.postCategoryMappings.clear(); diff --git a/src/main/java/com/back/domain/board/service/PostService.java b/src/main/java/com/back/domain/board/service/PostService.java index 6f464448..c2b9ebc4 100644 --- a/src/main/java/com/back/domain/board/service/PostService.java +++ b/src/main/java/com/back/domain/board/service/PostService.java @@ -80,4 +80,35 @@ public PostDetailResponse getPost(Long postId) { // 응답 반환 return PostDetailResponse.from(post); } + + /** + * 게시글 수정 서비스 + * 1. Post 조회 + * 2. 작성자 검증 + * 3. Post 업데이트 (제목, 내용, 카테고리) + * 4. PostResponse 반환 + */ + public PostResponse updatePost(Long postId, PostRequest request, Long userId) { + // Post 조회 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + // 작성자 검증 + if (!post.getUser().getId().equals(userId)) { + throw new CustomException(ErrorCode.POST_NO_PERMISSION); + } + + // Post 업데이트 + post.update(request.title(), request.content()); + + // Category 매핑 업데이트 + List categories = postCategoryRepository.findAllById(request.categoryIds()); + if (categories.size() != request.categoryIds().size()) { + throw new CustomException(ErrorCode.CATEGORY_NOT_FOUND); + } + post.updateCategories(categories); + + // 응답 반환 + return PostResponse.from(post); + } } \ No newline at end of file From dcd8c451fd9d820d611894e7dad07a448ccfbe1a Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:17:46 +0900 Subject: [PATCH 2/6] =?UTF-8?q?FeaTest:=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 --- .../board/controller/PostControllerTest.java | 196 +++++++++++++++++- .../domain/board/service/PostServiceTest.java | 105 +++++++++- 2 files changed, 297 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/back/domain/board/controller/PostControllerTest.java b/src/test/java/com/back/domain/board/controller/PostControllerTest.java index 2b492959..cbfa5736 100644 --- a/src/test/java/com/back/domain/board/controller/PostControllerTest.java +++ b/src/test/java/com/back/domain/board/controller/PostControllerTest.java @@ -102,7 +102,7 @@ void createPost_success() throws Exception { } @Test - @DisplayName("존재하지 않는 사용자 → 404 Not Found") + @DisplayName("게시글 생성 실패 - 존재하지 않는 사용자 → 404 Not Found") void createPost_userNotFound() throws Exception { // given: 토큰만 발급(실제 DB엔 없음) String fakeToken = testJwtTokenProvider.createAccessToken(999L, "ghost", "USER"); @@ -121,7 +121,7 @@ void createPost_userNotFound() throws Exception { } @Test - @DisplayName("존재하지 않는 카테고리 → 404 Not Found") + @DisplayName("게시글 생성 실패 - 존재하지 않는 카테고리 → 404 Not Found") void createPost_categoryNotFound() throws Exception { // given: 정상 유저 User user = User.createUser("writer2", "writer2@example.com", passwordEncoder.encode("P@ssw0rd!")); @@ -146,7 +146,7 @@ void createPost_categoryNotFound() throws Exception { } @Test - @DisplayName("잘못된 요청(필드 누락) → 400 Bad Request") + @DisplayName("게시글 생성 실패 - 잘못된 요청(필드 누락) → 400 Bad Request") void createPost_badRequest() throws Exception { // given: 정상 유저 생성 User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); @@ -174,6 +174,22 @@ void createPost_badRequest() throws Exception { .andExpect(jsonPath("$.message").value("잘못된 요청입니다.")); } + @Test + @DisplayName("게시글 생성 실패 - 토큰 없음 → 401 Unauthorized") + void createPost_noToken() throws Exception { + // given + PostRequest request = new PostRequest("제목", "내용", null); + + // when & then + mvc.perform(post("/api/posts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_001")) + .andExpect(jsonPath("$.message").value("인증이 필요합니다.")); + } + // ====================== 게시글 조회 테스트 ====================== @Test @@ -251,4 +267,178 @@ void getPost_fail_notFound() throws Exception { .andExpect(jsonPath("$.code").value("POST_001")) .andExpect(jsonPath("$.message").value("존재하지 않는 게시글입니다.")); } + + // ====================== 게시글 수정 테스트 ====================== + + @Test + @DisplayName("게시글 수정 성공 → 200 OK") + void updatePost_success() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "홍길동", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + PostCategory c1 = new PostCategory("공지사항"); + postCategoryRepository.save(c1); + + Post post = new Post(user, "원래 제목", "원래 내용"); + post.updateCategories(List.of(c1)); + postRepository.save(post); + + String accessToken = generateAccessToken(user); + + PostCategory c2 = new PostCategory("자유게시판"); + postCategoryRepository.save(c2); + + PostRequest request = new PostRequest("수정된 게시글", "안녕하세요, 수정했습니다!", List.of(c1.getId(), c2.getId())); + + // when & then + mvc.perform(put("/api/posts/{postId}", post.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.data.title").value("수정된 게시글")) + .andExpect(jsonPath("$.data.categories.length()").value(2)); + } + + @Test + @DisplayName("게시글 수정 실패 - 게시글 없음 → 404 Not Found") + void updatePost_fail_notFound() throws Exception { + // given + User user = User.createUser("writer2", "writer2@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "작성자2", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + PostRequest request = new PostRequest("수정된 제목", "내용", List.of()); + + // when & then + mvc.perform(put("/api/posts/{postId}", 999L) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("POST_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 게시글입니다.")); + } + + @Test + @DisplayName("게시글 수정 실패 - 작성자 아님 → 403 Forbidden") + void updatePost_fail_noPermission() throws Exception { + // given + User writer = User.createUser("writer3", "writer3@example.com", passwordEncoder.encode("P@ssw0rd!")); + writer.setUserProfile(new UserProfile(writer, "작성자3", null, null, null, 0)); + writer.setUserStatus(UserStatus.ACTIVE); + userRepository.save(writer); + + User another = User.createUser("other", "other@example.com", passwordEncoder.encode("P@ssw0rd!")); + another.setUserProfile(new UserProfile(another, "다른사람", null, null, null, 0)); + another.setUserStatus(UserStatus.ACTIVE); + userRepository.save(another); + + PostCategory c1 = new PostCategory("공지사항"); + postCategoryRepository.save(c1); + + Post post = new Post(writer, "원래 제목", "원래 내용"); + post.updateCategories(List.of(c1)); + postRepository.save(post); + + String accessToken = generateAccessToken(another); + + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", List.of(c1.getId())); + + // when & then + mvc.perform(put("/api/posts/{postId}", post.getId()) + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("POST_002")) + .andExpect(jsonPath("$.message").value("게시글 작성자만 수정/삭제할 수 있습니다.")); + } + + @Test + @DisplayName("게시글 수정 실패 - 존재하지 않는 카테고리 → 404 Not Found") + void updatePost_fail_categoryNotFound() throws Exception { + // given + User user = User.createUser("writer4", "writer4@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "작성자4", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + PostCategory c1 = new PostCategory("공지사항"); + postCategoryRepository.save(c1); + + Post post = new Post(user, "원래 제목", "원래 내용"); + post.updateCategories(List.of(c1)); + postRepository.save(post); + + String accessToken = generateAccessToken(user); + + // 존재하지 않는 카테고리 ID + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", List.of(999L)); + + // when & then + mvc.perform(put("/api/posts/{postId}", post.getId()) + .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 updatePost_fail_badRequest() throws Exception { + // given + User user = User.createUser("writer5", "writer5@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "작성자5", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + String accessToken = generateAccessToken(user); + + String invalidJson = """ + { + "content": "본문만 있음" + } + """; + + // when & then + mvc.perform(put("/api/posts/{postId}", 1L) + .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("잘못된 요청입니다.")); + } + + @Test + @DisplayName("게시글 수정 실패 - 인증 없음 → 401 Unauthorized") + void updatePost_fail_unauthorized() throws Exception { + // given + PostRequest request = new PostRequest("제목", "내용", List.of()); + + // when & then + mvc.perform(put("/api/posts/{postId}", 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_001")) + .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 index 41c49fd9..e276505c 100644 --- a/src/test/java/com/back/domain/board/service/PostServiceTest.java +++ b/src/test/java/com/back/domain/board/service/PostServiceTest.java @@ -66,7 +66,7 @@ void createPost_success_withCategories() { assertThat(response.content()).isEqualTo("내용"); assertThat(response.author().nickname()).isEqualTo("작성자"); assertThat(response.categories()).hasSize(1); - assertThat(response.categories().get(0).name()).isEqualTo("공지"); + assertThat(response.categories().getFirst().name()).isEqualTo("공지"); } @Test @@ -172,4 +172,107 @@ void getPost_fail_postNotFound() { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.POST_NOT_FOUND.getMessage()); } + + // ====================== 게시글 수정 테스트 ====================== + + @Test + @DisplayName("게시글 수정 성공 - 작성자 본인") + void updatePost_success() { + // 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 oldCategory = new PostCategory("공지"); + PostCategory newCategory = new PostCategory("자유"); + postCategoryRepository.saveAll(List.of(oldCategory, newCategory)); + + Post post = new Post(user, "원래 제목", "원래 내용"); + post.updateCategories(List.of(oldCategory)); + postRepository.save(post); + + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", List.of(newCategory.getId())); + + // when + PostResponse response = postService.updatePost(post.getId(), request, user.getId()); + + // then + assertThat(response.title()).isEqualTo("수정된 제목"); + assertThat(response.content()).isEqualTo("수정된 내용"); + assertThat(response.categories()).hasSize(1); + assertThat(response.categories().getFirst().name()).isEqualTo("자유"); + } + + @Test + @DisplayName("게시글 수정 실패 - 게시글 없음") + void updatePost_fail_postNotFound() { + // 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); + + PostRequest request = new PostRequest("제목", "내용", List.of()); + + // when & then + assertThatThrownBy(() -> postService.updatePost(999L, request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.POST_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("게시글 수정 실패 - 작성자 아님") + void updatePost_fail_noPermission() { + // given: 게시글 작성자 + User writer = User.createUser("writer3", "writer3@example.com", "encodedPwd"); + writer.setUserProfile(new UserProfile(writer, "작성자3", null, null, null, 0)); + writer.setUserStatus(UserStatus.ACTIVE); + userRepository.save(writer); + + // 다른 사용자 + User another = User.createUser("other", "other@example.com", "encodedPwd"); + another.setUserProfile(new UserProfile(another, "다른사람", null, null, null, 0)); + another.setUserStatus(UserStatus.ACTIVE); + userRepository.save(another); + + PostCategory category = new PostCategory("공지"); + postCategoryRepository.save(category); + + Post post = new Post(writer, "원래 제목", "원래 내용"); + post.updateCategories(List.of(category)); + postRepository.save(post); + + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", List.of(category.getId())); + + // when & then + assertThatThrownBy(() -> postService.updatePost(post.getId(), request, another.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.POST_NO_PERMISSION.getMessage()); + } + + @Test + @DisplayName("게시글 수정 실패 - 존재하지 않는 카테고리 포함") + void updatePost_fail_categoryNotFound() { + // given + User user = User.createUser("writer4", "writer4@example.com", "encodedPwd"); + user.setUserProfile(new UserProfile(user, "작성자4", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + PostCategory category = new PostCategory("공지"); + postCategoryRepository.save(category); + + Post post = new Post(user, "원래 제목", "원래 내용"); + post.updateCategories(List.of(category)); + postRepository.save(post); + + // 실제 DB에는 없는 카테고리 ID 전달 + PostRequest request = new PostRequest("수정된 제목", "수정된 내용", List.of(999L)); + + // when & then + assertThatThrownBy(() -> postService.updatePost(post.getId(), request, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.CATEGORY_NOT_FOUND.getMessage()); + } } From 82d03791aacef4ff8cb2b107ef90835a0aa2deaf Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:21:23 +0900 Subject: [PATCH 3/6] =?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/PostControllerDocs.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/src/main/java/com/back/domain/board/controller/PostControllerDocs.java b/src/main/java/com/back/domain/board/controller/PostControllerDocs.java index 0f09c418..ad7eb2d4 100644 --- a/src/main/java/com/back/domain/board/controller/PostControllerDocs.java +++ b/src/main/java/com/back/domain/board/controller/PostControllerDocs.java @@ -294,4 +294,148 @@ ResponseEntity>> getPosts( ResponseEntity> getPost( @PathVariable Long postId ); + + @Operation( + summary = "게시글 수정", + description = "로그인한 사용자가 자신의 게시글을 수정합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + 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 = "403", + description = "권한 없음 (작성자 아님)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "POST_002", + "message": "게시글 작성자만 수정/삭제할 수 있습니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글 또는 카테고리", + content = @Content( + mediaType = "application/json", + examples = { + @ExampleObject(name = "존재하지 않는 게시글", value = """ + { + "success": false, + "code": "POST_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> updatePost( + @PathVariable Long postId, + @RequestBody PostRequest request, + @AuthenticationPrincipal CustomUserDetails user + ); } From f2f39259a98fbff64a06bb9f488250c4d706d545 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:36:05 +0900 Subject: [PATCH 4/6] =?UTF-8?q?Feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=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 | 15 ++++++++++++++ .../domain/board/service/PostService.java | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) 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 f5c2d440..fa4c71cf 100644 --- a/src/main/java/com/back/domain/board/controller/PostController.java +++ b/src/main/java/com/back/domain/board/controller/PostController.java @@ -81,4 +81,19 @@ public ResponseEntity> updatePost( response )); } + + // 게시글 삭제 + @DeleteMapping("/{postId}") + public ResponseEntity> deletePost( + @PathVariable Long postId, + @AuthenticationPrincipal CustomUserDetails user + ) { + postService.deletePost(postId, user.getUserId()); + return ResponseEntity + .status(HttpStatus.OK) + .body(RsData.success( + "게시글이 삭제되었습니다.", + null + )); + } } \ No newline at end of file diff --git a/src/main/java/com/back/domain/board/service/PostService.java b/src/main/java/com/back/domain/board/service/PostService.java index c2b9ebc4..79f0bbec 100644 --- a/src/main/java/com/back/domain/board/service/PostService.java +++ b/src/main/java/com/back/domain/board/service/PostService.java @@ -111,4 +111,24 @@ public PostResponse updatePost(Long postId, PostRequest request, Long userId) { // 응답 반환 return PostResponse.from(post); } + + /** + * 게시글 삭제 서비스 + * 1. Post 조회 + * 2. 작성자 검증 + * 3. Post 삭제 + */ + public void deletePost(Long postId, Long userId) { + // Post 조회 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new CustomException(ErrorCode.POST_NOT_FOUND)); + + // 작성자 검증 + if (!post.getUser().getId().equals(userId)) { + throw new CustomException(ErrorCode.POST_NO_PERMISSION); + } + + // Post 삭제 + postRepository.delete(post); + } } \ No newline at end of file From 40fb4ddcd845378fba8a188f5eaea347258555f4 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:40:47 +0900 Subject: [PATCH 5/6] =?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 --- .../board/controller/PostControllerTest.java | 89 +++++++++++++++++++ .../domain/board/service/PostServiceTest.java | 59 ++++++++++++ 2 files changed, 148 insertions(+) diff --git a/src/test/java/com/back/domain/board/controller/PostControllerTest.java b/src/test/java/com/back/domain/board/controller/PostControllerTest.java index cbfa5736..475d25bb 100644 --- a/src/test/java/com/back/domain/board/controller/PostControllerTest.java +++ b/src/test/java/com/back/domain/board/controller/PostControllerTest.java @@ -441,4 +441,93 @@ void updatePost_fail_unauthorized() throws Exception { .andExpect(jsonPath("$.code").value("AUTH_001")) .andExpect(jsonPath("$.message").value("인증이 필요합니다.")); } + + // ====================== 게시글 삭제 테스트 ====================== + + @Test + @DisplayName("게시글 삭제 성공 → 200 OK") + void deletePost_success() throws Exception { + // given + User user = User.createUser("writer", "writer@example.com", passwordEncoder.encode("P@ssw0rd!")); + user.setUserProfile(new UserProfile(user, "홍길동", null, null, null, 0)); + user.setUserStatus(UserStatus.ACTIVE); + userRepository.save(user); + + Post post = new Post(user, "삭제할 제목", "삭제할 내용"); + postRepository.save(post); + + String accessToken = generateAccessToken(user); + + // when & then + mvc.perform(delete("/api/posts/{postId}", post.getId()) + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.code").value("SUCCESS_200")) + .andExpect(jsonPath("$.message").value("게시글이 삭제되었습니다.")) + .andExpect(jsonPath("$.data").doesNotExist()); + } + + @Test + @DisplayName("게시글 삭제 실패 - 게시글 없음 → 404 Not Found") + void deletePost_fail_postNotFound() 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); + + // when & then + mvc.perform(delete("/api/posts/{postId}", 999L) + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.code").value("POST_001")) + .andExpect(jsonPath("$.message").value("존재하지 않는 게시글입니다.")); + } + + @Test + @DisplayName("게시글 삭제 실패 - 작성자 아님 → 403 Forbidden") + void deletePost_fail_noPermission() throws Exception { + // given + User writer = User.createUser("writer3", "writer3@example.com", passwordEncoder.encode("P@ssw0rd!")); + writer.setUserProfile(new UserProfile(writer, "작성자3", null, null, null, 0)); + writer.setUserStatus(UserStatus.ACTIVE); + userRepository.save(writer); + + User another = User.createUser("other", "other@example.com", passwordEncoder.encode("P@ssw0rd!")); + another.setUserProfile(new UserProfile(another, "다른사람", null, null, null, 0)); + another.setUserStatus(UserStatus.ACTIVE); + userRepository.save(another); + + Post post = new Post(writer, "원래 제목", "원래 내용"); + postRepository.save(post); + + String accessToken = generateAccessToken(another); + + // when & then + mvc.perform(delete("/api/posts/{postId}", post.getId()) + .header("Authorization", "Bearer " + accessToken)) + .andDo(print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value("POST_002")) + .andExpect(jsonPath("$.message").value("게시글 작성자만 수정/삭제할 수 있습니다.")); + } + + @Test + @DisplayName("게시글 삭제 실패 - 인증 없음 → 401 Unauthorized") + void deletePost_fail_unauthorized() throws Exception { + // given + Post post = new Post(); // 굳이 저장 안 해도 됨, 그냥 요청만 보냄 + + // when & then + mvc.perform(delete("/api/posts/{postId}", 1L)) + .andDo(print()) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value("AUTH_001")) + .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 index e276505c..ec76332c 100644 --- a/src/test/java/com/back/domain/board/service/PostServiceTest.java +++ b/src/test/java/com/back/domain/board/service/PostServiceTest.java @@ -275,4 +275,63 @@ void updatePost_fail_categoryNotFound() { .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.CATEGORY_NOT_FOUND.getMessage()); } + + // ====================== 게시글 삭제 테스트 ====================== + + @Test + @DisplayName("게시글 삭제 성공 - 작성자 본인") + void deletePost_success() { + // 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); + + Post post = new Post(user, "삭제 대상 제목", "삭제 대상 내용"); + postRepository.save(post); + + // when + postService.deletePost(post.getId(), user.getId()); + + // then + assertThat(postRepository.findById(post.getId())).isEmpty(); + } + + @Test + @DisplayName("게시글 삭제 실패 - 게시글 없음") + void deletePost_fail_postNotFound() { + // 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); + + // when & then + assertThatThrownBy(() -> postService.deletePost(999L, user.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.POST_NOT_FOUND.getMessage()); + } + + @Test + @DisplayName("게시글 삭제 실패 - 작성자 아님") + void deletePost_fail_noPermission() { + // given + User writer = User.createUser("writer3", "writer3@example.com", "encodedPwd"); + writer.setUserProfile(new UserProfile(writer, "작성자3", null, null, null, 0)); + writer.setUserStatus(UserStatus.ACTIVE); + userRepository.save(writer); + + User another = User.createUser("other", "other@example.com", "encodedPwd"); + another.setUserProfile(new UserProfile(another, "다른사람", null, null, null, 0)); + another.setUserStatus(UserStatus.ACTIVE); + userRepository.save(another); + + Post post = new Post(writer, "원래 제목", "원래 내용"); + postRepository.save(post); + + // when & then + assertThatThrownBy(() -> postService.deletePost(post.getId(), another.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.POST_NO_PERMISSION.getMessage()); + } } From 2b3d63043ba677e918bd39e27553a49f3a3bbd42 Mon Sep 17 00:00:00 2001 From: joyewon0705 <77885098+joyewon0705@users.noreply.github.com> Date: Thu, 2 Oct 2025 11:41:24 +0900 Subject: [PATCH 6/6] =?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/PostControllerDocs.java | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/src/main/java/com/back/domain/board/controller/PostControllerDocs.java b/src/main/java/com/back/domain/board/controller/PostControllerDocs.java index ad7eb2d4..16dcd241 100644 --- a/src/main/java/com/back/domain/board/controller/PostControllerDocs.java +++ b/src/main/java/com/back/domain/board/controller/PostControllerDocs.java @@ -438,4 +438,123 @@ ResponseEntity> updatePost( @RequestBody PostRequest request, @AuthenticationPrincipal CustomUserDetails user ); + + @Operation( + summary = "게시글 삭제", + description = "로그인한 사용자가 자신의 게시글을 삭제합니다." + ) + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "게시글 삭제 성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": true, + "code": "SUCCESS_200", + "message": "게시글이 삭제되었습니다.", + "data": null + } + """) + ) + ), + @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 = "403", + description = "권한 없음 (작성자 아님)", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "POST_002", + "message": "게시글 작성자만 수정/삭제할 수 있습니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "404", + description = "존재하지 않는 게시글", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "POST_001", + "message": "존재하지 않는 게시글입니다.", + "data": null + } + """) + ) + ), + @ApiResponse( + responseCode = "500", + description = "서버 내부 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject(value = """ + { + "success": false, + "code": "COMMON_500", + "message": "서버 오류가 발생했습니다.", + "data": null + } + """) + ) + ) + }) + ResponseEntity> deletePost( + @PathVariable Long postId, + @AuthenticationPrincipal CustomUserDetails user + ); }