Skip to content
Merged
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 @@ -16,9 +16,20 @@ public record PostSummaryResponse(
String authorNickname,
Job authorJob,
String authorProfileImage,
Instant createdAt
Instant createdAt,
long answerCount,
String contentPreview,
String topAnswerPreview
) {
public static PostSummaryResponse of(Post post) {
private static final int CONTENT_PREVIEW_LENGTH = 150;
private static final int ANSWER_PREVIEW_LENGTH = 100;

public static PostSummaryResponse of(Post post, long answerCount, String topAnswerPreview) {
String content = post.getContent();
String contentPreview = content != null && content.length() > CONTENT_PREVIEW_LENGTH
? content.substring(0, CONTENT_PREVIEW_LENGTH) + "..."
: content;

return new PostSummaryResponse(
post.getId(),
post.getTitle(),
Expand All @@ -27,7 +38,17 @@ public static PostSummaryResponse of(Post post) {
post.getUser().getNickname(),
post.getUser().getJob(),
post.getUser().getProfileImage(),
post.getCreatedAt() != null ? post.getCreatedAt().toInstant(ZoneOffset.UTC) : null
post.getCreatedAt() != null ? post.getCreatedAt().toInstant(ZoneOffset.UTC) : null,
answerCount,
contentPreview,
topAnswerPreview
);
}

public static String truncateAnswerPreview(String content) {
if (content == null) return null;
return content.length() > ANSWER_PREVIEW_LENGTH
? content.substring(0, ANSWER_PREVIEW_LENGTH) + "..."
: content;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

import com.devpick.domain.community.entity.AiAnswer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;
import java.util.UUID;

public interface AiAnswerRepository extends JpaRepository<AiAnswer, UUID> {

Optional<AiAnswer> findByPost_Id(UUID postId);

@Modifying
@Query("DELETE FROM AiAnswer a WHERE a.post.id = :postId")
void deleteByPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

import com.devpick.domain.community.entity.AiQuestion;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;
import java.util.UUID;

public interface AiQuestionRepository extends JpaRepository<AiQuestion, UUID> {

Optional<AiQuestion> findByPost_Id(UUID postId);

@Modifying
@Query("DELETE FROM AiQuestion q WHERE q.post.id = :postId")
void deleteByPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.devpick.domain.community.entity.AnswerLike;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;
import java.util.UUID;
Expand All @@ -13,4 +16,12 @@ public interface AnswerLikeRepository extends JpaRepository<AnswerLike, UUID> {
Optional<AnswerLike> findByAnswer_IdAndUser_Id(UUID answerId, UUID userId);

long countByAnswer_Id(UUID answerId);

@Modifying
@Query("DELETE FROM AnswerLike al WHERE al.answer.id = :answerId")
void deleteByAnswerId(@Param("answerId") UUID answerId);

@Modifying
@Query("DELETE FROM AnswerLike al WHERE al.answer.id IN (SELECT a.id FROM Answer a WHERE a.post.id = :postId)")
void deleteByPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand All @@ -25,4 +26,11 @@ public interface AnswerRepository extends JpaRepository<Answer, UUID> {

@Query("SELECT a FROM Answer a JOIN FETCH a.post WHERE a.user.id = :userId ORDER BY a.createdAt DESC")
List<Answer> findByUserIdWithPost(@Param("userId") UUID userId);

@Query("SELECT a FROM Answer a JOIN FETCH a.post WHERE a.post.id IN :postIds ORDER BY a.post.id, a.createdAt ASC")
List<Answer> findByPostIdsOrderByCreatedAtAsc(@Param("postIds") List<UUID> postIds);

@Modifying
@Query("DELETE FROM Answer a WHERE a.post.id = :postId")
void deleteByPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@

import com.devpick.domain.community.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.UUID;

public interface CommentRepository extends JpaRepository<Comment, UUID> {

List<Comment> findByAnswer_IdOrderByCreatedAtAsc(UUID answerId);

@Modifying
@Query("DELETE FROM Comment c WHERE c.answer.id = :answerId")
void deleteByAnswerId(@Param("answerId") UUID answerId);

@Modifying
@Query("DELETE FROM Comment c WHERE c.answer.id IN (SELECT a.id FROM Answer a WHERE a.post.id = :postId)")
void deleteByPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.devpick.domain.community.entity.PostLike;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Optional;
import java.util.UUID;
Expand All @@ -13,4 +16,8 @@ public interface PostLikeRepository extends JpaRepository<PostLike, UUID> {
Optional<PostLike> findByPost_IdAndUser_Id(UUID postId, UUID userId);

long countByPost_Id(UUID postId);

@Modifying
@Query("DELETE FROM PostLike pl WHERE pl.post.id = :postId")
void deleteByPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

Expand All @@ -22,4 +23,6 @@ OR LOWER(p.content) LIKE LOWER(CONCAT('%', :q, '%'))
Page<Post> searchByTitleOrContentContaining(@Param("q") String q, Pageable pageable);

List<Post> findByUser_IdOrderByCreatedAtDesc(UUID userId);

boolean existsByUser_IdAndTitleAndCreatedAtAfter(UUID userId, String title, LocalDateTime after);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.devpick.domain.community.entity.Answer;
import com.devpick.domain.community.entity.Comment;
import com.devpick.domain.community.entity.Post;
import com.devpick.domain.community.repository.AnswerLikeRepository;
import com.devpick.domain.community.repository.AnswerRepository;
import com.devpick.domain.community.repository.CommentRepository;
import com.devpick.domain.community.repository.PostRepository;
Expand Down Expand Up @@ -36,6 +37,7 @@ public class AnswerService {
private final HistoryRepository historyRepository;
private final PointService pointService;
private final CommentRepository commentRepository;
private final AnswerLikeRepository answerLikeRepository;

@Transactional(readOnly = true)
public AnswerListResponse getAnswers(UUID postId) {
Expand Down Expand Up @@ -105,6 +107,10 @@ public void deleteAnswer(UUID userId, UUID postId, UUID answerId) {
throw new DevpickException(ErrorCode.COMMUNITY_UNAUTHORIZED_ANSWER_ACTION);
}

// 자식 레코드 순서대로 삭제 (FK 제약조건 준수)
commentRepository.deleteByAnswerId(answerId);
answerLikeRepository.deleteByAnswerId(answerId);
historyRepository.deleteByAnswerId(answerId);
answerRepository.delete(answer);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import com.devpick.domain.community.dto.PostSummaryResponse;
import com.devpick.domain.community.dto.PostUpdateRequest;
import com.devpick.domain.community.entity.Post;
import com.devpick.domain.community.repository.AiAnswerRepository;
import com.devpick.domain.community.repository.AiQuestionRepository;
import com.devpick.domain.community.repository.AnswerLikeRepository;
import com.devpick.domain.community.repository.AnswerRepository;
import com.devpick.domain.community.repository.CommentRepository;
import com.devpick.domain.community.repository.PostLikeRepository;
import com.devpick.domain.community.repository.PostRepository;
import com.devpick.domain.point.entity.PointAction;
import com.devpick.domain.point.service.PointService;
Expand All @@ -25,8 +30,11 @@
import org.springframework.util.StringUtils;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand All @@ -37,12 +45,22 @@ public class PostService {
private final UserRepository userRepository;
private final HistoryRepository historyRepository;
private final PointService pointService;
private final PostLikeRepository postLikeRepository;
private final AiAnswerRepository aiAnswerRepository;
private final AiQuestionRepository aiQuestionRepository;
private final CommentRepository commentRepository;
private final AnswerLikeRepository answerLikeRepository;

@Transactional
public PostDetailResponse createPost(UUID userId, PostCreateRequest request) {
User user = userRepository.findByIdAndIsActiveTrue(userId)
.orElseThrow(() -> new DevpickException(ErrorCode.USER_NOT_FOUND));

// 10초 이내 동일 제목 중복 제출 방지
if (postRepository.existsByUser_IdAndTitleAndCreatedAtAfter(userId, request.title(), LocalDateTime.now().minusSeconds(10))) {
throw new DevpickException(ErrorCode.COMMUNITY_DUPLICATE_POST);
}

Post post = Post.builder()
.user(user)
.title(request.title())
Expand Down Expand Up @@ -76,9 +94,39 @@ public PostListResponse getPosts(Pageable pageable, String query) {
} else {
page = postRepository.findAllByOrderByCreatedAtDesc(pageable);
}
List<PostSummaryResponse> posts = page.getContent().stream()
.map(PostSummaryResponse::of)

List<Post> postList = page.getContent();
if (postList.isEmpty()) {
return new PostListResponse(List.of(), page.getNumber(), page.getSize(),
page.getTotalElements(), page.getTotalPages());
}

List<UUID> postIds = postList.stream().map(Post::getId).toList();

// 답변 수 배치 조회
Map<UUID, Long> answerCountMap = answerRepository.countByPostIds(postIds).stream()
.collect(Collectors.toMap(
row -> (UUID) row[0],
row -> (Long) row[1]
));

// 첫 번째 답변 미리보기 배치 조회 (postId → truncated content)
Map<UUID, String> topAnswerPreviews = answerRepository.findByPostIdsOrderByCreatedAtAsc(postIds)
.stream()
.collect(Collectors.toMap(
a -> a.getPost().getId(),
a -> PostSummaryResponse.truncateAnswerPreview(a.getContent()),
(first, second) -> first // 가장 오래된 답변 유지
));

List<PostSummaryResponse> posts = postList.stream()
.map(post -> PostSummaryResponse.of(
post,
answerCountMap.getOrDefault(post.getId(), 0L),
topAnswerPreviews.get(post.getId())
))
.toList();

return new PostListResponse(posts, page.getNumber(), page.getSize(),
page.getTotalElements(), page.getTotalPages());
}
Expand Down Expand Up @@ -114,6 +162,15 @@ public void deletePost(UUID userId, UUID postId) {
throw new DevpickException(ErrorCode.COMMUNITY_UNAUTHORIZED_POST_ACTION);
}

// 자식 레코드 순서대로 삭제 (FK 제약조건 준수)
commentRepository.deleteByPostId(postId);
answerLikeRepository.deleteByPostId(postId);
historyRepository.deleteByAnswerPostId(postId);
answerRepository.deleteByPostId(postId);
postLikeRepository.deleteByPostId(postId);
aiAnswerRepository.deleteByPostId(postId);
aiQuestionRepository.deleteByPostId(postId);
historyRepository.deleteByPostId(postId);
postRepository.delete(post);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

Expand Down Expand Up @@ -99,4 +100,16 @@ Page<UUID> findHistoryIdsByDateRangeExcludingContentLiked(
"WHERE h.id IN :ids " +
"ORDER BY h.createdAt DESC")
List<History> findHistoriesWithAssociationsByIds(@Param("ids") List<UUID> ids);

@Modifying
@Query("DELETE FROM History h WHERE h.post.id = :postId")
void deleteByPostId(@Param("postId") UUID postId);

@Modifying
@Query("DELETE FROM History h WHERE h.answer.id = :answerId")
void deleteByAnswerId(@Param("answerId") UUID answerId);

@Modifying
@Query("DELETE FROM History h WHERE h.answer.id IN (SELECT a.id FROM Answer a WHERE a.post.id = :postId)")
void deleteByAnswerPostId(@Param("postId") UUID postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;


@Getter
@RequiredArgsConstructor
public enum ErrorCode {
Expand Down Expand Up @@ -81,6 +82,7 @@ public enum ErrorCode {
COMMUNITY_POST_NOT_LIKED(HttpStatus.NOT_FOUND, "COMMUNITY_010", "좋아요하지 않은 게시글입니다."),
COMMUNITY_ANSWER_ALREADY_LIKED(HttpStatus.CONFLICT, "COMMUNITY_011", "이미 좋아요한 답변입니다."),
COMMUNITY_ANSWER_NOT_LIKED(HttpStatus.NOT_FOUND, "COMMUNITY_012", "좋아요하지 않은 답변입니다."),
COMMUNITY_DUPLICATE_POST(HttpStatus.CONFLICT, "COMMUNITY_013", "잠시 후 다시 시도해 주세요."),

// Report
REPORT_NOT_FOUND(HttpStatus.NOT_FOUND, "REPORT_001", "주간 리포트를 찾을 수 없습니다."),
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/com/devpick/global/entity/BaseTimeEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ public abstract class BaseTimeEntity {

@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}

@PreUpdate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ void createPost_validationFails_returns400() throws Exception {
@DisplayName("GET /posts - 목록 조회 성공 시 200과 목록 반환")
void getPosts_success_returns200() throws Exception {
PostSummaryResponse summary = new PostSummaryResponse(
postId, "Test Post", Level.JUNIOR, userId, "tester", Job.BACKEND, null, Instant.now());
postId, "Test Post", Level.JUNIOR, userId, "tester", Job.BACKEND, null, Instant.now(), 2L, "content preview", null);
PostListResponse listResponse = new PostListResponse(List.of(summary), 0, 20, 1L, 1);
given(postService.getPosts(any(), any())).willReturn(listResponse);

Expand All @@ -140,7 +140,7 @@ void getPosts_success_returns200() throws Exception {
@DisplayName("GET /posts - 목록 응답에 authorJob 필드 포함")
void getPosts_responseIncludesAuthorJob() throws Exception {
PostSummaryResponse summary = new PostSummaryResponse(
postId, "Test Post", Level.JUNIOR, userId, "tester", Job.FRONTEND, null, Instant.now());
postId, "Test Post", Level.JUNIOR, userId, "tester", Job.FRONTEND, null, Instant.now(), 0L, "content preview", null);
PostListResponse listResponse = new PostListResponse(List.of(summary), 0, 20, 1L, 1);
given(postService.getPosts(any(), any())).willReturn(listResponse);

Expand All @@ -153,7 +153,7 @@ void getPosts_responseIncludesAuthorJob() throws Exception {
@DisplayName("GET /posts - authorJob이 null이어도 정상 반환")
void getPosts_nullAuthorJob_returns200() throws Exception {
PostSummaryResponse summary = new PostSummaryResponse(
postId, "Test Post", Level.JUNIOR, userId, "tester", null, null, Instant.now());
postId, "Test Post", Level.JUNIOR, userId, "tester", null, null, Instant.now(), 0L, "content preview", null);
PostListResponse listResponse = new PostListResponse(List.of(summary), 0, 20, 1L, 1);
given(postService.getPosts(any(), any())).willReturn(listResponse);

Expand Down
Loading
Loading