From 0b38cc70422e47ba4d607ebf2211170fd8e503f0 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:19:10 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[feat]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=A1=B0=ED=9A=8C=20api=20contro?= =?UTF-8?q?ller=20=EA=B0=9C=EB=B0=9C=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/RoomPostQueryController.java | 11 ++++++++++ .../response/AttendanceCheckShowResponse.java | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java index 94f7f01ef..2349613b8 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java @@ -6,8 +6,10 @@ import konkuk.thip.common.dto.BaseResponse; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.swagger.annotation.ExceptionDescription; +import konkuk.thip.roompost.adapter.in.web.response.AttendanceCheckShowResponse; import konkuk.thip.roompost.adapter.in.web.response.RecordPinResponse; import konkuk.thip.roompost.adapter.in.web.response.RoomPostSearchResponse; +import konkuk.thip.roompost.application.port.in.AttendanceCheckShowUseCase; import konkuk.thip.roompost.application.port.in.RecordPinUseCase; import konkuk.thip.roompost.application.port.in.RoomPostSearchUseCase; import konkuk.thip.roompost.application.port.in.dto.record.RecordPinQuery; @@ -28,6 +30,7 @@ public class RoomPostQueryController { private final RoomPostSearchUseCase roomPostSearchUseCase; private final RecordPinUseCase recordPinUseCase; + private final AttendanceCheckShowUseCase attendanceCheckShowUseCase; @Operation( summary = "방의 게시글(기록, 투표) 목록 조회", @@ -81,4 +84,12 @@ public BaseResponse pinRecord( return BaseResponse.ok(recordPinUseCase.pinRecord(new RecordPinQuery(roomId, recordId, userId))); } + @GetMapping("/rooms/{roomId}/daily-greeting") + public BaseResponse showDailyGreeting( + @Parameter(description = "게시글을 조회할 방 ID", example = "1") @PathVariable final Long roomId, + @Parameter(description = "커서 (첫번째 요청시 : null, 다음 요청시 : 이전 요청에서 반환받은 nextCursor 값)") + @RequestParam(required = false) final String cursor, + @Parameter(hidden = true) @UserId final Long userId) { + return BaseResponse.ok(attendanceCheckShowUseCase.showDailyGreeting(userId, roomId, cursor)); + } } diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java new file mode 100644 index 000000000..1f249d18d --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java @@ -0,0 +1,21 @@ +package konkuk.thip.roompost.adapter.in.web.response; + +import java.time.LocalDate; +import java.util.List; + +public record AttendanceCheckShowResponse( + List todayCommentList, + String nextCursor, + boolean isLast +) { + public record AttendanceCheckShowDto( + Long attendanceCheckId, + Long creatorId, + String creatorNickname, + String creatorProfileImageUrl, + String todayComment, + String postDate, + LocalDate date, // 해당 오늘의 한마디 데이터의 작성 날짜 + boolean isWriter + ) { } +} From 1cab733cfa88b2806395f397f1ebcc871b0e3c3b Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:19:26 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[feat]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=A1=B0=ED=9A=8C=20service=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../port/in/AttendanceCheckShowUseCase.java | 8 ++++ .../service/AttendanceCheckShowService.java | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckShowUseCase.java create mode 100644 src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckShowService.java diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckShowUseCase.java b/src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckShowUseCase.java new file mode 100644 index 000000000..d988ae8f0 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/in/AttendanceCheckShowUseCase.java @@ -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); +} diff --git a/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckShowService.java b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckShowService.java new file mode 100644 index 000000000..acd7fb1bf --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/service/AttendanceCheckShowService.java @@ -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 dtos = attendanceCheckQueryPort.findAttendanceChecksByCreatedAtDesc(roomId, cursor); + + // 4. response 로 매핑 후 반환 + return new AttendanceCheckShowResponse( + attendanceCheckQueryMapper.toAttendanceCheckShowResponse(dtos.contents(), userId), + dtos.nextCursor(), + dtos.isLast() + ); + } +} From dbbdba7197ad57ef0725b6855304ba94c3cf706d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:19:53 +0900 Subject: [PATCH 3/7] =?UTF-8?q?[feat]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=A1=B0=ED=9A=8C=20=EC=98=81?= =?UTF-8?q?=EC=86=8D=EC=84=B1=20=EC=BD=94=EB=93=9C=20=EA=B0=9C=EB=B0=9C=20?= =?UTF-8?q?(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceCheckQueryPersistenceAdapter.java | 17 +++++++++++++++++ .../port/out/AttendanceCheckQueryPort.java | 5 +++++ 2 files changed, 22 insertions(+) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java index 779131e78..bd9eb4418 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java @@ -1,12 +1,16 @@ package konkuk.thip.roompost.adapter.out.persistence; +import konkuk.thip.common.util.Cursor; +import konkuk.thip.common.util.CursorBasedList; import konkuk.thip.roompost.adapter.out.mapper.AttendanceCheckMapper; import konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; import konkuk.thip.roompost.application.port.out.AttendanceCheckQueryPort; +import konkuk.thip.roompost.application.port.out.dto.AttendanceCheckQueryDto; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; +import java.util.List; import static konkuk.thip.common.entity.StatusType.ACTIVE; @@ -24,4 +28,17 @@ public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) { return attendanceCheckJpaRepository.countByUserIdAndRoomIdAndCreatedAtBetween(userId, roomId, startOfDay, endOfDay, ACTIVE); } + + @Override + public CursorBasedList findAttendanceChecksByCreatedAtDesc(Long roomId, Cursor cursor) { + LocalDateTime lastCreateAt = cursor.isFirstRequest() ? null : cursor.getLocalDateTime(0); + int size = cursor.getPageSize(); + + List attendanceCheckQueryDtos = attendanceCheckJpaRepository.findAttendanceChecksByCreatedAtDesc(roomId, lastCreateAt, size); + + return CursorBasedList.of(attendanceCheckQueryDtos, size, attendanceCheckQueryDto -> { + Cursor nextCursor = new Cursor(List.of(attendanceCheckQueryDto.createdAt().toString())); + return nextCursor.toEncodedString(); + }); + } } diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckQueryPort.java b/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckQueryPort.java index d86d32f97..529977e3c 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckQueryPort.java +++ b/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckQueryPort.java @@ -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 findAttendanceChecksByCreatedAtDesc(Long roomId, Cursor cursor); } From 67b77acc6fd5bda596649e2f4bc47f72012dd6e1 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:20:36 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[feat]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=A1=B0=ED=9A=8C=20api=20QueryD?= =?UTF-8?q?SL=20=EC=BD=94=EB=93=9C=20=EA=B0=9C=EB=B0=9C=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QueryProjection 도입 --- .../AttendanceCheckJpaRepository.java | 2 +- .../AttendanceCheckQueryRepository.java | 11 ++++ .../AttendanceCheckQueryRepositoryImpl.java | 50 +++++++++++++++++++ .../port/out/dto/AttendanceCheckQueryDto.java | 17 +++++++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepository.java create mode 100644 src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepositoryImpl.java create mode 100644 src/main/java/konkuk/thip/roompost/application/port/out/dto/AttendanceCheckQueryDto.java diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java index 0e7a7fc46..31aa2637a 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java @@ -8,7 +8,7 @@ import java.time.LocalDateTime; -public interface AttendanceCheckJpaRepository extends JpaRepository { +public interface AttendanceCheckJpaRepository extends JpaRepository, AttendanceCheckQueryRepository { // TODO : count 값을 매번 쿼리를 통해 계산하는게 아니라 DB에 저장 or redis 캐시에 저장하는 방법도 좋을 듯 @Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " + diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepository.java new file mode 100644 index 000000000..a0c250171 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepository.java @@ -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 findAttendanceChecksByCreatedAtDesc(Long roomId, LocalDateTime lastCreatedAt, int size); +} diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepositoryImpl.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepositoryImpl.java new file mode 100644 index 000000000..393d3568c --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckQueryRepositoryImpl.java @@ -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 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(); + } +} diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/dto/AttendanceCheckQueryDto.java b/src/main/java/konkuk/thip/roompost/application/port/out/dto/AttendanceCheckQueryDto.java new file mode 100644 index 000000000..fe7176d44 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/port/out/dto/AttendanceCheckQueryDto.java @@ -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 {} +} From f8858174dc62df831adac1efb5b5d97a570177db Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:20:51 +0900 Subject: [PATCH 5/7] =?UTF-8?q?[feat]=20dto=20->=20response=20=EB=A7=A4?= =?UTF-8?q?=ED=8D=BC=20=EB=8F=84=EC=9E=85=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/AttendanceCheckQueryMapper.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/main/java/konkuk/thip/roompost/application/mapper/AttendanceCheckQueryMapper.java diff --git a/src/main/java/konkuk/thip/roompost/application/mapper/AttendanceCheckQueryMapper.java b/src/main/java/konkuk/thip/roompost/application/mapper/AttendanceCheckQueryMapper.java new file mode 100644 index 000000000..922d6b622 --- /dev/null +++ b/src/main/java/konkuk/thip/roompost/application/mapper/AttendanceCheckQueryMapper.java @@ -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())") + @Mapping(target = "isWriter", source = "dto.creatorId", qualifiedByName = "isWriter") + AttendanceCheckShowResponse.AttendanceCheckShowDto toAttendanceCheckShowDto(AttendanceCheckQueryDto dto, @Context Long userId); + + List toAttendanceCheckShowResponse(List dtos, @Context Long userId); + + @Named("isWriter") + default boolean isWriter(Long creatorId, @Context Long userId) { + return creatorId != null && creatorId.equals(userId); + } +} From ff54396199738b809b92d10d430311d5e24eb4aa Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:21:29 +0900 Subject: [PATCH 6/7] =?UTF-8?q?[test]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/AttendanceCheckShowApiTest.java | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java new file mode 100644 index 000000000..b2e096a7b --- /dev/null +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/AttendanceCheckShowApiTest.java @@ -0,0 +1,237 @@ +package konkuk.thip.roompost.adapter.in.web; + +import com.jayway.jsonpath.JsonPath; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.room.adapter.out.jpa.CategoryJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.category.CategoryJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.roompost.adapter.out.jpa.AttendanceCheckJpaEntity; +import konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; +import konkuk.thip.user.adapter.out.jpa.AliasJpaEntity; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.alias.AliasJpaRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.sql.Timestamp; +import java.time.LocalDateTime; + +import static konkuk.thip.common.exception.code.ErrorCode.ROOM_ACCESS_FORBIDDEN; +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 오늘의 한마디 조회 api 통합 테스트") +class AttendanceCheckShowApiTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired private AliasJpaRepository aliasJpaRepository; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private CategoryJpaRepository categoryJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + @Autowired private AttendanceCheckJpaRepository attendanceCheckJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + + @AfterEach + void tearDown() { + attendanceCheckJpaRepository.deleteAllInBatch(); + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAllInBatch(); + bookJpaRepository.deleteAllInBatch(); + categoryJpaRepository.deleteAllInBatch(); + userJpaRepository.deleteAllInBatch(); + aliasJpaRepository.deleteAllInBatch(); + } + + @Test + @DisplayName("방의 출석체크(= 오늘의 한마디) 조회 요청하면, [오늘의 한마디 작성자 정보, 오늘의 한마디 정보] 등을 최신순으로 반환한다.") + void attendance_check_show_test() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + UserJpaEntity user1 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user1")); + + CategoryJpaEntity c0 = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(a0)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); + RoomJpaEntity room = roomJpaRepository.save(TestEntityFactory.createRoom(book, c0)); + + // me, user1 이 room에 참여중인 상황 + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, me, RoomParticipantRole.MEMBER, 0.0)); + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, user1, RoomParticipantRole.MEMBER, 0.0)); + + // me, user1가 room에 오늘의 한마디 작성함 (me : ac1, ac3 작성, user1 : ac2 작성) + AttendanceCheckJpaEntity ac1 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한1", room, me)); + AttendanceCheckJpaEntity ac2 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한2", room, user1)); + AttendanceCheckJpaEntity ac3 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한3", room, me)); + + // ac1 -> ac2 -> ac3 순으로 작성 + LocalDateTime base = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(30)), ac1.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(20)), ac2.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(10)), ac3.getAttendanceCheckId()); + + //when //then + mockMvc.perform(get("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.todayCommentList", hasSize(3))) + // 정렬 순서 : 오늘의 한마디 작성 시각 기준 최신순 + .andExpect(jsonPath("$.data.todayCommentList[0].creatorId", is(me.getUserId().intValue()))) + .andExpect(jsonPath("$.data.todayCommentList[0].todayComment", is("오한3"))) + .andExpect(jsonPath("$.data.todayCommentList[1].creatorId", is(user1.getUserId().intValue()))) + .andExpect(jsonPath("$.data.todayCommentList[1].todayComment", is("오한2"))) + .andExpect(jsonPath("$.data.todayCommentList[2].creatorId", is(me.getUserId().intValue()))) + .andExpect(jsonPath("$.data.todayCommentList[2].todayComment", is("오한1"))); + } + + @Test + @DisplayName("방의 멤버가 아닌 사람이 오늘의 한마디 조회 요청을 보낼 경우, 403 error가 발생한다.") + void attendance_check_show_no_room_member() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + UserJpaEntity user1 = userJpaRepository.save(TestEntityFactory.createUser(a0, "user1")); + + CategoryJpaEntity c0 = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(a0)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); + RoomJpaEntity room = roomJpaRepository.save(TestEntityFactory.createRoom(book, c0)); + + // user1 이 room에 참여중인 상황 (me는 아님) + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, user1, RoomParticipantRole.MEMBER, 0.0)); + + // me, user1가 room에 오늘의 한마디 작성함 (me : ac1, ac3 작성, user1 : ac2 작성) + AttendanceCheckJpaEntity ac2 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한2", room, user1)); + + //when //then + mockMvc.perform(get("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.message", containsString(ROOM_ACCESS_FORBIDDEN.getMessage()))); + } + + @Test + @DisplayName("오늘의 한마디 조회는 작성 시각 기준 최신순 정렬 & 커서 기반 페이지네이션으로 동작한다.") + void attendance_check_show_page_test() throws Exception { + //given + AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); + UserJpaEntity me = userJpaRepository.save(TestEntityFactory.createUser(a0, "me")); + + CategoryJpaEntity c0 = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(a0)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); + RoomJpaEntity room = roomJpaRepository.save(TestEntityFactory.createRoom(book, c0)); + + // user1 이 room에 참여중인 상황 (me는 아님) + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, me, RoomParticipantRole.MEMBER, 0.0)); + + // me가 room에 오늘의 한마디 작성함 + AttendanceCheckJpaEntity ac1 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한1", room, me)); + AttendanceCheckJpaEntity ac2 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한2", room, me)); + AttendanceCheckJpaEntity ac3 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한3", room, me)); + AttendanceCheckJpaEntity ac4 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한4", room, me)); + AttendanceCheckJpaEntity ac5 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한5", room, me)); + AttendanceCheckJpaEntity ac6 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한6", room, me)); + AttendanceCheckJpaEntity ac7 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한7", room, me)); + AttendanceCheckJpaEntity ac8 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한8", room, me)); + AttendanceCheckJpaEntity ac9 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한9", room, me)); + AttendanceCheckJpaEntity ac10 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한10", room, me)); + AttendanceCheckJpaEntity ac11 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한11", room, me)); + AttendanceCheckJpaEntity ac12 = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck("오한12", room, me)); + + // ac1 -> ac2 -> ,,, -> ac12 순으로 작성 + LocalDateTime base = LocalDateTime.now(); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(60)), ac1.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(55)), ac2.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(50)), ac3.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(45)), ac4.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(40)), ac5.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(35)), ac6.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(30)), ac7.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(25)), ac8.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(20)), ac9.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(15)), ac10.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(10)), ac11.getAttendanceCheckId()); + jdbcTemplate.update( + "UPDATE attendance_checks SET created_at = ? WHERE attendancecheck_id = ?", + Timestamp.valueOf(base.minusMinutes(5)), ac12.getAttendanceCheckId()); + + //when //then + MvcResult firstResult = mockMvc.perform(get("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) + .requestAttr("userId", me.getUserId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.todayCommentList", hasSize(10))) + // 정렬 순서 : 오늘의 한마디 작성 시각 기준 최신순 + .andExpect(jsonPath("$.data.todayCommentList[0].todayComment", is("오한12"))) + .andExpect(jsonPath("$.data.todayCommentList[1].todayComment", is("오한11"))) + .andExpect(jsonPath("$.data.todayCommentList[2].todayComment", is("오한10"))) + .andExpect(jsonPath("$.data.todayCommentList[3].todayComment", is("오한9"))) + .andExpect(jsonPath("$.data.todayCommentList[4].todayComment", is("오한8"))) + .andExpect(jsonPath("$.data.todayCommentList[5].todayComment", is("오한7"))) + .andExpect(jsonPath("$.data.todayCommentList[6].todayComment", is("오한6"))) + .andExpect(jsonPath("$.data.todayCommentList[7].todayComment", is("오한5"))) + .andExpect(jsonPath("$.data.todayCommentList[8].todayComment", is("오한4"))) + .andExpect(jsonPath("$.data.todayCommentList[9].todayComment", is("오한3"))) + .andReturn(); + + String responseBody = firstResult.getResponse().getContentAsString(); + String nextCursor = JsonPath.read(responseBody, "$.data.nextCursor"); + + mockMvc.perform(get("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) + .requestAttr("userId", me.getUserId()) + .param("cursor", nextCursor)) // 2페이지 요청 + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.todayCommentList", hasSize(2))) + // 정렬 순서 : 오늘의 한마디 작성 시각 기준 최신순 + .andExpect(jsonPath("$.data.todayCommentList[0].todayComment", is("오한2"))) + .andExpect(jsonPath("$.data.todayCommentList[1].todayComment", is("오한1"))); + } +} From 318913c496a60977e86786347504d962dafa1660 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Sun, 17 Aug 2025 18:33:08 +0900 Subject: [PATCH 7/7] =?UTF-8?q?[docs]=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=ED=95=9C=EB=A7=88=EB=94=94=20=EC=A1=B0=ED=9A=8C=20api=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=84=A4=EB=AA=85=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../swagger/SwaggerResponseDescription.java | 4 ++++ .../in/web/RoomPostQueryController.java | 8 ++++++-- .../response/AttendanceCheckShowResponse.java | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index 03ba61e49..e7d1b2ffb 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -295,6 +295,10 @@ public enum SwaggerResponseDescription { ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED ))), + ATTENDANCE_CHECK_SHOW(new LinkedHashSet<>(Set.of( + ROOM_ACCESS_FORBIDDEN + ))), + ; private final Set errorCodeList; SwaggerResponseDescription(Set errorCodeList) { diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java index 2349613b8..f62435f6e 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/RoomPostQueryController.java @@ -20,8 +20,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import static konkuk.thip.common.swagger.SwaggerResponseDescription.RECORD_PIN; -import static konkuk.thip.common.swagger.SwaggerResponseDescription.RECORD_SEARCH; +import static konkuk.thip.common.swagger.SwaggerResponseDescription.*; @Tag(name = "RoomPost Query API", description = "방 게시글 조회 관련 API") @RestController @@ -84,6 +83,11 @@ public BaseResponse pinRecord( return BaseResponse.ok(recordPinUseCase.pinRecord(new RecordPinQuery(roomId, recordId, userId))); } + @Operation( + summary = "오늘의 한마디 조회", + description = "방 참여자가 오늘의 한마디를 조회합니다." + ) + @ExceptionDescription(ATTENDANCE_CHECK_SHOW) @GetMapping("/rooms/{roomId}/daily-greeting") public BaseResponse showDailyGreeting( @Parameter(description = "게시글을 조회할 방 ID", example = "1") @PathVariable final Long roomId, diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java index 1f249d18d..85edb6ed9 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/AttendanceCheckShowResponse.java @@ -1,11 +1,24 @@ 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 todayCommentList, + + @Schema( + description = "다음 페이지 조회를 위한 커서(없으면 null)" + ) String nextCursor, + boolean isLast ) { public record AttendanceCheckShowDto( @@ -14,8 +27,14 @@ public record AttendanceCheckShowDto( 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 ) { } }