Skip to content

☀️ Comment: 댓글별 첫 페이지 답글을 JSON 필드로 관리 #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
import lombok.Getter;

import java.io.Serializable;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Getter
@SuperBuilder
@NoArgsConstructor
@MappedSuperclass
public abstract class LongBaseEntity implements Serializable {
@Id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
Expand All @@ -11,6 +13,8 @@

@Getter
@MappedSuperclass
@SuperBuilder
@NoArgsConstructor
@EntityListeners(AuditingEntityListener.class)
public abstract class LongBaseTimeEntity extends LongBaseEntity {
@CreatedDate
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
CREATE TABLE IF NOT EXISTS article.comment (
id BIGSERIAL,
board_id BIGINT,

content VARCHAR(255),

status INTEGER,
replies JSON,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP,

Expand All @@ -18,5 +17,6 @@ COMMENT ON TABLE article.comment IS '댓글';
COMMENT ON COLUMN article.comment.content IS '내용';
COMMENT ON COLUMN article.comment.board_id IS '게시물 ID';
COMMENT ON COLUMN article.comment.status IS '상태';
COMMENT ON COLUMN article.comment.replies IS '답글 목록';
COMMENT ON COLUMN article.comment.created_at IS '생성시간';
COMMENT ON COLUMN article.comment.updated_at IS '마지막 수정시간';
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package nettee.comment.domain;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import nettee.comment.domain.type.CommentStatus;
import nettee.reply.domain.Reply;

@Getter
@Builder
Expand All @@ -26,6 +29,9 @@ public class Comment {

private Instant updatedAt;

@Builder.Default
private List<Reply> replies = new ArrayList<>();

@Builder(
builderClassName = "updateCommentBuilder",
builderMethodName = "prepareUpdate",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package nettee.comment.application.port;

import java.util.List;
import nettee.comment.domain.Comment;
import nettee.comment.domain.type.CommentStatus;
import nettee.reply.domain.Reply;

public interface CommentCommandRepositoryPort {

Comment save(Comment comment);

Comment update(Comment comment);

Comment findById(Long id);

void updateStatus(Long id, CommentStatus status);

void updateReplies(Long id, List<Reply> replyList);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,7 @@ public List<CommentDetail> getCommentsByBoardId(Long boardId) {
return result;
}

public List<CommentDetail> getJsonByBoardId(Long boardId) {
return commentQueryRepositoryPort.findPageByBoardId(boardId, 0, 10);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package nettee.reply.application.port;

import java.time.Instant;
import nettee.reply.domain.Reply;
import nettee.reply.domain.type.ReplyStatus;

Expand All @@ -9,5 +10,9 @@ public interface ReplyCommandRepositoryPort {

Reply update(Reply reply);

Reply findById(Long id);

void updateStatus(Long id, ReplyStatus status);

Reply findFirstByCommentIdAfter(Long commentId, Instant createdAt);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package nettee.reply.application.service;

import jakarta.transaction.Transactional;
import java.util.List;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import nettee.comment.application.port.CommentCommandRepositoryPort;
import nettee.reply.domain.Reply;
import nettee.reply.application.port.ReplyCommandRepositoryPort;
import nettee.reply.domain.type.ReplyStatus;
Expand All @@ -16,19 +19,77 @@
public class ReplyCommandService implements ReplyCreateUseCase, ReplyUpdateUseCase, ReplyDeleteUseCase {

private final ReplyCommandRepositoryPort replyCommandRepositoryPort;
private final CommentCommandRepositoryPort commentCommandRepositoryPort;

@Override
public Reply createReply(Reply reply) {
return replyCommandRepositoryPort.save(reply);
var saveReply = replyCommandRepositoryPort.save(reply);
var comment = commentCommandRepositoryPort.findById(reply.getCommentId());

List<Reply> replies = comment.getReplies();

// 답글의 개수가 10 미만이면, 해당 comment.replies에 reply를 추가합니다.
if(replies.size() < 10) {
replies.add(saveReply);
commentCommandRepositoryPort.updateReplies(comment.getId(), replies);
}

return saveReply;
}

@Override
public void deleteReply(Long id) {
var reply = replyCommandRepositoryPort.findById(id);
var comment = commentCommandRepositoryPort.findById(reply.getCommentId());

var replies = comment.getReplies();

// 1. comment.replies에 지우고자 하는 reply가 존재할 경우, comment도 업데이트 해야합니다.
// 1-1) comment.replies에 지우고자 하는 reply가 존재하는지 확인합니다.
boolean removed = replies.removeIf(r -> r.getId().equals(reply.getId()));

// 1-2) 지우고자 하는 reply가 있었다면
if(removed) {

// 1-3) comment.replies의 크기가 10미만이 되었으므로 다음 reply가 더 있는지 확인
var nextReply = replyCommandRepositoryPort.findFirstByCommentIdAfter(
comment.getId(), replies.getLast().getCreatedAt());

// 1-4) 다음 reply가 존재한다면 comment.replies에 추가해줍니다.
if(nextReply != null) {
replies.add(nextReply);
}
// 1-5) comment.replies에 변화가 있었으므로 업데이트합니다.
commentCommandRepositoryPort.updateReplies(comment.getId(),replies);
}

// 2. reply의 status를 REMOVE로 설정합니다.
replyCommandRepositoryPort.updateStatus(id, ReplyStatus.REMOVED);
}

@Override
public Reply updateReply(Reply reply) {
return replyCommandRepositoryPort.update(reply);
var updateReply = replyCommandRepositoryPort.update(reply);
var comment = commentCommandRepositoryPort.findById(updateReply.getCommentId());

List<Reply> replies = comment.getReplies();

// 1. comment.replies에 동일한 reply가 존재할 경우, comment도 업데이트 해야합니다.
// 1-1) comment.replies에 동일한 reply가 존재하는지 확인합니다.
boolean updated = false;
for(int i = 0; i < replies.size(); i++) {
if(Objects.equals(replies.get(i).getId(), reply.getId())) {
replies.set(i, updateReply);
updated = true;
break;
}
}

// 1-2) 존재한다면, comment 역시 업데이트 해야합니다.
if(updated) {
commentCommandRepositoryPort.updateReplies(comment.getId(),replies);
}

return updateReply;
}
}
3 changes: 3 additions & 0 deletions services/comment/driven/rdb/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ dependencies {
api(project(":comment:comment-application"))
api(project(":jpa-core"))

// for using json column
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.0")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")

// querydsl
implementation("com.querydsl:querydsl-jpa:${bom["querydsl.version"]}:jakarta")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,25 @@
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import nettee.comment.driven.rdb.entity.type.CommentEntityStatus;
import nettee.comment.driven.rdb.entity.type.CommentEntityStatusConverter;
import nettee.comment.driven.rdb.entity.type.ReplyListConverter;
import nettee.jpa.support.LongBaseTimeEntity;
import nettee.reply.driven.rdb.entity.ReplyEntity;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@DynamicUpdate
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SuperBuilder
@Entity(name = "comment")
public class CommentEntity extends LongBaseTimeEntity {

Expand All @@ -24,11 +33,16 @@ public class CommentEntity extends LongBaseTimeEntity {
@Convert(converter = CommentEntityStatusConverter.class)
public CommentEntityStatus status;

@Convert(converter = ReplyListConverter.class)
@JdbcTypeCode(SqlTypes.JSON)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entity에서의 json 컬럼 지정을 이렇게 하네요!

public List<ReplyEntity> replies = new ArrayList<>();

@Builder
public CommentEntity(Long boardId, String content, CommentEntityStatus status) {
public CommentEntity(Long boardId, String content, CommentEntityStatus status, List<ReplyEntity> replies) {
this.boardId = boardId;
this.content = content;
this.status = status;
this.replies = replies;
}

@Builder(
Expand All @@ -52,4 +66,15 @@ public void updateStatus(CommentEntityStatus status) {

this.status = status;
}

@Builder(
builderClassName = "updateReplyListBuilder",
builderMethodName = "prepareReplyListUpdate",
buildMethodName = "updateReplyList"
)
public void updateReplyList(List<ReplyEntity> replies) {
Objects.requireNonNull(replies, "replies cannot be null");

this.replies = replies;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package nettee.comment.driven.rdb.entity.type;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.ArrayList;
import java.util.List;
import nettee.reply.driven.rdb.entity.ReplyEntity;

import static nettee.comment.exception.CommentCommandErrorCode.DEFAULT;

@Converter
public class ReplyListConverter implements AttributeConverter<List<ReplyEntity>, String> {

private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 날짜 필드를 숫자가 아닌 ISO 8601 문자열("2025-06-06T04:37:00Z")로 직렬화합니다.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 ReplyEntity의 LongBaseTimeEntity 시간 관련 필드들을 직렬화, 역직렬화하기 위해서는 해당 작업이 필요하나 보네요!


@Override
public String convertToDatabaseColumn(List<ReplyEntity> replies) {
try {
return objectMapper.writeValueAsString(replies);
} catch (JsonProcessingException e) {
throw DEFAULT.exception();
}
}

@Override
public List<ReplyEntity> convertToEntityAttribute(String dbData) {
try {
if (dbData == null || dbData.isBlank()) return new ArrayList<>();
return objectMapper.readValue(dbData, new TypeReference<List<ReplyEntity>>() {});
} catch (JsonProcessingException e) {
throw DEFAULT.exception();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package nettee.comment.driven.rdb.persistence;

import java.util.List;
import lombok.RequiredArgsConstructor;
import nettee.comment.domain.Comment;
import nettee.comment.driven.rdb.entity.type.CommentEntityStatus;
import nettee.comment.driven.rdb.persistence.mapper.CommentEntityMapper;
import nettee.comment.application.port.CommentCommandRepositoryPort;
import nettee.comment.domain.type.CommentStatus;
import nettee.reply.domain.Reply;
import nettee.reply.driven.rdb.persistence.mapper.ReplyEntityMapper;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Repository;

Expand All @@ -18,6 +21,7 @@ public class CommentCommandAdapter implements CommentCommandRepositoryPort {

private final CommentJpaRepository commentJpaRepository;
private final CommentEntityMapper commentEntityMapper;
private final ReplyEntityMapper replyEntityMapper;

@Override
public Comment save(Comment comment) {
Expand All @@ -43,6 +47,14 @@ public Comment update(Comment comment) {
return commentEntityMapper.toDomain(existComment);
}

@Override
public Comment findById(Long id) {
var commentEntity = commentJpaRepository.findById(id)
.orElseThrow(COMMENT_NOT_FOUND::exception);

return commentEntityMapper.toDomain(commentEntity);
}

@Override
public void updateStatus(Long id, CommentStatus status) {
var existComment = commentJpaRepository.findById(id)
Expand All @@ -52,4 +64,16 @@ public void updateStatus(Long id, CommentStatus status) {
.status(CommentEntityStatus.valueOf(status))
.updateStatus();
}

@Override
public void updateReplies(Long id, List<Reply> replyList) {
var existComment = commentJpaRepository.findById(id)
.orElseThrow(COMMENT_NOT_FOUND::exception);

var replies = replyEntityMapper.toEntityList(replyList);

existComment.prepareReplyListUpdate()
.replies(replies)
.updateReplyList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
import nettee.comment.domain.Comment;
import nettee.comment.driven.rdb.entity.CommentEntity;
import nettee.comment.model.CommentQueryModels.CommentDetail;
import nettee.reply.driven.rdb.persistence.mapper.ReplyEntityMapper;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
@Mapper(componentModel = "spring", uses = ReplyEntityMapper.class)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 mapper 클래스에서 다른 mapper를 사용할 수 있게 지정할 수도 있네요..!!

public interface CommentEntityMapper {

Comment toDomain(CommentEntity commentEntity);
Expand Down
Loading