Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5cd03fc
[Feat]: 비관적 락 조회 메서드 추가
shihan00321 Sep 23, 2025
246e348
[Feat]: 이미 좋아요를 누른 사용자가 한 번 더 요청을 했을 때 예외코드 추가
shihan00321 Sep 23, 2025
c2cf189
[Refactor]: setter 제거, 생성자 접근제어자 PROTECTED로 변경
shihan00321 Sep 23, 2025
e5d04fa
[Feat]: 좋아요 추가, 삭제 기능 구현
shihan00321 Sep 23, 2025
0c3903f
[Test]: 좋아요 추가, 삭제 테스트 구현
shihan00321 Sep 23, 2025
abc15b4
[Refactor]: 게시글 요청 익명여부 추가
shihan00321 Sep 24, 2025
62ffb5d
[Refactor]: 게시글 DTO -> 상세 페이지 DTO로 이름 변경
shihan00321 Sep 24, 2025
3f1c5d5
[Feat]: 게시글 목록을 보여줄 때 사용할 SummaryDTO 구현
shihan00321 Sep 24, 2025
8cc9d40
[Feat]: 게시글 엔티티에 양방향 연관관계 추가
shihan00321 Sep 24, 2025
10a07d7
[Feat]: DTO 구조 변경
shihan00321 Sep 24, 2025
525df06
[Chore]: 불필요한 로깅용 출력문 제거
shihan00321 Sep 24, 2025
8e0ed54
[Refactor]: PostMapper 구조 변경 및 게시글 목록 요약 DTO로 응답하도록 변경
shihan00321 Sep 24, 2025
2759b18
[Feat]: 게시글 응답 시 좋아요 여부 표시 및 문서화
shihan00321 Sep 24, 2025
63a5d6e
[Feat]: 댓글 응답 DTO 구현
shihan00321 Sep 24, 2025
df49522
[Feat]: 댓글 mapper, request 구현
shihan00321 Sep 25, 2025
14b13af
[Feat]: 빌더 사용 시 댓글 빈 리스트로 초기화
shihan00321 Sep 25, 2025
58c64eb
[Docs]: 스웨거 파라미터 설명 추가
shihan00321 Sep 25, 2025
4785b68
[Chore]: Merge conflict
shihan00321 Sep 26, 2025
262a0ea
[Chore]: default_batch_fetch_size 전역 설정 추가
shihan00321 Sep 25, 2025
dd8e5cd
[Feat]: 좋아요 여부 판단을 위한 메서드 추가
shihan00321 Sep 25, 2025
09d6c78
[Refactor]: getPost 메서드 불필요한 부분 제거
shihan00321 Sep 25, 2025
80e20b3
[Feat]: 시간 형태 yyyy.mm.dd 형태로 반환되도록 수정
shihan00321 Sep 25, 2025
b82b9c9
[Chore]: 개발, 테스트 DB h2-postgre 모드로 수정
shihan00321 Sep 25, 2025
09f5467
[Feat]: Merge conflict
shihan00321 Sep 26, 2025
d5c9ce5
[Feat]: 댓글 등록 기능 구현
shihan00321 Sep 25, 2025
c19f687
[Feat]: 댓글 등록 테스트 구현
shihan00321 Sep 25, 2025
80cd4ee
[Feat]: 댓글 조회 기능 구현
shihan00321 Sep 26, 2025
7d11f88
[Feat]: DB URL 수정
shihan00321 Sep 26, 2025
d3357ce
[Fix]: 유저 엔티티 변경에 따른 테스트 변경
shihan00321 Sep 26, 2025
9421bb6
[Test]: 댓글 목록 조회 테스트 구현
shihan00321 Sep 26, 2025
377f6dc
[Feat]: 댓글 수정, 삭제 기능 구현
shihan00321 Sep 26, 2025
e8e625d
[Feat]: 댓글 수정, 삭제 테스트 구현
shihan00321 Sep 26, 2025
e99f878
[Refactor]: 인증 객체에서 유저 정보 받아오도록 수정
shihan00321 Sep 26, 2025
ae56526
[Test]: 테스트 코드 수정
shihan00321 Sep 26, 2025
1f999d4
[Refactor]: 응답 형태 ApiResponse -> ResponseEntity로 수정
shihan00321 Sep 26, 2025
5e9bee1
[Chore]: 테스트 db url 인메모리로 수정
shihan00321 Sep 26, 2025
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
@@ -1,18 +1,90 @@
package com.back.domain.comment.controller;

import com.back.domain.comment.dto.CommentRequest;
import com.back.domain.comment.dto.CommentResponse;
import com.back.domain.comment.enums.CommentSortType;
import com.back.domain.comment.service.CommentService;
import com.back.global.common.PageResponse;
import com.back.global.security.CustomUserDetails;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

