Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bef5696
[feat] VisibleForTesting 어노테이션을 위한 의존성 주입 (#119)
buzz0331 Jul 25, 2025
7633448
[feat] Cursor 객체 도입 (인코딩, 디코딩 담당) (#119)
buzz0331 Jul 25, 2025
1b555b1
[feat] 기록장 조회 api 핸들러 (#119)
buzz0331 Jul 25, 2025
57b8098
[feat] 기록장 조회 api 쿼리 작성 (#119)
buzz0331 Jul 25, 2025
8cf54bc
[feat] 기록장 조회 api 관련 dto 선언 (#119)
buzz0331 Jul 25, 2025
3948164
[feat] 필요한 Enum 타입 선언 (#119)
buzz0331 Jul 25, 2025
58cccf0
[refactor] 필요없는 클래스 제거 (#119)
buzz0331 Jul 25, 2025
d49d428
[feat] 특정 게시물을 사용자가 좋아요 눌렀는지 여부 조회 쿼리 (#119)
buzz0331 Jul 25, 2025
143605e
[feat] Query 조회용 dto (PostQueryDto) (#119)
buzz0331 Jul 25, 2025
68e07e6
[feat] 특정 투표에 대한 투표항목을 가져오는 쿼리 (#119)
buzz0331 Jul 25, 2025
5739d25
[feat] 필요없는 쿼리 제거 (#119)
buzz0331 Jul 25, 2025
2712947
[feat] VoteItem 조회용 Dto (#119)
buzz0331 Jul 25, 2025
df2dda0
[feat] VoteItem 투표 수 비율 구하는 알고리즘 (#119)
buzz0331 Jul 25, 2025
963813e
[test] VoteItem 투표 수 비율 구하는 알고리즘 단위 테스트 (#119)
buzz0331 Jul 25, 2025
6df9a82
[refactor] SearchTypeParams 예외 메시지 수정 (#119)
buzz0331 Jul 25, 2025
8f98b5a
[feat] 기록장 조회 서비스 로직 (#119)
buzz0331 Jul 25, 2025
6382803
[test] 기록장 조회 api 통합 테스트를 위한 update 메서드 (#119)
buzz0331 Jul 25, 2025
01a575e
[test] 기록장 조회 api 통합 테스트 (#119)
buzz0331 Jul 25, 2025
9df0382
[refactor] 블러처리 로직 수정 (#119)
buzz0331 Jul 25, 2025
8973ad2
[refactor] 검증 로직 수정 (#119)
buzz0331 Jul 25, 2025
ebb99fb
[fix] status ACTIVE만 조회되도록 수정 (#119)
buzz0331 Jul 26, 2025
43be9be
[merge] conflict 해결 (#100)
buzz0331 Jul 27, 2025
2adbc00
[refactor] 안쓰는 쿼리 삭제 (#100)
buzz0331 Jul 27, 2025
fd8792a
[refactor] PostType 통합 (#119)
buzz0331 Jul 27, 2025
e1496fb
[refactor] 에러코드 및 sql case 문 수정 (#119)
buzz0331 Jul 27, 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
Expand Up @@ -148,7 +148,7 @@ public enum ErrorCode implements ResponseCode {
/**
* 180000 : Post error
*/
POST_TYPE_NOT_MATCH(HttpStatus.BAD_REQUEST, 180000, "일치하는 게시물 타입 이름이 없습니다. [feed, record, vote] 중 하나여야 합니다."),
POST_TYPE_NOT_MATCH(HttpStatus.BAD_REQUEST, 180000, "일치하는 게시물 타입 이름이 없습니다. [FEED, RECORD, VOTE] 중 하나여야 합니다."),

/**
* 190000 : Comment error
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/konkuk/thip/common/post/PostType.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
@RequiredArgsConstructor
public enum PostType {

FEED("feed"),
RECORD("record"),
VOTE("vote");
FEED("FEED"),
RECORD("RECORD"),
VOTE("VOTE");

private final String type;

Expand Down
82 changes: 82 additions & 0 deletions src/main/java/konkuk/thip/common/util/Cursor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package konkuk.thip.common.util;

import lombok.Getter;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

@Getter
public class Cursor {

private static final String SPLIT_DELIMITER = "\\|";
private static final String JOIN_DELIMITER = "|";
private static final int DEFAULT_PAGE_SIZE = 10;

private final List<String> rawCursorList;
private final int pageSize;
private final boolean isFirstRequest;

// 인코딩용 생성자 (pageSize는 default 사용)
public Cursor(List<String> rawCursorList) {
this(rawCursorList, DEFAULT_PAGE_SIZE);
}

private Cursor(List<String> rawCursorList, int pageSize) {
this.rawCursorList = rawCursorList;
this.pageSize = pageSize;
this.isFirstRequest = rawCursorList.isEmpty();
}

// 디코딩을 위한 정적 팩토리 메서드
public static Cursor from(String encoded, int pageSize) {
if (encoded == null || !encoded.contains("|")) {
return new Cursor(List.of(), pageSize); // 빈 커서 생성
}
String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
List<String> parts = Arrays.asList(decoded.split(SPLIT_DELIMITER));
return new Cursor(parts, pageSize);
}
Comment on lines +36 to +43
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

커서 디코딩 로직의 견고성을 개선해주세요.

현재 encoded.contains("|") 체크만으로는 불완전한 커서 문자열을 정확히 검증하기 어렵습니다.

다음 케이스들을 고려해보세요:

  • 빈 문자열 ""
  • 파이프만 있는 경우 "|" 또는 "||"
  • URL 디코딩 실패 케이스
public static Cursor from(String encoded, int pageSize) {
    if (encoded == null || encoded.trim().isEmpty()) {
        return new Cursor(List.of(), pageSize);
    }
    
    try {
        String decoded = URLDecoder.decode(encoded, StandardCharsets.UTF_8);
        if (decoded.trim().isEmpty()) {
            return new Cursor(List.of(), pageSize);
        }
        List<String> parts = Arrays.asList(decoded.split(SPLIT_DELIMITER));
        return new Cursor(parts, pageSize);
    } catch (Exception e) {
        throw new IllegalArgumentException("잘못된 커서 형식입니다: " + encoded, e);
    }
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/util/Cursor.java around lines 36 to 43,
improve the robustness of the cursor decoding logic by replacing the simple
encoded.contains("|") check with a more comprehensive validation. First, check
if the encoded string is null or empty (after trimming) and return an empty
cursor if so. Then, wrap the URL decoding in a try-catch block to handle
decoding failures gracefully by throwing an IllegalArgumentException with a
clear message. After decoding, verify the decoded string is not empty before
splitting and creating the Cursor. This ensures all edge cases like empty
strings, strings with only pipes, and decoding errors are properly handled.


public String toEncodedString() {
String raw = String.join(JOIN_DELIMITER, rawCursorList);
return URLEncoder.encode(raw, StandardCharsets.UTF_8);
}

public LocalDateTime getLocalDateTime(int index) {
return getAs(index, LocalDateTime::parse, "LocalDateTime");
}

public Long getLong(int index) {
return getAs(index, Long::parseLong, "Long");
}

public Integer getInteger(int index) {
return getAs(index, Integer::parseInt, "Integer");
}

public String getString(int index) {
return get(index);
}

private String get(int index) {
if (index < 0 || index >= rawCursorList.size()) {
throw new IndexOutOfBoundsException("인덱스가 범위를 벗어났습니다: " + index);
}
return rawCursorList.get(index);
}

private <T> T getAs(int index, Function<String, T> parser, String typeName) {
try {
return parser.apply(get(index));
} catch (Exception e) {
throw new IllegalArgumentException(
String.format("커서에서 %s 값을 파싱할 수 없습니다: '%s'", typeName, get(index)), e
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity;
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;

import java.util.Set;

@Repository
public interface PostLikeJpaRepository extends JpaRepository<PostLikeJpaEntity, Long> {
int countByPostJpaEntity_PostId(Long postId);

boolean existsByPostJpaEntity_PostIdAndUserJpaEntity_UserId(Long postId, Long userId);

@Query(value = "SELECT pl.post_id FROM post_likes pl WHERE pl.user_id = :userId AND pl.post_id IN (:postIds)", nativeQuery = true)
Set<Long> findPostIdsLikedByUser(@Param("postIds") Set<Long> postIds,
@Param("userId") Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,16 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.Set;

@Repository
@RequiredArgsConstructor
public class PostLikeQueryPersistenceAdapter implements PostLikeQueryPort {

private final PostLikeJpaRepository postLikeJpaRepository;

@Override
public int countByPostId(Long postId) {
return postLikeJpaRepository.countByPostJpaEntity_PostId(postId);
}

@Override
public boolean existsByPostIdAndUserId(Long postId, Long userId) {
return postLikeJpaRepository.existsByPostJpaEntity_PostIdAndUserJpaEntity_UserId(postId, userId);
public Set<Long> findPostIdsLikedByUser(Set<Long> postIds, Long userId) {
return postLikeJpaRepository.findPostIdsLikedByUser(postIds, userId);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package konkuk.thip.post.application.port.out;

public interface PostLikeQueryPort {
import java.util.Set;

int countByPostId(Long postId);
boolean existsByPostIdAndUserId(Long postId, Long userId);
public interface PostLikeQueryPort {

Set<Long> findPostIdsLikedByUser(Set<Long> postIds, Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import konkuk.thip.common.dto.BaseResponse;
import konkuk.thip.common.security.annotation.UserId;
import konkuk.thip.record.adapter.in.web.response.RecordSearchResponse;
import konkuk.thip.record.application.port.in.dto.RecordSearchQuery;
import konkuk.thip.record.application.port.in.dto.RecordSearchUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -16,18 +17,43 @@ public class RecordQueryController {

private final RecordSearchUseCase recordSearchUseCase;

/**
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

* 방의 게시글(기록, 투표) 목록 조회
* @param roomId
* @param type : group , mine
* @param sort : 그룹 기록 -> 최신순 / 내 기록 -> 페이지 높은 순 default 정렬
* @param pageStart
* @param pageEnd
* @param isOverview : 총평보기 필터 여부
* @param isPageFilter : 페이지 필터 여부
* @param userId
* @return
*/
@GetMapping("/rooms/{roomId}/posts")
public BaseResponse<RecordSearchResponse> viewRecordList(
@PathVariable final Long roomId,
@RequestParam(required = false) final String type,
@RequestParam(required = false, defaultValue = "group") final String type,
@RequestParam(required = false) final String sort,
@RequestParam(required = false) final Integer pageStart,
@RequestParam(required = false) final Integer pageEnd,
@RequestParam final Boolean isOverview,
@RequestParam final Integer pageNum,
@RequestParam(required = false, defaultValue = "false") final Boolean isOverview,
@RequestParam(required = false, defaultValue = "false") final Boolean isPageFilter,
@RequestParam(required = false) final String cursor,
@UserId final Long userId
) {
return BaseResponse.ok(recordSearchUseCase.search(roomId, type, sort, pageStart, pageEnd, isOverview, pageNum, userId));
return BaseResponse.ok(recordSearchUseCase.search(
RecordSearchQuery.builder()
.roomId(roomId)
.type(type)
.sort(sort)
.pageStart(pageStart)
.pageEnd(pageEnd)
.isOverview(isOverview)
.isPageFilter(isPageFilter)
.nextCursor(cursor)
.userId(userId)
.build()
));
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
package konkuk.thip.record.adapter.in.web.response;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Builder;

import java.util.List;

@Builder
public record RecordSearchResponse(
List<RecordSearchResult> recordList,
Integer page,
Integer size,
Boolean first,
Boolean last
List<PostDto> postList,
String nextCursor,
Boolean isLast
){

public static RecordSearchResponse of(List<RecordSearchResult> recordList,
Integer page,
Integer size,
Boolean first,
Boolean last) {
return new RecordSearchResponse(recordList, page, size, first, last);
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = RecordDto.class, name = "RECORD"),
@JsonSubTypes.Type(value = VoteDto.class, name = "VOTE")
})
public sealed interface RecordSearchResult permits RecordDto, VoteDto {
String type();
String postDate();
int page();
Long userId();
String nickName();
String profileImageUrl();
String content();
int likeCount();
int commentCount();
boolean isLiked();
boolean isWriter();
@Builder
public record PostDto(
Long postId,
String postDate,
String postType,
int page,
Long userId,
String nickName,
String profileImageUrl,
String content,
int likeCount,
int commentCount,
boolean isLiked,
boolean isWriter,
boolean isLocked,
List<VoteItemDto> voteItems
) {
public record VoteItemDto(
Long voteItemId,
String itemName,
int percentage,
boolean isVoted
) {
public static VoteItemDto of(Long voteItemId, String itemName, int percentage, boolean isVoted) {
return new VoteItemDto(voteItemId, itemName, percentage, isVoted);
}
}
}
}

This file was deleted.

Loading