diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 24fda22..9dbb958 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -101,7 +101,6 @@ jobs: mybatis: mapper-locations: classpath:/bst/bobsoolting/mapper/**/*.xml configuration: - default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler map-underscore-to-camel-case: true type-handlers-package: bst.bobsoolting.util cors: 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..494cf27 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 { @@ -27,10 +31,10 @@ public class PostCommandController implements PostCommandControllerDocs { private final SecurityUtil securityUtil; @PostMapping - public ResponseEntity createPost(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @RequestBody RequestCreatePostVO request) { + public ResponseEntity createPost(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @RequestBody RequestCreatePostVO request) { 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")); PostDTO created = postCommandService.createPost(request, memberId); ResponseCreatePostVO response = postConverter.fromDTOToCreateVO(created); @@ -38,33 +42,40 @@ public ResponseEntity createPost(@RequestHeader(HttpHeader } @PutMapping("/{postId}") - public ResponseEntity updatePost(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @PathVariable Long postId, @RequestBody RequestUpdatePostVO request) { + public ResponseEntity updatePost(@RequestHeader(HttpHeaders.AUTHORIZATION) String token, @PathVariable Long postId, @RequestBody RequestUpdatePostVO request) { 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")); PostDTO updated = postCommandService.updatePost(memberId, postId, request); ResponseUpdatePostVO response = postConverter.fromDTOToUpdateVO(updated); 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..1a39a26 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 @@ -1,6 +1,5 @@ package bst.bobsoolting.post.command.application.controller.docs; -import bst.bobsoolting.post.command.application.dto.PostDTO; 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; @@ -16,6 +15,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 { @@ -27,7 +28,7 @@ public interface PostCommandControllerDocs { @ApiResponse(responseCode = "500", description = "서버 오류") }) @PostMapping - ResponseEntity createPost( + ResponseEntity createPost( @RequestHeader(HttpHeaders.AUTHORIZATION) String token, @RequestBody @io.swagger.v3.oas.annotations.parameters.RequestBody( content = @Content(schema = @Schema(implementation = RequestCreatePostVO.class)) @@ -42,7 +43,7 @@ ResponseEntity createPost( @ApiResponse(responseCode = "500", description = "서버 오류") }) @PutMapping("/{postId}") - ResponseEntity updatePost( + ResponseEntity updatePost( @RequestHeader(HttpHeaders.AUTHORIZATION) String token, @Parameter(description = "게시글 ID", required = true) @PathVariable Long postId, @RequestBody @io.swagger.v3.oas.annotations.parameters.RequestBody( @@ -57,8 +58,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 +72,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 ); 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/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; +} 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/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 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f9ba59c..31922e2 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -73,7 +73,6 @@ logging: mybatis: mapper-locations: classpath:/bst/bobsoolting/mapper/**/*.xml configuration: - default-enum-type-handler: org.apache.ibatis.type.EnumTypeHandler map-underscore-to-camel-case: true type-handlers-package: bst.bobsoolting.util diff --git a/src/main/resources/bst/bobsoolting/mapper/member/MemberMapper.xml b/src/main/resources/bst/bobsoolting/mapper/member/MemberMapper.xml index 2ae86e4..de8e255 100644 --- a/src/main/resources/bst/bobsoolting/mapper/member/MemberMapper.xml +++ b/src/main/resources/bst/bobsoolting/mapper/member/MemberMapper.xml @@ -14,7 +14,7 @@ - + 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