From fb0f7d4d6497c0df677c3b56a75b27e6dde29bfc Mon Sep 17 00:00:00 2001 From: JoJeHuni Date: Mon, 17 Mar 2025 00:51:51 +0900 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20post=20command=20api=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=B0=94=EB=94=94=EC=97=90=20=EB=A9=94=EC=84=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PostCommandController.java | 25 +++++++++++++------ .../docs/PostCommandControllerDocs.java | 8 +++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/main/java/bst/bobsoolting/post/command/application/controller/PostCommandController.java b/src/main/java/bst/bobsoolting/post/command/application/controller/PostCommandController.java index 4b84c80..728fae4 100644 --- a/src/main/java/bst/bobsoolting/post/command/application/controller/PostCommandController.java +++ b/src/main/java/bst/bobsoolting/post/command/application/controller/PostCommandController.java @@ -17,6 +17,10 @@ import org.springframework.http.HttpHeaders; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + @RestController @RequiredArgsConstructor public class PostCommandController implements PostCommandControllerDocs { @@ -48,23 +52,30 @@ public ResponseEntity updatePost(@RequestHeader(HttpHeader return ResponseEntity.ok(response); } - @PatchMapping("/{postId}/status") - public ResponseEntity updateRecruitmentStatus(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @PathVariable("postId") Long postId) { + @PatchMapping("/{postId}/recruit-status") + public ResponseEntity> updateRecruitmentStatus(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @PathVariable("postId") Long postId) { String kakaoId = securityUtil.getKakaoIdFromToken(token.replace("Bearer ", "")); String memberId = memberQueryService.getMemberIdByKakaoId(kakaoId); - if (memberId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); + if (memberId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Collections.singletonMap("message", "Unauthorized access")); postCommandService.updateRecruitmentStatus(memberId, postId); - return ResponseEntity.noContent().build(); + + Map response = new HashMap<>(); + response.put("message", "모집 상태가 변경되었습니다."); + + return ResponseEntity.ok(response); } @PatchMapping("/{postId}") - public ResponseEntity softDeletePost(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @PathVariable("postId") Long postId) { + public ResponseEntity> softDeletePost(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @PathVariable("postId") Long postId) { String kakaoId = securityUtil.getKakaoIdFromToken(token.replace("Bearer ", "")); String memberId = memberQueryService.getMemberIdByKakaoId(kakaoId); - if (memberId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); + if (memberId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Collections.singletonMap("message", "Unauthorized access")); postCommandService.deletePost(memberId, postId); - return ResponseEntity.noContent().build(); + Map response = new HashMap<>(); + response.put("message", "해당 게시글이 삭제되었습니다."); + + return ResponseEntity.ok(response); } } \ No newline at end of file diff --git a/src/main/java/bst/bobsoolting/post/command/application/controller/docs/PostCommandControllerDocs.java b/src/main/java/bst/bobsoolting/post/command/application/controller/docs/PostCommandControllerDocs.java index 5c74004..dbd2fa1 100644 --- a/src/main/java/bst/bobsoolting/post/command/application/controller/docs/PostCommandControllerDocs.java +++ b/src/main/java/bst/bobsoolting/post/command/application/controller/docs/PostCommandControllerDocs.java @@ -16,6 +16,8 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.Map; + @Tag(name = "게시글 API", description = "게시글 생성, 수정, 삭제 관련 API") @RequestMapping("/api/post") public interface PostCommandControllerDocs { @@ -57,8 +59,8 @@ ResponseEntity updatePost( @ApiResponse(responseCode = "403", description = "변경 권한 없음"), @ApiResponse(responseCode = "500", description = "서버 오류") }) - @PatchMapping("/{postId}/status") - ResponseEntity updateRecruitmentStatus( + @PatchMapping("/{postId}/recruit- status") + ResponseEntity> updateRecruitmentStatus( @RequestHeader(HttpHeaders.AUTHORIZATION) String token, @Parameter(description = "게시글 ID", required = true) @PathVariable("postId") Long postId ); @@ -71,7 +73,7 @@ ResponseEntity updateRecruitmentStatus( @ApiResponse(responseCode = "500", description = "서버 오류") }) @PatchMapping("/{postId}") - ResponseEntity softDeletePost( + ResponseEntity> softDeletePost( @RequestHeader(HttpHeaders.AUTHORIZATION) String token, // ✅ Authorization 헤더 추가 @Parameter(description = "게시글 ID", required = true) @PathVariable("postId") Long postId ); From 30ba193466c54c047a7e8cfbaaa18aa0e7f9dbee Mon Sep 17 00:00:00 2001 From: JoJeHuni Date: Mon, 17 Mar 2025 00:52:02 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20post=20=EC=A1=B0=ED=9A=8C=EC=9A=A9?= =?UTF-8?q?=20VO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/vo/response/ResponsePostVO.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/java/bst/bobsoolting/post/command/domain/vo/response/ResponsePostVO.java diff --git a/src/main/java/bst/bobsoolting/post/command/domain/vo/response/ResponsePostVO.java b/src/main/java/bst/bobsoolting/post/command/domain/vo/response/ResponsePostVO.java new file mode 100644 index 0000000..4008d74 --- /dev/null +++ b/src/main/java/bst/bobsoolting/post/command/domain/vo/response/ResponsePostVO.java @@ -0,0 +1,52 @@ +package bst.bobsoolting.post.command.domain.vo.response; + +import bst.bobsoolting.post.command.domain.aggregate.Category; +import bst.bobsoolting.post.command.domain.aggregate.RecruitmentStatus; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDate; +import java.util.List; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@ToString +@Schema(name = "ResponsePostVO", description = "게시글 조회 응답 객체") +public class ResponsePostVO { + + @Schema(description = "게시글 ID", example = "1") + @JsonProperty("post_id") + private Long postId; + + @Schema(description = "카테고리", example = "FOOD") + private Category category; + + @Schema(description = "제목", example = "저녁 드실 분!") + private String title; + + @Schema(description = "내용", example = "3월 15일에 저녁 드실 분 구합니다.") + private String content; + + @Schema(description = "최대 참가자 수", example = "5") + @JsonProperty("max_participants") + private Integer maxParticipants; + + @Schema(description = "참여자 목록", example = "[\"user1\", \"user2\"]") + private List participants; + + @Schema(description = "모집 상태", example = "OPEN") + private RecruitmentStatus recruitmentStatus; + + @Schema(description = "활동 날짜", example = "2025-03-15") + private LocalDate date; + + @Schema(description = "장소", example = "서울 강남구") + private String location; + + @Schema(description = "작성자 ID", example = "20250315-UUID") + @JsonProperty("member_id") + private String memberId; +} From 46059b3546bec27737ea9fd0e9b087659656754a Mon Sep 17 00:00:00 2001 From: JoJeHuni Date: Mon, 17 Mar 2025 00:52:19 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C?= =?UTF-8?q?=20import=EB=AC=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/bst/bobsoolting/security/SecurityConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/bst/bobsoolting/security/SecurityConfig.java b/src/main/java/bst/bobsoolting/security/SecurityConfig.java index 6a10180..ff65959 100644 --- a/src/main/java/bst/bobsoolting/security/SecurityConfig.java +++ b/src/main/java/bst/bobsoolting/security/SecurityConfig.java @@ -14,7 +14,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; -import java.util.Collections; @Configuration @EnableWebSecurity From 498bc2d8134c04498dc2775041cfaab446a3e8d1 Mon Sep 17 00:00:00 2001 From: JoJeHuni Date: Mon, 17 Mar 2025 00:52:57 +0900 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20post=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EC=98=88=EC=99=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EC=BF=BC=EB=A6=AC=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mapper/PostConverter.java | 19 ++++++++-- .../query/controller/PostQueryController.java | 35 +++++++++++++------ .../docs/PostQueryControllerDocs.java | 28 +++++---------- .../query/service/PostQueryServiceImpl.java | 10 ++++-- .../bobsoolting/mapper/post/PostMapper.xml | 1 + 5 files changed, 57 insertions(+), 36 deletions(-) diff --git a/src/main/java/bst/bobsoolting/post/command/application/mapper/PostConverter.java b/src/main/java/bst/bobsoolting/post/command/application/mapper/PostConverter.java index 9b583ed..ec28c51 100644 --- a/src/main/java/bst/bobsoolting/post/command/application/mapper/PostConverter.java +++ b/src/main/java/bst/bobsoolting/post/command/application/mapper/PostConverter.java @@ -6,6 +6,7 @@ import bst.bobsoolting.post.command.domain.vo.request.RequestCreatePostVO; import bst.bobsoolting.post.command.domain.vo.request.RequestUpdatePostVO; import bst.bobsoolting.post.command.domain.vo.response.ResponseCreatePostVO; +import bst.bobsoolting.post.command.domain.vo.response.ResponsePostVO; import bst.bobsoolting.post.command.domain.vo.response.ResponseUpdatePostVO; import org.springframework.stereotype.Component; @@ -15,7 +16,6 @@ @Component public class PostConverter { - // DTO -> Entity 변환 public Post fromCreateVOToEntity(RequestCreatePostVO request, String memberId) { if (request == null) return null; @@ -55,7 +55,6 @@ public Post fromUpdateVOToEntity(Post existingPost, RequestUpdatePostVO updateVO .build(); } - // Entity -> DTO 변환 public PostDTO toDTO(Post post) { if (post == null) return null; return PostDTO.builder() @@ -100,4 +99,20 @@ public ResponseUpdatePostVO fromDTOToUpdateVO(PostDTO updated) { .memberId(updated.getMemberId()) .build(); } + + public ResponsePostVO toResponseVO(PostDTO dto) { + if (dto == null) return null; + return ResponsePostVO.builder() + .postId(dto.getPostId()) + .category(dto.getCategory()) + .title(dto.getTitle()) + .content(dto.getContent()) + .maxParticipants(dto.getMaxParticipants()) + .participants(dto.getParticipants()) + .recruitmentStatus(dto.getRecruitmentStatus()) + .date(dto.getDate()) + .location(dto.getLocation()) + .memberId(dto.getMemberId()) + .build(); + } } diff --git a/src/main/java/bst/bobsoolting/post/query/controller/PostQueryController.java b/src/main/java/bst/bobsoolting/post/query/controller/PostQueryController.java index e70379c..2eaa19d 100644 --- a/src/main/java/bst/bobsoolting/post/query/controller/PostQueryController.java +++ b/src/main/java/bst/bobsoolting/post/query/controller/PostQueryController.java @@ -2,6 +2,8 @@ import bst.bobsoolting.member.query.service.MemberQueryService; import bst.bobsoolting.post.command.application.dto.PostDTO; +import bst.bobsoolting.post.command.application.mapper.PostConverter; +import bst.bobsoolting.post.command.domain.vo.response.ResponsePostVO; import bst.bobsoolting.post.query.controller.docs.PostQueryControllerDocs; import bst.bobsoolting.post.query.service.PostQueryService; import bst.bobsoolting.util.SecurityUtil; @@ -12,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; @@ -20,42 +23,52 @@ public class PostQueryController implements PostQueryControllerDocs { private final PostQueryService postQueryService; + private final PostConverter postConverter; private final MemberQueryService memberQueryService; private final SecurityUtil securityUtil; @GetMapping("/{postId}") - public ResponseEntity getPostById(@PathVariable("postId") Long postId) { + public ResponseEntity getPostById(@PathVariable("postId") Long postId) { PostDTO dto = postQueryService.getPostById(postId); - return ResponseEntity.ok(dto); + ResponsePostVO response = postConverter.toResponseVO(dto); + return ResponseEntity.ok(response); } @GetMapping - public ResponseEntity> getAllPosts(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { + public ResponseEntity> getAllPosts(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { PageHelper.startPage(page, size); - List posts = postQueryService.getAllPosts(); + List posts = postQueryService.getAllPosts().stream() + .map(postConverter::toResponseVO) + .collect(Collectors.toList()); return ResponseEntity.ok(new PageInfo<>(posts)); } @GetMapping("/category") - public ResponseEntity> getPostsByCategory(@RequestParam("category") String category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { + public ResponseEntity> getPostsByCategory(@RequestParam("category") String category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { PageHelper.startPage(page, size); - List posts = postQueryService.getPostsByCategory(category); + List posts = postQueryService.getPostsByCategory(category).stream() + .map(postConverter::toResponseVO) + .collect(Collectors.toList()); return ResponseEntity.ok(new PageInfo<>(posts)); } @GetMapping("/search") - public ResponseEntity> searchPosts(@RequestParam("keyword") String keyword) { - List posts = postQueryService.searchPostsByKeyword(keyword); + public ResponseEntity> searchPosts(@RequestParam("keyword") String keyword) { + List posts = postQueryService.searchPostsByKeyword(keyword).stream() + .map(postConverter::toResponseVO) + .collect(Collectors.toList()); return ResponseEntity.ok(posts); } @GetMapping("/member") - public ResponseEntity> getMyPosts(@RequestHeader(HttpHeaders.AUTHORIZATION) String token) { + public ResponseEntity> getMyPosts(@RequestHeader(HttpHeaders.AUTHORIZATION) String token) { String kakaoId = securityUtil.getKakaoIdFromToken(token.replace("Bearer ", "")); String memberId = memberQueryService.getMemberIdByKakaoId(kakaoId); if (memberId == null) return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null); - List posts = postQueryService.getPostsByMemberId(memberId); + + List posts = postQueryService.getPostsByMemberId(memberId).stream() + .map(postConverter::toResponseVO) + .collect(Collectors.toList()); return ResponseEntity.ok(posts); } } - diff --git a/src/main/java/bst/bobsoolting/post/query/controller/docs/PostQueryControllerDocs.java b/src/main/java/bst/bobsoolting/post/query/controller/docs/PostQueryControllerDocs.java index 3e25216..c879760 100644 --- a/src/main/java/bst/bobsoolting/post/query/controller/docs/PostQueryControllerDocs.java +++ b/src/main/java/bst/bobsoolting/post/query/controller/docs/PostQueryControllerDocs.java @@ -1,6 +1,6 @@ package bst.bobsoolting.post.query.controller.docs; -import bst.bobsoolting.post.command.application.dto.PostDTO; +import bst.bobsoolting.post.command.domain.vo.response.ResponsePostVO; import com.github.pagehelper.PageInfo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -21,40 +21,28 @@ public interface PostQueryControllerDocs { @Operation(summary = "게시글 단건 조회", description = "특정 ID를 가진 게시글을 조회합니다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = PostDTO.class))), + @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = ResponsePostVO.class))), @ApiResponse(responseCode = "404", description = "해당 게시글이 존재하지 않음"), @ApiResponse(responseCode = "500", description = "서버 오류") }) @GetMapping("/{postId}") - ResponseEntity getPostById( + ResponseEntity getPostById( @Parameter(description = "게시글 ID", required = true) @PathVariable("postId") Long postId ); @Operation(summary = "전체 게시글 조회 (페이지네이션)", description = "페이지네이션 적용된 모든 게시글을 조회합니다.") @GetMapping - ResponseEntity> getAllPosts(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size); + ResponseEntity> getAllPosts(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size); @Operation(summary = "카테고리별 게시글 조회 (페이지네이션)", description = "특정 카테고리에 속한 게시글을 페이지네이션하여 조회합니다.") @GetMapping("/category") - ResponseEntity> getPostsByCategory(@Parameter(description = "카테고리명", required = true, example = "FOOD") @RequestParam("category") String category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size); + ResponseEntity> getPostsByCategory(@RequestParam("category") String category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size); @Operation(summary = "게시글 검색", description = "키워드를 이용해 게시글을 검색합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "검색 성공", content = @Content(schema = @Schema(implementation = PostDTO.class))), - @ApiResponse(responseCode = "500", description = "서버 오류") - }) @GetMapping("/search") - ResponseEntity> searchPosts( - @Parameter(description = "검색 키워드", required = true, example = "스터디") @RequestParam("keyword") String keyword - ); + ResponseEntity> searchPosts(@RequestParam("keyword") String keyword); @Operation(summary = "내가 작성한 게시글 조회", description = "JWT에서 로그인한 사용자의 memberId를 가져와 해당 사용자의 게시글을 조회합니다.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "조회 성공", content = @Content(schema = @Schema(implementation = PostDTO.class))), - @ApiResponse(responseCode = "500", description = "서버 오류") - }) @GetMapping("/member") - ResponseEntity> getMyPosts( - @RequestHeader(HttpHeaders.AUTHORIZATION) String token - ); -} \ No newline at end of file + ResponseEntity> getMyPosts(@RequestHeader(HttpHeaders.AUTHORIZATION) String token); +} diff --git a/src/main/java/bst/bobsoolting/post/query/service/PostQueryServiceImpl.java b/src/main/java/bst/bobsoolting/post/query/service/PostQueryServiceImpl.java index 04fb4fa..788cadd 100644 --- a/src/main/java/bst/bobsoolting/post/query/service/PostQueryServiceImpl.java +++ b/src/main/java/bst/bobsoolting/post/query/service/PostQueryServiceImpl.java @@ -1,11 +1,15 @@ package bst.bobsoolting.post.query.service; +import bst.bobsoolting.common.exception.CommonException; +import bst.bobsoolting.common.exception.ErrorCode; import bst.bobsoolting.post.command.application.mapper.PostConverter; import bst.bobsoolting.post.command.application.dto.PostDTO; import bst.bobsoolting.post.command.domain.aggregate.entity.Post; import bst.bobsoolting.post.query.repository.PostMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; + +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; @@ -19,6 +23,7 @@ public class PostQueryServiceImpl implements PostQueryService { @Override public PostDTO getPostById(Long postId) { Post post = postMapper.findByPostId(postId); + if (post == null || !post.getPostStatus()) throw new CommonException(ErrorCode.NOT_FOUND_POST); return postConverter.toDTO(post); } @@ -36,10 +41,9 @@ public List getPostsByCategory(String category) { @Override public List searchPostsByKeyword(String keyword) { + if (keyword == null || keyword.trim().isEmpty()) return Collections.emptyList(); List posts = postMapper.searchByKeyword(keyword); - return posts.stream() - .map(postConverter::toDTO) - .collect(Collectors.toList()); + return posts.stream().map(postConverter::toDTO).collect(Collectors.toList()); } @Override diff --git a/src/main/resources/bst/bobsoolting/mapper/post/PostMapper.xml b/src/main/resources/bst/bobsoolting/mapper/post/PostMapper.xml index e116db3..e52dc57 100644 --- a/src/main/resources/bst/bobsoolting/mapper/post/PostMapper.xml +++ b/src/main/resources/bst/bobsoolting/mapper/post/PostMapper.xml @@ -24,6 +24,7 @@ SELECT post_id, category, title, content, max_participants, participants, recruitment_status, date, location, post_status, created_at, updated_at, member_id FROM post WHERE post_id = #{postId} + AND post_status = true