/**
* 댓글 관련 API 요청을 처리하는 컨트롤러.
*/
@Tag(name = "Comment", description = "댓글 관련 API")
@RestController
@RequestMapping("/api/v1/posts/{postId}/comments")
@RequiredArgsConstructor
public class CommentController {

private final CommentService commentService;

// 댓글 생성
@PostMapping
@Operation(summary = "댓글 생성", description = "새 댓글을 생성합니다.")
public ResponseEntity<CommentResponse> createPost(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "생성할 댓글 정보",
required = true
)
@RequestBody @Valid CommentRequest request,
@Parameter(description = "조회할 게시글 ID", required = true) @PathVariable("postId") Long postId,
@AuthenticationPrincipal CustomUserDetails cs
) {
CommentResponse response = commentService.createComment(cs.getUser().getId(), postId, request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

// fixme 게시글 목록 조회 - 정렬 조건 최신순, 좋아요순 추가하였는데 변경될 수 있음
@GetMapping
@Operation(summary = "댓글 목록 조회", description = "게시글 목록을 조회합니다.")
public ResponseEntity<PageResponse<CommentResponse>> getPosts(
@Parameter(description = "페이지 정보") Pageable pageable,
@Parameter(description = "조회할 게시글 ID", required = true) @PathVariable("postId") Long postId,
@Parameter(description = "정렬 조건 LATEST or LIKES") @RequestParam(defaultValue = "LATEST") CommentSortType sortType,
@AuthenticationPrincipal CustomUserDetails cs) {

Sort sort = Sort.by(Sort.Direction.DESC, sortType.getProperty());

Pageable sortedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
sort
);

Page<CommentResponse> responses = commentService.getComments(cs.getUser().getId(), postId, sortedPageable);
return ResponseEntity.ok(PageResponse.of(responses));
}


@PutMapping("/{commentId}")
@Operation(summary = "댓글 수정", description = "자신의 댓글을 수정합니다.")
public ResponseEntity<Long> updateComment(
@Parameter(description = "수정할 댓글 ID", required = true) @PathVariable Long commentId,
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "수정할 댓글 정보",
required = true
)
@RequestBody @Valid CommentRequest request,
@AuthenticationPrincipal CustomUserDetails cs) {
return ResponseEntity.ok(commentService.updateComment(cs.getUser().getId(), commentId, request));
}

