-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 오늘의 한마디 작성 api 개발 #180
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
Conversation
Walkthrough오늘의 한마디(AttendanceCheck) 작성 기능을 신설하고, 관련 코드를 room 모듈로 재구성했습니다. 엔드포인트 추가, 입력/출력 DTO, 유스케이스/서비스, 도메인 검증(일일 작성 횟수 제한), 퍼시스턴스 어댑터/리포지토리, 스웨거/에러코드, 테스트가 추가되었고 기존 attendancecheck 패키지의 빈/더미 코드는 제거되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Controller as RoomCommandController
participant UseCase as AttendanceCheckCreateUseCase
participant Service as AttendanceCheckCreateService
participant Validator as RoomParticipantValidator
participant QueryPort as AttendanceCheckQueryPort
participant Domain as AttendanceCheck
participant CmdPort as AttendanceCheckCommandPort
participant Repo as AttendanceCheckJpaRepository
Client->>Controller: POST /rooms/{roomId}/daily-greeting (content)
Controller->>UseCase: create(command)
UseCase->>Service: delegate
Service->>Validator: validateMember(creatorId, roomId)
Service->>QueryPort: countToday(userId, roomId)
QueryPort->>Repo: countByUserIdAndRoomIdAndCreatedAtBetween(...)
Repo-->>QueryPort: count
Service->>Domain: withoutId(roomId, creatorId, content, count)
Service->>CmdPort: save(attendanceCheck)
CmdPort->>Repo: save(entity)
Repo-->>CmdPort: id
Service-->>UseCase: AttendanceCheckCreateResult
UseCase-->>Controller: result
Controller-->>Client: BaseResponse(AttendanceCheckCreateResponse)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes(해당 없음) Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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.
Actionable comments posted: 3
🧹 Nitpick comments (13)
src/main/java/konkuk/thip/attendancecheck/domain/AttendanceCheck.java (1)
19-25: 도메인 불변식(Null/Blank) 방어 로직을 팩토리 메서드에 포함하는 것을 권장합니다.컨트롤러/서비스에서 1차 검증을 하더라도, 도메인 레벨에서 최소한의 불변식을 수호하면 결합도/변경 내성 면에서 안전합니다.
public static AttendanceCheck withoutId(Long roomId, Long creatorId, String todayComment) { - return AttendanceCheck.builder() + if (roomId == null) throw new IllegalArgumentException("roomId must not be null"); + if (creatorId == null) throw new IllegalArgumentException("creatorId must not be null"); + String trimmed = todayComment == null ? null : todayComment.trim(); + if (trimmed == null || trimmed.isEmpty()) { + throw new IllegalArgumentException("todayComment must not be blank"); + } + return AttendanceCheck.builder() .roomId(roomId) .creatorId(creatorId) - .todayComment(todayComment) + .todayComment(trimmed) .build(); }추가로, 메서드명이 withoutId인 점은 의도는 명확하지만, 프로젝트 전반의 팩토리 네이밍 컨벤션(of/newWithoutId 등)이 있다면 일치 여부 확인을 권장합니다.
src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/AttendanceCheckCreateCommand.java (1)
3-7: 필드명 일관성 정비 제안: content → todayComment도메인(AttendanceCheck.todayComment)과 명칭을 맞추면 계층 간 매핑 혼선을 줄이고 가독성이 좋아집니다.
public record AttendanceCheckCreateCommand( Long creatorId, Long roomId, - String content + String todayComment ) { }이 변경은 서비스/컨트롤러/요청 DTO의 toCommand 매핑에도 반영되어야 합니다.
src/main/java/konkuk/thip/attendancecheck/application/port/in/AttendanceCheckCreateUseCase.java (1)
5-8: 간단한 Javadoc 추가 제안메서드 파라미터/반환 의미, 예외 케이스(권한 없음 등)를 인터페이스 수준에서 문서화하면 구현/테스트 가이드라인 정합성이 좋아집니다.
/** * 오늘의 한마디를 생성한다. * @param command 작성자/방/내용 정보를 담은 커맨드 * @return 생성된 AttendanceCheck의 ID * @throws RoomAccessForbiddenException 방 참여자가 아닌 경우 * @throws RoomNotFoundException 방이 존재하지 않는 경우 * @throws UserNotFoundException 사용자 미존재 등 */ Long create(AttendanceCheckCreateCommand command);src/main/java/konkuk/thip/attendancecheck/adapter/in/web/response/AttendanceCheckCreateResponse.java (1)
3-8: 응답 DTO에 Swagger 메타데이터를 추가하고, ID의 null 불가를 코드로 보장하는 것을 권장합니다.
- 문서 품질: 클래스/필드에
@Schema를 추가해 예시/설명을 노출하면 Swagger UI 가독성이 좋아집니다.- 무결성: ID는 생성 성공 시 항상 존재하므로
Long대신long으로 null 불가를 보장하는 편이 안전합니다.of()정적 팩토리는 취향/컨벤션 문제라 유지해도 무방합니다.적용 예시:
- public record AttendanceCheckCreateResponse( - Long attendanceCheckId - ) { - public static AttendanceCheckCreateResponse of(Long attendanceCheckId) { + @Schema(name = "AttendanceCheckCreateResponse", description = "오늘의 한마디 생성 결과") + public record AttendanceCheckCreateResponse( + @Schema(description = "생성된 오늘의 한마디 ID", example = "123") + long attendanceCheckId + ) { + public static AttendanceCheckCreateResponse of(long attendanceCheckId) { return new AttendanceCheckCreateResponse(attendanceCheckId); } }추가 import:
import io.swagger.v3.oas.annotations.media.Schema;src/main/java/konkuk/thip/attendancecheck/application/service/AttendanceCheckCreateService.java (2)
21-28: 비즈니스 제약(1일 1회 작성 여부) 확인 및 중복 방지 로직/유니크 인덱스 고려를 권장합니다.
- 오늘의 한마디/출석체크 성격상 보통 “하루 1회” 제약이 존재합니다. 해당 제약이 있다면:
- 서비스 레벨에서 KST 기준
exists프리체크(예:AttendanceCheckQueryPort.existsForUserOnDate(roomId, userId, LocalDate.now(ZoneId.of("Asia/Seoul"))))를 수행- DB 레벨에서
(room_id, creator_id, date)유니크 인덱스로 최종 보장- 도메인 무결성:
AttendanceCheck.withoutId(...)내부에서 핵심 제약(내용 길이 등)을 한 번 더 방어적으로 검증하면 계층 간 일관성이 높아집니다.원하시면 Port/Repo/엔티티/테스트까지 포함한 중복 방지 구현 스켈레톤을 드리겠습니다.
23-24: Room 참가자 검증 컴포넌트의 계층/네이밍 합치성 점검이 프로젝트에서는 이전 PR에서 “RoomParticipantPolicy 도메인 서비스로 캡슐화”하는 방식을 선호한다고 학습되어 있습니다. 현재
RoomParticipantValidator(application.service.validator) 사용이 팀 컨벤션과 일치하는지 확인 부탁드립니다. 정책 객체가 이미 존재한다면 해당 의존성으로 교체를 고려해 주세요.(학습사항 반영) 도메인 정책 캡슐화 선호
src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java (1)
29-35: 불필요한 2회 SELECT 제거(getReferenceById)로 write-path 최적화 제안현재
findById로 Room/User를 조회해 2회의 SELECT가 발생합니다. 상위 계층(Validator/Policy)에서 이미 존재/참여 검증이 끝났다면, 여기서는 JPA 프록시만 참조해 INSERT 때 FK만 설정하면 충분합니다.
- 장점: 왕복 쿼리 2회 제거 → TPS/지연시간 개선
- 주의: 존재하지 않을 때 커스텀 에러코드 매핑이 필요하면 현 구현이 더 명확합니다.
- RoomJpaEntity roomJpaEntity = roomJpaRepository.findById(attendanceCheck.getRoomId()).orElseThrow( - () -> new EntityNotFoundException(ROOM_NOT_FOUND) - ); - - UserJpaEntity userJpaEntity = userJpaRepository.findById(attendanceCheck.getCreatorId()).orElseThrow( - () -> new EntityNotFoundException(USER_NOT_FOUND) - ); + RoomJpaEntity roomJpaEntity = roomJpaRepository.getReferenceById(attendanceCheck.getRoomId()); + UserJpaEntity userJpaEntity = userJpaRepository.getReferenceById(attendanceCheck.getCreatorId());(팀 컨벤션에 따라) 상위 Validator가 존재/권한을 이미 보장한다면 위 최적화를 적용하고, 아니라면 현재 방식 유지가 타당합니다.
(학습사항 반영) 성능 최적화에 대한 명확한 이점 제시를 선호하셔서 제안드립니다.src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateControllerTest.java (1)
29-43: 부정 케이스 확장: 공백/널/미존재 필드 파라미터화 테스트 추가 제안현 테스트는
""만 검증합니다.@NotBlank특성상" ","\t\n", (필드 누락→null) 등도 400이어야 하므로 파라미터화 해두면 회귀 방지에 유용합니다. 성공 케이스는 통합 테스트로 커버 중이므로 이 파일엔 부정 케이스 집중이 적절합니다.예시:
@ParameterizedTest @NullAndEmptySource @ValueSource(strings = {" ", " ", "\t", "\n", "\r\n"}) @DisplayName("오늘의 한마디 content가 공백/널/미입력인 경우 400 반환") void attendance_check_fail_content_blank_param(String value) throws Exception { AttendanceCheckCreateRequest request = new AttendanceCheckCreateRequest(value); mockMvc.perform(post("/rooms/{roomId}/daily-greeting", 1L) .requestAttr("userId", 1L) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())); }필요 import:
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource;src/main/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCommandController.java (2)
30-37: 메서드명(createFeed)과 도메인/엔드포인트 불일치 — 의미가 드러나는 이름으로 변경 권장AttendanceCheck를 생성하는 핸들러인데 feed라는 명칭은 오해 소지가 큽니다. 유지보수/검색성 측면에서 createAttendanceCheck(또는 createDailyGreeting)로 변경을 권장합니다.
- public BaseResponse<AttendanceCheckCreateResponse> createFeed( + public BaseResponse<AttendanceCheckCreateResponse> createAttendanceCheck(
29-31: POST 생성 API는 201 Created와 명시적 consumes/produces 선언을 권장REST 생성 규약에 맞춰 201 반환과 콘텐츠 협상 명시(consumes/produces)를 제안합니다. 핸들러 매핑의 애매함을 줄이고, 클라이언트/프록시 캐싱 정책 및 문서 일관성을 높일 수 있습니다. 팀 표준이 POST=200 OK라면 그대로 유지해도 무방하니 확인 부탁드립니다.
- @PostMapping("/rooms/{roomId}/daily-greeting") + @PostMapping( + value = "/rooms/{roomId}/daily-greeting", + consumes = MediaType.APPLICATION_JSON_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE + ) + @ResponseStatus(HttpStatus.CREATED) - public BaseResponse<AttendanceCheckCreateResponse> createFeed( + public BaseResponse<AttendanceCheckCreateResponse> createAttendanceCheck(추가 import(파일 상단)도 필요합니다:
import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.ResponseStatus;테스트도 상태 코드(200→201)와 Content-Type 검증 추가로 함께 맞춰드릴 수 있습니다. 원하시면 수정 패치 제안 드리겠습니다.
src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateApiTest.java (3)
85-90: roomId의 int 캐스팅 제거 및 응답 Content-Type 검증 추가 제안
- Long → int 캐스팅은 불필요하며(잠재적 오버플로 여지), 그대로 Long을 전달하면 됩니다.
- 성공 응답의 Content-Type 검증을 추가하면 회귀 방지에 유용합니다.
- ResultActions result = mockMvc.perform(post("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) + ResultActions result = mockMvc.perform(post("/rooms/{roomId}/daily-greeting", room.getRoomId()) .requestAttr("userId", me.getUserId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON));추가 static import가 필요합니다:
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
68-82: 중복된 given 블록 헬퍼 메서드로 추출하여 테스트 가독성/재사용성 개선두 테스트 모두 유사한 엔티티 준비 로직을 갖고 있습니다. 팩토리/헬퍼로 추출하면 테스트 본문이 의도 중심으로 간결해집니다.
예시:
private RoomJpaEntity createRoomWithParticipant(UserJpaEntity user) { AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); CategoryJpaEntity c0 = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(a0)); BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); RoomJpaEntity room = roomJpaRepository.save(TestEntityFactory.createRoom(book, c0)); roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, user, RoomParticipantRole.MEMBER, 0.0)); return room; } private RoomJpaEntity createRoomWithoutParticipant() { AliasJpaEntity a0 = aliasJpaRepository.save(TestEntityFactory.createScienceAlias()); CategoryJpaEntity c0 = categoryJpaRepository.save(TestEntityFactory.createScienceCategory(a0)); BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); return roomJpaRepository.save(TestEntityFactory.createRoom(book, c0)); }
85-90: (선택) Controller에서 201 Created로 변경 시 테스트 기대값도 함께 변경 필요API를 201로 바꾸는 경우 아래와 같이 수정해 주세요. 팀 표준이 200이라면 본 제안은 무시하셔도 됩니다.
- .andExpect(status().isOk()) + .andExpect(status().isCreated()) .andExpect(content().contentType(MediaType.APPLICATION_JSON));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/main/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCommandController.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/in/web/request/AttendanceCheckCreateRequest.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/in/web/request/UserSignupRequest.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/in/web/response/AttendanceCheckCreateResponse.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/in/web/response/DummyResponse.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/in/AttendanceCheckCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/in/DummyUseCase.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/AttendanceCheckCreateCommand.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/DummyCommand.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckCommandPort.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/application/service/AttendanceCheckCreateService.java(1 hunks)src/main/java/konkuk/thip/attendancecheck/application/service/AttendanceCheckService.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/domain/AttendanceCheck.java(1 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateApiTest.java(1 hunks)src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateControllerTest.java(1 hunks)
💤 Files with no reviewable changes (5)
- src/main/java/konkuk/thip/attendancecheck/application/port/in/DummyUseCase.java
- src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/DummyCommand.java
- src/main/java/konkuk/thip/attendancecheck/adapter/in/web/request/UserSignupRequest.java
- src/main/java/konkuk/thip/attendancecheck/adapter/in/web/response/DummyResponse.java
- src/main/java/konkuk/thip/attendancecheck/application/service/AttendanceCheckService.java
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#166
File: src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java:70-82
Timestamp: 2025-08-07T18:19:55.856Z
Learning: seongjunnoh는 Clean Architecture 원칙을 중시하며, 어댑터는 순수하게 저장/조회 기능만 담당하고 비즈니스 로직은 서비스 레이어에서 처리하는 것을 선호한다.
📚 Learning: 2025-07-03T03:05:05.031Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#43
File: src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java:0-0
Timestamp: 2025-07-03T03:05:05.031Z
Learning: THIP 프로젝트에서는 CQRS Port 분리 시 다음 컨벤션을 따름: CommandPort에는 findByXXX를 통해 도메인 엔티티를 찾아오는 메서드를 추가하고, QueryPort에는 조회 API의 response에 해당하는 데이터들을 DB로부터 조회하는 메서드를 추가함.
Applied to files:
src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckCommandPort.java
📚 Learning: 2025-07-26T06:09:00.850Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#101
File: src/main/java/konkuk/thip/comment/application/service/CommentCreateService.java:36-39
Timestamp: 2025-07-26T06:09:00.850Z
Learning: THIP 프로젝트에서는 "사용자가 방에 속하는지 검증" 로직을 RoomParticipantPolicy 도메인 서비스로 캡슐화하여 재사용성을 높이고 비즈니스 로직의 중복을 방지하는 방식을 선호한다.
Applied to files:
src/main/java/konkuk/thip/attendancecheck/application/service/AttendanceCheckCreateService.java
📚 Learning: 2025-07-23T17:41:55.507Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#101
File: src/test/java/konkuk/thip/comment/adapter/in/web/CommentControllerTest.java:118-265
Timestamp: 2025-07-23T17:41:55.507Z
Learning: CommentControllerTest는 댓글 생성 API의 검증 로직과 예외 상황만을 테스트하는 단위 테스트이며, 성공 케이스는 별도의 통합 테스트(CommentCreateAPITest)에서 다룬다.
Applied to files:
src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateControllerTest.javasrc/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateApiTest.java
📚 Learning: 2025-07-08T16:30:33.771Z
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#57
File: src/test/java/konkuk/thip/room/domain/RoomTest.java:0-0
Timestamp: 2025-07-08T16:30:33.771Z
Learning: Room 도메인에서 startDate는 현재 날짜 이후여야 하는 도메인 규칙이 있어서, 테스트에서 만료된 상태를 시뮬레이션하려면 reflection을 사용해야 한다.
Applied to files:
src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateApiTest.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckCommandPort.java (2)
7-7: Port 시그니처 적절. 응답 요구사항만 재확인 부탁드립니다.Long(ID) 반환은 커맨드 포트에서 흔한 선택이며 CQRS 분리에 부합합니다. 다만 API 응답에 생성 시각 등 추가 메타가 필요하다면 도메인 반환(또는 ID와 메타를 담은 VO)도 고려 대상입니다.
API 응답 스펙 상 ID 외 필드가 필요 없는지 확인해 주세요. 필요 시 Port/서비스에서 반환 타입을 조정하는 편이 깔끔합니다.
7-7: LGTM현재 요구사항(식별자 반환) 기준으로는 시그니처가 간결하고 충분합니다.
src/test/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCreateApiTest.java (1)
112-119: 비참여자 403 시나리오 검증 케이스 적절합니다예외 코드/메시지까지 검증해 회귀 위험을 낮추고 있습니다. 이 부분은 그대로 유지해도 좋습니다.
| public record AttendanceCheckCreateRequest( | ||
| @Schema(description = "유저가 작성한 오늘의 한마디 내용") | ||
| @NotBlank(message = "오늘의 한마디 내용은 필수입니다.") | ||
| String content | ||
| ) { | ||
| public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) { | ||
| return new AttendanceCheckCreateCommand(creatorId, roomId, content); | ||
| } |
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.
💡 Verification agent
🧩 Analysis chain
content 정규화(strip)와 길이 제한(@SiZe) 추가를 권장합니다.
- 정규화:
toCommand에서content.strip()으로 전후 공백을 제거해 저장 값을 표준화하세요. - 길이 제한: DB 컬럼 길이/도메인 정책에 맞춰
@Size(max=...)를 추가하면 과도한 입력과 저장 실패를 예방합니다. - 문서화:
@Schema(requiredMode = REQUIRED, example = "...")로 Swagger 명세를 보강하세요.
도메인/DB 제약 확인 후 max 값을 결정하세요.
public record AttendanceCheckCreateRequest(
- @Schema(description = "유저가 작성한 오늘의 한마디 내용")
- @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
- String content
+ @Schema(description = "유저가 작성한 오늘의 한마디 내용", requiredMode = Schema.RequiredMode.REQUIRED, example = "오늘도 파이팅!")
+ @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
+ @Size(max = 200, message = "오늘의 한마디 내용은 최대 200자까지 입력 가능합니다.") // max 값은 DB/정책에 맞게 조정
+ String content
) {
public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) {
- return new AttendanceCheckCreateCommand(creatorId, roomId, content);
+ return new AttendanceCheckCreateCommand(creatorId, roomId, content.strip());
}
}추가 import:
import jakarta.validation.constraints.Size;
import io.swagger.v3.oas.annotations.media.Schema;DB/엔티티 제약 자동 확인 스크립트:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[1] AttendanceCheck JPA 엔티티 내 content 컬럼 길이/유니크 제약 탐색"
fd -a "AttendanceCheckJpaEntity.java" | while read f; do
echo "=== Inspecting $f ==="
rg -n "class |@Table|@Column|content|length|unique|UniqueConstraint" "$f" || true
done
echo
echo "[2] 저장소/매퍼에서 content 관련 처리 탐색(트리밍/정규화 여부)"
rg -n "AttendanceCheck.*Mapper|toJpaEntity|strip|trim|content" || trueLength of output: 109462
AttendanceCheckCreateRequest: content 필드 정규화(strip) 및 길이 제한(@SiZe) 추가 제안
DB JPA 엔티티에서 @Column(name = "today_comment", length = 100, nullable = false) 제약을 확인했습니다. 이에 따라 다음 사항을 적용해주세요.
-
필드 검증 강화
@Size(max = 100, message = "오늘의 한마디 내용은 최대 100자까지 입력 가능합니다.")추가@NotBlank+@Size조합으로 빈 문자열과 과도한 길이 입력을 모두 방지
-
정규화
toCommand메서드에서content.strip()호출로 앞뒤 공백 제거
-
Swagger 문서 보강
@Schema(description = "유저가 작성한 오늘의 한마디 내용", requiredMode = Schema.RequiredMode.REQUIRED, example = "오늘도 파이팅!")추가
적용 예시(diff):
public record AttendanceCheckCreateRequest(
- @Schema(description = "유저가 작성한 오늘의 한마디 내용")
- @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
- String content
+ @Schema(
+ description = "유저가 작성한 오늘의 한마디 내용",
+ requiredMode = Schema.RequiredMode.REQUIRED,
+ example = "오늘도 파이팅!"
+ )
+ @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
+ @Size(max = 100, message = "오늘의 한마디 내용은 최대 100자까지 입력 가능합니다.")
+ String content
) {
public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) {
- return new AttendanceCheckCreateCommand(creatorId, roomId, content);
+ return new AttendanceCheckCreateCommand(creatorId, roomId, content.strip());
}
}추가 import:
import jakarta.validation.constraints.Size;
import io.swagger.v3.oas.annotations.media.Schema;🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/attendancecheck/adapter/in/web/request/AttendanceCheckCreateRequest.java
around lines 7 to 14, enhance the content field validation by adding @Size(max =
100, message = "오늘의 한마디 내용은 최대 100자까지 입력 가능합니다.") alongside @NotBlank to enforce
length limits and prevent empty input. Update the @Schema annotation to include
requiredMode = Schema.RequiredMode.REQUIRED and an example value for better
Swagger documentation. In the toCommand method, normalize the content by calling
content.strip() before passing it to the command constructor. Also, add the
necessary imports for @Size and @Schema annotations.
| public record AttendanceCheckCreateCommand( | ||
| Long creatorId, | ||
| Long roomId, | ||
| String content | ||
| ) { } |
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.
💡 Verification agent
🧩 Analysis chain
보안 검증 요청: creatorId는 반드시 인증 컨텍스트에서 주입되는지 확인 필요
클라이언트가 임의의 creatorId를 바인딩할 수 있으면 권한 상승/대리작성 위험이 있습니다. 요청 바디/파라미터로 받지 말고, 인증 주체에서 추출해 Command에 주입하세요.
다음 스크립트로 Request/Controller에서 creatorId의 출처를 점검할 수 있습니다.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[1/4] AttendanceCheckCreateRequest 위치 검색"
fd -a AttendanceCheckCreateRequest.java
echo "[2/4] Request 클래스 내용 확인"
fd -a AttendanceCheckCreateRequest.java | xargs -I {} sh -c 'echo "=== {} ==="; nl -ba "{}" | sed -n "1,200p"'
echo "[3/4] toCommand/creatorId 매핑 경로 추적"
rg -n --glob '**/*.java' 'toCommand\\s*\\(' -A 5
rg -n --glob '**/*.java' 'creatorId'
echo "[4/4] 인증 주체 사용 흔적 검색"
rg -n --glob '**/*.java' 'AuthenticationPrincipal|Principal|SecurityContextHolder|LoginUser|AuthUser|UserPrincipal|@CurrentUser'Length of output: 802
추가 검증을 위해 아래 스크립트를 실행해주세요:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "[1] AttendanceCheckCreateRequest.java 내용 출력"
fd -a AttendanceCheckCreateRequest.java | xargs -I {} sh -c 'echo "=== {} ==="; sed -n "1,200p" "{}"'
echo
echo "[2] toCommand() 메서드 및 creatorId 매핑 확인"
rg -n --glob '**/*.java' -e 'toCommand\s*\(' -A5
echo
echo "[3] AttendanceCheckCreateCommand 생성 위치 검색"
rg -n --glob '**/*.java' 'new AttendanceCheckCreateCommand' -A3
echo
echo "[4] 컨트롤러에서 인증 주체 사용 여부 검색"
rg -n --glob '**/*Controller*.java' -e 'AuthenticationPrincipal|Principal|SecurityContextHolder|@CurrentUser' -A3Length of output: 20930
보안 검증: creatorId는 반드시 인증 컨텍스트에서만 주입하도록 변경 필요
현재 AttendanceCheckCommandController.java(약 35행)에서 클라이언트로부터 전달된 userId 변수를 그대로 request.toCommand(userId, roomId)에 넘기고 있어, 권한 상승·위조 작성 위험이 있습니다.
- AttendanceCheckCommandController.java
• 메서드 시그니처에서userId를 @PathVariable/@RequestParam 등으로 받지 않고,
@AuthenticationPrincipal혹은SecurityContextHolder.getContext().getAuthentication()를 통해 추출하도록 수정하세요. - AttendanceCheckCreateRequest.toCommand 호출부
•creatorId인자를 클라이언트 입력이 아닌 “인증 주체에서 유래한 값”만 전달하도록 보장해야 합니다.
위 조치 후, controller 메서드 시그니처에서 외부 노출된 userId 파라미터를 제거하고 인증 주체에서 안전하게 획득된 ID만을 사용하도록 변경해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/AttendanceCheckCreateCommand.java
lines 3 to 7, the creatorId should not be set from client input to prevent
privilege escalation. Instead, modify AttendanceCheckCommandController.java
around line 35 to remove userId parameters annotated with @PathVariable or
@RequestParam and retrieve the authenticated user ID using
@AuthenticationPrincipal or
SecurityContextHolder.getContext().getAuthentication(). Then, ensure
AttendanceCheckCreateRequest.toCommand is called with this authenticated user ID
as creatorId, guaranteeing it comes only from the security context, not client
input.
src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
Show resolved
Hide resolved
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.
굿굿입니다~~ 오늘 회의에서 요구사항이 좀 바껴서 커멘트 좀 달아두었습니다!!
| description = "방 참여자가 오늘의 한마디를 작성합니다." | ||
| ) | ||
| @ExceptionDescription(ATTENDANCE_CHECK_CREATE) | ||
| @PostMapping("/rooms/{roomId}/daily-greeting") |
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.
ㅋㅋ path 좋네요
| public record AttendanceCheckCreateResponse( | ||
| Long attendanceCheckId | ||
| ) { | ||
| public static AttendanceCheckCreateResponse of(Long attendanceCheckId) { | ||
| return new AttendanceCheckCreateResponse(attendanceCheckId); | ||
| } | ||
| } |
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.
p1: 바뀐 요구사항에 따라 작성 후 반환시에 해당 사용자가 작성한 글이 오늘 첫 게시글인지 여부를 나타내는 isFirstWrite같은 플래그가 필요해보여요~! (카톡에도 얘기해두었지만 혹시 잊을까봐 여기도 남깁니다)
엇 추가적으로 roomId도 반환해야 하지 않을까요??
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.
아 다시 방으로 돌아가야할 플로우도 고려해야하군요
확인했습니다
| // 1. 유저가 해당 방에 오늘의 한마디를 작성할 수 있는지 검증 | ||
| roomParticipantValidator.validateUserIsRoomMember(command.roomId(), command.creatorId()); |
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.
p1: 검증하는 부분에서도 바뀐 요구사항에 따라 사용자가 오늘 이미 5개의 글을 모두 작성했으면 에러 메시지가 뜨는 예외처리가 필요할 것 같습니다.
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.
넵 확인했습니다 5개 넘었으면 400 error 를 던지도록 구현하겠습니다
| // Attendance Check | ||
| ATTENDANCE_CHECK_CREATE(new LinkedHashSet<>(Set.of( | ||
| ROOM_ACCESS_FORBIDDEN, | ||
| ROOM_NOT_FOUND, | ||
| USER_NOT_FOUND | ||
| ))), |
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.
👍🏻 👍🏻 5개 초과시 예외메시지도 추가해주시면 감사~
| @Test | ||
| @DisplayName("방의 참석자는 출석체크(= 오늘의 한마디) 를 작성할 수 있다.") | ||
| void attendance_check_create_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)); | ||
|
|
||
| // me 가 room에 참여중인 상황 | ||
| roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(room, me, RoomParticipantRole.MEMBER, 0.0)); | ||
|
|
||
| Map<String, Object> request = Map.of("content", "오늘의 한마디~~~"); | ||
|
|
||
| //when //then | ||
| ResultActions result = mockMvc.perform(post("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) | ||
| .requestAttr("userId", me.getUserId()) | ||
| .contentType(MediaType.APPLICATION_JSON) | ||
| .content(objectMapper.writeValueAsString(request))) | ||
| .andExpect(status().isOk()); | ||
|
|
||
| Long savedId = attendanceCheckJpaRepository.findAll().get(0).getAttendanceCheckId(); // 저장된 출석 체크 id값 | ||
|
|
||
| String responseBody = result.andReturn().getResponse().getContentAsString(); | ||
| JsonNode responseJson = objectMapper.readTree(responseBody); | ||
|
|
||
| assertThat(responseJson.path("data").path("attendanceCheckId").asLong()).isEqualTo(savedId); | ||
| } |
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.
LG™
hd0rable
left a comment
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.
수고하셨습니당!! 회의록 참고하셔서 바뀐 기획 내용 수정해주시면 감사하겠습니다 ! 추가적으로 간단한 리뷰 남겨보았는데 확인해주시면 감사하겠습니당
| public class AttendanceCheckCreateService implements AttendanceCheckCreateUseCase { | ||
|
|
||
| private final RoomParticipantValidator roomParticipantValidator; | ||
| private final AttendanceCheckCommandPort attendanceCheckCommandPort; |
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.
오늘의 한마디도 방 도메인에 속하는 것같은데 AttendanceCheckCommandPort의 별도의 port를 두지않고 RoomCommandPort에서 관리하는 것은 어떤가요?
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.
오 넵 좋은생각입니다! AttandanceCheck 매번 치기 어색했는데 좋네요
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.
좋아요! 추후에 방에 관련된 게시글 record, vote, attendancecheck 모두 roomPost라는 패키지로 관리할까 생각중인데 어떻게 생각하시나요?? 그렇게 되면 현재 RoomCommand/QueryPort에서 호출하는 쿼리를 방 관련 쿼리 / 방의 게시글 관련 쿼리로 분리할 수 있을 것 같아 유지보수하기 쉬워질 것이라고 생각했습니다
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.
오 roomPost 좋습니다! 일단 지금 브랜치에서는 attendanceCheck 만 room 패키지 하위로 옮기겠습니다
| } | ||
|
|
||
| @Test | ||
| @DisplayName("방의 참석자가 아닐 경우, 오늘의 한마디를 작성 요청 시 403 에러 발생한다(400 에러 아님).") |
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
- 유저가 이미 작성한 오늘의 한마디 개수를 조회하는 코드 추가
- roomId, isFirstWrite 필드 추가
seongjunnoh
left a comment
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.
Test Results390 tests 390 ✅ 32s ⏱️ Results for commit 155259b. ♻️ This comment has been updated with latest results. |
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.
Actionable comments posted: 8
🔭 Outside diff range comments (1)
src/main/java/konkuk/thip/room/adapter/out/jpa/AttendanceCheckJpaEntity.java (1)
24-30: 외래키 컬럼에 nullable=false를 명시해 무결성 보장AttendanceCheck는 Room/User 없이 존재할 수 없는 도메인으로 보입니다. DB 제약까지 일관되게 강제하는 것이 안전합니다.
아래와 같이 JoinColumn에 nullable=false를 추가하세요.
@ManyToOne(fetch = FetchType.LAZY) -@JoinColumn(name = "room_id") +@JoinColumn(name = "room_id", nullable = false) private RoomJpaEntity roomJpaEntity; @ManyToOne(fetch = FetchType.LAZY) -@JoinColumn(name = "user_id") +@JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity;
🧹 Nitpick comments (34)
src/main/java/konkuk/thip/room/adapter/out/jpa/AttendanceCheckJpaEntity.java (3)
9-9: room_id/user_id 인덱스 추가 제안 (카운트/조회 성능 개선)사용자별/방별 금일 작성 횟수 집계, 방별 조회가 잦을 것으로 예상됩니다. 해당 FK 컬럼에 인덱스를 추가하면 성능이 안정됩니다. 스키마를 JPA에서 생성하지 않고 마이그레이션 도구(Flyway 등)로 관리 중이라면, 동일 내용을 마이그레이션에 반영해 주세요.
다음과 같이 JPA 메타데이터에 인덱스를 선언할 수 있습니다:
-@Table(name = "attendance_checks") +@Table(name = "attendance_checks", indexes = { + @Index(name = "idx_attendance_checks_room_id", columnList = "room_id"), + @Index(name = "idx_attendance_checks_user_id", columnList = "user_id") +})
21-22: 길이(100) 제약과 요청 검증의 정합성 확인 필요DB 컬럼 제약(length=100, not null)과 웹 요청/도메인 검증이 일치해야 500으로 전파되지 않습니다. Request DTO에 @SiZe(max=100), @notblank 추가 여부를 확인해 주세요.
예) 웹 어댑터 Request에 다음과 같이 추가
@Size(max = 100) @NotBlank private String content;
18-19: 컬럼명 네이밍 컨벤션 정합성(nit): attendancecheck_id → attendance_check_id스네이크 케이스 일관성 관점에서 미세하게 어긋납니다. 운영 DB 스키마 변경/마이그레이션 비용이 크다면 보류 가능하나, 초기라면 정리 추천합니다.
-@Column(name = "attendancecheck_id") +@Column(name = "attendance_check_id")src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)
192-196: 에러 코드 추가 적절. HTTP 상태코드(400) 재검토 제안한도 초과 시 의미상 429(Too Many Requests) 또는 409(Conflict)도 후보입니다. 다만 비즈니스 유효성 위반을 BAD_REQUEST로 통일하는 기존 컨벤션을 따르는 것도 일관성 면에서 합리적입니다. 팀 컨벤션에 따라 최종 결정 바랍니다.
429를 채택할 경우:
-ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, 195000, "오늘의 한마디 작성 가능 횟수를 초과하였습니다."), +ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED(HttpStatus.TOO_MANY_REQUESTS, 195000, "오늘의 한마디 작성 가능 횟수를 초과하였습니다."),src/main/java/konkuk/thip/room/application/port/in/dto/AttendanceCheckCreateResult.java (3)
5-10: Lombok @builder on record 지원 여부 확인Lombok 버전에 따라 record + @builder 지원이 다릅니다. 현재 빌드 체인에서 문제 없다면 그대로 두셔도 됩니다. 호환성 이슈 시 간단한 정적 팩토리로 대체 가능합니다.
대체안:
-@Builder -public record AttendanceCheckCreateResult( - Long attendanceCheckId, - Long roomId, - int todayWriteCountOfUser -) { } +public record AttendanceCheckCreateResult( + Long attendanceCheckId, + Long roomId, + int todayWriteCountOfUser +) { + public static AttendanceCheckCreateResult of(Long attendanceCheckId, Long roomId, int todayWriteCountOfUser) { + return new AttendanceCheckCreateResult(attendanceCheckId, roomId, todayWriteCountOfUser); + } +}
9-9: 동시성 하에서 todayWriteCountOfUser 정확성 확보 방안동시 다중 요청이 같은 사용자/같은 날에 몰릴 경우, 단순 카운트 후 생성은 경쟁조건을 유발합니다. 다음 중 하나로 원자성을 확보하는 것을 권장합니다.
- 1일 1회 제한이라면 (room_id, user_id, date) 유니크 인덱스로 DB 레벨 제약
- N회 제한이라면 “일 단위 카운터 행”을 SELECT ... FOR UPDATE로 잠그고 증분(혹은 낙관적 락 버전 필드 사용)
- 최소한 “카운트 조회 + 저장”을 단일 트랜잭션 경계에서 수행하고, 조회에 적절한 잠금 또는 위반 시 재시도/롤백 처리
6-10: 타입 선택(nit): 박싱 타입 → 원시 타입 고려생성 결과의 id들은 null이 될 수 없으므로 long/int 원시 타입 사용을 검토할 수 있습니다. 미세하지만 오토박싱/널체크 비용을 줄입니다. 팀 컨벤션에 맞추어 선택하세요.
-public record AttendanceCheckCreateResult( - Long attendanceCheckId, - Long roomId, - int todayWriteCountOfUser -) { } +public record AttendanceCheckCreateResult( + long attendanceCheckId, + long roomId, + int todayWriteCountOfUser +) { }src/main/java/konkuk/thip/room/application/port/in/dto/AttendanceCheckCreateCommand.java (1)
3-7: 원시 타입 및 입력 정규화 제안
- creatorId/roomId는 필수 값이므로 원시 타입 사용을 고려할 수 있습니다.
- content는 트림/빈문자/길이 검증을 웹 요청 DTO 또는 도메인에서 보장해 주세요.
원시 타입 변경 예:
-public record AttendanceCheckCreateCommand( - Long creatorId, - Long roomId, - String content -) { } +public record AttendanceCheckCreateCommand( + long creatorId, + long roomId, + String content +) { }웹 요청에서 트림/검증 예시:
@NotBlank @Size(max = 100) private String content; public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) { return new AttendanceCheckCreateCommand(creatorId, roomId, content.trim()); }src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateControllerTest.java (3)
23-23: 용어 정합성: 현재 설정은 단위 테스트라기보다 웹 계층 통합 테스트에 가깝습니다@SpringBootTest + MockMvc 구성은 슬라이스 단위 유닛 테스트(@WebMvcTest)보다는 웹 계층 통합 검증에 가깝습니다. 표시만 정정하는 정도로도 독자의 기대치를 맞출 수 있습니다.
-@DisplayName("[단위] 오늘의 한마디 생성 api controller 단위 테스트") +@DisplayName("[웹] 오늘의 한마디 생성 API 컨트롤러 검증")
33-33: 경계값 보강: 공백·탭 등 다양한 공백 문자열 케이스 추가 권장현재는 빈 문자열("")만 검증합니다. NotBlank 제약의 경계값(띄어쓰기, 탭, 개행 등)을 파라미터라이즈 테스트로 추가해두면 회귀 방지에 도움이 됩니다.
아래 메서드를 동일 클래스에 추가:
// 추가 import 필요: // import org.junit.jupiter.params.ParameterizedTest; // import org.junit.jupiter.params.provider.ValueSource; @ParameterizedTest @ValueSource(strings = {"", " ", " ", "\t", "\n"}) @DisplayName("content가 공백류일 경우 400 에러가 발생한다(경계값: 빈문자/스페이스/탭/개행)") void attendance_check_fail_content_blank_variants(String blankContent) throws Exception { AttendanceCheckCreateRequest request = new AttendanceCheckCreateRequest(blankContent); mockMvc.perform(post("/rooms/{roomId}/daily-greeting", 1L) .requestAttr("userId", 1L) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code").value(API_INVALID_PARAM.getCode())) .andExpect(jsonPath("$.message", containsString("오늘의 한마디 내용은 필수입니다."))); }
29-43: 부작용 차단 검증: 유효성 실패 시 UseCase 호출 여부를 명시적으로 검증하세요입력 검증 실패 시 애플리케이션 계층 호출이 전혀 일어나지 않음을 검증하면 회귀 방지에 유용합니다.
- 아래 필드를 클래스에 추가:
// import org.springframework.boot.test.mock.mockito.MockBean; // import konkuk.thip.room.application.port.in.AttendanceCheckCreateUseCase; @MockBean private AttendanceCheckCreateUseCase attendanceCheckCreateUseCase;
- 테스트 말미에 다음 검증을 추가:
// import static org.mockito.Mockito.verifyNoInteractions; verifyNoInteractions(attendanceCheckCreateUseCase);src/test/java/konkuk/thip/room/domain/AttendanceCheckTest.java (2)
11-31: 중복 제거 및 경계 강화: 파라미터라이즈드 테스트로 0~4 전 구간을 커버하세요현재는 3만 검증합니다. 0~4 전 구간을 한 번에 커버하면 중복 없이 경계 신뢰도가 올라갑니다. 또한 throws Exception은 불필요합니다.
- @Test - @DisplayName("유저가 오늘 하루동안 이미 작성한 오늘의 한마디가 5개 미만일 경우, id가 null인 AttendanceCheck 도메인이 생성된다.") - void withoutId_success_test() throws Exception { + // 필요 import: + // import org.junit.jupiter.params.ParameterizedTest; + // import org.junit.jupiter.params.provider.ValueSource; + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4}) + @DisplayName("유저가 오늘 작성한 오늘의 한마디가 5개 미만(0~4)일 경우, id가 null인 AttendanceCheck 도메인이 생성된다.") + void withoutId_success_test(int alreadyWrittenCountTodayOfUser) { //given Long userId = 1L; Long roomId = 1L; String todayComment = "오늘의 한마디~~"; - int alreadyWrittenCountTodayOfUser = 3; //when AttendanceCheck attendanceCheck = AttendanceCheck.withoutId( roomId, userId, todayComment, alreadyWrittenCountTodayOfUser ); //then assertNotNull(attendanceCheck); assertNull(attendanceCheck.getId()); assertEquals(roomId, attendanceCheck.getRoomId()); assertEquals(userId, attendanceCheck.getCreatorId()); assertEquals(todayComment, attendanceCheck.getTodayComment()); }
43-45: 에러 정밀도 검증 추가: 예외 코드/메시지까지 단언하세요비즈니스 규칙 위반에 대한 식별력을 높이기 위해, 예외 타입뿐 아니라 코드/메시지도 단언하는 것을 권장합니다.
아래와 같이 보강해 주세요(예외에 에러코드 접근자가 있다면 1안, 없다면 2안):
- InvalidStateException에 getErrorCode()가 있는 경우
// 필요 import: import static konkuk.thip.common.exception.code.ErrorCode.ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED; InvalidStateException ex = assertThrows(InvalidStateException.class, () -> AttendanceCheck.withoutId(roomId, userId, todayComment, alreadyWrittenCountTodayOfUser)); assertEquals(ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED, ex.getErrorCode());
- 에러코드 접근자가 없다면, 메시지에 대한 contains 단언
InvalidStateException ex = assertThrows(InvalidStateException.class, () -> AttendanceCheck.withoutId(roomId, userId, todayComment, alreadyWrittenCountTodayOfUser)); assertTrue(ex.getMessage().contains("ATTENDANCE_CHECK_WRITE_LIMIT_EXCEEDED"));실제 InvalidStateException 구현에 맞게 선택 적용 부탁드립니다.
src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java (2)
3-7: 시간 경계의 명시화: Today 의존을 제거하고 범위를 파라미터로 받는 설계 제안“오늘”의 해석은 타임존/서머타임/배치 시나리오에 취약합니다. 포트에서 시간 범위를 직접 받도록 하면 테스트 용이성과 이식성이 개선됩니다.
예시(오버로드 혹은 교체):
import java.time.LocalDateTime; int countAttendanceChecksByUserBetween(Long userId, LocalDateTime fromInclusive, LocalDateTime toExclusive); // (방 단위로 갈 경우) // int countAttendanceChecksByUserInRoomBetween(Long userId, Long roomId, LocalDateTime fromInclusive, LocalDateTime toExclusive);Service에서는 Clock을 주입받아 “금일”의 [startOfDay, nextStartOfDay) 범위를 생성해 본 메서드를 호출하도록 구성하면 테스트가 쉬워집니다.
3-7: 성능 튜닝 가이드: 조회 인덱스 추가 권장오늘자 카운트는 고빈도 호출 지점일 가능성이 큽니다. 저장소 계층에 아래 인덱스를 권장합니다.
- 현 시그니처 유지 시: (user_id, created_at) 복합 인덱스
- 방 단위 전환 시: (room_id, user_id, created_at) 복합 인덱스
created_at BETWEEN 조건과 정렬/필터에 모두 유리합니다. JPA 스키마 관리 방식을 감안해 마이그레이션 포함 검토 부탁드립니다.
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (2)
18-18: 불필요한 import 제거RoomParticipant 타입은 본 파일에서 사용되지 않습니다. 정리하여 가독성과 정적 분석 경고를 줄여주세요.
-import konkuk.thip.room.domain.RoomParticipant;
360-366: 테스트 유연성 강화: createdAt 지정 가능한 팩토리 오버로드 추가 제안“오늘” 집계 로직의 신뢰도 향상을 위해 어제/내일 등 임의 시간에 저장된 AttendanceCheck를 쉽게 만들 수 있도록 createdAt을 받는 오버로드를 권장합니다.
예시(엔티티 빌더가 createdAt 설정을 지원하는 경우):
public static AttendanceCheckJpaEntity createAttendanceCheck( String todayComment, RoomJpaEntity roomJpaEntity, UserJpaEntity userJpaEntity, LocalDateTime createdAt ) { return AttendanceCheckJpaEntity.builder() .todayComment(todayComment) .roomJpaEntity(roomJpaEntity) .userJpaEntity(userJpaEntity) .createdAt(createdAt) // 빌더 지원 여부 확인 필요 .build(); }만약 BaseEntity에서 createdAt을 자동 세팅하고 빌더로 주입 불가하다면, 테스트 전용 setter 제공 또는 테스트 전용 PersistenceAdapter 헬퍼로 “임의 createdAt” 업데이트 유틸을 두는 방안을 함께 검토해주세요.
src/main/java/konkuk/thip/room/adapter/in/web/request/AttendanceCheckCreateRequest.java (1)
12-14: 공백 트리밍 처리검증을 통과하더라도 좌우 공백을 제거해 저장하는 편이 데이터 품질에 유리합니다.
strip()은 유니코드 공백을 처리합니다.public AttendanceCheckCreateCommand toCommand(Long creatorId, Long roomId) { - return new AttendanceCheckCreateCommand(creatorId, roomId, content); + return new AttendanceCheckCreateCommand(creatorId, roomId, content.strip()); }src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (1)
11-19: 읽기 전용 트랜잭션 및 시간 소스 주입 제안카운트 쿼리는 읽기 전용 트랜잭션으로 선언하는 것이 안전하며, 테스트와 타임존 일관성을 위해
Clock을 주입해 사용하는 것을 권장합니다(서버 기본 타임존에 종속되지 않도록).import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +import java.time.Clock; +import java.time.LocalDate; @Repository @RequiredArgsConstructor public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort { private final AttendanceCheckJpaRepository attendanceCheckJpaRepository; - private final AttendanceCheckMapper attendanceCheckMapper; + private final Clock clock; @Override + @Transactional(readOnly = true) public int countAttendanceChecksOnTodayByUser(Long userId) { - LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); + LocalDate today = LocalDate.now(clock); + LocalDateTime startOfDay = today.atStartOfDay(); LocalDateTime endOfDay = startOfDay.plusDays(1); return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween(userId, startOfDay, endOfDay); }추가로 DB 관점에서는
(user_id, created_at)복합 인덱스가 있으면 본 카운트 쿼리 성능이 크게 개선됩니다. 운영 트래픽 수준에 따라 인덱스 검토를 권장드립니다.src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java (2)
119-126: 메서드 명칭을 도메인에 맞게 정정 제안
createFeed는 도메인 용어와 맞지 않습니다. 엔드포인트에 맞춰 명확하게 변경하는 것이 가독성에 유리합니다.- public BaseResponse<AttendanceCheckCreateResponse> createFeed( + public BaseResponse<AttendanceCheckCreateResponse> createDailyGreeting( @RequestBody @Valid final AttendanceCheckCreateRequest request, @Parameter(description = "오늘의 한마디를 작성할 방 id값") @PathVariable("roomId") final Long roomId, @Parameter(hidden = true) @UserId final Long userId) { return BaseResponse.ok(AttendanceCheckCreateResponse.of( attendanceCheckCreateUseCase.create(request.toCommand(userId, roomId)) )); }
121-121: Swagger 파라미터 예시 추가API 문서 가독성을 위해 예시 값을 제공하면 좋습니다.
- @Parameter(description = "오늘의 한마디를 작성할 방 id값") @PathVariable("roomId") final Long roomId, + @Parameter(description = "오늘의 한마디를 작성할 방 id값", example = "1") @PathVariable("roomId") final Long roomId,src/main/java/konkuk/thip/room/adapter/in/web/response/AttendanceCheckCreateResponse.java (2)
10-18: 불리언 파생 값 계산 단순화가독성을 위해 단일 표현식으로 축약 가능합니다.
public static AttendanceCheckCreateResponse of(AttendanceCheckCreateResult result) { - boolean isFirstWrite = false; - if (result.todayWriteCountOfUser() == 1) isFirstWrite = true; - + boolean isFirstWrite = result.todayWriteCountOfUser() == 1; return new AttendanceCheckCreateResponse( result.attendanceCheckId(), result.roomId(), isFirstWrite ); }
3-9: 응답 스키마 명세 보강Swagger 문서를 강화하기 위해 필드별 스키마를 명시하면 좋습니다. 클라이언트 측 개발/QA에 유용합니다.
+import io.swagger.v3.oas.annotations.media.Schema; import konkuk.thip.room.application.port.in.dto.AttendanceCheckCreateResult; public record AttendanceCheckCreateResponse( - Long attendanceCheckId, - Long roomId, - boolean isFirstWrite + @Schema(description = "오늘의 한마디 ID", example = "101") + Long attendanceCheckId, + @Schema(description = "방 ID", example = "7") + Long roomId, + @Schema(description = "해당 유저의 금일 첫 작성 여부", example = "true") + boolean isFirstWrite ) {참고: boolean 컴포넌트명을
isFirstWrite로 둔 것은 Jackson Record 지원(컴포넌트명 기준)과 호환되므로 그대로 직렬화됩니다.src/main/java/konkuk/thip/room/domain/AttendanceCheck.java (2)
14-22: 도메인 무결성 우회 가능성: public 빌더 노출@SuperBuilder가 공개되어 있어, 외부에서 validateWriteCount를 거치지 않고 인스턴스를 만들 수 있습니다. 팀 규칙에 따라 정적 팩토리만 허용하거나(빌더 제거), 빌더 접근을 제한하는 방향을 고려해주세요. 단기적으로는 “도메인 생성은 반드시 withoutId를 사용할 것”을 컨벤션/도큐로 명시해도 됩니다.
18-31: 콘텐츠 도메인 제약(트림/최대 길이) 보강 고려컨트롤러에서 @notblank 등으로 1차 방어가 되더라도 도메인에서도 todayComment에 대한 기본 제약(예: trim, 최대 길이)을 가지면 DB 제약(컬럼 길이) 불일치로 인한 DataIntegrityViolation을 예방할 수 있습니다. 필요 시 validateWriteCount와 유사한 validateComment를 추가하는 방안을 제안합니다.
예시(외부 변경 코드):
private static final int MAX_COMMENT_LENGTH = 300; private static void validateComment(String todayComment) { if (todayComment == null || todayComment.trim().isEmpty()) { throw new InvalidStateException(API_INVALID_PARAM); } if (todayComment.length() > MAX_COMMENT_LENGTH) { throw new InvalidStateException(API_INVALID_PARAM); } }src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java (5)
86-90: roomId를 int로 캐스팅하지 말고 Long 그대로 사용하세요intValue()는 불필요하며, 아주 큰 ID 사용 시 오동작 여지가 있습니다. Long 그대로 넘기는 편이 안전하고 일관적입니다.
다음 변경을 권장합니다(아래 다른 테스트의 동일 패턴도 함께 수정):
- ResultActions result = mockMvc.perform(post("/rooms/{roomId}/daily-greeting", room.getRoomId().intValue()) + ResultActions result = mockMvc.perform(post("/rooms/{roomId}/daily-greeting", room.getRoomId())동일 수정 위치: Line 113, 139, 175.
92-98: 검증 강도 보강: 응답-DB 동등성 추가 확인현재는 저장된 첫 엔티티의 ID와 응답의 ID만 비교합니다. content, roomId, userId까지 교차 검증하면 회귀를 더 잘 잡을 수 있습니다(매핑/엔티티 연관 오류 조기 탐지).
예시:
- repository.findById(responseId)로 조회 후, 요청 content와 일치, 연관된 room/user가 기대값과 일치하는지 assert 추가
- repository.count() == 1 보장
151-182: 제한 기준(전역 vs 방 단위) 테스트 케이스 추가 제안현재 테스트는 단일 방에서 5회 초과를 검증합니다. 요구사항이 “유저 전역 5회”라면 다른 방에서 이미 5회 작성된 상태에서 신규 방에 작성 시 400이 나오는 케이스도 추가하는 것이 좋습니다. 반대로 “방별 5회”라면 동일 유저의 다른 방에서 5+회 작성이 허용되는지 테스트로 명시해 주세요.
122-149: 첫 작성 응답 검증 보강isFirstWrite가 true인 것 외에 todayWriteCountOfUser == 1, attendanceCheckId 존재성 등을 함께 검증하면 스펙이 명확해집니다.
58-67: 테스트 데이터 정리 전략 점검deleteAllInBatch 순서가 FK 제약을 고려해 잘 정리되고 있습니다. 대안으로 @SQL(scripts=..., executionPhase=AFTER_TEST_METHOD)나 Truncate 유틸을 사용하면 테스트 성능과 단순성이 향상될 수 있습니다. 현재로도 문제 없어 보이므로 선택 사항입니다.
src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java (1)
28-40: 테스트 가능성/시간대 의존성 개선: Clock 주입오늘의 범위 계산이 어댑터 내부 LocalDateTime.now()에 의존하면 경계 시각(자정 근처) 테스트가 불안정합니다. java.time.Clock을 주입해 시작/끝 시각을 계산하도록 리팩토링하면 재현성과 테스트 용이성이 개선됩니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendanceCheck/AttendanceCheckJpaRepository.java (1)
12-17: 전역 vs 방 단위 제한 쿼리 확인현재 쿼리는 userId와 createdAt만 조건으로 사용합니다. “방별 5회” 제한이 의도라면 room_id 조건을 추가해야 합니다.
예시:
@Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " + "WHERE a.userJpaEntity.userId = :userId " + "AND a.roomJpaEntity.roomId = :roomId " + "AND a.createdAt >= :startOfDay AND a.createdAt < :endOfDay") int countByUserIdAndRoomIdAndCreatedAtBetween(...);src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java (2)
27-40: 존재 검증 쿼리 2회 비용 vs 의미적 404 매핑 트레이드오프현재 room, user를 각각 findById로 조회해 명시적인 404를 매핑하고 있습니다. 의미적으로 명확해 좋지만, 매 요청 2번의 SELECT가 발생합니다. 트래픽/지연 민감도에 따라 다음 두 가지 대안을 고려할 수 있습니다.
- 유지(권장, 의미 우선): 현재처럼 명시적 조회 후 커스텀 에러코드 매핑 유지
- 최적화(성능 우선): getReferenceById로 프록시 참조를 넘기고 flush 시 존재하지 않으면 JPA EntityNotFoundException을 캐치해 커스텀 예외로 변환
주의: 후자의 경우 예외 타입 변환/flush 타이밍 제어가 필요하며 코드가 복잡해질 수 있습니다. 현재 구현은 가독성과 오류 명확성 측면에서 충분히 합리적입니다.
22-26: 매퍼에 전달되는 도메인 값 사전 정규화(트림 등) 고려매퍼로 내려가기 전에 todayComment에 대한 trim 등을 적용하면 저장 데이터 품질이 균일해집니다(앞단 DTO 검증이 있더라도 방어적 정규화는 유익할 수 있음).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (30)
src/main/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCommandController.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckQuerydController.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/repository/AttendanceCheckJpaRepository.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/DummyQuery.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/DummyResult.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckCommandPort.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckQueryPort.java(0 hunks)src/main/java/konkuk/thip/attendancecheck/domain/AttendanceCheck.java(0 hunks)src/main/java/konkuk/thip/common/exception/code/ErrorCode.java(1 hunks)src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java(1 hunks)src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java(3 hunks)src/main/java/konkuk/thip/room/adapter/in/web/request/AttendanceCheckCreateRequest.java(1 hunks)src/main/java/konkuk/thip/room/adapter/in/web/response/AttendanceCheckCreateResponse.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/jpa/AttendanceCheckJpaEntity.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/mapper/AttendanceCheckMapper.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendanceCheck/AttendanceCheckJpaRepository.java(1 hunks)src/main/java/konkuk/thip/room/application/port/in/AttendanceCheckCreateUseCase.java(1 hunks)src/main/java/konkuk/thip/room/application/port/in/dto/AttendanceCheckCreateCommand.java(1 hunks)src/main/java/konkuk/thip/room/application/port/in/dto/AttendanceCheckCreateResult.java(1 hunks)src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckCommandPort.java(1 hunks)src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java(1 hunks)src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java(1 hunks)src/main/java/konkuk/thip/room/domain/AttendanceCheck.java(1 hunks)src/test/java/konkuk/thip/common/util/TestEntityFactory.java(2 hunks)src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java(1 hunks)src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateControllerTest.java(1 hunks)src/test/java/konkuk/thip/room/domain/AttendanceCheckTest.java(1 hunks)
💤 Files with no reviewable changes (9)
- src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckCommandPort.java
- src/main/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckQuerydController.java
- src/main/java/konkuk/thip/attendancecheck/adapter/in/web/AttendanceCheckCommandController.java
- src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/repository/AttendanceCheckJpaRepository.java
- src/main/java/konkuk/thip/attendancecheck/application/port/out/AttendanceCheckQueryPort.java
- src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/DummyResult.java
- src/main/java/konkuk/thip/attendancecheck/application/port/in/dto/DummyQuery.java
- src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
- src/main/java/konkuk/thip/attendancecheck/domain/AttendanceCheck.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
🧬 Code Graph Analysis (3)
src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java (2)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
TestEntityFactory(33-367)src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateControllerTest.java (1)
SpringBootTest(20-44)
src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateControllerTest.java (1)
src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java (1)
SpringBootTest(40-183)
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java (3)
src/main/java/konkuk/thip/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(5-10)src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (1)
Repository(11-25)src/main/java/konkuk/thip/attendancecheck/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java (1)
AttendanceCheckCommandPersistenceAdapter(9-16)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (7)
src/main/java/konkuk/thip/room/adapter/out/jpa/AttendanceCheckJpaEntity.java (1)
1-1: 패키지 이동 및 import 제거 확인 완료모든 코드에서
konkuk.thip.attendancecheck관련 참조가 존재하지 않음을 확인했습니다.
추가 조치 필요 없으며, 변경 사항을 승인합니다.src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckCommandPort.java (1)
5-8: 포트 설계 적절도메인 모델을 저장하고 식별자를 반환하는 책임을 분리한 포트 설계가 명확합니다. QueryPort와의 역할 구분도 기대됩니다.
src/main/java/konkuk/thip/room/application/port/in/AttendanceCheckCreateUseCase.java (1)
6-9: 포트 정의 적합 — 단일 책임에 충실한 인터페이스입니다입력/출력 DTO를 경계로 분리한 형태가 명확합니다. 구현체/컨트롤러에 무리 없는 수준으로 보입니다.
src/main/java/konkuk/thip/room/adapter/out/mapper/AttendanceCheckMapper.java (2)
12-18: toJpaEntity에서 status 매핑 누락 가능성도메인 → JPA 매핑 시 status가 누락되어, 도메인에서 의도한 상태값이 저장되지 않을 수 있습니다. 아래와 같이 status를 함께 매핑하는 것을 고려해 주세요. JPA 엔티티 빌더에 status 세터가 없다면 스킵하되, 생성 시 기본값 의존이 의도된 것인지 확인 부탁드립니다.
public AttendanceCheckJpaEntity toJpaEntity(AttendanceCheck attendanceCheck, RoomJpaEntity roomJpaEntity, UserJpaEntity userJpaEntity) { return AttendanceCheckJpaEntity.builder() .todayComment(attendanceCheck.getTodayComment()) + .status(attendanceCheck.getStatus()) .roomJpaEntity(roomJpaEntity) .userJpaEntity(userJpaEntity) .build(); }
20-29: 도메인 매핑 정합성 양호id, roomId, creatorId, 생성/수정 시각, status 등 역매핑이 일관적입니다. 프록시에서 getRoomId()/getUserId() 접근 시 Hibernate는 DB 초기화 없이 식별자 접근을 허용하므로 성능 이슈 우려도 낮습니다.
src/main/java/konkuk/thip/room/adapter/in/web/RoomCommandController.java (1)
117-119: 생성 API의 HTTP 상태 코드 확인 요청 (201 Created 고려)현재 BaseResponse 컨벤션상 200 OK로 응답하는 것으로 보이나, "생성" API는 201 Created + Location 헤더를 사용하는 것이 일반적입니다. 팀의 API 정책과 Swagger 스펙이 200을 강제하는지 확인 부탁드립니다. 정책이 허용한다면 201로 전환을 제안합니다.
src/main/java/konkuk/thip/room/domain/AttendanceCheck.java (1)
24-32: 작성 가능 횟수 제한 범위(전역 vs 방 단위) 재확인 필요
현재 구현은 유저별 하루 총합(전역) 기준으로 제한하고 있습니다. 만약 요구사항이 방별 하루 5회라면, 아래 항목들을 검토 및 수정해야 합니다.• AttendanceCheckQueryPort.countAttendanceChecksOnTodayByUser (src/.../AttendanceCheckQueryPort.java)
– userId만 기준으로 카운트 조회
• AttendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween (src/.../attendanceCheck/AttendanceCheckJpaRepository.java)
– JPQL에 roomId 필터 미포함
• AttendanceCheckCreateService.create → AttendanceCheck.withoutId 호출 (src/.../AttendanceCheckCreateService.java:라인 28–32)
– roomId와 상관없이 user 전체 카운트만 전달
• AttendanceCheck.withoutId.validateWriteCount (src/.../AttendanceCheck.java:라인 24–32)
– 이미 작성한 수만 검증, roomId 기준 검증 로직 없음요구사항이 “사용자 전역 하루 5회”인지, “방별 하루 5회”인지 확실히 해두시고, 후자라면 service·port·repository 시그니처 및 쿼리에 roomId 필터를 추가해 주세요.
| @Schema(description = "유저가 작성한 오늘의 한마디 내용") | ||
| @NotBlank(message = "오늘의 한마디 내용은 필수입니다.") | ||
| String content | ||
| ) { |
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.
🛠️ Refactor suggestion
입력 길이 제약 및 예시 추가 제안
DB 컬럼 길이와 정합성을 맞추기 위해 최대 길이 제한을 권장합니다. 또한 Swagger 예시를 제공하면 클라이언트 UX가 좋아집니다.
public record AttendanceCheckCreateRequest(
- @Schema(description = "유저가 작성한 오늘의 한마디 내용")
- @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
+ @Schema(description = "유저가 작성한 오늘의 한마디 내용", example = "오늘도 화이팅!")
+ @NotBlank(message = "오늘의 한마디 내용은 필수입니다.")
+ @jakarta.validation.constraints.Size(max = 255, message = "오늘의 한마디 내용은 255자를 초과할 수 없습니다.")
String content
) {추가로 import가 필요 없다면 FQN 유지로 충분하지만, 선호하시면 상단에 import jakarta.validation.constraints.Size;를 추가하고 어노테이션에 FQN을 제거해도 됩니다.
📝 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.
| @Schema(description = "유저가 작성한 오늘의 한마디 내용") | |
| @NotBlank(message = "오늘의 한마디 내용은 필수입니다.") | |
| String content | |
| ) { | |
| public record AttendanceCheckCreateRequest( | |
| @Schema(description = "유저가 작성한 오늘의 한마디 내용", example = "오늘도 화이팅!") | |
| @NotBlank(message = "오늘의 한마디 내용은 필수입니다.") | |
| @jakarta.validation.constraints.Size( | |
| max = 255, | |
| message = "오늘의 한마디 내용은 255자를 초과할 수 없습니다." | |
| ) | |
| String content | |
| ) { |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/in/web/request/AttendanceCheckCreateRequest.java
around lines 8 to 11, add a max-length constraint and a Swagger example for the
content field to match DB column limits and improve client UX: annotate the
field with a @Size(max = 255) (or the actual DB column max) and update @Schema
to include an example like @Schema(description = "유저가 작성한 오늘의 한마디 내용", example =
"오늘도 화이팅!") — optionally add import jakarta.validation.constraints.Size; at the
top and remove the FQN if you prefer imports.
...in/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
Show resolved
Hide resolved
| LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); | ||
| LocalDateTime endOfDay = startOfDay.plusDays(1); | ||
|
|
||
| return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween(userId, startOfDay, endOfDay); | ||
| } |
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.
하루 범위 계산의 경계값 포함 버그 가능성 (Between의 양끝 포함 특성)
Spring Data JPA의 Between은 양끝을 포함합니다. 현재 구현은 [startOfDay, nextDayStart] 구간이라, 정확히 자정(다음날 00:00:00.000)의 데이터가 포함되는 경계 버그가 발생할 수 있습니다. 즉, 오늘 00:00부터 내일 00:00까지를 포함하므로 내일 00:00에 생성된 레코드가 집계될 수 있습니다.
간단한 수정안: 상한을 배타로 만들기 위해 1나노초 감산을 적용합니다.
- LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
- LocalDateTime endOfDay = startOfDay.plusDays(1);
-
- return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween(userId, startOfDay, endOfDay);
+ LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
+ LocalDateTime endOfDayExclusive = startOfDay.plusDays(1);
+ return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween(
+ userId, startOfDay, endOfDayExclusive.minusNanos(1)
+ );보다 정석적인 대안(선호): 레포지토리 메서드를 < 비교로 분리해 배타 상한을 명확히 표현하세요.
countByUserIdAndCreatedAtGreaterThanEqualAndCreatedAtLessThan(userId, startOfDay, endOfDayExclusive)
📝 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.
| LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); | |
| LocalDateTime endOfDay = startOfDay.plusDays(1); | |
| return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween(userId, startOfDay, endOfDay); | |
| } | |
| LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); | |
| LocalDateTime endOfDayExclusive = startOfDay.plusDays(1); | |
| return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween( | |
| userId, startOfDay, endOfDayExclusive.minusNanos(1) | |
| ); |
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.
이미 jpql 에서 createdAt >= :startOfDay AND createdAt < :endOfDay 로 반열림 구간을 구현하고있어.
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.
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
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.
🧩 Analysis chain
@seongjunnoh 맞습니다! JPQL에서 이미 createdAt >= :startOfDay AND createdAt < :endOfDay로 반열림구간을 정확히 구현하고 계시네요. 제가 Spring Data JPA의 Between 메서드 이름만 보고 잘못 판단했습니다. 실제 구현을 확인하지 않고 성급하게 지적한 점 죄송합니다.
사용자 정의 JPQL 쿼리로 경계값 처리를 정확하게 하신 것을 확인했습니다. 감사합니다! 🙇♂️
(\ /)
( ._.)
o_(")(") *learning noted*
🏁 Script executed:
#!/bin/bash
# Check the JPQL implementation in AttendanceCheckJpaRepository
find . -name "AttendanceCheckJpaRepository.java" -exec cat {} \;
Length of output: 972
@seongjunnoh 정확히 확인했습니다! JPQL에서 a.createdAt >= :startOfDay AND a.createdAt < :endOfDay로 반열림구간을 완벽하게 구현하고 계시네요.
제가 메서드명 countByUserIdAndCreatedAtBetween만 보고 Spring Data JPA의 기본 Between 동작을 가정해서 잘못 판단했습니다. 실제 구현을 먼저 확인했어야 했는데 성급한 지적이었습니다.
사용자 정의 JPQL로 경계값 처리를 정확하게 하신 점 잘 배웠습니다. 감사합니다!
(\ /)
( ._.)
o_(")(") *humbled and learning*
✏️ Learnings added
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java:20-24
Timestamp: 2025-08-14T09:27:36.393Z
Learning: AttendanceCheckJpaRepository에서 countByUserIdAndCreatedAtBetween 메서드는 실제로는 사용자 정의 JPQL 쿼리 "createdAt >= :startOfDay AND createdAt < :endOfDay"를 사용하여 반열림구간 [startOfDay, endOfDay)을 구현하고 있다. 메서드 이름만으로 Spring Data JPA의 기본 Between 동작을 가정하면 안 된다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java:20-24
Timestamp: 2025-08-14T09:27:36.393Z
Learning: AttendanceCheckJpaRepository의 countByUserIdAndCreatedAtBetween 메서드는 메서드명과 달리 사용자 정의 JPQL 쿼리를 사용한다. "a.createdAt >= :startOfDay AND a.createdAt < :endOfDay"로 반열림구간 [startOfDay, endOfDay)을 정확히 구현하고 있어서 경계값 버그가 없다. 메서드명만으로 Spring Data JPA의 기본 Between 동작을 가정하면 안 된다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.332Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
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.
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.
| public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> { | ||
|
|
||
| // TODO : count 값을 매번 쿼리를 통해 계산하는게 아니라 DB에 저장 or redis 캐시에 저장하는 방법도 좋을 듯 | ||
| @Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " + | ||
| "WHERE a.userJpaEntity.userId = :userId " + | ||
| "AND a.createdAt >= :startOfDay " + | ||
| "AND a.createdAt < :endOfDay") | ||
| int countByUserIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay); | ||
| } |
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.
🛠️ Refactor suggestion
패키지 네이밍 케이스(대소문자) 일관성 문제
package 경로에 attendanceCheck처럼 카멜케이스가 포함되어 있습니다. 자바 관례상 전부 소문자를 사용합니다. 케이스 민감한 파일시스템/툴링에서 혼선을 줄이기 위해 attendancecheck 또는 attendance 등 소문자로 정규화하는 것을 권장합니다.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendanceCheck/AttendanceCheckJpaRepository.java
around lines 10-18 the package path uses camelCase ("attendanceCheck") which
violates Java naming conventions; rename the package path and package
declarations to all lowercase (e.g., attendancecheck or attendance), move the
directory to match the new lowercase name, and update this file's package
statement plus all imports/references across the codebase (other classes, build
configs, tests) to the new lowercase package so package names and file system
paths are consistent.
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.
오호 굿 리뷰
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.
@seongjunnoh 감사합니다! 패키지 네이밍 컨벤션을 일관성 있게 유지하는 것이 코드베이스의 품질 향상에 도움이 되죠. 👍
🧠 Learnings used
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.250Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
| // TODO : count 값을 매번 쿼리를 통해 계산하는게 아니라 DB에 저장 or redis 캐시에 저장하는 방법도 좋을 듯 | ||
| @Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " + | ||
| "WHERE a.userJpaEntity.userId = :userId " + | ||
| "AND a.createdAt >= :startOfDay " + | ||
| "AND a.createdAt < :endOfDay") | ||
| int countByUserIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay); |
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.
🛠️ Refactor suggestion
카운트 성능 최적화 및 인덱스 권장
하루 범위 카운트는 빈도 높은 경로일 가능성이 큽니다. 다음을 권장합니다.
- DB 인덱스: (user_id, created_at) 복합 인덱스 추가 → 범위 COUNT 최적화
- 카운트 결과 캐싱(Redis): userId+yyyyMMdd 키로 INCR/GET 활용(동시성 제어에도 도움)
JPA 엔티티 수준에서 인덱스 예시(엔티티 외부 변경 코드):
@Entity
@Table(name = "attendance_check",
indexes = {
@Index(name = "idx_attchk_user_created_at", columnList = "user_id, created_at")
})
public class AttendanceCheckJpaEntity { ... }또한, 반환 타입은 int로도 충분하나 일반적인 COUNT는 long을 사용하는 패턴이 많습니다(선택 사항).
|
|
||
| public interface AttendanceCheckQueryPort { | ||
|
|
||
| int countAttendanceChecksOnTodayByUser(Long userId); |
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.
💡 Verification agent
🧩 Analysis chain
제한 범위 재검토: 일-유저 전역 제한인지, 방 단위 제한인지 확인 필요
현재 포트는 유저 기준 전역 카운트입니다. 엔드포인트가 /rooms/{roomId}/daily-greeting 인 점을 고려하면, “하루 최대 5회” 제한은 방 단위일 가능성이 큽니다. 방 단위라면 roomId가 포함되어야 합니다.
방 단위 제한으로 확정 시 아래와 같이 시그니처를 교체하는 것을 제안합니다:
- int countAttendanceChecksOnTodayByUser(Long userId);
+ int countAttendanceChecksOnTodayByUserInRoom(Long userId, Long roomId);이 변경은 Service/Adapter/Repository에 동일하게 파급 적용되어야 합니다.
메소드 정의 및 호출 위치를 확인하기 위해 아래 스크립트를 실행합니다.
🏁 Script executed:
#!/bin/bash
# 메소드 정의 위치 확인
rg "countAttendanceChecksOnTodayByUser" -n src
# 인터페이스 내용 출력
sed -n '1,200p' src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.javaLength of output: 814
제한 범위 재검토 및 방 단위 제한 반영 안내
현재 countAttendanceChecksOnTodayByUser 메소드 시그니처와 이를 사용하는 Service/Adapter/Repository 모두 userId만 기준으로 카운팅하고 있어, /rooms/{roomId}/daily-greeting 엔드포인트에서 기대하는 “하루 최대 5회” 제한을 방 단위로 적용할 수 없습니다. 방 단위 제한을 적용하려면 roomId 파라미터를 추가해야 합니다.
수정 대상:
- AttendanceCheckQueryPort 인터페이스
- AttendanceCheckCreateService 호출부
- AttendanceCheckQueryPersistenceAdapter 구현부
- JPA Repository (예: AttendanceCheckRepository) 메소드 시그니처
추천 변경 예시:
// AttendanceCheckQueryPort.java
- int countAttendanceChecksOnTodayByUser(Long userId);
+ int countAttendanceChecksOnTodayByUserInRoom(Long userId, Long roomId);위와 같이 시그니처를 변경한 뒤, service에서는
- attendanceCheckQueryPort.countAttendanceChecksOnTodayByUser(command.creatorId());
+ attendanceCheckQueryPort.countAttendanceChecksOnTodayByUserInRoom(command.creatorId(), command.roomId());로 호출을 업데이트하고, Adapter와 Repository에서도 roomId를 WHERE 절에 포함해 카운팅하도록 수정해주세요.
📝 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.
| int countAttendanceChecksOnTodayByUser(Long userId); | |
| // AttendanceCheckQueryPort.java | |
| - int countAttendanceChecksOnTodayByUser(Long userId); | |
| + int countAttendanceChecksOnTodayByUserInRoom(Long userId, Long roomId); |
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.
p1: 엇 코래 말처럼 지금 사용자가 오늘 작성한 모든 오한을 카운트하고 있는데 특정 roomId에서 작성한 오한을 카운트해야 될 것 같아요!!
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.
@buzz0331 맞습니다! 현재 구현이 전역 카운트로 되어 있어서 수정이 필요하네요.
방(room) 단위로 제한을 적용하려면 다음과 같이 수정해주세요:
1. AttendanceCheckQueryPort 인터페이스 수정:
// 현재
int countAttendanceChecksOnTodayByUser(Long userId);
// 수정 후
int countAttendanceChecksOnTodayByUserInRoom(Long userId, Long roomId);2. AttendanceCheckCreateService에서 호출 부분 수정:
// 현재
int alreadyWrittenCountToday = attendanceCheckQueryPort.countAttendanceChecksOnTodayByUser(command.creatorId());
// 수정 후
int alreadyWrittenCountToday = attendanceCheckQueryPort.countAttendanceChecksOnTodayByUserInRoom(command.creatorId(), command.roomId());3. AttendanceCheckJpaRepository 쿼리 수정:
@Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a WHERE a.userJpaEntity.userId = :userId AND a.roomJpaEntity.roomId = :roomId AND a.createdAt >= :startOfDay AND a.createdAt < :endOfDay")
int countByUserIdAndRoomIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("roomId") Long roomId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay);이렇게 수정하면 각 방별로 하루 5회 제한이 올바르게 적용될 것입니다!
✏️ Learnings added
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:15:54.900Z
Learning: 오늘의 한마디(AttendanceCheck) 작성 제한은 사용자 전역이 아닌 방(room) 단위로 적용되어야 함. 각 사용자는 각 방에서 하루 최대 5개의 오늘의 한마디를 작성할 수 있음.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
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.
추가적으로 출석체크가 소프트 딜리트인걸로 알아서 state = 'ACTIVE' 필터링도 하면 좋을 것 같습니다!!
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check AttendanceCheckJpaEntity structure to see soft delete implementation
cat src/main/java/konkuk/thip/room/adapter/out/jpa/AttendanceCheckJpaEntity.java
# Also check if there's a base entity that might contain the state field
rg "class.*BaseDomainEntity" -A 10 -B 2
# Look for state field usage in JPA entities
rg "state.*ACTIVE" -A 2 -B 2
Length of output: 17308
🏁 Script executed:
#!/bin/bash
# Check StatusType enum to see available values
rg "enum StatusType" -A 10 -B 2
# Also check BaseJpaEntity to see if it has the status field
cat src/main/java/konkuk/thip/common/entity/BaseJpaEntity.java
Length of output: 1899
@buzz0331 맞습니다! BaseJpaEntity에서 status 필드로 소프트 딜리트를 구현하고 있네요. StatusType.ACTIVE로 필터링해야 삭제된 레코드를 제외할 수 있습니다.
방(room) 단위 제한과 소프트 딜리트 필터링을 모두 포함하여 다음과 같이 수정해주세요:
1. AttendanceCheckQueryPort 인터페이스:
int countAttendanceChecksOnTodayByUserInRoom(Long userId, Long roomId);2. AttendanceCheckJpaRepository 쿼리:
@Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a WHERE a.userJpaEntity.userId = :userId AND a.roomJpaEntity.roomId = :roomId AND a.status = 'ACTIVE' AND a.createdAt >= :startOfDay AND a.createdAt < :endOfDay")
int countByUserIdAndRoomIdAndCreatedAtBetweenAndStatusActive(
@Param("userId") Long userId,
@Param("roomId") Long roomId,
@Param("startOfDay") LocalDateTime startOfDay,
@Param("endOfDay") LocalDateTime endOfDay
);3. AttendanceCheckQueryPersistenceAdapter 구현:
@Override
public int countAttendanceChecksOnTodayByUserInRoom(Long userId, Long roomId) {
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
LocalDateTime endOfDay = startOfDay.plusDays(1);
return attendanceCheckJpaRepository.countByUserIdAndRoomIdAndCreatedAtBetweenAndStatusActive(
userId, roomId, startOfDay, endOfDay);
}이렇게 하면 특정 방에서 활성 상태인 오늘의 한마디만 정확히 카운트할 수 있습니다!
✏️ Learnings added
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.250Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:15:54.900Z
Learning: 오늘의 한마디(AttendanceCheck) 작성 제한은 사용자 전역이 아닌 방(room) 단위로 적용되어야 함. 각 사용자는 각 방에서 하루 최대 5개의 오늘의 한마디를 작성할 수 있음.
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.
엇 이런 실수를 허허허 수정하겠습니다!
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.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java
Show resolved
Hide resolved
src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java
Outdated
Show resolved
Hide resolved
buzz0331
left a comment
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.
수고하셨습니다~!! 리뷰 확인 부탁드려욥
| @Override | ||
| public int countAttendanceChecksOnTodayByUser(Long userId) { | ||
| LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); | ||
| LocalDateTime endOfDay = startOfDay.plusDays(1); | ||
|
|
||
| return attendanceCheckJpaRepository.countByUserIdAndCreatedAtBetween(userId, startOfDay, endOfDay); | ||
| } |
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.
오호 이렇게 하셨군요 굳입니다!
|
|
||
| public interface AttendanceCheckQueryPort { | ||
|
|
||
| int countAttendanceChecksOnTodayByUser(Long userId); |
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.
p1: 엇 코래 말처럼 지금 사용자가 오늘 작성한 모든 오한을 카운트하고 있는데 특정 roomId에서 작성한 오한을 카운트해야 될 것 같아요!!
| @SuperBuilder | ||
| public class AttendanceCheck extends BaseDomainEntity { | ||
|
|
||
| private static final int LIMIT_WRITE_COUNT_PER_DAY = 5; |
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
| import konkuk.thip.attendancecheck.application.port.out.AttendanceCheckCommandPort; | ||
| import konkuk.thip.attendancecheck.domain.AttendanceCheck; | ||
| import konkuk.thip.room.adapter.out.mapper.AttendanceCheckMapper; | ||
| import konkuk.thip.room.adapter.out.persistence.repository.attendanceCheck.AttendanceCheckJpaRepository; |
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.
p2: 패키지 네이밍할때 카멜케이스 말고 전부 소문자로 적는 것으로 통일하는 것 어떨까요!
- 패키지 네이밍에는 카멜 케이스를 적용하지 않는것이 관례라고 함
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.
Actionable comments posted: 4
♻️ Duplicate comments (1)
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (1)
3-3: 사용하지 않는 의존성 제거
AttendanceCheckMapper를 주입하지만 사용하지 않습니다. 불필요 의존성은 제거해주세요(기존 코멘트와 동일).아래 변경으로 필드/임포트를 정리할 수 있습니다.
-import konkuk.thip.room.adapter.out.mapper.AttendanceCheckMapper; @@ - private final AttendanceCheckMapper attendanceCheckMapper;Also applies to: 18-18
🧹 Nitpick comments (3)
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (1)
21-26: 포트/리포지토리 메서드 이름 정합성 개선 제안현재 리포지토리 메서드명이 “Between”을 포함하지만 실제로는 커스텀 JPQL로 반열림 조건을 사용합니다. 오해를 줄이기 위해 메서드명을 의미 중심으로 리네이밍하는 것을 권장합니다(아래 리포지토리 코멘트 참고). 어댑터 호출부도 함께 변경해 주세요.
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java (2)
14-20: 메서드 명확성 개선(의미 기반 네이밍)커스텀 JPQL을 사용하므로 메서드명이 파라미터와 의미를 잘 드러내도록 리네이밍을 권장합니다. “Between”은 Spring Data 파생 쿼리의 포함 구간 의미를 연상시켜 혼동을 줄 수 있습니다.
예시:
- int countByUserIdAndRoomIdAndCreatedAtBetween(...) + long countActiveByUserIdAndRoomIdCreatedAtInDay(...)어댑터 호출부도 동일하게 변경해 주세요.
13-13: 성능 관점: 인덱스/캐시 전략 제안(정량 근거 포함)
- 현재 쿼리는 조건이 잘 구성되어 있어 적절한 인덱스만 있으면 대부분 “index-only scan”으로 매우 빠르게 처리됩니다. 대규모 데이터에서도 userId+roomId+status+createdAt 복합 인덱스가 있으면 범위 스캔 비용은 O(logN + K) (K는 해당 조건의 일일 레코드 수)로 제한됩니다.
- 권장 인덱스(DDL 예시, PostgreSQL 기준):
- CREATE INDEX CONCURRENTLY idx_attchk_uid_rid_status_createdat ON attendance_check (user_id, room_id, status, created_at);
- 캐시(예: Redis) 도입은 트래픽/쓰기 패턴에 따라 유효하나, 동시성 제어/정합성 복잡도가 증가합니다. 우선 인덱스로 모니터링 후 병목이 확인될 때 도입을 권장합니다.
추가로 JPA 엔티티에 @Index로 스키마 관리 중이라면 아래와 같이 선언할 수 있습니다(별도 파일):
@Table(indexes = { @Index(name = "idx_attchk_uid_rid_status_createdat", columnList = "user_id, room_id, status, created_at") })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (6)
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java(1 hunks)src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java(1 hunks)src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java(1 hunks)src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java(1 hunks)src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java
- src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java
- src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:15:54.900Z
Learning: 오늘의 한마디(AttendanceCheck) 작성 제한은 사용자 전역이 아닌 방(room) 단위로 적용되어야 함. 각 사용자는 각 방에서 하루 최대 5개의 오늘의 한마디를 작성할 수 있음.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#113
File: src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java:38-44
Timestamp: 2025-07-30T14:05:04.945Z
Learning: seongjunnoh는 코드 최적화 제안에 대해 구체적인 기술적 근거와 효율성 차이를 이해하고 싶어하며, 성능 개선 방식에 대한 상세한 설명을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#93
File: src/main/java/konkuk/thip/room/adapter/out/persistence/RoomQueryPersistenceAdapter.java:49-114
Timestamp: 2025-07-28T16:44:31.224Z
Learning: seongjunnoh는 코드 중복 문제에 대한 리팩토링 제안을 적극적으로 수용하고 함수형 인터페이스를 활용한 해결책을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#195
File: src/main/java/konkuk/thip/feed/application/mapper/FeedQueryMapper.java:0-0
Timestamp: 2025-08-13T05:22:32.287Z
Learning: seongjunnoh는 데이터 무결성과 중복 방지에 대한 고민이 깊으며, LinkedHashSet을 활용한 중복 제거와 순서 보장을 동시에 달성하는 솔루션을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/service/AttendanceCheckCreateService.java:22-40
Timestamp: 2025-08-14T09:15:31.332Z
Learning: seongjunnoh는 기술적 완벽성보다 실제 비즈니스 시나리오와 사용자 행동 패턴을 우선 고려하며, 발생 가능성이 낮은 엣지 케이스보다는 실용적인 구현을 선호한다.
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
📚 Learning: 2025-08-14T09:27:36.393Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java:20-24
Timestamp: 2025-08-14T09:27:36.393Z
Learning: AttendanceCheckJpaRepository의 countByUserIdAndCreatedAtBetween 메서드는 메서드명과 달리 사용자 정의 JPQL 쿼리를 사용한다. "a.createdAt >= :startOfDay AND a.createdAt < :endOfDay"로 반열림구간 [startOfDay, endOfDay)을 정확히 구현하고 있어서 경계값 버그가 없다. 메서드명만으로 Spring Data JPA의 기본 Between 동작을 가정하면 안 된다.
Applied to files:
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.javasrc/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
📚 Learning: 2025-08-14T09:27:36.393Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java:20-24
Timestamp: 2025-08-14T09:27:36.393Z
Learning: AttendanceCheckJpaRepository에서 countByUserIdAndCreatedAtBetween 메서드는 실제로는 사용자 정의 JPQL 쿼리 "createdAt >= :startOfDay AND createdAt < :endOfDay"를 사용하여 반열림구간 [startOfDay, endOfDay)을 구현하고 있다. 메서드 이름만으로 Spring Data JPA의 기본 Between 동작을 가정하면 안 된다.
Applied to files:
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.javasrc/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
📚 Learning: 2025-08-14T07:19:27.250Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:19:27.250Z
Learning: AttendanceCheck 엔티티는 BaseJpaEntity를 상속받아 status 필드(StatusType.ACTIVE/INACTIVE/EXPIRED)로 소프트 딜리트를 구현하므로, 카운트 쿼리에서 status = 'ACTIVE' 조건을 포함해야 삭제된 레코드를 제외할 수 있음.
Applied to files:
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.javasrc/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java
📚 Learning: 2025-07-30T10:44:34.115Z
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#112
File: src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedQueryRepositoryImpl.java:272-272
Timestamp: 2025-07-30T10:44:34.115Z
Learning: seongjunnoh는 피드 커서 페이지네이션에서 LocalDateTime 단일 커서 방식을 선호하며, 복합 키 기반 커서보다 구현 단순성과 성능을 우선시한다.
Applied to files:
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
📚 Learning: 2025-08-14T07:15:54.900Z
Learnt from: buzz0331
PR: THIP-TextHip/THIP-Server#180
File: src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java:5-5
Timestamp: 2025-08-14T07:15:54.900Z
Learning: 오늘의 한마디(AttendanceCheck) 작성 제한은 사용자 전역이 아닌 방(room) 단위로 적용되어야 함. 각 사용자는 각 방에서 하루 최대 5개의 오늘의 한마디를 작성할 수 있음.
Applied to files:
src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (1)
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java (1)
Repository(18-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java (1)
3-7: 방 단위 카운트 도입은 방향성 적합Room 단위 제한을 포트에 반영한 점 좋습니다. 비즈니스 요구사항(방 별 하루 5회 제한)에 부합합니다.
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java (1)
25-25: 경계 처리/상태 필터 적절함
- [startOfDay, endOfDay) 반열림 구간과
status = ACTIVE필터링이 정확히 반영되어 있습니다. 소프트 딜리트/만료 레코드 제외 요건 충족합니다.src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java (1)
14-20: 반열림구간/상태 필터 구현 적합
createdAt >= :startOfDay AND createdAt < :endOfDay로 경계값 버그 없이 일자 범위를 정확히 집계합니다.status조건 포함으로 소프트 딜리트 제외 요건 충족합니다.
| LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); | ||
| LocalDateTime endOfDay = startOfDay.plusDays(1); | ||
|
|
||
| return attendanceCheckJpaRepository.countByUserIdAndRoomIdAndCreatedAtBetween(userId, roomId, startOfDay, endOfDay, ACTIVE); |
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.
🛠️ Refactor suggestion
시간대 의존성 및 테스트 용이성 개선: Clock 주입으로 교체
LocalDateTime.now()는 서버/OS/JVM 타임존에 의존합니다. 운영/개발 환경 타임존을 맞추셨더라도, 테스트 용이성과 이식성을 위해 Clock을 주입받아 사용하시길 권장합니다(비즈니스 타임존이 Asia/Seoul이라면 해당 Zone으로 고정된 Clock 빈 권장).
아래와 같이 적용 가능합니다(별도의 @Configuration에서 Clock.system(ZoneId.of("Asia/Seoul")) 빈 제공 가정).
+import java.time.Clock;
@@
-public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort {
+public class AttendanceCheckQueryPersistenceAdapter implements AttendanceCheckQueryPort {
@@
- public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) {
- LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
- LocalDateTime endOfDay = startOfDay.plusDays(1);
+ private final Clock clock;
+
+ public int countAttendanceChecksOnTodayByUser(Long userId, Long roomId) {
+ LocalDateTime startOfDay = LocalDateTime.now(clock).toLocalDate().atStartOfDay();
+ LocalDateTime endOfDay = startOfDay.plusDays(1);추가(별도 파일) 예시:
// 예: TimeConfig.java
@Bean
public Clock clock() {
return Clock.system(ZoneId.of("Asia/Seoul"));
}🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/AttendanceCheckQueryPersistenceAdapter.java
around lines 22 to 25, replace the direct use of LocalDateTime.now() with an
injected java.time.Clock to remove timezone/OS dependency: add a Clock field
injected via constructor (or @Autowired), then compute startOfDay as
LocalDate.now(clock).atStartOfDay() and endOfDay as startOfDay.plusDays(1);
ensure callers/DI provide a Clock bean (e.g.,
Clock.system(ZoneId.of("Asia/Seoul"))) and use a fixed Clock in tests for
determinism.
| package konkuk.thip.room.adapter.out.persistence.repository.attendancecheck; | ||
|
|
||
| import konkuk.thip.common.entity.StatusType; | ||
| import konkuk.thip.room.adapter.out.jpa.AttendanceCheckJpaEntity; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> { | ||
|
|
||
| // TODO : count 값을 매번 쿼리를 통해 계산하는게 아니라 DB에 저장 or redis 캐시에 저장하는 방법도 좋을 듯 | ||
| @Query("SELECT COUNT(a) FROM AttendanceCheckJpaEntity a " + | ||
| "WHERE a.userJpaEntity.userId = :userId " + | ||
| "AND a.roomJpaEntity.roomId = :roomId " + | ||
| "AND a.status = :status " + | ||
| "AND a.createdAt >= :startOfDay " + | ||
| "AND a.createdAt < :endOfDay") | ||
| int countByUserIdAndRoomIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("roomId") Long roomId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay, @Param("status")StatusType status); | ||
| } |
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.
💡 Verification agent
🧩 Analysis chain
리포지토리 단위 테스트/슬라이스 테스트 보강 제안
경계시간(자정 직전/직후), 타임존(Asia/Seoul) 및 status 필터링(활성/삭제/만료)을 검증하는 슬라이스 테스트를 추가하면 회귀를 방지할 수 있습니다.
아래 스크립트로 관련 테스트 존재 여부를 확인해 주세요. 없다면 추가를 권장합니다.
🏁 Script executed:
#!/bin/bash
# 리포지토리/어댑터 테스트 존재 여부 점검
rg -n "AttendanceCheckJpaRepository|AttendanceCheckQueryPersistenceAdapter" src/test src/it || true
# 경계 케이스 키워드 탐색
rg -n "startOfDay|endOfDay|Asia/Seoul|ZoneId|Clock" src/test || trueLength of output: 569
테스트 보강 필요 — AttendanceCheckJpaRepository의 경계시간/타임존/상태 필터링 검증 추가
간단 검증 결과: src/test에서 AttendanceCheckCreateApiTest가 AttendanceCheckJpaRepository를 사용함을 확인했으나, "startOfDay|endOfDay|Asia/Seoul|ZoneId|Clock" 관련 키워드는 검색되지 않아 경계/타임존/상태 케이스에 대한 테스트가 없습니다.
확인된 위치
- 검토 대상 레포지토리: src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java
- 현재 테스트: src/test/java/konkuk/thip/room/adapter/in/web/AttendanceCheckCreateApiTest.java (레포지토리 주입 확인)
권장 조치 (간단)
- 리포지토리 슬라이스(@DataJpaTest 등) 또는 해당 어댑터용 단위/슬라이스 테스트 추가.
- 필수 케이스: (1) 자정 직전/직후 경계 테스트, (2) 타임존(Asia/Seoul) 영향 검증 — 가능하면 Clock 주입으로 시간 제어, (3) status별 필터(활성/삭제/만료) 검증.
- 테스트 위치 제안: src/test/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepositoryTest.java 또는 기존 AttendanceCheckCreateApiTest 확장.
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> { |
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.
🛠️ Refactor suggestion
COUNT 반환 타입은 long 사용 권장
JPA COUNT는 Long으로 계산됩니다. 불필요한 narrowing을 피하려면 반환 타입을 long으로 두는 것이 안전합니다.
아래처럼 시그니처를 변경해주세요(어댑터/포트/서비스 호출부 전파 수정 필요).
-public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> {
+public interface AttendanceCheckJpaRepository extends JpaRepository<AttendanceCheckJpaEntity, Long> {
@@
- int countByUserIdAndRoomIdAndCreatedAtBetween(...);
+ long countByUserIdAndRoomIdAndCreatedAtBetween(...);Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java
around line 11, the repository currently exposes a COUNT return type that is not
long; change the method signature(s) that return COUNT to use primitive long
instead of int/Long wrapper to match JPA COUNT semantics. Update the repository
interface signature to return long and then propagate that change through the
adapter, port, and service layers (update return types and any
callers/assignments) so all usages accept/handle a long. Ensure compilation by
adjusting method declarations, any interfaces, DTOs or mappers, and tests that
expect the previous type.
|
|
||
| public interface AttendanceCheckQueryPort { | ||
|
|
||
| int countAttendanceChecksOnTodayByUser(Long userId, Long roomId); |
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.
🛠️ Refactor suggestion
메서드 명세 명확화 및 반환 타입 재고(일관성/확장성)
- 현재 시그니처는 방 단위 제한을 잘 반영하고 있으나, 메서드명은 room 범위를 드러내지 않아 혼동 소지가 있습니다. 또한 COUNT는 DB에서 Long으로 계산되는 것이 일반적이므로, 반환 타입을 long으로 두는 것이 안전합니다.
아래와 같이 명확성/일관성을 개선하는 것을 제안합니다(호출부/구현부 전파 수정 필요).
- int countAttendanceChecksOnTodayByUser(Long userId, Long roomId);
+ long countAttendanceChecksTodayByUserInRoom(Long userId, Long roomId);📝 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.
| int countAttendanceChecksOnTodayByUser(Long userId, Long roomId); | |
| - int countAttendanceChecksOnTodayByUser(Long userId, Long roomId); | |
| + long countAttendanceChecksTodayByUserInRoom(Long userId, Long roomId); |
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/room/application/port/out/AttendanceCheckQueryPort.java
around line 5, the method name and return type are unclear: rename the method to
reflect the room scope (e.g., countAttendanceChecksTodayByUserAndRoom or
countAttendanceChecksOnTodayByUserInRoom) and change the return type from
Integer to primitive long to match typical DB COUNT semantics; then propagate
this change to all callers and implementing classes, update method signatures
and imports, and adjust any tests or mappings to handle the long return value.
buzz0331
left a comment
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.
굿굿 좋습니다~ 수고하셨어요!
|
|
||
| import konkuk.thip.room.adapter.out.mapper.AttendanceCheckMapper; | ||
| import konkuk.thip.room.adapter.out.persistence.repository.attendanceCheck.AttendanceCheckJpaRepository; | ||
| import konkuk.thip.room.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; |
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.
👍🏻 👍🏻
#️⃣ 연관된 이슈
📝 작업 내용
오늘의 한마디를 작성하는 api 를 개발하였습니다
간단한 api 입니다
주요 로직
📸 스크린샷
💬 리뷰 요구사항
원래 오늘의 한마디를 조회하는 api 와 같이 개발하여 pr 날리려고 했으나
해서 일단 오늘의 한마디 작성 api 만 pr 날리겠습니다!!
오늘의 한마디 조회 api 의 response 논의는 카톡으로 말씀드리겠습니다!
📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
신기능
문서
리팩터
테스트