diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/example/bak/comment/application/command/CommentCommandService.java b/src/main/java/com/example/bak/comment/application/command/CommentCommandService.java new file mode 100644 index 0000000..292d635 --- /dev/null +++ b/src/main/java/com/example/bak/comment/application/command/CommentCommandService.java @@ -0,0 +1,52 @@ +package com.example.bak.comment.application.command; + +import com.example.bak.comment.application.command.port.CommentCommandPort; +import com.example.bak.comment.application.command.port.ProfileDataPort; +import com.example.bak.comment.domain.Comment; +import com.example.bak.comment.domain.ProfileSnapShot; +import com.example.bak.feed.application.command.port.FeedValidationPort; +import com.example.bak.global.exception.BusinessException; +import com.example.bak.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class CommentCommandService { + + private final CommentCommandPort commentCommandPort; + private final FeedValidationPort feedValidationPort; + private final ProfileDataPort profileDataPort; + + public void createComment(Long feedId, String content, Long userId) { + if (!feedValidationPort.existsById(feedId)) { + throw new BusinessException(ErrorCode.FEED_NOT_FOUND); + } + + ProfileSnapShot userProfile = profileDataPort.findSnapshotByUserId(userId) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + Comment newComment = Comment.create( + feedId, + content, + userProfile.userId(), + userProfile.nickname() + ); + + commentCommandPort.save(newComment); + } + + public void updateComment(Long commentId, String content, Long userId) { + Comment comment = commentCommandPort.findById(commentId) + .orElseThrow(() -> new BusinessException(ErrorCode.COMMENT_NOT_FOUND)); + + if (!comment.isWrittenBy(userId)) { + throw new BusinessException(ErrorCode.UNAUTHORIZED_ACTION); + } + + comment.updateComment(content); + commentCommandPort.save(comment); + } +} diff --git a/src/main/java/com/example/bak/comment/application/command/port/CommentCommandPort.java b/src/main/java/com/example/bak/comment/application/command/port/CommentCommandPort.java new file mode 100644 index 0000000..3d38b72 --- /dev/null +++ b/src/main/java/com/example/bak/comment/application/command/port/CommentCommandPort.java @@ -0,0 +1,11 @@ +package com.example.bak.comment.application.command.port; + +import com.example.bak.comment.domain.Comment; +import java.util.Optional; + +public interface CommentCommandPort { + + Comment save(Comment comment); + + Optional findById(Long commentId); +} diff --git a/src/main/java/com/example/bak/comment/application/command/port/ProfileDataPort.java b/src/main/java/com/example/bak/comment/application/command/port/ProfileDataPort.java new file mode 100644 index 0000000..3001eda --- /dev/null +++ b/src/main/java/com/example/bak/comment/application/command/port/ProfileDataPort.java @@ -0,0 +1,9 @@ +package com.example.bak.comment.application.command.port; + +import com.example.bak.comment.domain.ProfileSnapShot; +import java.util.Optional; + +public interface ProfileDataPort { + + Optional findSnapshotByUserId(Long userId); +} diff --git a/src/main/java/com/example/bak/comment/application/query/CommentQueryService.java b/src/main/java/com/example/bak/comment/application/query/CommentQueryService.java new file mode 100644 index 0000000..ebd45a6 --- /dev/null +++ b/src/main/java/com/example/bak/comment/application/query/CommentQueryService.java @@ -0,0 +1,20 @@ +package com.example.bak.comment.application.query; + +import com.example.bak.comment.application.query.dto.CommentInfo; +import com.example.bak.comment.application.query.port.CommentQueryPort; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class CommentQueryService { + + private final CommentQueryPort commentQueryPort; + + public List getComments(Long feedId) { + return commentQueryPort.findByFeedId(feedId); + } +} diff --git a/src/main/java/com/example/bak/comment/application/query/dto/CommentInfo.java b/src/main/java/com/example/bak/comment/application/query/dto/CommentInfo.java new file mode 100644 index 0000000..9f5aa1a --- /dev/null +++ b/src/main/java/com/example/bak/comment/application/query/dto/CommentInfo.java @@ -0,0 +1,23 @@ +package com.example.bak.comment.application.query.dto; + +import com.example.bak.comment.domain.Comment; + +/** + * Comment의 기본 정보를 담는 DTO Feed 상세 조회 시 포함됨 + */ +public record CommentInfo( + Long id, + Long authorId, + String authorName, + String content +) { + + public static CommentInfo from(Comment comment) { + return new CommentInfo( + comment.getId(), + comment.getAuthorId(), + comment.getAuthorNickname(), + comment.getContent() + ); + } +} diff --git a/src/main/java/com/example/bak/comment/application/query/port/CommentQueryPort.java b/src/main/java/com/example/bak/comment/application/query/port/CommentQueryPort.java new file mode 100644 index 0000000..1ff558a --- /dev/null +++ b/src/main/java/com/example/bak/comment/application/query/port/CommentQueryPort.java @@ -0,0 +1,9 @@ +package com.example.bak.comment.application.query.port; + +import com.example.bak.comment.application.query.dto.CommentInfo; +import java.util.List; + +public interface CommentQueryPort { + + List findByFeedId(Long feedId); +} diff --git a/src/main/java/com/example/bak/comment/domain/Comment.java b/src/main/java/com/example/bak/comment/domain/Comment.java new file mode 100644 index 0000000..b9d42e1 --- /dev/null +++ b/src/main/java/com/example/bak/comment/domain/Comment.java @@ -0,0 +1,69 @@ +package com.example.bak.comment.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity(name = "comments") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "feed_id", nullable = false) + private Long feedId; + + @Column(nullable = false) + private Long authorId; + + @Column(nullable = false) + private String authorNickname; + + @Column(nullable = false) + private String content; + + private Comment(Long id, Long feedId, String content, Long authorId, + String authorNickname) { + this.id = id; + this.feedId = feedId; + this.content = content; + this.authorId = authorId; + this.authorNickname = authorNickname; + } + + private Comment(Long feedId, String content, Long authorId, String authorNickname) { + this.feedId = feedId; + this.content = content; + this.authorId = authorId; + this.authorNickname = authorNickname; + } + + public static Comment create(Long feedId, String comment, Long authorId, + String authorNickname) { + return new Comment(feedId, comment, authorId, authorNickname); + } + + public static Comment testInstance(Long id, Long feedId, String comment, Long authorId, + String authorNickname) { + return new Comment(id, feedId, comment, authorId, authorNickname); + } + + public void updateComment(String comment) { + this.content = comment; + } + + public boolean isWrittenBy(Long authorId) { + return Objects.equals(this.authorId, authorId); + } +} diff --git a/src/main/java/com/example/bak/comment/domain/ProfileSnapShot.java b/src/main/java/com/example/bak/comment/domain/ProfileSnapShot.java new file mode 100644 index 0000000..07374ac --- /dev/null +++ b/src/main/java/com/example/bak/comment/domain/ProfileSnapShot.java @@ -0,0 +1,5 @@ +package com.example.bak.comment.domain; + +public record ProfileSnapShot(Long userId, String nickname) { + +} diff --git a/src/main/java/com/example/bak/comment/infra/persistence/CommentCommandAdapter.java b/src/main/java/com/example/bak/comment/infra/persistence/CommentCommandAdapter.java new file mode 100644 index 0000000..e048f7a --- /dev/null +++ b/src/main/java/com/example/bak/comment/infra/persistence/CommentCommandAdapter.java @@ -0,0 +1,24 @@ +package com.example.bak.comment.infra.persistence; + +import com.example.bak.comment.application.command.port.CommentCommandPort; +import com.example.bak.comment.domain.Comment; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class CommentCommandAdapter implements CommentCommandPort { + + private final CommentJpaRepository commentJpaRepository; + + @Override + public Comment save(Comment comment) { + return commentJpaRepository.save(comment); + } + + @Override + public Optional findById(Long commentId) { + return commentJpaRepository.findById(commentId); + } +} diff --git a/src/main/java/com/example/bak/comment/infra/persistence/CommentJpaRepository.java b/src/main/java/com/example/bak/comment/infra/persistence/CommentJpaRepository.java new file mode 100644 index 0000000..6579811 --- /dev/null +++ b/src/main/java/com/example/bak/comment/infra/persistence/CommentJpaRepository.java @@ -0,0 +1,10 @@ +package com.example.bak.comment.infra.persistence; + +import com.example.bak.comment.domain.Comment; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentJpaRepository extends JpaRepository { + + List findByFeedId(Long feedId); +} diff --git a/src/main/java/com/example/bak/comment/infra/persistence/CommentQueryAdapter.java b/src/main/java/com/example/bak/comment/infra/persistence/CommentQueryAdapter.java new file mode 100644 index 0000000..8e02131 --- /dev/null +++ b/src/main/java/com/example/bak/comment/infra/persistence/CommentQueryAdapter.java @@ -0,0 +1,21 @@ +package com.example.bak.comment.infra.persistence; + +import com.example.bak.comment.application.query.dto.CommentInfo; +import com.example.bak.comment.application.query.port.CommentQueryPort; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class CommentQueryAdapter implements CommentQueryPort { + + private final CommentJpaRepository commentJpaRepository; + + @Override + public List findByFeedId(Long feedId) { + return commentJpaRepository.findByFeedId(feedId).stream() + .map(CommentInfo::from) + .toList(); + } +} diff --git a/src/main/java/com/example/bak/feedcomment/presentation/FeedCommentController.java b/src/main/java/com/example/bak/comment/presentation/CommentController.java similarity index 60% rename from src/main/java/com/example/bak/feedcomment/presentation/FeedCommentController.java rename to src/main/java/com/example/bak/comment/presentation/CommentController.java index 5f47bd2..77d3b4e 100644 --- a/src/main/java/com/example/bak/feedcomment/presentation/FeedCommentController.java +++ b/src/main/java/com/example/bak/comment/presentation/CommentController.java @@ -1,12 +1,14 @@ -package com.example.bak.feedcomment.presentation; +package com.example.bak.comment.presentation; -import com.example.bak.feedcomment.application.FeedCommentService; -import com.example.bak.feedcomment.application.dto.CommentInfo; -import com.example.bak.feedcomment.presentation.dto.CommentRequest; -import com.example.bak.feedcomment.presentation.swagger.FeedCommentSwagger; +import com.example.bak.comment.application.command.CommentCommandService; +import com.example.bak.comment.application.query.CommentQueryService; +import com.example.bak.comment.application.query.dto.CommentInfo; +import com.example.bak.comment.presentation.dto.CommentRequest; +import com.example.bak.comment.presentation.swagger.FeedCommentSwagger; import com.example.bak.global.common.response.ApiResponse; import com.example.bak.global.common.response.ApiResponseFactory; import com.example.bak.global.common.utils.UriUtils; +import com.example.bak.global.security.annotation.AuthUser; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,38 +23,44 @@ @RestController @RequestMapping("/api/v1") @RequiredArgsConstructor -public class FeedCommentController implements FeedCommentSwagger { +public class CommentController implements FeedCommentSwagger { - private final FeedCommentService feedCommentService; + private final CommentCommandService commentCommandService; + private final CommentQueryService commentQueryService; + @Override @PostMapping("/feeds/{feedId}/comments") public ResponseEntity createComment( @PathVariable Long feedId, - @RequestBody CommentRequest request + @RequestBody CommentRequest request, + @AuthUser Long userId ) { - feedCommentService.createComment( + commentCommandService.createComment( feedId, request.content(), - request.userId() + userId ); ApiResponse response = ApiResponseFactory.successVoid("댓글을 성공적으로 생성하였습니다."); return ResponseEntity.created(UriUtils.current()) .body(response); } + @Override @GetMapping("/feeds/{feedId}/comments") - public ResponseEntity getComment(@PathVariable Long feedId) { - List comments = feedCommentService.getComments(feedId); + public ResponseEntity getComments(@PathVariable Long feedId) { + List comments = commentQueryService.getComments(feedId); ApiResponse response = ApiResponseFactory.success("댓글을 성공적으로 조회하였습니다.", comments); return ResponseEntity.ok(response); } + @Override @PutMapping("/comments/{commentId}") public ResponseEntity updateComment( @PathVariable Long commentId, - @RequestBody CommentRequest request + @RequestBody CommentRequest request, + @AuthUser Long userId ) { - feedCommentService.updateComment(commentId, request.content(), request.userId()); + commentCommandService.updateComment(commentId, request.content(), userId); ApiResponse response = ApiResponseFactory.successVoid("댓글을 성공적으로 수정하였습니다."); return ResponseEntity.ok(response); } diff --git a/src/main/java/com/example/bak/comment/presentation/dto/CommentRequest.java b/src/main/java/com/example/bak/comment/presentation/dto/CommentRequest.java new file mode 100644 index 0000000..d167c55 --- /dev/null +++ b/src/main/java/com/example/bak/comment/presentation/dto/CommentRequest.java @@ -0,0 +1,5 @@ +package com.example.bak.comment.presentation.dto; + +public record CommentRequest(String content) { + +} diff --git a/src/main/java/com/example/bak/feedcomment/presentation/swagger/FeedCommentSwagger.java b/src/main/java/com/example/bak/comment/presentation/swagger/FeedCommentSwagger.java similarity index 59% rename from src/main/java/com/example/bak/feedcomment/presentation/swagger/FeedCommentSwagger.java rename to src/main/java/com/example/bak/comment/presentation/swagger/FeedCommentSwagger.java index 267962f..a13ff65 100644 --- a/src/main/java/com/example/bak/feedcomment/presentation/swagger/FeedCommentSwagger.java +++ b/src/main/java/com/example/bak/comment/presentation/swagger/FeedCommentSwagger.java @@ -1,6 +1,6 @@ -package com.example.bak.feedcomment.presentation.swagger; +package com.example.bak.comment.presentation.swagger; -import com.example.bak.feedcomment.presentation.dto.CommentRequest; +import com.example.bak.comment.presentation.dto.CommentRequest; import com.example.bak.global.common.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -18,7 +18,7 @@ public interface FeedCommentSwagger { @Operation( summary = "댓글 생성", - description = "특정 피드에 새로운 댓글을 작성합니다." + description = "특정 피드에 인증된 사용자가 새로운 댓글을 작성합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -28,6 +28,8 @@ public interface FeedCommentSwagger { mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "CommentCreateSuccess", + summary = "댓글 생성 성공", value = """ { "status": "SUCCESS", @@ -38,16 +40,54 @@ public interface FeedCommentSwagger { ) ) ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청 - 댓글 본문 누락", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "CommentCreateBadRequest", + summary = "댓글 내용 없음", + value = """ + { + "status": "ERROR", + "message": "댓글 내용은 필수입니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 실패", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "CommentCreateUnauthorized", + summary = "토큰 누락", + value = """ + { + "status": "ERROR", + "message": "토큰이 없습니다.", + "data": null + } + """ + ) + ) + ), @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "404", description = "피드를 찾을 수 없음", content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "CommentCreateFeedNotFound", + summary = "댓글 대상 피드 없음", value = """ { "status": "ERROR", - "message": "피드를 찾을 수 없습니다.", + "message": "피드 리소스를 찾을 수 없습니다.", "data": null } """ @@ -64,21 +104,24 @@ ResponseEntity createComment( content = @Content( schema = @Schema(implementation = CommentRequest.class), examples = @ExampleObject( + name = "CommentCreateRequest", + summary = "댓글 생성 요청", value = """ { - "content": "좋은 정보 감사합니다!", - "userId": 1 + "content": "좋은 정보 감사합니다!" } """ ) ) ) - @RequestBody CommentRequest request + @RequestBody CommentRequest request, + @Parameter(hidden = true, description = "인증된 사용자 ID", required = true) + Long userId ); @Operation( summary = "댓글 목록 조회", - description = "특정 피드의 모든 댓글을 조회합니다." + description = "특정 피드에 작성된 모든 댓글을 최신순으로 조회합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -88,18 +131,18 @@ ResponseEntity createComment( mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "CommentList", + summary = "댓글 목록 응답", value = """ { "status": "SUCCESS", "message": "댓글을 성공적으로 조회하였습니다.", "data": [ { - "commentId": 1, - "content": "좋은 정보 감사합니다!", - "authorId": 1, - "authorName": "홍길동", - "createdAt": "2024-01-15T11:00:00", - "updatedAt": "2024-01-15T11:00:00" + "id": 1, + "authorId": 5, + "authorName": "infra-cat", + "content": "좋은 정보 감사합니다!" } ] } @@ -113,10 +156,12 @@ ResponseEntity createComment( content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "CommentListFeedNotFound", + summary = "피드 미존재", value = """ { "status": "ERROR", - "message": "피드를 찾을 수 없습니다.", + "message": "피드 리소스를 찾을 수 없습니다.", "data": null } """ @@ -124,14 +169,14 @@ ResponseEntity createComment( ) ) }) - ResponseEntity getComment( + ResponseEntity getComments( @Parameter(description = "피드 ID", required = true, example = "1") @PathVariable Long feedId ); @Operation( summary = "댓글 수정", - description = "특정 댓글의 내용을 수정합니다." + description = "댓글 작성자가 본인의 댓글 본문을 수정합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -141,6 +186,8 @@ ResponseEntity getComment( mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "CommentUpdateSuccess", + summary = "수정 성공", value = """ { "status": "SUCCESS", @@ -151,12 +198,50 @@ ResponseEntity getComment( ) ) ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청 - 본문 누락", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "CommentUpdateBadRequest", + summary = "본문 없음", + value = """ + { + "status": "ERROR", + "message": "댓글 내용은 필수입니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 실패", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "CommentUpdateUnauthorized", + summary = "토큰 없음", + value = """ + { + "status": "ERROR", + "message": "토큰이 없습니다.", + "data": null + } + """ + ) + ) + ), @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "403", - description = "권한 없음 - 댓글 작성자만 수정 가능", + description = "권한 없음 - 작성자 불일치", content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "CommentUpdateForbidden", + summary = "다른 사용자가 수정 시도", value = """ { "status": "ERROR", @@ -173,10 +258,12 @@ ResponseEntity getComment( content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "CommentUpdateNotFound", + summary = "댓글 미존재", value = """ { "status": "ERROR", - "message": "댓글을 찾을 수 없습니다.", + "message": "댓글 리소스를 찾을 수 없습니다.", "data": null } """ @@ -193,15 +280,18 @@ ResponseEntity updateComment( content = @Content( schema = @Schema(implementation = CommentRequest.class), examples = @ExampleObject( + name = "CommentUpdateRequest", + summary = "댓글 수정 요청", value = """ { - "content": "수정된 댓글 내용입니다.", - "userId": 1 + "content": "수정된 댓글 내용입니다." } """ ) ) ) - @RequestBody CommentRequest request + @RequestBody CommentRequest request, + @Parameter(hidden = true, description = "인증된 사용자 ID", required = true) + Long userId ); } diff --git a/src/main/java/com/example/bak/community/infra/command/CommunityCommandAdaptor.java b/src/main/java/com/example/bak/community/infra/command/CommunityCommandAdaptor.java index f2ab41a..c07a906 100644 --- a/src/main/java/com/example/bak/community/infra/command/CommunityCommandAdaptor.java +++ b/src/main/java/com/example/bak/community/infra/command/CommunityCommandAdaptor.java @@ -8,8 +8,7 @@ @Repository @RequiredArgsConstructor -public class CommunityCommandAdaptor - implements CommunityCommandPort, com.example.bak.feed.application.port.CommunityCommandPort { +public class CommunityCommandAdaptor implements CommunityCommandPort { private final CommunityJpaRepository communityJpaRepository; diff --git a/src/main/java/com/example/bak/community/infra/command/CommunityValidationAdapter.java b/src/main/java/com/example/bak/community/infra/command/CommunityValidationAdapter.java new file mode 100644 index 0000000..6f3f9c3 --- /dev/null +++ b/src/main/java/com/example/bak/community/infra/command/CommunityValidationAdapter.java @@ -0,0 +1,17 @@ +package com.example.bak.community.infra.command; + +import com.example.bak.feed.application.command.port.CommunityValidationPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CommunityValidationAdapter implements CommunityValidationPort { + + private final CommunityJpaRepository communityJpaRepository; + + @Override + public boolean isCommunityExists(Long communityId) { + return communityJpaRepository.existsById(communityId); + } +} diff --git a/src/main/java/com/example/bak/feed/application/FeedService.java b/src/main/java/com/example/bak/feed/application/FeedService.java deleted file mode 100644 index 1fae7fe..0000000 --- a/src/main/java/com/example/bak/feed/application/FeedService.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.bak.feed.application; - -import com.example.bak.community.domain.Community; -import com.example.bak.feed.application.dto.FeedDetail; -import com.example.bak.feed.application.dto.FeedResult; -import com.example.bak.feed.application.dto.FeedSummary; -import com.example.bak.feed.application.port.CommunityCommandPort; -import com.example.bak.feed.domain.Feed; -import com.example.bak.feed.domain.FeedRepository; -import com.example.bak.global.exception.BusinessException; -import com.example.bak.global.exception.ErrorCode; -import com.example.bak.user.application.query.port.UserQueryPort; -import com.example.bak.user.domain.User; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true, propagation = Propagation.SUPPORTS) -public class FeedService { - - private final FeedRepository feedRepository; - private final UserQueryPort userQueryPort; - private final CommunityCommandPort communityCommandPort; - - @Transactional - public FeedResult createFeed(String title, String content, Long communityId, Long userId) { - User user = userQueryPort.findById(userId) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - - Community community = communityCommandPort.findById(communityId) - .orElseThrow(() -> new BusinessException(ErrorCode.COMMUNITY_NOT_FOUND)); - - Feed newFeed = Feed.create(title, content, community, user); - Feed savedFeed = feedRepository.save(newFeed); - - return FeedResult.of(savedFeed.getId()); - } - - @Transactional(readOnly = true) - public FeedDetail getFeedDetail(Long feedId) { - Feed feed = feedRepository.findById(feedId) - .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); - - return FeedDetail.from(feed); - } - - @Transactional(readOnly = true) - public FeedSummary getFeedSummary(Long feedId) { - Feed feed = feedRepository.findById(feedId) - .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); - - return FeedSummary.from(feed); - } - - @Transactional(readOnly = true) - public List getFeeds(int page, int size) { - Pageable pageable = PageRequest.of(page, size); - Page feedPage = feedRepository.findAll(pageable); - return FeedSummary.listFrom(feedPage); - } -} diff --git a/src/main/java/com/example/bak/feed/application/command/FeedCommandService.java b/src/main/java/com/example/bak/feed/application/command/FeedCommandService.java new file mode 100644 index 0000000..3976565 --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/command/FeedCommandService.java @@ -0,0 +1,50 @@ +package com.example.bak.feed.application.command; + +import com.example.bak.feed.application.command.dto.FeedResult; +import com.example.bak.feed.application.command.port.CommunityValidationPort; +import com.example.bak.feed.application.command.port.FeedCommandPort; +import com.example.bak.feed.domain.Feed; +import com.example.bak.global.exception.BusinessException; +import com.example.bak.global.exception.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class FeedCommandService { + + private final FeedCommandPort feedCommandPort; + private final CommunityValidationPort communityValidationPort; + + public FeedResult createFeed(String title, String content, Long communityId, Long userId) { + if (!communityValidationPort.isCommunityExists(communityId)) { + throw new BusinessException(ErrorCode.COMMUNITY_NOT_FOUND); + } + + Feed newFeed = Feed.create(title, content, communityId, userId); + Feed savedFeed = feedCommandPort.save(newFeed); + + return FeedResult.of(savedFeed.getId()); + } + + public void updateFeed(Long feedId, String title, String content, Long userId) { + Feed feed = feedCommandPort.findById(feedId) + .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); + + feed.validateAuthor(userId); + + feed.update(title, content); + feedCommandPort.save(feed); + } + + public void deleteFeed(Long feedId, Long userId) { + Feed feed = feedCommandPort.findById(feedId) + .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); + + feed.validateAuthor(userId); + + feedCommandPort.delete(feed); + } +} diff --git a/src/main/java/com/example/bak/feed/application/dto/FeedResult.java b/src/main/java/com/example/bak/feed/application/command/dto/FeedResult.java similarity index 71% rename from src/main/java/com/example/bak/feed/application/dto/FeedResult.java rename to src/main/java/com/example/bak/feed/application/command/dto/FeedResult.java index c154625..726e451 100644 --- a/src/main/java/com/example/bak/feed/application/dto/FeedResult.java +++ b/src/main/java/com/example/bak/feed/application/command/dto/FeedResult.java @@ -1,4 +1,4 @@ -package com.example.bak.feed.application.dto; +package com.example.bak.feed.application.command.dto; public record FeedResult( Long id diff --git a/src/main/java/com/example/bak/feed/application/command/port/CommunityValidationPort.java b/src/main/java/com/example/bak/feed/application/command/port/CommunityValidationPort.java new file mode 100644 index 0000000..a6062ac --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/command/port/CommunityValidationPort.java @@ -0,0 +1,6 @@ +package com.example.bak.feed.application.command.port; + +public interface CommunityValidationPort { + + boolean isCommunityExists(Long communityId); +} diff --git a/src/main/java/com/example/bak/feed/application/command/port/FeedCommandPort.java b/src/main/java/com/example/bak/feed/application/command/port/FeedCommandPort.java new file mode 100644 index 0000000..ff4ff23 --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/command/port/FeedCommandPort.java @@ -0,0 +1,13 @@ +package com.example.bak.feed.application.command.port; + +import com.example.bak.feed.domain.Feed; +import java.util.Optional; + +public interface FeedCommandPort { + + Feed save(Feed feed); + + Optional findById(Long id); + + void delete(Feed feed); +} diff --git a/src/main/java/com/example/bak/feed/application/command/port/FeedValidationPort.java b/src/main/java/com/example/bak/feed/application/command/port/FeedValidationPort.java new file mode 100644 index 0000000..beb0383 --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/command/port/FeedValidationPort.java @@ -0,0 +1,6 @@ +package com.example.bak.feed.application.command.port; + +public interface FeedValidationPort { + + boolean existsById(Long feedId); +} diff --git a/src/main/java/com/example/bak/feed/application/dto/FeedDetail.java b/src/main/java/com/example/bak/feed/application/dto/FeedDetail.java deleted file mode 100644 index 47696ea..0000000 --- a/src/main/java/com/example/bak/feed/application/dto/FeedDetail.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.bak.feed.application.dto; - -import com.example.bak.community.application.query.dto.CommunityResult; -import com.example.bak.feed.domain.Feed; -import com.example.bak.user.application.query.dto.UserResult; - -/** - * Feed 도메인의 상세 정보를 담는 DTO 단건 조회 시 사용 - */ -public record FeedDetail( - Long id, - String title, - String content, - UserResult author, - CommunityResult.Detail community -) { - - public static FeedDetail from(Feed feed) { - final UserResult author = UserResult.from(feed.getAuthor()); - final CommunityResult.Detail community = CommunityResult.Detail.from(feed.getCommunity()); - - return new FeedDetail( - feed.getId(), - feed.getTitle(), - feed.getContent(), - author, - community - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/bak/feed/application/dto/FeedSummary.java b/src/main/java/com/example/bak/feed/application/dto/FeedSummary.java deleted file mode 100644 index 7743fc1..0000000 --- a/src/main/java/com/example/bak/feed/application/dto/FeedSummary.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.bak.feed.application.dto; - -import com.example.bak.community.application.query.dto.CommunityResult; -import com.example.bak.feed.domain.Feed; -import com.example.bak.user.application.query.dto.UserResult; -import java.util.List; -import org.springframework.data.domain.Page; - -/** - * Feed 도메인의 간단한 정보를 담는 DTO 목록 조회 시 사용 - */ -public record FeedSummary( - Long id, - String title, - UserResult author, - CommunityResult.Detail community, - int commentCount -) { - - public static FeedSummary from(Feed feed) { - final UserResult author = UserResult.from(feed.getAuthor()); - final CommunityResult.Detail community = CommunityResult.Detail.from(feed.getCommunity()); - final int commentCount = feed.getComments().size(); - - return new FeedSummary( - feed.getId(), - feed.getTitle(), - author, - community, - commentCount - ); - } - - public static List listFrom(Page page) { - return page.getContent().stream() - .map(FeedSummary::from) - .toList(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/bak/feed/application/port/CommunityCommandPort.java b/src/main/java/com/example/bak/feed/application/port/CommunityCommandPort.java deleted file mode 100644 index ca91c4b..0000000 --- a/src/main/java/com/example/bak/feed/application/port/CommunityCommandPort.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.bak.feed.application.port; - -import com.example.bak.community.domain.Community; -import java.util.Optional; - -public interface CommunityCommandPort { - - Optional findById(Long communityId); -} diff --git a/src/main/java/com/example/bak/feed/application/query/FeedQueryService.java b/src/main/java/com/example/bak/feed/application/query/FeedQueryService.java new file mode 100644 index 0000000..a61c143 --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/query/FeedQueryService.java @@ -0,0 +1,36 @@ +package com.example.bak.feed.application.query; + +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; +import com.example.bak.feed.application.query.port.FeedQueryPort; +import com.example.bak.global.exception.BusinessException; +import com.example.bak.global.exception.ErrorCode; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FeedQueryService { + + private final FeedQueryPort feedQueryPort; + + public FeedDetail getFeedDetail(Long feedId) { + return feedQueryPort.findDetailById(feedId) + .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); + } + + public FeedSummary getFeedSummary(Long feedId) { + return feedQueryPort.findSummaryById(feedId) + .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); + } + + public List getFeeds(int page, int size) { + Pageable pageable = PageRequest.of(page, size); + Page feedPage = feedQueryPort.findAll(pageable); + return feedPage.getContent(); + } +} diff --git a/src/main/java/com/example/bak/feed/application/query/dto/FeedDetail.java b/src/main/java/com/example/bak/feed/application/query/dto/FeedDetail.java new file mode 100644 index 0000000..eb4e253 --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/query/dto/FeedDetail.java @@ -0,0 +1,16 @@ +package com.example.bak.feed.application.query.dto; + +import com.example.bak.community.application.query.dto.CommunityResult; +import com.example.bak.user.application.query.dto.UserResult; + +/** + * Feed 도메인의 상세 정보를 담는 DTO 단건 조회 시 사용 + */ +public record FeedDetail( + Long id, + String title, + String content, + UserResult author, + CommunityResult.Detail community +) { +} diff --git a/src/main/java/com/example/bak/feed/application/query/dto/FeedSummary.java b/src/main/java/com/example/bak/feed/application/query/dto/FeedSummary.java new file mode 100644 index 0000000..8af1bbd --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/query/dto/FeedSummary.java @@ -0,0 +1,17 @@ +package com.example.bak.feed.application.query.dto; + +import com.example.bak.community.application.query.dto.CommunityResult; +import com.example.bak.user.application.query.dto.UserResult; + +/** + * Feed 도메인의 간단한 정보를 담는 DTO 목록 조회 시 사용 + */ +public record FeedSummary( + Long id, + String title, + UserResult author, + CommunityResult.Detail community, + int commentCount +) { + +} diff --git a/src/main/java/com/example/bak/feed/application/query/port/FeedQueryPort.java b/src/main/java/com/example/bak/feed/application/query/port/FeedQueryPort.java new file mode 100644 index 0000000..a119765 --- /dev/null +++ b/src/main/java/com/example/bak/feed/application/query/port/FeedQueryPort.java @@ -0,0 +1,16 @@ +package com.example.bak.feed.application.query.port; + +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FeedQueryPort { + + Page findAll(Pageable pageable); + + Optional findSummaryById(Long feedId); + + Optional findDetailById(Long feedId); +} diff --git a/src/main/java/com/example/bak/feed/domain/Feed.java b/src/main/java/com/example/bak/feed/domain/Feed.java index a202cd5..0096e0b 100644 --- a/src/main/java/com/example/bak/feed/domain/Feed.java +++ b/src/main/java/com/example/bak/feed/domain/Feed.java @@ -1,28 +1,19 @@ package com.example.bak.feed.domain; -import com.example.bak.community.domain.Community; -import com.example.bak.feedcomment.domain.FeedComment; -import com.example.bak.user.domain.User; -import jakarta.persistence.CascadeType; +import com.example.bak.global.exception.BusinessException; +import com.example.bak.global.exception.ErrorCode; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; -import java.util.ArrayList; -import java.util.List; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Entity(name = "feeds") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) public class Feed { @Id @@ -35,51 +26,54 @@ public class Feed { @Column(nullable = false) private String content; - @OneToMany(mappedBy = "feed", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - private List comments = new ArrayList<>(); - - @ManyToOne(fetch = FetchType.LAZY) - private Community community; + @Column(nullable = false) + private Long communityId; - @ManyToOne(fetch = FetchType.LAZY) - private User author; + @Column(nullable = false) + private Long authorId; - private Feed(Long id, String title, String content, Community community, User author) { + private Feed(Long id, String title, String content, Long communityId, Long authorId) { this.id = id; this.title = title; this.content = content; - this.community = community; - this.author = author; + this.communityId = communityId; + this.authorId = authorId; } - private Feed(String title, String content, Community community, User author) { + private Feed(String title, String content, Long communityId, Long authorId) { this.title = title; this.content = content; - this.community = community; - this.author = author; + this.communityId = communityId; + this.authorId = authorId; } public static Feed create( String title, String content, - Community community, - User author + Long communityId, + Long authorId ) { - return new Feed(title, content, community, author); + return new Feed(title, content, communityId, authorId); } public static Feed testInstance( Long id, String title, String content, - Community community, - User author + Long communityId, + Long userId ) { - return new Feed(id, title, content, community, author); + return new Feed(id, title, content, communityId, userId); + } + + public void update(String title, String content) { + this.title = title; + this.content = content; } - public void addComment(FeedComment comment) { - comment.joinFeed(this); - this.comments.add(comment); + public void validateAuthor(Long userId) { + if (!this.authorId.equals(userId)) { + throw new BusinessException(ErrorCode.UNAUTHORIZED_ACTION); + } } } diff --git a/src/main/java/com/example/bak/feed/infra/command/FeedCommandAdapter.java b/src/main/java/com/example/bak/feed/infra/command/FeedCommandAdapter.java new file mode 100644 index 0000000..e66e639 --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/command/FeedCommandAdapter.java @@ -0,0 +1,29 @@ +package com.example.bak.feed.infra.command; + +import com.example.bak.feed.application.command.port.FeedCommandPort; +import com.example.bak.feed.domain.Feed; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FeedCommandAdapter implements FeedCommandPort { + + private final FeedJpaRepository feedJpaRepository; + + @Override + public Feed save(Feed feed) { + return feedJpaRepository.save(feed); + } + + @Override + public Optional findById(Long id) { + return feedJpaRepository.findById(id); + } + + @Override + public void delete(Feed feed) { + feedJpaRepository.delete(feed); + } +} diff --git a/src/main/java/com/example/bak/feed/infra/persistence/FeedJpaRepository.java b/src/main/java/com/example/bak/feed/infra/command/FeedJpaRepository.java similarity index 58% rename from src/main/java/com/example/bak/feed/infra/persistence/FeedJpaRepository.java rename to src/main/java/com/example/bak/feed/infra/command/FeedJpaRepository.java index d18fd9d..2825d70 100644 --- a/src/main/java/com/example/bak/feed/infra/persistence/FeedJpaRepository.java +++ b/src/main/java/com/example/bak/feed/infra/command/FeedJpaRepository.java @@ -1,24 +1,16 @@ -package com.example.bak.feed.infra.persistence; +package com.example.bak.feed.infra.command; import com.example.bak.feed.domain.Feed; import com.example.bak.feed.domain.FeedRepository; -import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; public interface FeedJpaRepository extends JpaRepository, FeedRepository { - @Override - List findAll(); - - @Override - Page findAll(Pageable pageable); - @Override Feed save(Feed feed); @Override Optional findById(Long id); + } diff --git a/src/main/java/com/example/bak/feed/infra/command/FeedValidationAdapter.java b/src/main/java/com/example/bak/feed/infra/command/FeedValidationAdapter.java new file mode 100644 index 0000000..b68833d --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/command/FeedValidationAdapter.java @@ -0,0 +1,17 @@ +package com.example.bak.feed.infra.command; + +import com.example.bak.feed.application.command.port.FeedValidationPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class FeedValidationAdapter implements FeedValidationPort { + + private final FeedJpaRepository feedJpaRepository; + + @Override + public boolean existsById(Long feedId) { + return feedJpaRepository.existsById(feedId); + } +} diff --git a/src/main/java/com/example/bak/feed/infra/query/FeedQueryAdapter.java b/src/main/java/com/example/bak/feed/infra/query/FeedQueryAdapter.java new file mode 100644 index 0000000..f5f3af0 --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/query/FeedQueryAdapter.java @@ -0,0 +1,33 @@ +package com.example.bak.feed.infra.query; + +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; +import com.example.bak.feed.application.query.port.FeedQueryPort; +import com.example.bak.feed.infra.query.jdbc.FeedJdbcRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FeedQueryAdapter implements FeedQueryPort { + + private final FeedJdbcRepository feedJdbcRepository; + + @Override + public Page findAll(Pageable pageable) { + return feedJdbcRepository.findAll(pageable); + } + + @Override + public Optional findSummaryById(Long feedId) { + return feedJdbcRepository.findSummaryById(feedId); + } + + @Override + public Optional findDetailById(Long feedId) { + return feedJdbcRepository.findDetailById(feedId); + } +} diff --git a/src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepository.java b/src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepository.java new file mode 100644 index 0000000..1357342 --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepository.java @@ -0,0 +1,16 @@ +package com.example.bak.feed.infra.query.jdbc; + +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FeedJdbcRepository { + + Page findAll(Pageable pageable); + + Optional findSummaryById(Long feedId); + + Optional findDetailById(Long feedId); +} diff --git a/src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepositoryImpl.java b/src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepositoryImpl.java new file mode 100644 index 0000000..68d2745 --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/query/jdbc/FeedJdbcRepositoryImpl.java @@ -0,0 +1,137 @@ +package com.example.bak.feed.infra.query.jdbc; + +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; +import com.example.bak.feed.infra.query.jdbc.mapper.FeedDetailRowMapper; +import com.example.bak.feed.infra.query.jdbc.mapper.FeedSummaryRowMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class FeedJdbcRepositoryImpl implements FeedJdbcRepository { + + private static final FeedSummaryRowMapper feedSummaryMapper = new FeedSummaryRowMapper(); + private static final FeedDetailRowMapper feedDetailMapper = new FeedDetailRowMapper(); + private static final String SUMMARY_SELECT = """ + SELECT + f.id AS id, + f.title AS title, + u.id AS author_id, + p.nickname AS author_nickname, + c.id AS community_id, + c.name AS community_name, + c.job_group AS community_job_group, + COUNT(cm.id) AS comment_count + FROM feeds f + JOIN users u ON f.author_id = u.id + JOIN profiles p ON p.user_id = u.id + JOIN communities c ON f.community_id = c.id + LEFT JOIN comments cm ON cm.feed_id = f.id + """; + + private static final String SUMMARY_GROUP_BY = """ + GROUP BY f.id, f.title, u.id, p.nickname, c.id, c.name, c.job_group + """; + + private static final String DETAIL_SELECT = """ + SELECT + f.id AS id, + f.title AS title, + f.content AS content, + u.id AS author_id, + p.nickname AS author_nickname, + c.id AS community_id, + c.name AS community_name, + c.job_group AS community_job_group + FROM feeds f + JOIN users u ON f.author_id = u.id + JOIN profiles p ON p.user_id = u.id + JOIN communities c ON f.community_id = c.id + WHERE f.id = :feedId + LIMIT 1 + """; + + private static final String DEFAULT_ORDER_BY = "ORDER BY f.id DESC"; + + private final NamedParameterJdbcTemplate jdbc; + + @Override + public Page findAll(Pageable pageable) { + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("limit", pageable.getPageSize()) + .addValue("offset", pageable.getOffset()); + + String sql = SUMMARY_SELECT + '\n' + SUMMARY_GROUP_BY + '\n' + + buildOrderByClause(pageable) + '\n' + + "LIMIT :limit OFFSET :offset"; + + List content = jdbc.query(sql, params, feedSummaryMapper); + long total = countFeeds(); + + return new PageImpl<>(content, pageable, total); + } + + @Override + public Optional findSummaryById(Long feedId) { + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("feedId", feedId); + + String sql = SUMMARY_SELECT + '\n' + + "WHERE f.id = :feedId\n" + + SUMMARY_GROUP_BY; + + return jdbc.query(sql, params, feedSummaryMapper).stream().findFirst(); + } + + @Override + public Optional findDetailById(Long feedId) { + MapSqlParameterSource params = new MapSqlParameterSource() + .addValue("feedId", feedId); + + return jdbc.query(DETAIL_SELECT, params, feedDetailMapper).stream().findFirst(); + } + + private long countFeeds() { + Long total = jdbc.queryForObject("SELECT COUNT(*) FROM feeds", new MapSqlParameterSource(), + Long.class); + return total == null ? 0L : total; + } + + private String buildOrderByClause(Pageable pageable) { + if (pageable.getSort().isUnsorted()) { + return DEFAULT_ORDER_BY; + } + + List orderParts = new ArrayList<>(); + for (Sort.Order order : pageable.getSort()) { + String column = mapPropertyToColumn(order.getProperty()); + if (column != null) { + orderParts.add(column + " " + order.getDirection().name()); + } + } + + if (orderParts.isEmpty()) { + return DEFAULT_ORDER_BY; + } + + return "ORDER BY " + String.join(", ", orderParts); + } + + private String mapPropertyToColumn(String property) { + return switch (property) { + case "id" -> "f.id"; + case "title" -> "f.title"; + default -> null; + }; + } +} diff --git a/src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedDetailRowMapper.java b/src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedDetailRowMapper.java new file mode 100644 index 0000000..e8e352b --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedDetailRowMapper.java @@ -0,0 +1,33 @@ +package com.example.bak.feed.infra.query.jdbc.mapper; + +import com.example.bak.community.application.query.dto.CommunityResult; +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.user.application.query.dto.UserResult; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.springframework.jdbc.core.RowMapper; + +public class FeedDetailRowMapper implements RowMapper { + + @Override + public FeedDetail mapRow(ResultSet rs, int rowNum) throws SQLException { + UserResult author = UserResult.from( + rs.getLong("author_id"), + rs.getString("author_nickname") + ); + + CommunityResult.Detail community = new CommunityResult.Detail( + rs.getLong("community_id"), + rs.getString("community_name"), + rs.getString("community_job_group") + ); + + return new FeedDetail( + rs.getLong("id"), + rs.getString("title"), + rs.getString("content"), + author, + community + ); + } +} diff --git a/src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedSummaryRowMapper.java b/src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedSummaryRowMapper.java new file mode 100644 index 0000000..7419866 --- /dev/null +++ b/src/main/java/com/example/bak/feed/infra/query/jdbc/mapper/FeedSummaryRowMapper.java @@ -0,0 +1,33 @@ +package com.example.bak.feed.infra.query.jdbc.mapper; + +import com.example.bak.community.application.query.dto.CommunityResult; +import com.example.bak.feed.application.query.dto.FeedSummary; +import com.example.bak.user.application.query.dto.UserResult; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.springframework.jdbc.core.RowMapper; + +public class FeedSummaryRowMapper implements RowMapper { + + @Override + public FeedSummary mapRow(ResultSet rs, int rowNum) throws SQLException { + UserResult author = UserResult.from( + rs.getLong("author_id"), + rs.getString("author_nickname") + ); + + CommunityResult.Detail community = new CommunityResult.Detail( + rs.getLong("community_id"), + rs.getString("community_name"), + rs.getString("community_job_group") + ); + + return new FeedSummary( + rs.getLong("id"), + rs.getString("title"), + author, + community, + rs.getInt("comment_count") + ); + } +} diff --git a/src/main/java/com/example/bak/feed/presentation/FeedController.java b/src/main/java/com/example/bak/feed/presentation/FeedController.java index 55f3444..4ba411d 100644 --- a/src/main/java/com/example/bak/feed/presentation/FeedController.java +++ b/src/main/java/com/example/bak/feed/presentation/FeedController.java @@ -1,20 +1,25 @@ package com.example.bak.feed.presentation; -import com.example.bak.feed.application.FeedService; -import com.example.bak.feed.application.dto.FeedDetail; -import com.example.bak.feed.application.dto.FeedResult; -import com.example.bak.feed.application.dto.FeedSummary; +import com.example.bak.feed.application.command.FeedCommandService; +import com.example.bak.feed.application.command.dto.FeedResult; +import com.example.bak.feed.application.query.FeedQueryService; +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; import com.example.bak.feed.presentation.dto.FeedRequest; +import com.example.bak.feed.presentation.dto.FeedUpdateRequest; import com.example.bak.feed.presentation.swagger.FeedSwagger; import com.example.bak.global.common.response.ApiResponse; import com.example.bak.global.common.response.ApiResponseFactory; import com.example.bak.global.common.utils.UriUtils; +import com.example.bak.global.security.annotation.AuthUser; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -25,15 +30,17 @@ @RequiredArgsConstructor public class FeedController implements FeedSwagger { - private final FeedService feedService; + private final FeedCommandService feedCommandService; + private final FeedQueryService feedQueryService; @PostMapping() - public ResponseEntity createFeed(@RequestBody FeedRequest request) { - FeedResult feedResult = feedService.createFeed( + public ResponseEntity createFeed(@AuthUser Long userId, + @RequestBody FeedRequest request) { + FeedResult feedResult = feedCommandService.createFeed( request.title(), request.content(), request.communityId(), - request.userId() + userId ); ApiResponse response = ApiResponseFactory.successVoid("피드를 성공적으로 생성하였습니다."); return ResponseEntity.created(UriUtils.current(feedResult.id())) @@ -45,22 +52,43 @@ public ResponseEntity getFeeds( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size ) { - List feeds = feedService.getFeeds(page, size); + List feeds = feedQueryService.getFeeds(page, size); ApiResponse response = ApiResponseFactory.success("피드 목록을 성공적으로 조회하였습니다.", feeds); return ResponseEntity.ok(response); } @GetMapping("/{feedId}/summary") public ResponseEntity getFeedSummary(@PathVariable Long feedId) { - FeedSummary summary = feedService.getFeedSummary(feedId); + FeedSummary summary = feedQueryService.getFeedSummary(feedId); ApiResponse response = ApiResponseFactory.success("피드 요약을 성공적으로 조회하였습니다.", summary); return ResponseEntity.ok(response); } @GetMapping("/{feedId}") public ResponseEntity getFeedDetail(@PathVariable Long feedId) { - FeedDetail detail = feedService.getFeedDetail(feedId); + FeedDetail detail = feedQueryService.getFeedDetail(feedId); ApiResponse response = ApiResponseFactory.success("피드 상세를 성공적으로 조회하였습니다.", detail); return ResponseEntity.ok(response); } + + @PutMapping("/{feedId}") + public ResponseEntity updateFeed( + @AuthUser Long userId, + @PathVariable Long feedId, + @RequestBody FeedUpdateRequest request + ) { + feedCommandService.updateFeed(feedId, request.title(), request.content(), userId); + ApiResponse response = ApiResponseFactory.successVoid("피드를 성공적으로 수정하였습니다."); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{feedId}") + public ResponseEntity deleteFeed( + @AuthUser Long userId, + @PathVariable Long feedId + ) { + feedCommandService.deleteFeed(feedId, userId); + ApiResponse response = ApiResponseFactory.successVoid("피드를 성공적으로 삭제하였습니다."); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/example/bak/feed/presentation/dto/FeedRequest.java b/src/main/java/com/example/bak/feed/presentation/dto/FeedRequest.java index 1794b74..300feee 100644 --- a/src/main/java/com/example/bak/feed/presentation/dto/FeedRequest.java +++ b/src/main/java/com/example/bak/feed/presentation/dto/FeedRequest.java @@ -3,8 +3,7 @@ public record FeedRequest( String title, String content, - Long communityId, - Long userId + Long communityId ) { } \ No newline at end of file diff --git a/src/main/java/com/example/bak/feed/presentation/dto/FeedUpdateRequest.java b/src/main/java/com/example/bak/feed/presentation/dto/FeedUpdateRequest.java new file mode 100644 index 0000000..97e89de --- /dev/null +++ b/src/main/java/com/example/bak/feed/presentation/dto/FeedUpdateRequest.java @@ -0,0 +1,8 @@ +package com.example.bak.feed.presentation.dto; + +public record FeedUpdateRequest( + String title, + String content +) { + +} diff --git a/src/main/java/com/example/bak/feed/presentation/swagger/FeedSwagger.java b/src/main/java/com/example/bak/feed/presentation/swagger/FeedSwagger.java index f939798..9ea7e63 100644 --- a/src/main/java/com/example/bak/feed/presentation/swagger/FeedSwagger.java +++ b/src/main/java/com/example/bak/feed/presentation/swagger/FeedSwagger.java @@ -1,9 +1,11 @@ package com.example.bak.feed.presentation.swagger; import com.example.bak.feed.presentation.dto.FeedRequest; +import com.example.bak.feed.presentation.dto.FeedUpdateRequest; import com.example.bak.global.common.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.headers.Header; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -19,16 +21,19 @@ public interface FeedSwagger { @Operation( summary = "피드 생성", - description = "새로운 피드를 생성합니다. 제목, 내용, 커뮤니티 ID, 사용자 ID를 입력받습니다." + description = "인증된 사용자가 제목, 내용, 커뮤니티 정보를 입력하여 새 피드를 생성합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "201", description = "피드 생성 성공", + headers = @Header(name = "Location", description = "생성된 피드 상세 조회 URI (/api/v1/feeds/{feedId})"), content = @Content( mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "FeedCreateSuccess", + summary = "피드 생성 성공 응답", value = """ { "status": "SUCCESS", @@ -41,14 +46,34 @@ public interface FeedSwagger { ), @io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "400", - description = "잘못된 요청", + description = "잘못된 요청 - 필수 입력 누락 또는 형식 오류", content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "FeedCreateInvalidRequest", + summary = "필수 값 누락 시 응답", value = """ { "status": "ERROR", - "message": "유효하지 않은 요청입니다.", + "message": "제목은 필수 입력입니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 실패 - 토큰이 없거나 만료됨", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedCreateUnauthorized", + summary = "토큰 누락 시 응답", + value = """ + { + "status": "ERROR", + "message": "토큰이 없습니다.", "data": null } """ @@ -61,10 +86,12 @@ public interface FeedSwagger { content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "FeedCreateCommunityNotFound", + summary = "커뮤니티가 존재하지 않을 때", value = """ { "status": "ERROR", - "message": "커뮤니티를 찾을 수 없습니다.", + "message": "커뮤니티 리소스를 찾을 수 없습니다.", "data": null } """ @@ -73,18 +100,21 @@ public interface FeedSwagger { ) }) ResponseEntity createFeed( + @Parameter(hidden = true, description = "AccessToken으로 식별된 인증 사용자 ID", required = true) + Long userId, @io.swagger.v3.oas.annotations.parameters.RequestBody( description = "피드 생성 요청 정보", required = true, content = @Content( schema = @Schema(implementation = FeedRequest.class), examples = @ExampleObject( + name = "FeedCreateRequest", + summary = "피드 생성 요청", value = """ { - "title": "신입 개발자 채용 정보 공유", - "content": "우리 회사에서 신입 개발자를 채용합니다...", - "communityId": 1, - "userId": 1 + "title": "신입 백엔드 개발자 채용 정보 공유", + "content": "우리 팀에서 진행 중인 채용 공고와 준비 TIP을 공유합니다.", + "communityId": 1 } """ ) @@ -95,7 +125,7 @@ ResponseEntity createFeed( @Operation( summary = "피드 목록 조회", - description = "페이지네이션을 적용하여 피드 목록을 조회합니다." + description = "페이지 번호와 페이지 크기를 지정해 최신 피드 목록을 조회합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -105,37 +135,64 @@ ResponseEntity createFeed( mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "FeedList", + summary = "피드 목록 응답", value = """ { "status": "SUCCESS", "message": "피드 목록을 성공적으로 조회하였습니다.", "data": [ { - "feedId": 1, - "title": "신입 개발자 채용 정보 공유", - "authorName": "홍길동", - "communityName": "백엔드 개발자", - "createdAt": "2024-01-15T10:30:00", - "commentCount": 5, - "likeCount": 10 + "id": 7, + "title": "Infra 스터디 회고", + "author": { + "id": 21, + "nickname": "infra-cat" + }, + "community": { + "id": 3, + "name": "백엔드 개발자", + "jobGroup": "개발" + }, + "commentCount": 2 } ] } """ ) ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청 - page 또는 size 값 오류", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedListBadRequest", + summary = "page/size 값이 음수일 때", + value = """ + { + "status": "ERROR", + "message": "페이지 번호와 크기는 음수가 될 수 없습니다.", + "data": null + } + """ + ) + ) ) }) ResponseEntity getFeeds( - @Parameter(description = "페이지 번호 (0부터 시작)", example = "0") + @Parameter(description = "페이지 번호 (0부터 시작)", example = "0", + schema = @Schema(minimum = "0", defaultValue = "0")) @RequestParam(defaultValue = "0") int page, - @Parameter(description = "페이지 크기", example = "10") + @Parameter(description = "페이지 크기 (1 이상)", example = "10", + schema = @Schema(minimum = "1", defaultValue = "10")) @RequestParam(defaultValue = "10") int size ); @Operation( summary = "피드 요약 조회", - description = "특정 피드의 요약 정보를 조회합니다." + description = "피드 카드 노출용으로 필요한 최소 정보(제목, 작성자, 커뮤니티, 댓글 수)를 조회합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -145,18 +202,25 @@ ResponseEntity getFeeds( mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "FeedSummary", + summary = "요약 응답", value = """ { "status": "SUCCESS", "message": "피드 요약을 성공적으로 조회하였습니다.", "data": { - "feedId": 1, + "id": 1, "title": "신입 개발자 채용 정보 공유", - "authorName": "홍길동", - "communityName": "백엔드 개발자", - "createdAt": "2024-01-15T10:30:00", - "commentCount": 5, - "likeCount": 10 + "author": { + "id": 5, + "nickname": "backend-dev" + }, + "community": { + "id": 2, + "name": "백엔드 개발자", + "jobGroup": "개발" + }, + "commentCount": 5 } } """ @@ -169,10 +233,12 @@ ResponseEntity getFeeds( content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "FeedSummaryNotFound", + summary = "요약 대상 피드 없음", value = """ { "status": "ERROR", - "message": "피드를 찾을 수 없습니다.", + "message": "피드 리소스를 찾을 수 없습니다.", "data": null } """ @@ -187,7 +253,7 @@ ResponseEntity getFeedSummary( @Operation( summary = "피드 상세 조회", - description = "특정 피드의 상세 정보를 조회합니다." + description = "본문과 커뮤니티 정보까지 포함한 피드 전체 정보를 조회합니다." ) @ApiResponses(value = { @io.swagger.v3.oas.annotations.responses.ApiResponse( @@ -197,22 +263,25 @@ ResponseEntity getFeedSummary( mediaType = "application/json", schema = @Schema(implementation = ApiResponse.class), examples = @ExampleObject( + name = "FeedDetail", + summary = "상세 응답", value = """ { "status": "SUCCESS", "message": "피드 상세를 성공적으로 조회하였습니다.", "data": { - "feedId": 1, + "id": 1, "title": "신입 개발자 채용 정보 공유", - "content": "우리 회사에서 신입 개발자를 채용합니다...", - "authorId": 1, - "authorName": "홍길동", - "communityId": 1, - "communityName": "백엔드 개발자", - "createdAt": "2024-01-15T10:30:00", - "updatedAt": "2024-01-15T10:30:00", - "commentCount": 5, - "likeCount": 10 + "content": "채용 절차와 준비 Tip을 정리했습니다.", + "author": { + "id": 5, + "nickname": "backend-dev" + }, + "community": { + "id": 2, + "name": "백엔드 개발자", + "jobGroup": "개발" + } } } """ @@ -225,10 +294,12 @@ ResponseEntity getFeedSummary( content = @Content( mediaType = "application/json", examples = @ExampleObject( + name = "FeedDetailNotFound", + summary = "존재하지 않는 피드", value = """ { "status": "ERROR", - "message": "피드를 찾을 수 없습니다.", + "message": "피드 리소스를 찾을 수 없습니다.", "data": null } """ @@ -240,4 +311,212 @@ ResponseEntity getFeedDetail( @Parameter(description = "피드 ID", required = true, example = "1") @PathVariable Long feedId ); + + @Operation( + summary = "피드 수정", + description = "작성자가 본인이 작성한 피드의 제목과 내용을 수정합니다." + ) + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "피드 수정 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject( + name = "FeedUpdateSuccess", + summary = "수정 성공 응답", + value = """ + { + "status": "SUCCESS", + "message": "피드를 성공적으로 수정하였습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "400", + description = "잘못된 요청 - 수정 값 누락", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedUpdateBadRequest", + summary = "수정 본문 없음", + value = """ + { + "status": "ERROR", + "message": "수정할 제목 또는 내용이 필요합니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 실패", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedUpdateUnauthorized", + summary = "토큰 없음", + value = """ + { + "status": "ERROR", + "message": "토큰이 없습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "작성자가 아닌 경우", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedUpdateForbidden", + summary = "다른 사용자의 피드 수정", + value = """ + { + "status": "ERROR", + "message": "권한이 없습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "피드를 찾을 수 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedUpdateNotFound", + summary = "수정 대상 없음", + value = """ + { + "status": "ERROR", + "message": "피드 리소스를 찾을 수 없습니다.", + "data": null + } + """ + ) + ) + ) + }) + ResponseEntity updateFeed( + @Parameter(hidden = true, description = "인증된 사용자 ID", required = true) + Long userId, + @Parameter(description = "피드 ID", required = true, example = "1") + @PathVariable Long feedId, + @io.swagger.v3.oas.annotations.parameters.RequestBody( + description = "피드 수정 요청 정보", + required = true, + content = @Content( + schema = @Schema(implementation = FeedUpdateRequest.class), + examples = @ExampleObject( + name = "FeedUpdateRequest", + summary = "피드 수정 요청", + value = """ + { + "title": "제목을 업데이트합니다", + "content": "본문을 최신 정보로 업데이트합니다." + } + """ + ) + ) + ) + @RequestBody FeedUpdateRequest request + ); + + @Operation( + summary = "피드 삭제", + description = "작성자가 본인이 작성한 피드를 영구 삭제합니다." + ) + @ApiResponses(value = { + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "피드 삭제 성공", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ApiResponse.class), + examples = @ExampleObject( + name = "FeedDeleteSuccess", + summary = "삭제 성공 응답", + value = """ + { + "status": "SUCCESS", + "message": "피드를 성공적으로 삭제하였습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "401", + description = "인증 실패", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedDeleteUnauthorized", + summary = "토큰 누락", + value = """ + { + "status": "ERROR", + "message": "토큰이 없습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "403", + description = "작성자가 아닌 경우", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedDeleteForbidden", + summary = "다른 사용자의 피드 삭제", + value = """ + { + "status": "ERROR", + "message": "권한이 없습니다.", + "data": null + } + """ + ) + ) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "피드를 찾을 수 없음", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + name = "FeedDeleteNotFound", + summary = "삭제 대상 없음", + value = """ + { + "status": "ERROR", + "message": "피드 리소스를 찾을 수 없습니다.", + "data": null + } + """ + ) + ) + ) + }) + ResponseEntity deleteFeed( + @Parameter(hidden = true, description = "인증된 사용자 ID", required = true) + Long userId, + @Parameter(description = "피드 ID", required = true, example = "1") + @PathVariable Long feedId + ); } diff --git a/src/main/java/com/example/bak/feedcomment/application/FeedCommentService.java b/src/main/java/com/example/bak/feedcomment/application/FeedCommentService.java deleted file mode 100644 index 37789fb..0000000 --- a/src/main/java/com/example/bak/feedcomment/application/FeedCommentService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.bak.feedcomment.application; - -import com.example.bak.feed.domain.Feed; -import com.example.bak.feed.domain.FeedRepository; -import com.example.bak.feedcomment.application.dto.CommentInfo; -import com.example.bak.feedcomment.domain.FeedComment; -import com.example.bak.feedcomment.domain.FeedCommentRepository; -import com.example.bak.global.exception.BusinessException; -import com.example.bak.global.exception.ErrorCode; -import com.example.bak.user.domain.User; -import com.example.bak.user.domain.UserRepository; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class FeedCommentService { - - private final FeedCommentRepository commentRepository; - private final FeedRepository feedRepository; - private final UserRepository userRepository; - - @Transactional - public void createComment(Long feedId, String content, Long userId) { - Feed feed = feedRepository.findById(feedId) - .orElseThrow(() -> new BusinessException(ErrorCode.FEED_NOT_FOUND)); - - User user = userRepository.findById(userId) - .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); - - FeedComment newComment = FeedComment.create(content, user); - - feed.addComment(newComment); - commentRepository.save(newComment); - } - - @Transactional - public void updateComment(Long commentId, String content, Long userId) { - FeedComment comment = commentRepository.findById(commentId) - .orElseThrow(() -> new BusinessException(ErrorCode.COMMENT_NOT_FOUND)); - - if (!isAuthor(comment, userId)) { - throw new BusinessException(ErrorCode.UNAUTHORIZED_ACTION); - } - - comment.updateComment(content); - } - - public List getComments(Long feedId) { - return commentRepository.findByFeedId(feedId).stream() - .map(CommentInfo::from) - .toList(); - } - - private boolean isAuthor(FeedComment comment, Long userId) { - return comment.getAuthor().getId().equals(userId); - } -} diff --git a/src/main/java/com/example/bak/feedcomment/application/dto/CommentInfo.java b/src/main/java/com/example/bak/feedcomment/application/dto/CommentInfo.java deleted file mode 100644 index d4432ec..0000000 --- a/src/main/java/com/example/bak/feedcomment/application/dto/CommentInfo.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.bak.feedcomment.application.dto; - -import com.example.bak.feedcomment.domain.FeedComment; - -/** - * FeedComment의 기본 정보를 담는 DTO Feed 상세 조회 시 포함됨 - */ -public record CommentInfo( - Long id, - Long authorId, - String authorName, - String content -) { - - public static CommentInfo from(FeedComment comment) { - return new CommentInfo( - comment.getId(), - comment.getAuthor().getId(), - comment.getAuthor().getProfile().getNickname(), - comment.getComment() - ); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/bak/feedcomment/domain/FeedComment.java b/src/main/java/com/example/bak/feedcomment/domain/FeedComment.java deleted file mode 100644 index 7ba377b..0000000 --- a/src/main/java/com/example/bak/feedcomment/domain/FeedComment.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.example.bak.feedcomment.domain; - -import com.example.bak.feed.domain.Feed; -import com.example.bak.user.domain.User; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity(name = "feed_comments") -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) -public class FeedComment { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private User author; - - @Column(nullable = false) - private String comment; - - @ManyToOne(fetch = FetchType.LAZY) - private Feed feed; - - private FeedComment(Long id, String comment, User author, Feed feed) { - this.id = id; - this.comment = comment; - this.author = author; - this.feed = feed; - } - - private FeedComment(String comment, User author) { - this.comment = comment; - this.author = author; - } - - public static FeedComment create(String comment, User author) { - return new FeedComment(comment, author); - } - - public static FeedComment testInstance(Long id, String comment, User author, Feed feed) { - return new FeedComment(id, comment, author, feed); - } - - public void joinFeed(Feed feed) { - this.feed = feed; - } - - public void updateComment(String comment) { - this.comment = comment; - } -} diff --git a/src/main/java/com/example/bak/feedcomment/domain/FeedCommentRepository.java b/src/main/java/com/example/bak/feedcomment/domain/FeedCommentRepository.java deleted file mode 100644 index 1890f76..0000000 --- a/src/main/java/com/example/bak/feedcomment/domain/FeedCommentRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.bak.feedcomment.domain; - -import java.util.List; -import java.util.Optional; - -public interface FeedCommentRepository { - - FeedComment save(FeedComment comment); - - List findByFeedId(Long feedId); - - Optional findById(Long id); -} diff --git a/src/main/java/com/example/bak/feedcomment/infra/persistence/FeedCommentJpaRepository.java b/src/main/java/com/example/bak/feedcomment/infra/persistence/FeedCommentJpaRepository.java deleted file mode 100644 index 82391a9..0000000 --- a/src/main/java/com/example/bak/feedcomment/infra/persistence/FeedCommentJpaRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.bak.feedcomment.infra.persistence; - -import com.example.bak.feedcomment.domain.FeedComment; -import com.example.bak.feedcomment.domain.FeedCommentRepository; -import java.util.List; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; - - -public interface FeedCommentJpaRepository extends JpaRepository, - FeedCommentRepository { - - @Override - FeedComment save(FeedComment comment); - - @Override - List findByFeedId(Long feedId); - - @Override - Optional findById(Long id); -} diff --git a/src/main/java/com/example/bak/feedcomment/presentation/dto/CommentRequest.java b/src/main/java/com/example/bak/feedcomment/presentation/dto/CommentRequest.java deleted file mode 100644 index b570baf..0000000 --- a/src/main/java/com/example/bak/feedcomment/presentation/dto/CommentRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.bak.feedcomment.presentation.dto; - -public record CommentRequest( - String content, - Long userId -) { - -} diff --git a/src/main/java/com/example/bak/user/application/command/UserCommandService.java b/src/main/java/com/example/bak/user/application/command/UserCommandService.java index e5e1d6b..019b55a 100644 --- a/src/main/java/com/example/bak/user/application/command/UserCommandService.java +++ b/src/main/java/com/example/bak/user/application/command/UserCommandService.java @@ -1,6 +1,6 @@ package com.example.bak.user.application.command; -import com.example.bak.user.application.command.dto.UserResult; +import com.example.bak.user.application.command.dto.UserCommandResult; import com.example.bak.user.application.command.port.UserCommandPort; import com.example.bak.user.application.query.dto.ProfileResult; import com.example.bak.user.domain.User; @@ -15,11 +15,11 @@ public class UserCommandService { private final UserCommandPort userCommandPort; @Transactional - public UserResult createUser(String email, String password) { + public UserCommandResult createUser(String email, String password) { User user = User.create(email, password); User savedUser = userCommandPort.save(user); - return UserResult.of(savedUser.getId()); + return UserCommandResult.of(savedUser.getId()); } @Transactional diff --git a/src/main/java/com/example/bak/user/application/command/dto/UserCommandResult.java b/src/main/java/com/example/bak/user/application/command/dto/UserCommandResult.java new file mode 100644 index 0000000..d52478b --- /dev/null +++ b/src/main/java/com/example/bak/user/application/command/dto/UserCommandResult.java @@ -0,0 +1,10 @@ +package com.example.bak.user.application.command.dto; + +public record UserCommandResult( + Long id +) { + + public static UserCommandResult of(Long id) { + return new UserCommandResult(id); + } +} diff --git a/src/main/java/com/example/bak/user/application/command/dto/UserResult.java b/src/main/java/com/example/bak/user/application/command/dto/UserResult.java deleted file mode 100644 index dd2a29e..0000000 --- a/src/main/java/com/example/bak/user/application/command/dto/UserResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.bak.user.application.command.dto; - -public record UserResult( - Long id -) { - - public static UserResult of(Long id) { - return new UserResult(id); - } -} diff --git a/src/main/java/com/example/bak/user/application/query/dto/UserResult.java b/src/main/java/com/example/bak/user/application/query/dto/UserResult.java index dec2542..2c910a7 100644 --- a/src/main/java/com/example/bak/user/application/query/dto/UserResult.java +++ b/src/main/java/com/example/bak/user/application/query/dto/UserResult.java @@ -1,17 +1,10 @@ package com.example.bak.user.application.query.dto; -import com.example.bak.user.domain.User; - public record UserResult( Long id, String nickname ) { - /** - * @Param - id: User Id - * @Param - nickname: User's Profile nickname - * - */ public static UserResult from(Long id, String nickname) { return new UserResult( id, @@ -19,10 +12,4 @@ public static UserResult from(Long id, String nickname) { ); } - public static UserResult from(User user) { - return new UserResult( - user.getId(), - user.getProfile().getNickname() - ); - } } diff --git a/src/main/java/com/example/bak/user/domain/Profile.java b/src/main/java/com/example/bak/user/domain/Profile.java index c47db8f..a7cd109 100644 --- a/src/main/java/com/example/bak/user/domain/Profile.java +++ b/src/main/java/com/example/bak/user/domain/Profile.java @@ -1,13 +1,10 @@ package com.example.bak.user.domain; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -29,9 +26,7 @@ public class Profile { @Column(nullable = false) private String nickname; - @OneToOne(cascade = CascadeType.ALL) - @JoinColumn(name = "profile") - private User user; + private Long userId; private Profile(Long id, String name, String nickname) { this.id = id; @@ -55,7 +50,7 @@ public static Profile create(String name, String nickname) { return new Profile(name, nickname); } - public void assignUser(User user) { - this.user = user; + public void assignUser(Long userId) { + this.userId = userId; } } diff --git a/src/main/java/com/example/bak/user/domain/User.java b/src/main/java/com/example/bak/user/domain/User.java index e187e2f..608a2b5 100644 --- a/src/main/java/com/example/bak/user/domain/User.java +++ b/src/main/java/com/example/bak/user/domain/User.java @@ -2,7 +2,6 @@ import com.example.bak.global.exception.BusinessException; import com.example.bak.global.exception.ErrorCode; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -10,8 +9,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -33,9 +30,7 @@ public class User { @Column(nullable = false) private String password; - @OneToOne(cascade = CascadeType.ALL) - @JoinColumn(name = "user", nullable = false) - private Profile profile; + private Long profileId; @Enumerated(EnumType.STRING) private UserRole role = UserRole.NORMAL; @@ -51,12 +46,6 @@ private User(Long id, String email, String password) { this.password = password; } - public void matchPassword(String newPassword) { - if (!newPassword.equals(this.password)) { - throw new BusinessException(ErrorCode.INCORRECT_PASSWORD); - } - } - public static User createInstance(Long id, String email, String password) { return new User(id, email, password); } @@ -69,7 +58,13 @@ public static User testInstance(Long id, String email, String password) { return new User(id, email, password); } - public void addProfile(Profile profile) { - this.profile = profile; + public void matchPassword(String newPassword) { + if (!newPassword.equals(this.password)) { + throw new BusinessException(ErrorCode.INCORRECT_PASSWORD); + } + } + + public void addProfile(Long profileId) { + this.profileId = profileId; } } diff --git a/src/main/java/com/example/bak/user/domain/UserRepository.java b/src/main/java/com/example/bak/user/domain/UserRepository.java deleted file mode 100644 index 255945b..0000000 --- a/src/main/java/com/example/bak/user/domain/UserRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.bak.user.domain; - -import java.util.Optional; - -public interface UserRepository { - - User save(User user); - - Optional findById(Long userId); - - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/bak/user/infra/persistence/command/ProfileJpaRepository.java b/src/main/java/com/example/bak/user/infra/persistence/command/ProfileJpaRepository.java new file mode 100644 index 0000000..8ac5441 --- /dev/null +++ b/src/main/java/com/example/bak/user/infra/persistence/command/ProfileJpaRepository.java @@ -0,0 +1,12 @@ +package com.example.bak.user.infra.persistence.command; + +import com.example.bak.comment.domain.ProfileSnapShot; +import com.example.bak.user.domain.Profile; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ProfileJpaRepository extends JpaRepository { + + Optional findProfileSnapShotByUserId(Long userId); +} + diff --git a/src/main/java/com/example/bak/user/infra/persistence/command/UserCommandAdaptor.java b/src/main/java/com/example/bak/user/infra/persistence/command/UserCommandAdaptor.java index 8ba76e3..e1724b8 100644 --- a/src/main/java/com/example/bak/user/infra/persistence/command/UserCommandAdaptor.java +++ b/src/main/java/com/example/bak/user/infra/persistence/command/UserCommandAdaptor.java @@ -1,7 +1,5 @@ package com.example.bak.user.infra.persistence.command; -import com.example.bak.global.exception.BusinessException; -import com.example.bak.global.exception.ErrorCode; import com.example.bak.user.application.command.port.UserCommandPort; import com.example.bak.user.application.query.dto.ProfileResult; import com.example.bak.user.domain.Profile; @@ -14,6 +12,7 @@ public class UserCommandAdaptor implements UserCommandPort { private final UserJpaRepository userJpaRepository; + private final ProfileJpaRepository profileJpaRepository; @Override public User save(User user) { @@ -22,10 +21,9 @@ public User save(User user) { @Override public ProfileResult createProfile(Long userId, String name, String nickname) { - User user = userJpaRepository.findById(userId).orElseThrow(() -> new BusinessException( - ErrorCode.USER_NOT_FOUND)); Profile profile = Profile.create(name, nickname); - user.addProfile(profile); + profile.assignUser(userId); + profileJpaRepository.save(profile); return ProfileResult.from(profile.getName(), profile.getNickname()); } } diff --git a/src/main/java/com/example/bak/user/infra/persistence/command/UserJpaRepository.java b/src/main/java/com/example/bak/user/infra/persistence/command/UserJpaRepository.java index a3ff40e..12ba2f6 100644 --- a/src/main/java/com/example/bak/user/infra/persistence/command/UserJpaRepository.java +++ b/src/main/java/com/example/bak/user/infra/persistence/command/UserJpaRepository.java @@ -1,18 +1,10 @@ package com.example.bak.user.infra.persistence.command; import com.example.bak.user.domain.User; -import com.example.bak.user.domain.UserRepository; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface UserJpaRepository extends JpaRepository, UserRepository { +public interface UserJpaRepository extends JpaRepository { - @Override - User save(User user); - - @Override - Optional findById(Long id); - - @Override Optional findByEmail(String email); } diff --git a/src/main/java/com/example/bak/user/infra/persistence/query/ProfileDataAdapter.java b/src/main/java/com/example/bak/user/infra/persistence/query/ProfileDataAdapter.java new file mode 100644 index 0000000..78201b7 --- /dev/null +++ b/src/main/java/com/example/bak/user/infra/persistence/query/ProfileDataAdapter.java @@ -0,0 +1,21 @@ +package com.example.bak.user.infra.persistence.query; + +import com.example.bak.comment.application.command.port.ProfileDataPort; +import com.example.bak.comment.domain.ProfileSnapShot; +import com.example.bak.user.infra.persistence.command.ProfileJpaRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ProfileDataAdapter implements ProfileDataPort { + + private final ProfileJpaRepository profileJpaRepository; + + @Override + public Optional findSnapshotByUserId(Long userId) { + return profileJpaRepository.findProfileSnapShotByUserId(userId); + } + +} diff --git a/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepository.java b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepository.java new file mode 100644 index 0000000..369fea5 --- /dev/null +++ b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepository.java @@ -0,0 +1,15 @@ +package com.example.bak.user.infra.persistence.query.jdbc; + +import com.example.bak.user.domain.Profile; +import com.example.bak.user.domain.User; +import java.util.Optional; + +public interface ProfileJdbcRepository { + + Optional findById(Long id); + + Optional findByName(String name); + + Optional findUserById(Long userId); +} + diff --git a/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepositoryImpl.java b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepositoryImpl.java new file mode 100644 index 0000000..f918622 --- /dev/null +++ b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepositoryImpl.java @@ -0,0 +1,67 @@ +package com.example.bak.user.infra.persistence.query.jdbc; + +import com.example.bak.user.domain.Profile; +import com.example.bak.user.domain.User; +import com.example.bak.user.infra.persistence.query.jdbc.mapper.ProfileMapper; +import com.example.bak.user.infra.persistence.query.jdbc.mapper.UserMapper; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ProfileJdbcRepositoryImpl implements ProfileJdbcRepository { + + private final NamedParameterJdbcTemplate repository; + + @Override + public Optional findById(Long id) { + String sql = """ + SELECT + p.id as profile_id, + p.name as profile_name, + p.nickname as profile_nickname + FROM profiles p + WHERE p.id = :id + """; + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("id", id); + Profile profile = repository.queryForObject(sql, params, new ProfileMapper()); + return Optional.ofNullable(profile); + } + + @Override + public Optional findByName(String name) { + String sql = """ + SELECT + p.id as profile_id, + p.name as profile_name, + p.nickname as profile_nickname + FROM profiles p + WHERE p.name = :name + """; + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("name", name); + Profile profile = repository.queryForObject(sql, params, new ProfileMapper()); + return Optional.ofNullable(profile); + } + + @Override + public Optional findUserById(Long userId) { + String sql = """ + select + u.id as user_id, + u.email as user_email, + u.password as user_password + from users u + left join profiles p on p.user_id = u.id + where u.id = :userId + """; + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("userId", userId); + User user = repository.queryForObject(sql, params, new UserMapper()); + return Optional.ofNullable(user); + } +} diff --git a/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryImpl.java b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryImpl.java index d2ffa33..aa85a36 100644 --- a/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryImpl.java +++ b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryImpl.java @@ -4,8 +4,8 @@ import com.example.bak.user.domain.Profile; import com.example.bak.user.domain.User; import com.example.bak.user.infra.persistence.query.jdbc.mapper.ProfileMapper; -import com.example.bak.user.infra.persistence.query.jdbc.mapper.UserInfoMapper; import com.example.bak.user.infra.persistence.query.jdbc.mapper.UserMapper; +import com.example.bak.user.infra.persistence.query.jdbc.mapper.UserResultMapper; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -76,7 +76,7 @@ public Optional getUserInfoById(Long userId) { where u.id = :userId """; MapSqlParameterSource params = new MapSqlParameterSource().addValue("userId", userId); - List result = repository.query(sql, params, new UserInfoMapper()); + List result = repository.query(sql, params, new UserResultMapper()); return result.stream().findFirst(); } } diff --git a/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/mapper/UserInfoMapper.java b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/mapper/UserResultMapper.java similarity index 87% rename from src/main/java/com/example/bak/user/infra/persistence/query/jdbc/mapper/UserInfoMapper.java rename to src/main/java/com/example/bak/user/infra/persistence/query/jdbc/mapper/UserResultMapper.java index 0d94adb..7b7a1bb 100644 --- a/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/mapper/UserInfoMapper.java +++ b/src/main/java/com/example/bak/user/infra/persistence/query/jdbc/mapper/UserResultMapper.java @@ -5,7 +5,7 @@ import java.sql.SQLException; import org.springframework.jdbc.core.RowMapper; -public class UserInfoMapper implements RowMapper { +public class UserResultMapper implements RowMapper { @Override public UserResult mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/src/main/java/com/example/bak/user/presentation/UserController.java b/src/main/java/com/example/bak/user/presentation/UserController.java index 20e20d3..bfd2c4a 100644 --- a/src/main/java/com/example/bak/user/presentation/UserController.java +++ b/src/main/java/com/example/bak/user/presentation/UserController.java @@ -5,7 +5,7 @@ import com.example.bak.global.common.utils.UriUtils; import com.example.bak.global.security.annotation.AuthUser; import com.example.bak.user.application.command.UserCommandService; -import com.example.bak.user.application.command.dto.UserResult; +import com.example.bak.user.application.command.dto.UserCommandResult; import com.example.bak.user.application.query.UserQueryService; import com.example.bak.user.application.query.dto.ProfileResult; import com.example.bak.user.presentation.dto.ProfileRequest; @@ -33,7 +33,7 @@ public class UserController implements UserSwagger { public ResponseEntity createUser( @RequestBody UserRequest request ) { - UserResult userResult = userCommandService.createUser( + UserCommandResult userResult = userCommandService.createUser( request.email(), request.password() ); diff --git a/src/test/java/com/example/bak/comment/application/CommentServiceUnitTest.java b/src/test/java/com/example/bak/comment/application/CommentServiceUnitTest.java new file mode 100644 index 0000000..1cf09bc --- /dev/null +++ b/src/test/java/com/example/bak/comment/application/CommentServiceUnitTest.java @@ -0,0 +1,219 @@ +package com.example.bak.comment.application; + +import static com.example.bak.global.utils.AssertionsErrorCode.assertBusiness; +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.bak.comment.application.command.CommentCommandService; +import com.example.bak.comment.application.query.CommentQueryService; +import com.example.bak.comment.domain.Comment; +import com.example.bak.comment.domain.CommentRepositoryStub; +import com.example.bak.community.domain.Community; +import com.example.bak.company.domain.Company; +import com.example.bak.feed.domain.Feed; +import com.example.bak.feed.domain.FeedRepositoryStub; +import com.example.bak.global.exception.ErrorCode; +import com.example.bak.user.domain.Profile; +import com.example.bak.user.domain.ProfileRepositoryStub; +import com.example.bak.user.domain.User; +import java.util.List; +import java.util.stream.IntStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("CommentService 단위 테스트") +class CommentServiceUnitTest { + + private static final Long EXISTING_USER_ID = 1L; + private static final Long EXISTING_FEED_ID = 1L; + private static final Long NOT_FOUND_USER_ID = 999L; + private static final Long NOT_FOUND_FEED_ID = 999L; + + private static final String COMMENT_CONTENT = "기존댓글내용"; + private static final String UPDATED_CONTENT = "수정된댓글내용"; + + private static final String USER_EMAIL = "test@test.com"; + private static final String USER_PASSWORD = "password"; + private static final String PROFILE_NAME = "name"; + private static final String USER_NICKNAME = "nickname"; + + private final Company company = + Company.testInstance(1L, "testDotCom", "test.com", "image.url.com", + "testing company1"); + private final Community community = + Community.testInstance(1L, "name", "jobGroup", 1L); + + private User testUser; + private Profile testProfile; + private Feed testFeed; + + @BeforeEach + void initFixtures() { + testUser = createUser(); + testProfile = createProfile(); + testFeed = Feed.testInstance( + EXISTING_FEED_ID, + "title", + COMMENT_CONTENT, + community.getId(), + testUser.getId() + ); + } + + private User createUser() { + return User.testInstance(EXISTING_USER_ID, USER_EMAIL, USER_PASSWORD); + } + + private Profile createProfile() { + Profile profile = Profile.createInstance(1L, PROFILE_NAME, USER_NICKNAME); + profile.assignUser(EXISTING_USER_ID); + return profile; + } + + private String nickname() { + return USER_NICKNAME; + } + + private List createComments(int count) { + return IntStream.rangeClosed(1, count) + .mapToObj(i -> Comment.testInstance( + (long) i, + testFeed.getId(), + COMMENT_CONTENT + i, + testUser.getId(), + nickname() + )) + .toList(); + } + + @Nested + @DisplayName("CommentCommandService") + class CommentCommandServiceTest { + + private CommentCommandService commentCommandService; + private CommentRepositoryStub commentRepository; + private ProfileRepositoryStub profileDataPort; + + @BeforeEach + void setUp() { + FeedRepositoryStub feedRepository = new FeedRepositoryStub(); + feedRepository.save(testFeed); + + profileDataPort = new ProfileRepositoryStub(); + profileDataPort.save(testProfile); + + commentRepository = new CommentRepositoryStub(); + + commentCommandService = new CommentCommandService( + commentRepository, + feedRepository, + profileDataPort + ); + } + + @Test + @DisplayName("피드 댓글 생성에 성공한다") + void createComment_success() { + commentCommandService.createComment(EXISTING_FEED_ID, COMMENT_CONTENT, + EXISTING_USER_ID); + + var savedComment = commentRepository.findAll().getFirst(); + assertThat(savedComment.getAuthorId()).isEqualTo(EXISTING_USER_ID); + assertThat(savedComment.getAuthorNickname()).isEqualTo(nickname()); + assertThat(savedComment.getFeedId()).isEqualTo(EXISTING_FEED_ID); + assertThat(savedComment.getContent()).isEqualTo(COMMENT_CONTENT); + } + + @Test + @DisplayName("존재하지 않는 피드에 예외를 던진다") + void createComment_when_feedNotFound() { + assertBusiness( + () -> commentCommandService.createComment( + NOT_FOUND_FEED_ID, + COMMENT_CONTENT, + EXISTING_USER_ID + ), + ErrorCode.FEED_NOT_FOUND + ); + } + + @Test + @DisplayName("존재하지 않는 사용자에 예외를 던진다") + void createComment_when_userNotFound() { + assertBusiness( + () -> commentCommandService.createComment( + EXISTING_FEED_ID, + COMMENT_CONTENT, + NOT_FOUND_USER_ID + ), + ErrorCode.USER_NOT_FOUND + ); + } + + @Test + @DisplayName("피드 댓글 업데이트에 성공한다") + void updateComment_success() { + commentRepository.save( + Comment.testInstance(1L, testFeed.getId(), COMMENT_CONTENT, + EXISTING_USER_ID, nickname()) + ); + + commentCommandService.updateComment(1L, UPDATED_CONTENT, EXISTING_USER_ID); + + var updated = commentRepository.findById(1L); + assertThat(updated).isPresent(); + assertThat(updated.get().getContent()).isEqualTo(UPDATED_CONTENT); + } + + @Test + @DisplayName("피드 댓글 업데이트 권한이 없을 때 예외를 던진다") + void updateComment_when_isNotAuthor() { + commentRepository.save( + Comment.testInstance(1L, testFeed.getId(), COMMENT_CONTENT, + EXISTING_USER_ID, nickname()) + ); + + assertBusiness( + () -> commentCommandService.updateComment(1L, UPDATED_CONTENT, + NOT_FOUND_USER_ID), + ErrorCode.UNAUTHORIZED_ACTION + ); + } + + @Test + @DisplayName("존재하지 않는 댓글 업데이트 시 예외") + void updateComment_when_commentNotFound() { + assertBusiness( + () -> commentCommandService.updateComment(1L, UPDATED_CONTENT, + EXISTING_USER_ID), + ErrorCode.COMMENT_NOT_FOUND + ); + } + } + + @Nested + @DisplayName("CommentQueryService") + class CommentQueryServiceTest { + + private CommentQueryService commentQueryService; + private CommentRepositoryStub commentRepository; + + @BeforeEach + void setUp() { + commentRepository = new CommentRepositoryStub(); + commentQueryService = new CommentQueryService(commentRepository); + } + + @Test + @DisplayName("댓글 목록 조회 성공") + void getComments_success() { + createComments(4).forEach(commentRepository::save); + + var comments = commentQueryService.getComments(EXISTING_FEED_ID); + + assertThat(comments).hasSize(4); + assertThat(comments.getFirst().authorId()).isEqualTo(EXISTING_USER_ID); + } + } +} diff --git a/src/test/java/com/example/bak/comment/domain/CommentRepositoryStub.java b/src/test/java/com/example/bak/comment/domain/CommentRepositoryStub.java new file mode 100644 index 0000000..7e98bb5 --- /dev/null +++ b/src/test/java/com/example/bak/comment/domain/CommentRepositoryStub.java @@ -0,0 +1,37 @@ +package com.example.bak.comment.domain; + +import com.example.bak.comment.application.command.port.CommentCommandPort; +import com.example.bak.comment.application.query.dto.CommentInfo; +import com.example.bak.comment.application.query.port.CommentQueryPort; +import com.example.bak.global.support.AbstractStubRepository; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class CommentRepositoryStub + extends AbstractStubRepository + implements CommentCommandPort, CommentQueryPort { + + @Override + protected Long getId(Comment comment) { + return comment.getId(); + } + + @Override + protected boolean isSame(Long left, Long right) { + return Objects.equals(left, right); + } + + @Override + public List findByFeedId(Long feedId) { + return findAll().stream() + .filter(comment -> comment.getFeedId().equals(feedId)) + .map(CommentInfo::from) + .toList(); + } + + @Override + public Optional findById(Long commentId) { + return super.findById(commentId); + } +} diff --git a/src/test/java/com/example/bak/community/domain/CommunityRepositoryStub.java b/src/test/java/com/example/bak/community/domain/CommunityRepositoryStub.java index de75bdb..0450d79 100644 --- a/src/test/java/com/example/bak/community/domain/CommunityRepositoryStub.java +++ b/src/test/java/com/example/bak/community/domain/CommunityRepositoryStub.java @@ -5,8 +5,7 @@ import java.util.Objects; public class CommunityRepositoryStub - extends AbstractStubRepository - implements CommunityCommandPort, com.example.bak.feed.application.port.CommunityCommandPort { + extends AbstractStubRepository implements CommunityCommandPort { @Override protected Long getId(Community community) { diff --git a/src/test/java/com/example/bak/feed/application/FeedServiceUnitTest.java b/src/test/java/com/example/bak/feed/application/FeedServiceUnitTest.java new file mode 100644 index 0000000..17058be --- /dev/null +++ b/src/test/java/com/example/bak/feed/application/FeedServiceUnitTest.java @@ -0,0 +1,148 @@ +package com.example.bak.feed.application; + +import static com.example.bak.global.utils.AssertionsErrorCode.assertBusiness; +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.bak.feed.application.command.FeedCommandService; +import com.example.bak.feed.application.command.dto.FeedResult; +import com.example.bak.feed.application.command.port.CommunityValidationPortStub; +import com.example.bak.feed.domain.Feed; +import com.example.bak.feed.domain.FeedRepositoryStub; +import com.example.bak.global.exception.ErrorCode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("FeedService 단위 테스트") +class FeedServiceUnitTest { + + private static final Long EXISTING_USER_ID = 1L; + private static final Long EXISTING_COMMUNITY_ID = 10L; + private static final Long NOT_FOUND_COMMUNITY_ID = 999L; + + private static final String TITLE = "title"; + private static final String CONTENT = "content"; + + @Nested + @DisplayName("FeedCommandService") + class FeedCommandServiceTest { + + private FeedCommandService feedCommandService; + private FeedRepositoryStub feedRepository; + private CommunityValidationPortStub communityValidationPort; + + @BeforeEach + void setUp() { + feedRepository = new FeedRepositoryStub(); + communityValidationPort = new CommunityValidationPortStub(); + communityValidationPort.registerCommunity(EXISTING_COMMUNITY_ID); + feedCommandService = new FeedCommandService(feedRepository, communityValidationPort); + } + + @Test + @DisplayName("피드 생성 성공") + void createFeed_success() { + FeedResult result = feedCommandService.createFeed( + TITLE, + CONTENT, + EXISTING_COMMUNITY_ID, + EXISTING_USER_ID + ); + + assertThat(result).isNotNull(); + Feed saved = feedRepository.findById(result.id()).orElse(null); + assertThat(saved).isNotNull(); + assertThat(saved.getTitle()).isEqualTo(TITLE); + assertThat(saved.getContent()).isEqualTo(CONTENT); + assertThat(saved.getCommunityId()).isEqualTo(EXISTING_COMMUNITY_ID); + assertThat(saved.getAuthorId()).isEqualTo(EXISTING_USER_ID); + } + + @Test + @DisplayName("존재하지 않는 커뮤니티면 예외 발생") + void createFeed_when_communityNotFound() { + communityValidationPort.removeCommunity(EXISTING_COMMUNITY_ID); + + assertBusiness( + () -> feedCommandService.createFeed( + TITLE, + CONTENT, + NOT_FOUND_COMMUNITY_ID, + EXISTING_USER_ID + ), + ErrorCode.COMMUNITY_NOT_FOUND + ); + } + + @Test + @DisplayName("피드를 수정한다") + void updateFeed_success() { + feedRepository.save(Feed.testInstance(1L, TITLE, CONTENT, EXISTING_COMMUNITY_ID, + EXISTING_USER_ID)); + + feedCommandService.updateFeed(1L, "updatedTitle", "updatedContent", + EXISTING_USER_ID); + + Feed updated = feedRepository.findById(1L).orElseThrow(); + assertThat(updated.getTitle()).isEqualTo("updatedTitle"); + assertThat(updated.getContent()).isEqualTo("updatedContent"); + } + + @Test + @DisplayName("피드 수정 시 작성자가 아니면 예외") + void updateFeed_when_notAuthor() { + feedRepository.save(Feed.testInstance(1L, TITLE, CONTENT, EXISTING_COMMUNITY_ID, + EXISTING_USER_ID)); + + assertBusiness( + () -> feedCommandService.updateFeed(1L, "updatedTitle", "updatedContent", + 999L), + ErrorCode.UNAUTHORIZED_ACTION + ); + } + + @Test + @DisplayName("존재하지 않는 피드 수정 시 예외") + void updateFeed_when_notFound() { + assertBusiness( + () -> feedCommandService.updateFeed(1L, "title", "content", + EXISTING_USER_ID), + ErrorCode.FEED_NOT_FOUND + ); + } + + @Test + @DisplayName("피드를 삭제한다") + void deleteFeed_success() { + feedRepository.save(Feed.testInstance(1L, TITLE, CONTENT, EXISTING_COMMUNITY_ID, + EXISTING_USER_ID)); + + feedCommandService.deleteFeed(1L, EXISTING_USER_ID); + + assertThat(feedRepository.findById(1L)).isEmpty(); + } + + @Test + @DisplayName("피드 삭제 시 작성자가 아니면 예외") + void deleteFeed_when_notAuthor() { + feedRepository.save(Feed.testInstance(1L, TITLE, CONTENT, EXISTING_COMMUNITY_ID, + EXISTING_USER_ID)); + + assertBusiness( + () -> feedCommandService.deleteFeed(1L, 999L), + ErrorCode.UNAUTHORIZED_ACTION + ); + } + + @Test + @DisplayName("존재하지 않는 피드 삭제 시 예외") + void deleteFeed_when_notFound() { + assertBusiness( + () -> feedCommandService.deleteFeed(1L, EXISTING_USER_ID), + ErrorCode.FEED_NOT_FOUND + ); + } + } + +} diff --git a/src/test/java/com/example/bak/feed/application/command/port/CommunityValidationPortStub.java b/src/test/java/com/example/bak/feed/application/command/port/CommunityValidationPortStub.java new file mode 100644 index 0000000..efa3988 --- /dev/null +++ b/src/test/java/com/example/bak/feed/application/command/port/CommunityValidationPortStub.java @@ -0,0 +1,22 @@ +package com.example.bak.feed.application.command.port; + +import java.util.HashSet; +import java.util.Set; + +public class CommunityValidationPortStub implements CommunityValidationPort { + + private final Set existingCommunityIds = new HashSet<>(); + + public void registerCommunity(Long communityId) { + existingCommunityIds.add(communityId); + } + + public void removeCommunity(Long communityId) { + existingCommunityIds.remove(communityId); + } + + @Override + public boolean isCommunityExists(Long communityId) { + return existingCommunityIds.contains(communityId); + } +} diff --git a/src/test/java/com/example/bak/feed/domain/FeedRepositoryStub.java b/src/test/java/com/example/bak/feed/domain/FeedRepositoryStub.java index 7151f49..476b8f4 100644 --- a/src/test/java/com/example/bak/feed/domain/FeedRepositoryStub.java +++ b/src/test/java/com/example/bak/feed/domain/FeedRepositoryStub.java @@ -1,11 +1,17 @@ package com.example.bak.feed.domain; +import com.example.bak.feed.application.command.port.FeedCommandPort; +import com.example.bak.feed.application.command.port.FeedValidationPort; import com.example.bak.global.support.AbstractStubRepository; +import java.util.List; import java.util.Objects; +import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public class FeedRepositoryStub extends AbstractStubRepository - implements FeedRepository { + implements FeedRepository, FeedCommandPort, FeedValidationPort { @Override protected Long getId(Feed feed) { @@ -16,4 +22,34 @@ protected Long getId(Feed feed) { protected boolean isSame(Long left, Long right) { return Objects.equals(left, right); } + + @Override + public List findAll() { + return super.findAll(); + } + + @Override + public Page findAll(Pageable pageable) { + return super.findAll(pageable); + } + + @Override + public Feed save(Feed feed) { + return super.save(feed); + } + + @Override + public Optional findById(Long id) { + return super.findById(id); + } + + @Override + public void delete(Feed feed) { + store.removeIf(it -> isSame(getId(it), getId(feed))); + } + + @Override + public boolean existsById(Long feedId) { + return findById(feedId).isPresent(); + } } diff --git a/src/test/java/com/example/bak/feed/domain/FeedTest.java b/src/test/java/com/example/bak/feed/domain/FeedTest.java new file mode 100644 index 0000000..2519f5f --- /dev/null +++ b/src/test/java/com/example/bak/feed/domain/FeedTest.java @@ -0,0 +1,28 @@ +package com.example.bak.feed.domain; + +import static com.example.bak.global.utils.AssertionsErrorCode.assertBusiness; +import static org.assertj.core.api.Assertions.assertThatCode; + +import com.example.bak.global.exception.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("Feed 도메인 테스트") +class FeedTest { + + @Test + @DisplayName("작성자 검증에 성공한다") + void validateAuthor_success() { + Feed feed = Feed.testInstance(1L, "title", "content", 1L, 10L); + + assertThatCode(() -> feed.validateAuthor(10L)).doesNotThrowAnyException(); + } + + @Test + @DisplayName("작성자 검증 실패 시 예외") + void validateAuthor_when_unauthorized() { + Feed feed = Feed.testInstance(1L, "title", "content", 1L, 10L); + + assertBusiness(() -> feed.validateAuthor(99L), ErrorCode.UNAUTHORIZED_ACTION); + } +} diff --git a/src/test/java/com/example/bak/feed/infra/query/FeedJdbcRepositoryIntegrationTest.java b/src/test/java/com/example/bak/feed/infra/query/FeedJdbcRepositoryIntegrationTest.java new file mode 100644 index 0000000..3c19c58 --- /dev/null +++ b/src/test/java/com/example/bak/feed/infra/query/FeedJdbcRepositoryIntegrationTest.java @@ -0,0 +1,73 @@ +package com.example.bak.feed.infra.query; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.bak.feed.application.query.dto.FeedDetail; +import com.example.bak.feed.application.query.dto.FeedSummary; +import com.example.bak.feed.infra.query.jdbc.FeedJdbcRepository; +import com.example.bak.global.AbstractMySqlContainerTest; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.jdbc.Sql; + +@JdbcTest +@DisplayName("FeedJdbcRepository 통합 테스트") +@Sql(scripts = "/sql/feed/data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +class FeedJdbcRepositoryIntegrationTest extends AbstractMySqlContainerTest { + + @Autowired + private FeedJdbcRepository feedJdbcRepository; + + @Nested + @DisplayName("findDetailById") + class FindDetailById { + + @Test + @DisplayName("피드 상세 정보를 조회한다") + void success() { + FeedDetail detail = feedJdbcRepository.findDetailById(1L).orElseThrow(); + + assertThat(detail.id()).isEqualTo(1L); + assertThat(detail.title()).isEqualTo("title1"); + assertThat(detail.author().nickname()).isEqualTo("nick1"); + assertThat(detail.community().name()).isEqualTo("backend"); + } + } + + @Nested + @DisplayName("findSummaryById") + class FindSummaryById { + + @Test + @DisplayName("피드 요약 정보를 조회하고 댓글 수를 포함한다") + void success() { + FeedSummary summary = feedJdbcRepository.findSummaryById(1L).orElseThrow(); + + assertThat(summary.id()).isEqualTo(1L); + assertThat(summary.commentCount()).isEqualTo(2); + assertThat(summary.author().nickname()).isEqualTo("nick1"); + } + } + + @Nested + @DisplayName("findAll") + class FindAll { + + @Test + @DisplayName("페이지네이션으로 피드 목록을 조회한다") + void success() { + Page page = feedJdbcRepository.findAll(PageRequest.of(0, 2)); + + assertThat(page.getTotalElements()).isEqualTo(3); + List content = page.getContent(); + assertThat(content).hasSize(2); + assertThat(content.getFirst().id()).isEqualTo(3L); // DESC order + } + } +} diff --git a/src/test/java/com/example/bak/feedcomment/domain/FeedCommentRepositoryStub.java b/src/test/java/com/example/bak/feedcomment/domain/FeedCommentRepositoryStub.java deleted file mode 100644 index 17580e5..0000000 --- a/src/test/java/com/example/bak/feedcomment/domain/FeedCommentRepositoryStub.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.bak.feedcomment.domain; - -import com.example.bak.global.support.AbstractStubRepository; -import java.util.List; -import java.util.Objects; - -public class FeedCommentRepositoryStub - extends AbstractStubRepository - implements FeedCommentRepository { - - @Override - protected Long getId(FeedComment feedComment) { - return feedComment.getId(); - } - - @Override - protected boolean isSame(Long left, Long right) { - return Objects.equals(left, right); - } - - @Override - public List findByFeedId(Long feedId) { - return findAll().stream() - .filter(comment -> comment.getFeed().getId().equals(feedId)) - .toList(); - } -} diff --git a/src/test/java/com/example/bak/global/JdbcRepositoryTestConfig.java b/src/test/java/com/example/bak/global/JdbcRepositoryTestConfig.java index 4a420bc..ff6b800 100644 --- a/src/test/java/com/example/bak/global/JdbcRepositoryTestConfig.java +++ b/src/test/java/com/example/bak/global/JdbcRepositoryTestConfig.java @@ -1,7 +1,11 @@ package com.example.bak.global; +import com.example.bak.feed.infra.query.jdbc.FeedJdbcRepository; +import com.example.bak.feed.infra.query.jdbc.FeedJdbcRepositoryImpl; import com.example.bak.privatemessage.infra.query.jdbc.MessageJdbcRepository; import com.example.bak.privatemessage.infra.query.jdbc.MessageJdbcRepositoryImpl; +import com.example.bak.user.infra.persistence.query.jdbc.ProfileJdbcRepository; +import com.example.bak.user.infra.persistence.query.jdbc.ProfileJdbcRepositoryImpl; import com.example.bak.user.infra.persistence.query.jdbc.UserJdbcRepository; import com.example.bak.user.infra.persistence.query.jdbc.UserJdbcRepositoryImpl; import org.springframework.boot.test.context.TestConfiguration; @@ -16,8 +20,18 @@ public MessageJdbcRepository messageJdbcRepository(NamedParameterJdbcTemplate jd return new MessageJdbcRepositoryImpl(jdbc); } + @Bean + public FeedJdbcRepository feedJdbcRepository(NamedParameterJdbcTemplate jdbc) { + return new FeedJdbcRepositoryImpl(jdbc); + } + @Bean public UserJdbcRepository userJdbcRepository(NamedParameterJdbcTemplate jdbc) { return new UserJdbcRepositoryImpl(jdbc); } + + @Bean + public ProfileJdbcRepository profileJdbcRepository(NamedParameterJdbcTemplate jdbc) { + return new ProfileJdbcRepositoryImpl(jdbc); + } } diff --git a/src/test/java/com/example/bak/user/domain/ProfileRepositoryStub.java b/src/test/java/com/example/bak/user/domain/ProfileRepositoryStub.java new file mode 100644 index 0000000..e1796e0 --- /dev/null +++ b/src/test/java/com/example/bak/user/domain/ProfileRepositoryStub.java @@ -0,0 +1,35 @@ +package com.example.bak.user.domain; + +import com.example.bak.comment.application.command.port.ProfileDataPort; +import com.example.bak.comment.domain.ProfileSnapShot; +import com.example.bak.global.support.AbstractStubRepository; +import java.util.Objects; +import java.util.Optional; + +public class ProfileRepositoryStub extends AbstractStubRepository + implements ProfileDataPort { + + @Override + protected Long getId(Profile profile) { + return profile.getUserId(); + } + + @Override + protected boolean isSame(Long left, Long right) { + return Objects.equals(left, right); + } + + @Override + public Profile save(Profile profile) { + if (profile.getUserId() == null) { + throw new IllegalArgumentException("Profile must have assigned userId"); + } + return super.save(profile); + } + + @Override + public Optional findSnapshotByUserId(Long userId) { + return super.findById(userId) + .map(profile -> new ProfileSnapShot(profile.getUserId(), profile.getNickname())); + } +} diff --git a/src/test/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepositoryImplTest.java b/src/test/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepositoryImplTest.java new file mode 100644 index 0000000..8729de8 --- /dev/null +++ b/src/test/java/com/example/bak/user/infra/persistence/query/jdbc/ProfileJdbcRepositoryImplTest.java @@ -0,0 +1,68 @@ +package com.example.bak.user.infra.persistence.query.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.example.bak.global.AbstractMySqlContainerTest; +import com.example.bak.user.domain.Profile; +import com.example.bak.user.domain.User; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.test.context.jdbc.Sql; + +@Slf4j +@JdbcTest +@DisplayName("ProfileJdbcRepository 통합 테스트") +@Sql(scripts = "/sql/user/data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) +class ProfileJdbcRepositoryImplTest extends AbstractMySqlContainerTest { + + @Autowired + private ProfileJdbcRepository profileJdbcRepository; + + @Nested + @DisplayName("Profile 조회 관련 Tests") + class ProfileFind { + + @Test + void findById() { + Long profileId = 1L; + Optional profile = profileJdbcRepository.findById(profileId); + + assertThat(profile).isNotEmpty(); + + assertThat(profile.get().getId()).isEqualTo(profileId); + } + + @Test + void findByName() { + String username = "User One"; + Optional profile = profileJdbcRepository.findByName(username); + + assertThat(profile).isNotEmpty(); + + assertThat(profile.get().getName()).isEqualTo(username); + } + } + + @Nested + @DisplayName("User 조회 관련 Test") + class UserFind { + + @Test + void findUserById() { + Long userId = 1L; + + Optional user = profileJdbcRepository.findUserById(userId); + + assertThat(user).isNotEmpty(); + + assertThat(user.get().getId()).isEqualTo(userId); + } + } + + +} diff --git a/src/test/java/com/example/bak/user/infra/query/UserJdbcRepositoryIntegrationTest.java b/src/test/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryIntegrationTest.java similarity index 94% rename from src/test/java/com/example/bak/user/infra/query/UserJdbcRepositoryIntegrationTest.java rename to src/test/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryIntegrationTest.java index d2091ee..a35122a 100644 --- a/src/test/java/com/example/bak/user/infra/query/UserJdbcRepositoryIntegrationTest.java +++ b/src/test/java/com/example/bak/user/infra/persistence/query/jdbc/UserJdbcRepositoryIntegrationTest.java @@ -1,4 +1,4 @@ -package com.example.bak.user.infra.query; +package com.example.bak.user.infra.persistence.query.jdbc; import static org.assertj.core.api.Assertions.assertThat; @@ -6,7 +6,6 @@ import com.example.bak.global.exception.BusinessException; import com.example.bak.global.exception.ErrorCode; import com.example.bak.user.domain.Profile; -import com.example.bak.user.infra.persistence.query.jdbc.UserJdbcRepository; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; diff --git a/src/test/resources/sql/feed/data.sql b/src/test/resources/sql/feed/data.sql new file mode 100644 index 0000000..c6e54ba --- /dev/null +++ b/src/test/resources/sql/feed/data.sql @@ -0,0 +1,39 @@ +DELETE +FROM comments; +DELETE +FROM feeds; +DELETE +FROM communities; +DELETE +FROM profiles; +DELETE +FROM users; +DELETE +FROM companies; + +INSERT INTO companies (id, name, career_link, logo_url, description) +VALUES (1, 'company', 'company.com', 'logo.png', 'desc'); + +INSERT INTO users (id, email, password) +VALUES (1, 'author1@test.com', 'pw'), + (2, 'author2@test.com', 'pw'), + (3, 'author3@test.com', 'pw'); + +INSERT INTO profiles (id, name, nickname, user_id) +VALUES (1, 'user1', 'nick1', 1), + (2, 'user2', 'nick2', 2), + (3, 'user3', 'nick3', 3); + +INSERT INTO communities (id, name, job_group, company_id) +VALUES (1, 'backend', 'dev', 1), + (2, 'frontend', 'dev', 1); + +INSERT INTO feeds (id, title, content, community_id, author_id) +VALUES (1, 'title1', 'content1', 1, 1), + (2, 'title2', 'content2', 1, 2), + (3, 'title3', 'content3', 2, 3); + +INSERT INTO comments (id, content, author_id, author_nickname, feed_id) +VALUES (1, 'c1', 2, 'nick2', 1), + (2, 'c2', 3, 'nick3', 1), + (3, 'c3', 1, 'nick1', 2); \ No newline at end of file diff --git a/src/test/resources/sql/schema.sql b/src/test/resources/sql/schema.sql index 25cce40..0b8924d 100644 --- a/src/test/resources/sql/schema.sql +++ b/src/test/resources/sql/schema.sql @@ -1,6 +1,5 @@ - -- Drop tables if they exist to start with a clean slate -DROP TABLE IF EXISTS feed_comments; +DROP TABLE IF EXISTS comments; DROP TABLE IF EXISTS feeds; DROP TABLE IF EXISTS communities; DROP TABLE IF EXISTS profiles; @@ -9,68 +8,76 @@ DROP TABLE IF EXISTS companies; DROP TABLE IF EXISTS private_messages; -- Table for Companies -CREATE TABLE companies ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, +CREATE TABLE companies +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, career_link VARCHAR(255) NOT NULL, - logo_url VARCHAR(255) NOT NULL, + logo_url VARCHAR(255) NOT NULL, description VARCHAR(255) NOT NULL ); -- Table for Users -CREATE TABLE users ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - email VARCHAR(255) NOT NULL UNIQUE, +CREATE TABLE users +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL ); -- Table for Profiles -CREATE TABLE profiles ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, +CREATE TABLE profiles +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, nickname VARCHAR(255) NOT NULL, - user_id BIGINT, - FOREIGN KEY (user_id) REFERENCES users(id) + user_id BIGINT, + FOREIGN KEY (user_id) REFERENCES users (id) ); -- Table for Communities -CREATE TABLE communities ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, - job_group VARCHAR(255) NOT NULL, +CREATE TABLE communities +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + job_group VARCHAR(255) NOT NULL, company_id BIGINT, - FOREIGN KEY (company_id) REFERENCES companies(id) + FOREIGN KEY (company_id) REFERENCES companies (id) ); -- Table for Feeds -CREATE TABLE feeds ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - title VARCHAR(255) NOT NULL, - content TEXT NOT NULL, +CREATE TABLE feeds +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(255) NOT NULL, + content TEXT NOT NULL, community_id BIGINT, - author_id BIGINT, - FOREIGN KEY (community_id) REFERENCES communities(id), - FOREIGN KEY (author_id) REFERENCES users(id) + author_id BIGINT, + FOREIGN KEY (community_id) REFERENCES communities (id), + FOREIGN KEY (author_id) REFERENCES users (id) ); --- Table for Feed Comments -CREATE TABLE feed_comments ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - comment TEXT NOT NULL, - author_id BIGINT, - feed_id BIGINT, - FOREIGN KEY (author_id) REFERENCES users(id), - FOREIGN KEY (feed_id) REFERENCES feeds(id) +-- Table for Comments +CREATE TABLE comments +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + content TEXT NOT NULL, + author_id BIGINT, + author_nickname VARCHAR(255), + feed_id BIGINT, + FOREIGN KEY (author_id) REFERENCES users (id), + FOREIGN KEY (feed_id) REFERENCES feeds (id) ); -- Table for Private Messages -CREATE TABLE private_messages ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - sender_id BIGINT NOT NULL, - receiver_id BIGINT NOT NULL, - content TEXT NOT NULL, - read_at DATETIME, - deleted_at_by_sender DATETIME, +CREATE TABLE private_messages +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + sender_id BIGINT NOT NULL, + receiver_id BIGINT NOT NULL, + content TEXT NOT NULL, + read_at DATETIME, + deleted_at_by_sender DATETIME, deleted_at_by_receiver DATETIME, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP + created_at DATETIME DEFAULT CURRENT_TIMESTAMP );