@DeleteMapping("/{commentId}")
@Operation(summary = "댓글 삭제", description = "자신의 댓글을 삭제합니다.")
public ResponseEntity<Void> deletePost(
@Parameter(description = "삭제할 댓글 ID", required = true) @PathVariable Long commentId,
@AuthenticationPrincipal CustomUserDetails cs) {
commentService.deleteComment(cs.getUser().getId(), commentId);
return ResponseEntity.ok(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.back.domain.comment.dto;

import jakarta.validation.constraints.NotBlank;

public record CommentRequest(
@NotBlank(message = "내용은 필수입니다")
String content,
Boolean hide) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.back.domain.comment.dto;

import com.back.global.common.DateFormat;
import io.swagger.v3.oas.annotations.media.Schema;

import java.time.LocalDateTime;

@Schema(description = "댓글 DTO")
public record CommentResponse(
@Schema(description = "댓글 ID", example = "1")
Long commentId,

@Schema(description = "댓글 작성자 닉네임 또는 익명", example = "홍길동")
String author,

@Schema(description = "댓글 내용", example = "좋은 글이네요!")
String content,

@Schema(description = "댓글 좋아요 수", example = "10")
int likeCount,

@Schema(description = "사용자가 해당 댓글의 작성자인지 여부", example = "true")
boolean isMine,

@Schema(description = "사용자가 해당 댓글에 좋아요를 눌렀는지 여부", example = "true")
boolean isLiked,

@Schema(description = "댓글 작성일자", example = "2025.09.23")
@DateFormat
LocalDateTime createdDate
) {}
22 changes: 14 additions & 8 deletions back/src/main/java/com/back/domain/comment/entity/Comment.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@
import com.back.domain.post.entity.Post;
import com.back.domain.user.entity.User;
import com.back.global.baseentity.BaseEntity;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.data.annotation.LastModifiedDate;

Expand All @@ -24,9 +22,8 @@
@Entity
@Table(name = "comments")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
public class Comment extends BaseEntity {

Expand Down Expand Up @@ -56,4 +53,13 @@ public class Comment extends BaseEntity {

@LastModifiedDate
private LocalDateTime updatedAt;

public void checkUser(Long userId) {
if (!user.getId().equals(userId))
throw new ApiException(ErrorCode.UNAUTHORIZED_USER);
}

public void updateContent(String content) {
this.content = content;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.back.domain.comment.enums;

import lombok.Getter;

/**
* 최신순, 좋아요순
*/
@Getter
public enum CommentSortType {
LATEST("createdDate"),
LIKES("likeCount");

private final String property;

CommentSortType(String property) {
this.property = property;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.back.domain.comment.mapper;

import com.back.domain.comment.dto.CommentRequest;
import com.back.domain.comment.dto.CommentResponse;
import com.back.domain.comment.entity.Comment;
import com.back.domain.post.entity.Post;
import com.back.domain.user.entity.User;
import com.back.global.mapper.Mapper;
import com.back.global.mapper.MappingException;
import com.back.global.mapper.TwoWayMapper;


/**
* CommentMappers
* 댓글(Comment) 관련 엔티티 ↔ DTO 매핑 유틸리티 클래스.
* 구성:
* - COMMENT_READ : Comment → CommentResponse 변환 (읽기 전역 매퍼)
* - CommentCtxMapper : CommentRequest ↔ Comment ↔ CommentResponse 변환 (쓰기 매퍼, 컨텍스트 보유)
* - 사용자(User), 게시글(Post) 컨텍스트를 보유하여 엔티티 생성 시 활용
* - 내가 쓴 댓글 여부, 좋아요 여부 등은 추후 구현 예정
*/
public final class CommentMappers {

private CommentMappers() {}

public static final Mapper<Comment, CommentResponse> COMMENT_READ = e -> {
if (e == null) throw new MappingException("Comment is null");
return new CommentResponse(
e.getId(),
e.isHide() ? "익명" : (e.getUser() != null ? e.getUser().getNickname() : null),
e.getContent(),
e.getLikeCount(),
false, // todo : 내가 쓴 댓글 여부 추후 구현
false, // todo : 좋아요 여부 추후 구현
e.getCreatedDate()
);
};

public static final class CommentCtxMapper implements TwoWayMapper<CommentRequest, Comment, CommentResponse> {
private final User user;
private final Post post;

public CommentCtxMapper(User user, Post post) {
this.user = user;
this.post = post;
}

@Override
public Comment toEntity(CommentRequest req) {
if (req == null) throw new MappingException("CommentRequest is null");
return Comment.builder()
.user(user)
.post(post)
.content(req.content())
.hide(req.hide() != null ? req.hide() : false)
.build();
}

@Override
public CommentResponse toResponse(Comment entity) {
return COMMENT_READ.map(entity);
}
}
}

Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.back.domain.comment.repository;

import com.back.domain.comment.entity.Comment;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
* 댓글 엔티티에 대한 데이터베이스 접근을 담당하는 JpaRepository.
*/
@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
@Query("SELECT c FROM Comment c WHERE c.post.id = :postId")
Page<Comment> findCommentsByPostId(@Param("postId") Long postId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,69 @@
package com.back.domain.comment.service;

import com.back.domain.comment.dto.CommentRequest;
import com.back.domain.comment.dto.CommentResponse;
import com.back.domain.comment.entity.Comment;
import com.back.domain.comment.enums.CommentSortType;
import com.back.domain.comment.mapper.CommentMappers;
import com.back.domain.comment.repository.CommentRepository;
import com.back.domain.post.entity.Post;
import com.back.domain.post.repository.PostRepository;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.ApiException;
import com.back.global.exception.ErrorCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* 댓글 관련 비즈니스 로직을 처리하는 서비스.
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CommentService {

private final UserRepository userRepository;
private final PostRepository postRepository;
private final CommentRepository commentRepository;

public CommentResponse createComment(Long userId, Long postId, CommentRequest request) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND));

Post post = postRepository.findById(postId)
.orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND));
CommentMappers.CommentCtxMapper ctxMapper = new CommentMappers.CommentCtxMapper(user, post);
Comment savedComment = commentRepository.save(ctxMapper.toEntity(request));
return ctxMapper.toResponse(savedComment);
}

public Page<CommentResponse> getComments(Long userId, Long postId, Pageable pageable) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND));
Page<Comment> commentsPage = commentRepository.findCommentsByPostId(postId, pageable);
return commentsPage.map(CommentMappers.COMMENT_READ::map);
}

@Transactional
public Long updateComment(Long userId, Long commentId, CommentRequest request) {
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND));
comment.checkUser(userId);
comment.updateContent(request.content());
return comment.getId();
}

public void deleteComment(Long userId, Long commentId) {
Comment comment = commentRepository.findById(commentId)
.orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND));
comment.checkUser(userId);
commentRepository.delete(comment);
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
package com.back.domain.like.controller;

import com.back.domain.like.service.LikeService;
import com.back.global.security.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

/**
* 좋아요 관련 API 요청을 처리하는 컨트롤러.
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/posts")
public class LikeController {

private final LikeService likeService;

@PostMapping("/{postId}/likes")
public ResponseEntity<Void> addLike(@PathVariable Long postId, @AuthenticationPrincipal CustomUserDetails cs) {
likeService.addLike(cs.getUser().getId(), postId);
return ResponseEntity.status(HttpStatus.CREATED).body(null);
}

@DeleteMapping("/{postId}/likes")
public ResponseEntity<Void> removeLike(@PathVariable Long postId, @AuthenticationPrincipal CustomUserDetails cs) {
likeService.removeLike(postId, cs.getUser().getId());
return ResponseEntity.ok(null);
}
}
Loading