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
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM openjdk:17

ARG PORT=8000
ENV JAVA_TOOL_OPTIONS="-Xms512m -Xmx2g -XX:+ExitOnOutOfMemoryError"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

도커 컨테이너 실행시 JVM 의 힙 메모리 크기를 제한하셨군요!
좀 찾아보니 컨테이너 전체의 메모리를 제한하고, 이 중 일정 비율을 힙 메모리에 할당하는 방식도 있는 것 같은데, 저희는 우선 테이블 구조 수정으로 쿼리 성능을 먼저 개선하는 것이 최우선순위 인 것 같네요 하하


EXPOSE ${PORT}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public record PostDto(
String content,
int likeCount,
int commentCount,
boolean isOverview,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

API 응답 변경에 대한 하위 호환성 확인 필요

PostDto에 새로운 필드 isOverview가 추가되었습니다. 이는 API 응답 구조를 변경하므로 클라이언트 측 영향도 확인이 필요합니다.

또한 이 필드가 기록 타입과 투표 타입 모두에 적용되는 것이 맞는지 확인이 필요합니다.


🏁 Script executed:

#!/bin/bash
# API 문서나 클라이언트 코드에서 RecordSearchResponse 사용 확인
rg "RecordSearchResponse|PostDto" --type-add 'docs:*.{md,adoc,txt}' -t docs -t java -t kotlin -t swift -A 2

Length of output: 7142


🏁 Script executed:

#!/bin/bash
# Display the PostDto record definition around the isOverview field
rg -n "record PostDto" -A 10 src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java

Length of output: 453


🏁 Script executed:

#!/bin/bash
# PostDto 에 isOverview 필드가 포함되었는지 확인
rg -n "isOverview" -A3 src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java
# RecordQueryMapper 의 toPostDto 시그니처 확인
rg -n "PostDto toPostDto" -A5 src/main/java/konkuk/thip/record/application/mapper/RecordQueryMapper.java

Length of output: 645


추가 확인: RecordQueryMapper.toPostDtoisOverview 파라미터 미반영

다음 지점에서 PostDto 레코드에 선언된 boolean isOverview 필드를 매핑하기 위한 인자가 빠져 있습니다.
MapStruct 매핑 인터페이스 시그니처를 업데이트해야 컴파일 에러 및 API 응답 누락을 방지할 수 있습니다.

  • 파일: src/main/java/konkuk/thip/record/application/mapper/RecordQueryMapper.java
    • 기존 시그니처:
      RecordSearchResponse.PostDto toPostDto(
          PostQueryDto dto,
          String content,
          boolean isLiked,
          boolean isWriter,
          boolean isLocked,
          List<RecordSearchResponse.PostDto.VoteItemDto> voteItems
      );
    • 수정안 예시:
      RecordSearchResponse.PostDto toPostDto(
          PostQueryDto dto,
          String content,
          boolean isLiked,
          boolean isWriter,
          boolean isLocked,
          boolean isOverview, // ➡️ 추가
          List<RecordSearchResponse.PostDto.VoteItemDto> voteItems
      );
  • RecordSearchService 등 호출부에서도 isOverview 값을 함께 전달하도록 변경 필요

또한 isOverview가 기록 타입(RECORD)과 투표 타입(VOTE) 모두에 올바르게 적용되는지 로직 검증을 권장합니다.

🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/record/adapter/in/web/response/RecordSearchResponse.java
around line 28, the PostDto declares a boolean isOverview but
RecordQueryMapper.toPostDto signature is missing that parameter; update the
mapper interface signature to include boolean isOverview in the correct
position, update all callers (e.g., RecordSearchService and other call sites) to
pass the isOverview value when invoking toPostDto, and ensure the MapStruct
mapping and any conditional logic apply isOverview correctly for both RECORD and
VOTE types.

boolean isLiked,
boolean isWriter,
boolean isLocked,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,24 @@
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.core.types.dsl.StringExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import konkuk.thip.common.entity.StatusType;
import konkuk.thip.common.util.Cursor;
import konkuk.thip.post.adapter.out.jpa.QPostJpaEntity;
import konkuk.thip.record.adapter.out.jpa.QRecordJpaEntity;
import konkuk.thip.record.adapter.out.jpa.RecordJpaEntity;
import konkuk.thip.record.adapter.out.persistence.constants.SortType;
import konkuk.thip.record.application.port.out.dto.PostQueryDto;
import konkuk.thip.record.application.port.out.dto.QPostQueryDto;
import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity;
import konkuk.thip.vote.adapter.out.jpa.QVoteJpaEntity;
import konkuk.thip.vote.adapter.out.jpa.VoteJpaEntity;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

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

import static konkuk.thip.common.post.PostType.*;
import static konkuk.thip.common.post.PostType.RECORD;
import static konkuk.thip.common.post.PostType.VOTE;


@Repository
Expand Down Expand Up @@ -62,11 +60,11 @@ private BooleanBuilder buildMyRecordCondition(Long roomId, Long userId) {
BooleanBuilder where = new BooleanBuilder();

BooleanBuilder voteCondition = new BooleanBuilder();
voteCondition.and(post.instanceOf(VoteJpaEntity.class))
voteCondition.and(post.dtype.eq(VOTE.getType()))
Comment on lines -65 to +63
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM
instanceOf(Vote ,,) 는 타입이 Vote 임을 확인하기 위해 posts table과 votes table 의 join 이 발생하는데, post의 dtype으로 비교하는 쿼리는 단순히 where 절로 비교하니 쿼리가 훨씬 가벼워질것 같습니다!

.and(vote.roomJpaEntity.roomId.eq(roomId));

BooleanBuilder recordCondition = new BooleanBuilder();
recordCondition.and(post.instanceOf(RecordJpaEntity.class))
recordCondition.and(post.dtype.eq(RECORD.getType()))
.and(record.roomJpaEntity.roomId.eq(roomId));

where.and(voteCondition.or(recordCondition))
Expand Down Expand Up @@ -99,7 +97,7 @@ private BooleanBuilder buildRecordVoteCondition(Long roomId, Integer pageStart,
BooleanBuilder where = new BooleanBuilder();

BooleanBuilder voteCondition = new BooleanBuilder();
voteCondition.and(post.instanceOf(VoteJpaEntity.class))
voteCondition.and(post.dtype.eq(VOTE.getType()))
.and(vote.roomJpaEntity.roomId.eq(roomId));

if (isOverview) {
Expand All @@ -110,7 +108,7 @@ private BooleanBuilder buildRecordVoteCondition(Long roomId, Integer pageStart,
}

BooleanBuilder recordCondition = new BooleanBuilder();
recordCondition.and(post.instanceOf(RecordJpaEntity.class))
recordCondition.and(post.dtype.eq(RECORD.getType()))
.and(record.roomJpaEntity.roomId.eq(roomId));

if (isOverview) {
Expand All @@ -128,27 +126,19 @@ private BooleanBuilder buildRecordVoteCondition(Long roomId, Integer pageStart,
// Case: pageExpr (Record, Vote 분기)
private NumberExpression<Integer> pageExpr() {
return new CaseBuilder()
.when(post.instanceOf(RecordJpaEntity.class)).then(record.page)
.when(post.instanceOf(VoteJpaEntity.class)).then(vote.page)
.when(post.dtype.eq(RECORD.getType())).then(record.page)
.when(post.dtype.eq(VOTE.getType())).then(vote.page)
.otherwise(0);
}

// Case: isOverviewExpr (총평 여부를 정렬 기준으로 사용)
private NumberExpression<Integer> isOverviewExpr() {
return new CaseBuilder()
.when(post.instanceOf(RecordJpaEntity.class)).then(record.isOverview.castToNum(Integer.class))
.when(post.instanceOf(VoteJpaEntity.class)).then(vote.isOverview.castToNum(Integer.class))
.when(post.dtype.eq(RECORD.getType())).then(record.isOverview.castToNum(Integer.class))
.when(post.dtype.eq(VOTE.getType())).then(vote.isOverview.castToNum(Integer.class))
.otherwise(0);
}

// Case: postTypeExpr ("RECORD" or "VOTE")
private StringExpression postTypeExpr() {
return new CaseBuilder()
.when(post.instanceOf(RecordJpaEntity.class)).then(RECORD.getType())
.when(post.instanceOf(VoteJpaEntity.class)).then(VOTE.getType())
.otherwise(FEED.getType());
}

private BooleanBuilder buildCursorPredicateForSortType(SortType sortType, Cursor cursor) {
BooleanBuilder builder = new BooleanBuilder();

Expand Down Expand Up @@ -198,7 +188,7 @@ private OrderSpecifier<?>[] getOrderSpecifiers(SortType sortType) {
private QPostQueryDto selectPostQueryDto() {
return new QPostQueryDto(
post.postId,
postTypeExpr(), //추후에 상속 구조 해지시 type 필드로 구분
post.dtype, //추후에 상속 구조 해지시 type 필드로 구분
post.createdAt,
pageExpr(),
user.userId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package konkuk.thip.record.application.mapper;

import konkuk.thip.common.util.DateUtil;
import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse;
import konkuk.thip.record.application.port.out.dto.PostQueryDto;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;

import java.util.List;

@Mapper(componentModel = "spring", imports = DateUtil.class, unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface RecordQueryMapper {

@Mapping(target = "postId", source = "dto.postId")
@Mapping(target = "postDate", expression = "java(DateUtil.formatBeforeTime(dto.postDate()))")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

날짜 포맷팅 null 체크 필요

dto.postDate()가 null일 경우 NPE가 발생할 수 있습니다. DateUtil.formatBeforeTime 메서드가 null을 처리하는지 확인이 필요합니다.

필요시 null 체크 추가:

-@Mapping(target = "postDate",       expression = "java(DateUtil.formatBeforeTime(dto.postDate()))")
+@Mapping(target = "postDate",       expression = "java(dto.postDate() != null ? DateUtil.formatBeforeTime(dto.postDate()) : null)")

🏁 Script executed:

#!/bin/bash
# DateUtil.formatBeforeTime 메서드의 null 처리 확인
ast-grep --pattern 'public static String formatBeforeTime($_) {
  $$$
}'

Length of output: 883


날짜 포맷팅 시 NullPointerException 방지용 null 체크 추가 필요

RecordQueryMapper.java에서 dto.postDate()가 null일 경우 DateUtil.formatBeforeTime이 null을 처리하지 않아 NPE가 발생할 수 있습니다.

  • 파일: src/main/java/konkuk/thip/record/application/mapper/RecordQueryMapper.java
  • 라인: 16

아래와 같이 null 체크를 추가해 주세요:

- @Mapping(target = "postDate", expression = "java(DateUtil.formatBeforeTime(dto.postDate()))")
+ @Mapping(target = "postDate", expression = "java(dto.postDate() != null ? DateUtil.formatBeforeTime(dto.postDate()) : null)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Mapping(target = "postDate", expression = "java(DateUtil.formatBeforeTime(dto.postDate()))")
@Mapping(target = "postDate", expression = "java(dto.postDate() != null ? DateUtil.formatBeforeTime(dto.postDate()) : null)")
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/record/application/mapper/RecordQueryMapper.java
around line 16, the Mapping expression uses
DateUtil.formatBeforeTime(dto.postDate()) which will NPE if dto.postDate() is
null; update the mapping expression to perform a null check and return null when
postDate is null (e.g. use a conditional/ternary: dto.postDate() != null ?
DateUtil.formatBeforeTime(dto.postDate()) : null) so MapStruct won't call the
formatter with a null value.

@Mapping(target = "postType", source = "dto.postType")
@Mapping(target = "page", source = "dto.page")
@Mapping(target = "userId", source = "dto.userId")
@Mapping(target = "nickName", source = "dto.nickName")
@Mapping(target = "profileImageUrl",source = "dto.profileImageUrl")
@Mapping(target = "content", source = "content")
@Mapping(target = "likeCount", source = "dto.likeCount")
@Mapping(target = "commentCount", source = "dto.commentCount")
@Mapping(target = "isOverview", source = "dto.isOverview")
@Mapping(target = "isLiked", source = "isLiked")
@Mapping(target = "isWriter", source = "isWriter")
@Mapping(target = "isLocked", source = "isLocked")
@Mapping(target = "voteItems", source = "voteItems")
RecordSearchResponse.PostDto toPostDto(
PostQueryDto dto,
String content,
boolean isLiked,
boolean isWriter,
boolean isLocked,
List<RecordSearchResponse.PostDto.VoteItemDto> voteItems
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
import konkuk.thip.common.exception.code.ErrorCode;
import konkuk.thip.common.util.Cursor;
import konkuk.thip.common.util.CursorBasedList;
import konkuk.thip.common.util.DateUtil;
import konkuk.thip.post.application.port.out.PostLikeQueryPort;
import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse;
import konkuk.thip.record.adapter.out.persistence.RecordSearchSortParams;
import konkuk.thip.record.adapter.out.persistence.RecordSearchTypeParams;
import konkuk.thip.record.application.mapper.RecordQueryMapper;
import konkuk.thip.record.application.port.in.dto.RecordSearchQuery;
import konkuk.thip.record.application.port.in.dto.RecordSearchUseCase;
import konkuk.thip.record.application.port.out.RecordQueryPort;
import konkuk.thip.record.application.port.out.dto.PostQueryDto;
import konkuk.thip.record.application.service.validator.RecordAccessValidator;
import konkuk.thip.room.application.port.out.RoomParticipantCommandPort;
import konkuk.thip.room.domain.RoomParticipant;
import konkuk.thip.vote.application.port.out.VoteQueryPort;
Expand All @@ -32,6 +33,7 @@
import java.util.stream.IntStream;

import static konkuk.thip.common.post.PostType.RECORD;
import static konkuk.thip.common.post.PostType.VOTE;

@Slf4j
@Service
Expand All @@ -44,8 +46,10 @@ public class RecordSearchService implements RecordSearchUseCase {
private final PostLikeQueryPort postLikeQueryPort;
private final RoomParticipantCommandPort roomParticipantCommandPort;

private final RecordAccessValidator recordAccessValidator;
private final RecordQueryMapper recordQueryMapper;

private static final int DEFAULT_PAGE_SIZE = 10;
private static final String BLURRED_STRING = "여긴 못 지나가지롱~~";

@Override
@Transactional(readOnly = true)
Expand All @@ -68,27 +72,23 @@ public RecordSearchResponse search(RecordSearchQuery recordSearchQuery) {
// Type에 따라 그룹기록, 내 기록 조회 분기처리
CursorBasedList<PostQueryDto> cursorBasedList = switch(RecordSearchTypeParams.from(recordSearchQuery.type())) {
case GROUP -> {
validateGroupRecordFilters(pageStart, pageEnd, isPageFilter, isOverview, book.getPageCount(), roomParticipant.getUserPercentage());
recordAccessValidator.validateGroupRecordFilters(pageStart, pageEnd, isPageFilter, isOverview, book.getPageCount(), roomParticipant.getUserPercentage());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Boolean null 처리 누락으로 NPE 가능 — null-safe 비교로 수정 필요

isOverviewnull일 경우 if (!isOverview)에서 NPE가 발생합니다. Validator가 선행 검증을 하더라도 이 지점에서 바로 NPE가 날 수 있으므로 null-safe 비교로 바꿔주세요.

적용 diff:

-                if(!isOverview) {
+                if (Boolean.FALSE.equals(isOverview)) {
                     if(pageStart == null) pageStart = 0;
                     if(pageEnd == null) pageEnd = book.getPageCount();
                 }

추가로, 기본값 주입 로직(pageStart, pageEnd)은 Validator로 이관하면 서비스 레이어가 더 단순해집니다.

Also applies to: 78-80

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/record/application/service/RecordSearchService.java
around lines 75 (and also apply same change at 78-80), the direct boolean
unboxing `if (!isOverview)` can throw NPE when isOverview is null; replace those
checks with a null-safe comparison such as `if
(!Boolean.TRUE.equals(isOverview))` (or `if (Boolean.FALSE.equals(isOverview))`
depending on intended semantics) so null won't NPE, and move the defaulting
logic for pageStart/pageEnd into the validator to simplify the service layer.

// 총평 보기가 아닌 경우, pageStart와 pageEnd를 default 값 주입
if(!isOverview) {
if(pageStart == null) {
pageStart = 0;
}
if(pageEnd == null) {
pageEnd = book.getPageCount();
}
if(pageStart == null) pageStart = 0;
if(pageEnd == null) pageEnd = book.getPageCount();
}
yield getGroupRecordBySortParams(recordSearchQuery.sort(), roomId, userId, cursor, pageStart, pageEnd, isOverview);
}
case MINE -> {
validateMyRecordFilters(pageStart, pageEnd, isPageFilter, isOverview, recordSearchQuery.sort());
recordAccessValidator.validateMyRecordFilters(pageStart, pageEnd, isPageFilter, isOverview, recordSearchQuery.sort());
yield recordQueryPort.searchMyRecords(roomId, userId, cursor);
}
Comment on lines +84 to 86
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

MINE 케이스에 정렬(sort) 파라미터가 반영되지 않습니다 — 요구사항 확인 필요

validateMyRecordFilters에서 sort를 검증하지만, 실제 조회는 recordQueryPort.searchMyRecords(...)로 고정되어 정렬이 적용되지 않습니다. 이전 동작이 정렬 지원이었다면 회귀 가능성이 있습니다. 의도라면 Validator에서 sort를 제한(LATEST만 허용 등)하고 API 문서로 명확히 하거나, 아니라면 MINE도 정렬 분기를 추가해야 합니다.

필요시, GROUP과 유사한 정렬 위임 메서드를 도입하는 방안을 고려해 주세요.

아래 스크립트로 포트/리포지토리에 MINE 정렬 메서드 존재 여부를 확인할 수 있습니다.


🏁 Script executed:

#!/bin/bash
# MINE 정렬 관련 메서드 존재 여부 확인
rg -n "searchMyRecords\(" -A 3
rg -n "searchMyRecordsBy" -A 2
rg -n "RecordSearchSortParams" -A 10
fd -a "RecordQueryPort.java" | xargs -I{} rg -n "searchMyRecords" {}
fd -a "RecordQueryRepository" | xargs -I{} rg -n "searchMyRecords" {}

Length of output: 7788


MINE 케이스에 sort 파라미터가 실제로 적용되지 않음 — 구현 수정 필요
validateMyRecordFilters에서만 sort를 검증하고, 실제 호출(recordQueryPort.searchMyRecords)에는 전달되지 않아 정렬이 기본값으로 고정됩니다.
요구사항에 따라 아래 중 하나를 적용해주세요:

  • MINE 케이스에도 GROUP과 유사하게 RecordSearchSortParams.from(sort) 분기 추가
    searchMyRecordsByLatest / searchMyRecordsByLike / searchMyRecordsByComment 등 메서드 추가 및 호출
  • 또는 API 문서·Validator에서 허용 가능한 정렬 조건(LATEST만 허용 등)을 명시하여 sort 파라미터 사용을 제한

확인 위치:

  • src/main/java/konkuk/thip/record/application/service/RecordSearchService.java (84–86줄)
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/record/application/service/RecordSearchService.java
around lines 84–86, the MINE branch validates sort but does not apply it to the
query; update the MINE case to mirror GROUP: convert the incoming sort via
RecordSearchSortParams.from(sort) and branch on that enum to call the
corresponding recordQueryPort methods (e.g., searchMyRecordsByLatest /
searchMyRecordsByLike / searchMyRecordsByComment) passing roomId, userId and
cursor as appropriate; ensure method signatures on recordQueryPort exist or are
added and that the existing validator remains unchanged.

};

// VoteItem 한번에 조회 (투표 게시물에 대한 투표 항목 조회)
Map<Long, List<VoteItemQueryDto>> voteItemQueryMap = voteQueryPort.findVoteItemsByVoteIds(cursorBasedList.contents().stream()
.filter(postQueryDto -> postQueryDto.postType().equals("VOTE"))
.filter(postQueryDto -> postQueryDto.postType().equals(VOTE.getType()))
.map(PostQueryDto::postId)
.collect(Collectors.toSet()), userId);
Comment on lines +91 to 93
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

NPE 위험: equals 호출 순서 통일 필요 (상수-우선 비교)

postQueryDto.postType()가 null이면 NPE가 발생합니다. 아래와 같이 상수-우선 비교로 변경해 주세요. 137라인에서는 이미 상수-우선 비교를 사용 중이므로 스타일도 통일됩니다.

적용 diff:

-                .filter(postQueryDto -> postQueryDto.postType().equals(VOTE.getType()))
+                .filter(postQueryDto -> VOTE.getType().equals(postQueryDto.postType()))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.filter(postQueryDto -> postQueryDto.postType().equals(VOTE.getType()))
.map(PostQueryDto::postId)
.collect(Collectors.toSet()), userId);
.filter(postQueryDto -> VOTE.getType().equals(postQueryDto.postType()))
.map(PostQueryDto::postId)
.collect(Collectors.toSet()), userId);
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/record/application/service/RecordSearchService.java
around lines 91 to 93, the current equals call uses
postQueryDto.postType().equals(VOTE.getType()) which can NPE if postType() is
null; change to the constant-first comparison
VOTE.getType().equals(postQueryDto.postType()) to avoid NPE and match the style
used at line 137, ensuring null-safe comparison and consistent coding style.


Expand All @@ -99,7 +99,17 @@ public RecordSearchResponse search(RecordSearchQuery recordSearchQuery) {

// 게시물 DTO 변환
var postDtos = cursorBasedList.contents().stream()
.map(postQueryDto -> toPostDto(postQueryDto, roomParticipant, userId, voteItemQueryMap, likedPostIds))
.map(dto -> {
boolean isLocked = recordAccessValidator.isLocked(roomParticipant.getCurrentPage(), dto.page());
boolean isWriter = dto.userId().equals(userId);
boolean isLiked = likedPostIds.contains(dto.postId());
String content = isLocked ? recordAccessValidator.createBlurredString(dto.content()) : dto.content();

List<RecordSearchResponse.PostDto.VoteItemDto> voteItems =
getVoteItemDtosIfApplicable(dto, voteItemQueryMap, isLocked);

return recordQueryMapper.toPostDto(dto, content, isLiked, isWriter, isLocked, voteItems);
})
Comment on lines +102 to +112
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

잠재적 NPE 및 소폭의 가독성 개선 제안

  • dto.userId().equals(userId)dto.userId()가 null이면 NPE가 날 수 있습니다. Objects.equals 사용을 권장합니다.
  • createBlurredString(dto.content())dto.content()가 null일 때의 처리 방식을 확인해 주세요. null을 그대로 반환해야 한다면 Validator 메서드가 null-safe임을 보장해야 합니다.

적용 diff:

-                    boolean isWriter = dto.userId().equals(userId);
+                    boolean isWriter = java.util.Objects.equals(dto.userId(), userId);

추가(선택): 파일 상단 import로 대체 가능

import java.util.Objects;

🏁 Script executed:

#!/bin/bash
# RecordSearchService 파일에서 map(dto -> ...) 주변 코드 확인
sed -n '80,140p' src/main/java/konkuk/thip/record/application/service/RecordSearchService.java

# RecordAccessValidator에서 createBlurredString의 null 처리 확인
fd RecordAccessValidator.java | xargs sed -n '1,200p'

Length of output: 6849


Objects.equals로 NPE 방지 제안
dto.userId().equals(userId) 대신 Objects.equals(dto.userId(), userId)를 사용해 NPE를 방지하세요.

createBlurredString()는 내부에서 contents == null일 때 그대로 반환하므로 null-safe가 이미 보장되어 있습니다. 별도 수정은 필요하지 않습니다.

수정 위치:

  • 파일: src/main/java/konkuk/thip/record/application/service/RecordSearchService.java
  • 약 105번째 줄

적용 예시 diff:

-    boolean isWriter = dto.userId().equals(userId);
+    boolean isWriter = Objects.equals(dto.userId(), userId);

선택: 파일 상단에 import 추가

import java.util.Objects;
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/record/application/service/RecordSearchService.java
around lines 102 to 112, replace the null-unsafe equals call
dto.userId().equals(userId) with Objects.equals(dto.userId(), userId) to avoid
possible NPEs, and add the import java.util.Objects at the top of the file.

.toList();

// RecordSearchResponse 생성
Expand All @@ -113,6 +123,7 @@ public RecordSearchResponse search(RecordSearchQuery recordSearchQuery) {
.build();
}

// 그룹 기록을 정렬 파라미터에 따라 조회하는 메서드
private CursorBasedList<PostQueryDto> getGroupRecordBySortParams(String sort, Long roomId, Long userId, Cursor cursor, Integer pageStart, Integer pageEnd, Boolean isOverview) {
return switch(RecordSearchSortParams.from(sort)) {
case LATEST -> recordQueryPort.searchGroupRecordsByLatest(roomId, userId, cursor, pageStart, pageEnd, isOverview);
Expand All @@ -121,29 +132,7 @@ private CursorBasedList<PostQueryDto> getGroupRecordBySortParams(String sort, Lo
};
}

private RecordSearchResponse.PostDto toPostDto(PostQueryDto dto, RoomParticipant participant, Long userId, Map<Long, List<VoteItemQueryDto>> voteItemMap, Set<Long> likedPostIds) {
boolean isLocked = participant.getCurrentPage() < dto.page();
boolean isWriter = dto.userId().equals(userId);
String content = isLocked ? createBlurredString(dto.content()) : dto.content();

return RecordSearchResponse.PostDto.builder()
.postId(dto.postId())
.postDate(DateUtil.formatBeforeTime(dto.postDate()))
.postType(dto.postType())
.page(dto.page())
.userId(dto.userId())
.nickName(dto.nickName())
.profileImageUrl(dto.profileImageUrl())
.content(content)
.likeCount(dto.likeCount())
.commentCount(dto.commentCount())
.isLiked(likedPostIds.contains(dto.postId()))
.isWriter(isWriter)
.isLocked(isLocked)
.voteItems(getVoteItemDtosIfApplicable(dto, voteItemMap, isLocked))
.build();
}

// 투표 게시물인 경우 VoteItem DTO 목록을 생성하는 메서드
private List<RecordSearchResponse.PostDto.VoteItemDto> getVoteItemDtosIfApplicable(PostQueryDto dto, Map<Long, List<VoteItemQueryDto>> voteItemMap, boolean isLocked) {
if (RECORD.getType().equals(dto.postType())) {
return List.of();
Expand All @@ -153,6 +142,7 @@ private List<RecordSearchResponse.PostDto.VoteItemDto> getVoteItemDtosIfApplicab
return mapToVoteItemDtos(items, isLocked);
}

// VoteItemQueryDto 목록을 RecordSearchResponse.PostDto.VoteItemDto 목록으로 변환하는 메서드
private List<RecordSearchResponse.PostDto.VoteItemDto> mapToVoteItemDtos(List<VoteItemQueryDto> items, boolean isLocked) {
// voteCount를 모아 리스트로 변환
List<Integer> counts = items.stream()
Expand All @@ -166,71 +156,12 @@ private List<RecordSearchResponse.PostDto.VoteItemDto> mapToVoteItemDtos(List<Vo
return IntStream.range(0, items.size())
.mapToObj(i -> RecordSearchResponse.PostDto.VoteItemDto.of(
items.get(i).voteItemId(),
isLocked ? createBlurredString(items.get(i).itemName()) : items.get(i).itemName(),
isLocked ? recordAccessValidator.createBlurredString(items.get(i).itemName()) : items.get(i).itemName(),
percentages.get(i),
items.get(i).isVoted()
))
.toList();
}

private String createBlurredString(String contents) {
if (contents == null || contents.isEmpty()) {
return contents;
}

int originalLength = contents.length();
int blurLen = BLURRED_STRING.length();

// 필요한 전체 반복 횟수 계산
int repeat = originalLength / blurLen;

StringBuilder sb = new StringBuilder(originalLength);

// 몫 만큼 반복
for (int i = 0; i < repeat + 1; i++) {
sb.append(BLURRED_STRING);
}

return sb.toString();
}

private void validateGroupRecordFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, int bookPageSize, double currentPercentage) {
if(!isPageFilter && !isOverview) { // 어떤 필터도 적용되지 않는 경우
if (pageStart != null || pageEnd != null) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("어떤 필터도 적용되지 않는 경우 pageStart와 pageEnd는 null이어야 합니다."));
}
}
if(!isPageFilter && isOverview) { // 총평보기 필터만 적용된 경우
if (pageStart != null || pageEnd != null) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("총평보기 필터만 적용된 경우 pageStart와 pageEnd는 null이어야 합니다."));
}
if (currentPercentage < 80) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("총평보기 필터가 적용된 경우 현재 독서 진행률은 80% 이상이어야 합니다."));
}
}
if(isPageFilter && !isOverview) { // 페이지 필터만 적용된 경우는 pageStart와 pageEnd가 null이여도 됨
if(pageStart != null && (pageStart < 0 || pageStart > bookPageSize)) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageStart는 책의 페이지 범위 내에 있어야 합니다."));
}
if(pageEnd != null && (pageEnd < 0 || pageEnd > bookPageSize)) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageEnd는 책의 페이지 범위 내에 있어야 합니다."));
}
if(pageStart != null && pageEnd != null && pageStart > pageEnd) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("pageStart는 pageEnd보다 작아야 합니다."));
}
}
if(isPageFilter && isOverview) { // 페이지 필터와 총평보기 필터가 동시에 적용된 경우
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("페이지 필터와 총평보기 필터는 동시에 적용될 수 없습니다."));
}
}

private void validateMyRecordFilters(Integer pageStart, Integer pageEnd, Boolean isPageFilter, Boolean isOverview, String sort) {
// 모든 파라미터중 하나라도 null이 아닌 경우 예외 발생
if (pageStart != null || pageEnd != null || isPageFilter || isOverview || sort != null) {
throw new BusinessException(ErrorCode.API_INVALID_PARAM, new IllegalArgumentException("내 기록 조회에서는 roomId, type, cursor를 제외한 모든 파라미터는 null이어야 합니다."));
}

}
}


Loading