-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 오늘의 한마디 조회 api 개발 #242
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
Changes from all commits
0b38cc7
1cab733
dbbdba7
67b77ac
f885817
ff54396
1037a2b
318913c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package konkuk.thip.roompost.adapter.in.web.response; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| import java.time.LocalDate; | ||
| import java.util.List; | ||
|
|
||
| @Schema( | ||
| description = "오늘의 한마디 조회 응답. 작성 시각 기준으로 최신순으로 정렬하여 응답합니다." | ||
| ) | ||
| public record AttendanceCheckShowResponse( | ||
| @Schema( | ||
| description = "오늘의 한마디 목록, 10개씩 끊어서 응답합니다." | ||
| ) | ||
| List<AttendanceCheckShowDto> todayCommentList, | ||
|
|
||
| @Schema( | ||
| description = "다음 페이지 조회를 위한 커서(없으면 null)" | ||
| ) | ||
| String nextCursor, | ||
|
|
||
| boolean isLast | ||
| ) { | ||
| public record AttendanceCheckShowDto( | ||
| Long attendanceCheckId, | ||
| Long creatorId, | ||
| String creatorNickname, | ||
| String creatorProfileImageUrl, | ||
| String todayComment, | ||
|
|
||
| @Schema(description = "작성 시각(상대 시간 등 가공된 문자열)", example = "5분 전") | ||
| String postDate, | ||
|
|
||
| @Schema(description = "작성 날짜(yyyy-MM-dd), 이걸로 날짜별로 끊어서 화면에 보여주시면 됩니다.", example = "2025-08-17") | ||
| LocalDate date, // 해당 오늘의 한마디 데이터의 작성 날짜 | ||
|
|
||
| @Schema(description = "현재 사용자가 해당 글을 작성했는지 여부") | ||
| boolean isWriter | ||
| ) { } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck; | ||
|
|
||
| import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| public interface AttendanceCheckQueryRepository { | ||
|
|
||
| List<AttendanceCheckQueryDto> findAttendanceChecksByCreatedAtDesc(Long roomId, LocalDateTime lastCreatedAt, int size); | ||
| } | ||
|
Comment on lines
+10
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain페이지네이션 안정성(배타 경계·타이브레이커) 명시 필요
다음 스크립트로 구현체에서 배타 경계( 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) 커스텀 구현체 확인
fd -t f 'AttendanceCheckQueryRepositoryImpl.java' | while read -r file; do
echo "== $file =="
rg -n -C3 -e 'findAttendanceChecksByCreatedAtDesc' -e 'where' -e 'status' -e 'createdAt' -e 'order' "$file" || sed -n '1,200p' "$file"
done
# 2) JPA 리포지토리에 커스텀 인터페이스가 혼합되어 있는지 확인
fd -t f 'AttendanceCheckJpaRepository.java' | while read -r file; do
echo "== $file =="
sed -n '1,200p' "$file"
doneLength of output: 3628 페이지네이션 안정성: 배타 경계 확인됐으나 2차 정렬 키·상태 필터 누락 현재 QueryDSL 구현체에서
수정 제안:
// 기존
.where(roomPredicate, cursorPredicate)
.orderBy(
attendanceCheck.createdAt.desc()
)
// 제안
.where(
roomPredicate,
cursorPredicate,
+ attendanceCheck.status.eq(StatusType.ACTIVE)
)
.orderBy(
attendanceCheck.createdAt.desc(),
+ attendanceCheck.attendanceCheckId.desc()
)AttendanceCheckQueryRepository.java 메소드 Javadoc 예시: /**
* roomId에 해당하는 출석 체크를 createdAt 내림차순으로 조회합니다.
* • 배타경계(cursor): createdAt < :lastCreatedAt
* • 2차정렬(stable sort): attendanceCheckId DESC
* • 상태필터: status = ACTIVE
* @param roomId 조회할 방 ID
* @param lastCreatedAt 커서(Null 시 최신부터 조회)
* @param size 페이지당 개수
*/
List<AttendanceCheckQueryDto> findAttendanceChecksByCreatedAtDesc(...);🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck; | ||
|
|
||
| import com.querydsl.core.types.dsl.BooleanExpression; | ||
| import com.querydsl.jpa.impl.JPAQueryFactory; | ||
| import konkuk.thip.roompost.adapter.out.jpa.QAttendanceCheckJpaEntity; | ||
| import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; | ||
| import konkuk.thip.roompost.application.port.out.dto.QAttendanceCheckQueryDto; | ||
| import konkuk.thip.user.adapter.out.jpa.QAliasJpaEntity; | ||
| import konkuk.thip.user.adapter.out.jpa.QUserJpaEntity; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.time.LocalDateTime; | ||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class AttendanceCheckQueryRepositoryImpl implements AttendanceCheckQueryRepository { | ||
|
|
||
| private final JPAQueryFactory jpaQueryFactory; | ||
|
|
||
| @Override | ||
| public List<AttendanceCheckQueryDto> findAttendanceChecksByCreatedAtDesc(Long roomId, LocalDateTime lastCreatedAt, int size) { | ||
| QAttendanceCheckJpaEntity attendanceCheck = QAttendanceCheckJpaEntity.attendanceCheckJpaEntity; | ||
| QUserJpaEntity user = QUserJpaEntity.userJpaEntity; | ||
| QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity; | ||
|
|
||
| BooleanExpression roomPredicate = attendanceCheck.roomJpaEntity.roomId.eq(roomId); | ||
| BooleanExpression cursorPredicate = (lastCreatedAt == null) ? null : attendanceCheck.createdAt.lt(lastCreatedAt); | ||
|
|
||
| return jpaQueryFactory | ||
| .select(new QAttendanceCheckQueryDto( | ||
| attendanceCheck.attendanceCheckId, | ||
| user.userId, | ||
| user.nickname, | ||
| alias.imageUrl, | ||
| attendanceCheck.todayComment, | ||
| attendanceCheck.createdAt | ||
| )) | ||
| .from(attendanceCheck) | ||
| .join(attendanceCheck.userJpaEntity, user) | ||
| .join(user.aliasForUserJpaEntity, alias) | ||
| .where(roomPredicate, cursorPredicate) | ||
| .orderBy( | ||
| attendanceCheck.createdAt.desc() | ||
| ) | ||
| .limit(size + 1) // 다음 페이지 존재 여부를 확인하기 위해 | ||
| .fetch(); | ||
| } | ||
|
Comment on lines
+22
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오호 가독성 굿굿입니다!! |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package konkuk.thip.roompost.application.mapper; | ||
|
|
||
| import konkuk.thip.common.util.DateUtil; | ||
| import konkuk.thip.roompost.adapter.in.web.response.AttendanceCheckShowResponse; | ||
| import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; | ||
| import org.mapstruct.*; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Mapper( | ||
| componentModel = "spring", | ||
| imports = DateUtil.class, | ||
| unmappedTargetPolicy = ReportingPolicy.ERROR | ||
| ) | ||
| public interface AttendanceCheckQueryMapper { | ||
|
|
||
| @Mapping(target = "postDate", expression = "java(DateUtil.formatBeforeTime(dto.createdAt()))") | ||
| @Mapping(target = "date", expression = "java(dto.createdAt().toLocalDate())") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 굿입니다 |
||
| @Mapping(target = "isWriter", source = "dto.creatorId", qualifiedByName = "isWriter") | ||
| AttendanceCheckShowResponse.AttendanceCheckShowDto toAttendanceCheckShowDto(AttendanceCheckQueryDto dto, @Context Long userId); | ||
|
|
||
| List<AttendanceCheckShowResponse.AttendanceCheckShowDto> toAttendanceCheckShowResponse(List<AttendanceCheckQueryDto> dtos, @Context Long userId); | ||
|
|
||
| @Named("isWriter") | ||
| default boolean isWriter(Long creatorId, @Context Long userId) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 굿굿 |
||
| return creatorId != null && creatorId.equals(userId); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package konkuk.thip.roompost.application.port.in; | ||
|
|
||
| import konkuk.thip.roompost.adapter.in.web.response.AttendanceCheckShowResponse; | ||
|
|
||
| public interface AttendanceCheckShowUseCase { | ||
|
|
||
| AttendanceCheckShowResponse showDailyGreeting(Long userId, Long roomId, String cursorStr); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| package konkuk.thip.roompost.application.port.out; | ||
|
|
||
| import konkuk.thip.common.util.Cursor; | ||
| import konkuk.thip.common.util.CursorBasedList; | ||
| import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; | ||
|
|
||
| public interface AttendanceCheckQueryPort { | ||
|
|
||
| int countAttendanceChecksOnTodayByUser(Long userId, Long roomId); | ||
|
|
||
| CursorBasedList<AttendanceCheckQueryDto> findAttendanceChecksByCreatedAtDesc(Long roomId, Cursor cursor); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| package konkuk.thip.roompost.application.port.out.dto; | ||
|
|
||
| import com.querydsl.core.annotations.QueryProjection; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public record AttendanceCheckQueryDto( | ||
| Long attendanceCheckId, | ||
| Long creatorId, | ||
| String creatorNickname, | ||
| String creatorProfileImageUrl, | ||
| String todayComment, | ||
| LocalDateTime createdAt | ||
| ) { | ||
| @QueryProjection | ||
| public AttendanceCheckQueryDto {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| package konkuk.thip.roompost.application.service; | ||
|
|
||
| import konkuk.thip.common.util.Cursor; | ||
| import konkuk.thip.common.util.CursorBasedList; | ||
| import konkuk.thip.room.application.service.validator.RoomParticipantValidator; | ||
| import konkuk.thip.roompost.adapter.in.web.response.AttendanceCheckShowResponse; | ||
| import konkuk.thip.roompost.application.mapper.AttendanceCheckQueryMapper; | ||
| import konkuk.thip.roompost.application.port.in.AttendanceCheckShowUseCase; | ||
| import konkuk.thip.roompost.application.port.out.AttendanceCheckQueryPort; | ||
| import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AttendanceCheckShowService implements AttendanceCheckShowUseCase { | ||
|
|
||
| private static final int PAGE_SIZE = 10; | ||
| private final RoomParticipantValidator roomParticipantValidator; | ||
| private final AttendanceCheckQueryPort attendanceCheckQueryPort; | ||
| private final AttendanceCheckQueryMapper attendanceCheckQueryMapper; | ||
|
|
||
| @Override | ||
| @Transactional(readOnly = true) | ||
| public AttendanceCheckShowResponse showDailyGreeting(Long userId, Long roomId, String cursorStr) { | ||
| // 1. 유저가 방 멤버가 맞는지 검사 | ||
| roomParticipantValidator.validateUserIsRoomMember(roomId, userId); | ||
|
|
||
| // 2. Cursor 생성 | ||
| Cursor cursor = Cursor.from(cursorStr, PAGE_SIZE); | ||
|
|
||
| // 3. 오늘의 한마디 조회 | ||
| CursorBasedList<AttendanceCheckQueryDto> dtos = attendanceCheckQueryPort.findAttendanceChecksByCreatedAtDesc(roomId, cursor); | ||
|
|
||
| // 4. response 로 매핑 후 반환 | ||
| return new AttendanceCheckShowResponse( | ||
| attendanceCheckQueryMapper.toAttendanceCheckShowResponse(dtos.contents(), userId), | ||
| dtos.nextCursor(), | ||
| dtos.isLast() | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
친절하시네여 LGTM