Conversation
Walkthrough이 PR은 커서 기반 페이징을 지원하는 그룹 조회 기능을 추가합니다. 새로운 컨트롤러(GroupQueryController, MemberController), 페이징 요청/응답 DTO, QueryDSL 기반 저장소 구현, 그리고 관련 서비스 및 use case를 도입합니다. 또한 GroupMember 도메인 모델을 리팩토링하여 RoleEntity 객체로 역할을 관리합니다. Changes
Sequence Diagram(s)sequenceDiagram
actor Client
participant GroupQueryController
participant FindGroupService
participant GroupRepositoryPort
participant GroupRepositoryImpl
participant Database
Client->>GroupQueryController: GET /v1/groups<br/>(userId, GroupListRequestDto)
activate GroupQueryController
GroupQueryController->>FindGroupService: findAllGroup(userId, req)
activate FindGroupService
FindGroupService->>GroupRepositoryPort: findAllByCursor(cursorId, category, size)
activate GroupRepositoryPort
GroupRepositoryPort->>GroupRepositoryImpl: findAllByCursor(...)
activate GroupRepositoryImpl
GroupRepositoryImpl->>Database: QueryDSL 쿼리<br/>(그룹 조회, 필터링)
Database-->>GroupRepositoryImpl: List<GroupInfo>
deactivate GroupRepositoryImpl
GroupRepositoryPort-->>FindGroupService: List<GroupInfo>
deactivate GroupRepositoryPort
FindGroupService->>FindGroupService: 페이지 계산<br/>(hasNext, nextCursor)
FindGroupService-->>GroupQueryController: CursorPagingResponseDto<GroupInfo>
deactivate FindGroupService
GroupQueryController-->>Client: 200 OK<br/>CursorPagingResponseDto
deactivate GroupQueryController
sequenceDiagram
actor Client
participant MemberController
participant FindGroupMemberService
participant GroupMemberRepositoryPort
participant GroupMemberRepositoryAdapter
participant Database
Client->>MemberController: GET /v1/groups/{groupId}/members<br/>(userId, groupId)
activate MemberController
MemberController->>FindGroupMemberService: findGroupMember(cmd)
activate FindGroupMemberService
FindGroupMemberService->>GroupMemberRepositoryPort: existsUserInGroup(groupId, userId)
activate GroupMemberRepositoryPort
GroupMemberRepositoryPort->>GroupMemberRepositoryAdapter: existsUserInGroup(...)
activate GroupMemberRepositoryAdapter
GroupMemberRepositoryAdapter->>Database: 사용자 멤버십 확인
Database-->>GroupMemberRepositoryAdapter: 확인 완료/예외
deactivate GroupMemberRepositoryAdapter
GroupMemberRepositoryPort-->>FindGroupMemberService: void/예외
deactivate GroupMemberRepositoryPort
FindGroupMemberService->>GroupMemberRepositoryPort: findMemberInfo(groupId)
activate GroupMemberRepositoryPort
GroupMemberRepositoryPort->>GroupMemberRepositoryAdapter: findMemberInfo(...)
activate GroupMemberRepositoryAdapter
GroupMemberRepositoryAdapter->>Database: 그룹 멤버 조회
Database-->>GroupMemberRepositoryAdapter: List<GroupMemberEntity>
GroupMemberRepositoryAdapter->>GroupMemberRepositoryAdapter: 매퍼: toMemberInfo(entities)
GroupMemberRepositoryAdapter-->>GroupMemberRepositoryPort: List<MemberInfo>
deactivate GroupMemberRepositoryAdapter
GroupMemberRepositoryPort-->>FindGroupMemberService: List<MemberInfo>
deactivate GroupMemberRepositoryPort
FindGroupMemberService-->>MemberController: FindGroupMemberResult
deactivate FindGroupMemberService
MemberController->>MemberController: 응답 변환<br/>(FindGroupMemberResponseDto)
MemberController-->>Client: 200 OK<br/>FindGroupMemberResponseDto
deactivate MemberController
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 20
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java (1)
33-36:⚠️ Potential issue | 🟡 MinorJavadoc
@return태그가void메서드에 남아 있습니다.메서드 반환형이
void로 변경되었으나 Javadoc의@return태그가 제거되지 않았습니다.📝 수정 제안
/** * 유저가 그룹 내에 있는지 체크 * `@param` groupId * `@param` userId - * `@return` */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java` around lines 33 - 36, In GroupMemberRepositoryAdapter, locate the Javadoc block that documents `@param` groupId and `@param` userId for the method whose return type was changed to void and remove the stray `@return` tag; ensure the Javadoc now only contains the descriptive text and the `@param` tags (and update the description if necessary) so it correctly matches the void method signature.
🧹 Nitpick comments (16)
src/main/java/flipnote/group/adapter/out/persistence/GroupRoleRepositoryAdapter.java (1)
15-16:GroupMemberRepositoryRepository이름에 "Repository"가 중복됩니다.
GroupMemberRepositoryRepository는 명명 오류로 보입니다. 이 파일뿐 아니라 해당 인터페이스 자체를GroupMemberJpaRepository등으로 변경하는 것이 좋겠습니다. (해당 인터페이스 파일 리뷰에서 자세히 언급합니다.)Also applies to: 26-26
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/adapter/out/persistence/GroupRoleRepositoryAdapter.java` around lines 15 - 16, The import and type name GroupMemberRepositoryRepository is duplicated ("Repository" twice); rename the interface to a clearer name such as GroupMemberJpaRepository, update the import in GroupRoleRepositoryAdapter (and any other files) to use the new name, and update all usages (field types, constructor parameters, method calls) that reference GroupMemberRepositoryRepository inside this class to the new symbol; perform the refactor via your IDE (rename file and type) or update the interface file name and all references so imports and types (e.g., the field/parameter where GroupMemberRepositoryRepository is used) match the new GroupMemberJpaRepository identifier.src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java (1)
6-6: 사용되지 않는 import가 있습니다.
org.springframework.data.jpa.repository.Query와flipnote.group.adapter.out.entity.RoleEntity가 import되었지만 이 인터페이스에서 사용되지 않습니다.🧹 미사용 import 제거
-import org.springframework.data.jpa.repository.Query; - -import flipnote.group.adapter.out.entity.GroupMemberEntity; -import flipnote.group.adapter.out.entity.RoleEntity; +import flipnote.group.adapter.out.entity.GroupMemberEntity;Also applies to: 9-9
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java` at line 6, Remove the unused imports in GroupMemberRepositoryRepository: delete the import org.springframework.data.jpa.repository.Query and the import flipnote.group.adapter.out.entity.RoleEntity since they are not referenced in the interface (GroupMemberRepositoryRepository); ensure no other code in that file depends on Query or RoleEntity and run a quick compile to confirm unused-import warnings are gone.src/main/java/flipnote/group/application/port/out/GroupMemberRepositoryPort.java (1)
11-11:existsUserInGroup의void반환 타입은 포트 계약 수준에서의 의미 불명확성을 야기합니다.포트를 구현하는 어댑터가 미가입 케이스에서 예외를 던지는지, 아니면 단순히 아무것도 하지 않는지를 시그니처만으로 파악할 수 없습니다. 포트에 Javadoc 또는 명칭 변경(
assertUserInGroup)으로 명확한 계약을 명시하는 것을 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/application/port/out/GroupMemberRepositoryPort.java` at line 11, The method signature void existsUserInGroup(Long groupId, Long userId) is ambiguous at the port level; update the contract to make behavior explicit by either (A) changing the return type to boolean (e.g., existsUserInGroup returns true/false) so adapters return existence, or (B) renaming to assertUserInGroup and document/implement it to throw a specific unchecked exception when the user is not a member. Add Javadoc to GroupMemberRepositoryPort describing the chosen contract (return boolean vs throw) so adapters implementing existsUserInGroup/assertUserInGroup know whether to return a flag or throw an exception.src/main/java/flipnote/group/api/dto/response/FindGroupMemberResponseDto.java (1)
8-14: API 응답 DTO가 도메인 타입MemberInfo를 직접 노출합니다.
MemberInfo는 도메인 레이어의 객체로, 이를 API 응답 DTO에 그대로 사용하면 도메인 변경 사항이 API 계약에 직접 영향을 미치게 됩니다.MemberInfoDto와 같은 별도의 API 전용 응답 타입으로 분리하는 것을 권장합니다.♻️ 리팩토링 제안
별도 API 응답 레코드 생성:
// api/dto/response/MemberInfoDto.java (신규) public record MemberInfoDto(Long userId, GroupMemberRole role) { public static MemberInfoDto from(MemberInfo info) { return new MemberInfoDto(info.getUserId(), info.getRole()); } }
FindGroupMemberResponseDto수정:public record FindGroupMemberResponseDto( - List<MemberInfo> memberInfoList + List<MemberInfoDto> memberInfoList ) { public static FindGroupMemberResponseDto from(FindGroupMemberResult result) { - return new FindGroupMemberResponseDto(result.memberInfoList()); + return new FindGroupMemberResponseDto( + result.memberInfoList().stream() + .map(MemberInfoDto::from) + .toList() + ); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/api/dto/response/FindGroupMemberResponseDto.java` around lines 8 - 14, The response DTO currently exposes the domain type MemberInfo; create a new API record MemberInfoDto (e.g., record MemberInfoDto(Long userId, GroupMemberRole role) with a static from(MemberInfo)) and update FindGroupMemberResponseDto.from to convert result.memberInfoList() into a List<MemberInfoDto> (mapping each MemberInfo via MemberInfoDto.from) so FindGroupMemberResponseDto holds List<MemberInfoDto> instead of List<MemberInfo>, leaving FindGroupMemberResult and other domain classes untouched.src/main/java/flipnote/group/application/service/FindGroupMemberService.java (1)
25-35: 읽기 전용 서비스 메서드에@Transactional(readOnly = true)어노테이션이 누락되어 있습니다.
findGroupMember는 순수 조회 작업으로,@Transactional(readOnly = true)어노테이션을 추가하면 JPQL 쿼리 최적화(dirty checking 비활성화 등)의 이점을 얻을 수 있습니다.♻️ 수정 제안
+ import org.springframework.transaction.annotation.Transactional; ... `@Override` + `@Transactional`(readOnly = true) public FindGroupMemberResult findGroupMember(FindGroupMemberCommand cmd) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/application/service/FindGroupMemberService.java` around lines 25 - 35, The findGroupMember method in FindGroupMemberService is a read-only operation but lacks the `@Transactional`(readOnly = true) annotation; update the FindGroupMemberService class by annotating the findGroupMember(FindGroupMemberCommand cmd) method with `@Transactional`(readOnly = true) (and add the import for org.springframework.transaction.annotation.Transactional if missing) so JPQL/ORM read optimizations apply while leaving groupMemberRepository.existsUserInGroup and groupMemberRepository.findMemberInfo calls unchanged.src/main/java/flipnote/group/adapter/in/web/MemberController.java (1)
43-43: TODO: 하위 권한 수정 항목이 해결되지 않았습니다.권한 수정 기능이 필요하다면 별도 이슈로 추적할 것을 권장합니다.
해당 TODO를 별도 이슈로 등록하길 원하시면 구현 스켈레톤 생성을 도와드릴 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/adapter/in/web/MemberController.java` at line 43, The inline TODO "하위 권한 수정" in MemberController is unresolved; either track it as a separate issue and replace the comment with a TODO referencing that issue ID, or implement a minimal scaffold: add a controller endpoint stub (e.g., modifySubPermissions or updateSubPermissions) in MemberController that validates the request DTO and delegates to a MemberService.updateSubPermissions method (create the service method signature if missing) so the intent is explicit and testable; include input validation and a TODO comment in the new stub linking to the issue if full implementation is deferred.src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java (3)
39-46: 비즈니스 규칙 위반에IllegalArgumentException대신 도메인 전용 예외 사용을 권장합니다.
IllegalArgumentException은 메서드에 잘못된 인자가 전달되는 프로그래밍 오류를 나타내는 JDK 표준 예외입니다. "사용자가 그룹 내에 없음"은 비즈니스 규칙 위반이므로, 도메인 레이어에 별도의 예외 클래스(UserNotInGroupException등)를 정의하여 사용하면 오류 유형을 명확히 구분할 수 있고, 상위 레이어의 예외 처리(@ExceptionHandler) 도 일관성 있게 유지할 수 있습니다.♻️ 리팩토링 제안
도메인 레이어에 예외 클래스 추가 (예:
src/main/java/flipnote/group/domain/exception/UserNotInGroupException.java):public class UserNotInGroupException extends RuntimeException { public UserNotInGroupException() { super("user not in Group"); } }어댑터에서 교체:
if (!isMember) { - throw new IllegalArgumentException("user not in Group"); + throw new UserNotInGroupException(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java` around lines 39 - 46, Replace the use of IllegalArgumentException in GroupMemberRepositoryAdapter.existsUserInGroup with a domain-specific runtime exception: create a UserNotInGroupException in the domain layer (e.g., extend RuntimeException with a default message "user not in Group"), then change the method so that when groupMemberRepository.existsByGroupIdAndUserId(groupId, userId) is false it throws new UserNotInGroupException(); keep the boolean check and repository call intact but swap the thrown exception to the new domain exception so upper layers can handle this business-rule violation consistently.
54-60: 중간 변수memberInfo제거로 코드를 간결하게 만들 수 있습니다.♻️ 리팩토링 제안
`@Override` public List<MemberInfo> findMemberInfo(Long groupId) { List<GroupMemberEntity> entities = groupMemberRepository.findAllByGroupId(groupId); - - List<MemberInfo> memberInfo = GroupMemberMapper.toMemberInfo(entities); - - return memberInfo; + return GroupMemberMapper.toMemberInfo(entities); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java` around lines 54 - 60, The method findMemberInfo in GroupMemberRepositoryAdapter unnecessarily stores the result in a local variable memberInfo; remove that intermediate variable and return GroupMemberMapper.toMemberInfo(...) directly (either by passing the entities list from groupMemberRepository.findAllByGroupId(groupId) or by keeping the entities local and returning GroupMemberMapper.toMemberInfo(entities) inline) so the method becomes a single return statement that calls GroupMemberMapper.toMemberInfo on the results from groupMemberRepository.findAllByGroupId.
20-20:GroupMemberRepositoryRepository인터페이스 이름에서 "Repository"가 중복됩니다.인프라 레이어의 JPA 리포지토리 인터페이스 이름(
GroupMemberRepositoryRepository)에 "Repository"가 이중으로 포함되어 있습니다. 같은 패키지의 다른 리포지토리들(GroupRoleRepository,GroupRolePermissionRepository)과의 명명 규칙 일관성을 위해GroupMemberJpaRepository또는GroupMemberRepository로 개선하는 것을 권장합니다. 코드베이스에 2곳에만 참조되어 있으므로 리팩토링 비용이 낮습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java` at line 20, The interface name GroupMemberRepositoryRepository is redundant; rename the JPA repository interface to a consistent name (e.g., GroupMemberJpaRepository or GroupMemberRepository) and update all usages (including the field in GroupMemberRepositoryAdapter and the other reference site) to the new type name; ensure imports and constructor/injection points in GroupMemberRepositoryAdapter reflect the renamed interface and run compilation to catch any remaining references, aligning naming with GroupRoleRepository and GroupRolePermissionRepository.src/main/java/flipnote/group/api/dto/request/GroupListRequestDto.java (1)
16-19:getPageRequest()오버라이드가 부모의sortBy/order필드를 무시합니다.하드코딩된 정렬(
Sort.by(Sort.Direction.DESC, "id"))로 인해CursorPagingRequest에서 상속받은sortBy와order필드가 이 DTO에서는 의미 없는 값이 됩니다. 의도된 설계라면 괜찮지만, 커서 기반 페이지네이션에서PageRequest가 실제로 사용되는 곳이 있는지 확인해 보세요. 사용되지 않는다면 오버라이드 자체가 불필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/api/dto/request/GroupListRequestDto.java` around lines 16 - 19, getPageRequest() currently hardcodes Sort.by(Sort.Direction.DESC, "id") and thus ignores the inherited sortBy and order fields from CursorPagingRequest; either remove this override if PageRequest isn't actually used for cursor pagination, or change getPageRequest() to respect the parent fields by building the PageRequest using getSize() and Sort.by(convert order and sortBy into Sort.Direction and property) (reference the getPageRequest() method, the sortBy and order fields from CursorPagingRequest, and the PageRequest.of(...) call to locate the code).src/main/java/flipnote/group/domain/model/group/GroupInfo.java (1)
9-11:from팩토리 메서드가 생성자와 동일하여 불필요합니다.레코드의 canonical 생성자와 동일한 파라미터를 받아 그대로 전달만 하고 있어, 별도의 변환이나 검증 로직이 없다면 이 팩토리 메서드는 중복됩니다. 호출부에서
new GroupInfo(...)를 직접 사용하거나, 향후 변환/검증 로직이 추가될 예정이라면 그대로 유지해도 무방합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/domain/model/group/GroupInfo.java` around lines 9 - 11, The static factory method from in the GroupInfo record is redundant because it simply forwards to the canonical constructor; remove the from(Long groupId, String name, String description, Category category, Long imageRefId) method from the GroupInfo record and update all callers to use the record's canonical constructor new GroupInfo(...) instead, or if you prefer to keep a factory for future validation, replace the body with the intended validation/translation logic inside GroupInfo.from before constructing the record.src/main/java/flipnote/group/api/dto/request/CursorPagingRequest.java (1)
26-41:normalized.isEmpty()체크가 중복됩니다.
StringUtils.hasText(cursor)는 이미null, 빈 문자열, 공백만 있는 문자열을 모두false로 처리합니다.hasText가true를 반환한 경우cursor.trim()의 결과가 빈 문자열일 수 없으므로 Line 32-33의 체크는 불필요합니다.♻️ 중복 체크 제거 제안
public Long getCursorId() { if (!StringUtils.hasText(cursor)) { return null; } - final String normalized = cursor.trim(); - if (normalized.isEmpty()) { - return null; - } - try { - return Long.valueOf(normalized); + return Long.valueOf(cursor.trim()); } catch (NumberFormatException ex) { return null; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/api/dto/request/CursorPagingRequest.java` around lines 26 - 41, The getCursorId method currently redundantly checks normalized.isEmpty() after StringUtils.hasText(cursor); remove the duplicate check (lines that return null when normalized.isEmpty()) and simply trim the cursor and parse it, keeping the try/catch for NumberFormatException; update the method in CursorPagingRequest to rely on StringUtils.hasText(cursor) and then do Long.valueOf(cursor.trim()) (or use the existing normalized variable) so behavior remains the same but without the unnecessary empty-string branch.src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryCustom.java (1)
8-13: 파라미터 네이밍이 메서드 간에 일관성이 없습니다.세 메서드가 동일한 역할의 파라미터를 서로 다른 이름으로 사용하고 있습니다:
- 커서 ID:
lastId(메서드 1, 2) vscursorId(메서드 3)- 페이지 크기:
pageSize(메서드 1, 2) vssize(메서드 3)- 사용자 ID:
userId(메서드 2) vsid(메서드 3)특히
id는 어떤 엔티티의 ID인지 모호합니다. 일관된 네이밍을 적용해 주세요.♻️ 일관된 네이밍 제안
public interface GroupRepositoryCustom { List<GroupInfo> findAllByCursor(Long cursorId, Category category, int pageSize); List<GroupInfo> findAllByCursorAndUserId(Long cursorId, Category category, int pageSize, Long userId); - List<GroupInfo> findAllByCursorAndCreatedUserId(Long cursorId, Category category, int size, Long id); + List<GroupInfo> findAllByCursorAndCreatedUserId(Long cursorId, Category category, int pageSize, Long createdUserId); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryCustom.java` around lines 8 - 13, The three methods use inconsistent parameter names; make them consistent and explicit: use lastId for the cursor, pageSize for the page size, and a clear user parameter name that matches the method intent (userId for findAllByCursorAndUserId, createdUserId for findAllByCursorAndCreatedUserId). Update the method signature of findAllByCursorAndCreatedUserId(Long cursorId, Category category, int size, Long id) to findAllByCursorAndCreatedUserId(Long lastId, Category category, int pageSize, Long createdUserId) and ensure corresponding implementations/referrers use the same names.src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryImpl.java (1)
24-25: Q타입 필드를private static final로 선언하세요
QGroupEntity.groupEntity는 이미public static final싱글턴이며 재사용해도 메모리와 초기화 비용을 절약할 수 있습니다. 현재 선언은private도,final도 아니어서 의도치 않은 재할당 및 패키지 내 접근이 가능합니다.♻️ 수정 제안
-QGroupEntity group = QGroupEntity.groupEntity; -QGroupMemberEntity groupMember = QGroupMemberEntity.groupMemberEntity; +private static final QGroupEntity group = QGroupEntity.groupEntity; +private static final QGroupMemberEntity groupMember = QGroupMemberEntity.groupMemberEntity;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryImpl.java` around lines 24 - 25, The Q-type fields are declared as mutable instance variables; change the declarations for the Q-type references (currently named group and groupMember) to private static final and initialize them with QGroupEntity.groupEntity and QGroupMemberEntity.groupMemberEntity respectively so they reuse the existing public singletons and prevent reassignment and unintended package access.src/main/java/flipnote/group/application/port/in/FindGroupUseCase.java (1)
3-4: [선택적 개선] 인바운드 포트에서 웹 레이어 DTO 직접 참조
FindGroupUseCase포트가flipnote.group.api.dto패키지에 속한GroupListRequestDto와CursorPagingResponseDto를 직접 참조하고 있습니다. 헥사고날 아키텍처 관점에서 애플리케이션 포트(인바운드)는 웹 어댑터 전용 DTO에 의존하지 않고, 별도의 쿼리 커맨드 객체(FindAllGroupQuery등)를 사용하는 것이 경계를 명확히 합니다. 현재 구조에서는 향후 non-HTTP 어댑터(예: gRPC) 추가 시 웹 DTO를 재사용해야 하는 부자연스러운 결합이 발생할 수 있습니다.Also applies to: 12-16
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/application/port/in/FindGroupUseCase.java` around lines 3 - 4, FindGroupUseCase currently depends on web-layer DTOs (GroupListRequestDto, CursorPagingResponseDto); replace those web DTO types with application-level query/response objects (e.g., create FindAllGroupQuery for input and a port-level PagedGroupResult or FindGroupResult for output) and update the FindGroupUseCase method signature(s) to accept the new query and return the new port response type so the inbound port no longer imports flipnote.group.api.dto; update any adapters/controllers to map between web DTOs and the new query/response objects.src/main/java/flipnote/group/application/service/FindGroupService.java (1)
49-57:findAllGroup에서userId파라미터가 사용되지 않습니다
userId를 파라미터로 받지만groupRepository.findAllByCursor(...)호출 시 전달하지 않습니다. 전체 그룹 조회에서 인증/인가 목적으로 남겨둔 것이라면 주석으로 의도를 명시하고, 현재 불필요하다면 제거하는 것이 명확합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/flipnote/group/application/service/FindGroupService.java` around lines 49 - 57, The userId parameter in findAllGroup(Long userId, GroupListRequestDto req) is unused; either pass it into the repository call or remove it/comment intent. If the service should scope results by user, update the call to groupRepository.findAllByCursor(...) to include userId (adjust repository/findAllByCursor signature and callers accordingly) so the method uses user context; otherwise remove the userId parameter from findAllGroup (and update callers) or add a clear comment above the method explaining why userId is intentionally unused for authorization/telemetry purposes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/main/java/flipnote/group/adapter/in/web/GroupQueryController.java`:
- Around line 17-20: GroupQueryController shares the same
`@RequestMapping`("/v1/groups") base path as GroupController which can cause
routing conflicts; change the base path on GroupQueryController (the class
annotated with `@RestController` and `@RequiredArgsConstructor`) to a distinct
prefix such as `@RequestMapping`("/v1/groups/list") and update its handler method
mappings accordingly (or alternatively consolidate GroupQueryController into
GroupController by moving its methods into the existing GroupController class)
so that methods like the list endpoints no longer collide with GroupController's
/v1/groups/{groupId} routes.
In `@src/main/java/flipnote/group/adapter/in/web/InvitationController.java`:
- Line 4: Fix the typo in the TODO comment inside InvitationController (class
InvitationController) by replacing "가인" with "가입" so the comment reads "//todo
가입 신청 요청"; update any matching TODO occurrences in that file to keep wording
consistent.
- Around line 1-3: Add Spring MVC annotations to register the controller:
annotate the InvitationController class with `@RestController` and a class-level
`@RequestMapping` (e.g. "/invitations" or another suitable base path) so Spring
will detect and route requests to it; ensure you import
org.springframework.web.bind.annotation.RestController and
org.springframework.web.bind.annotation.RequestMapping and place the annotations
directly above the public class InvitationController declaration.
In `@src/main/java/flipnote/group/adapter/out/entity/GroupMemberEntity.java`:
- Around line 46-48: The repository method
GroupMemberRepositoryRepository.findAllByGroupId currently triggers N+1 when
GroupMemberMapper.toMemberInfo accesses entity.getRole().getRole(); fix by
eager-loading role in the query — either annotate findAllByGroupId with
`@EntityGraph`(attributePaths = "role") or replace it with a JPQL fetch-join query
(SELECT m FROM GroupMemberEntity m JOIN FETCH m.role WHERE m.groupId = :groupId)
so the RoleEntity referenced by GroupMemberEntity.role is loaded in the same
query.
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java`:
- Around line 48-51: Javadoc "그룹 멤버 아이디 조회"는 실제 반환값과 맞지 않습니다; 수정하려는 Javadoc을
GroupMemberRepositoryAdapter 클래스의 해당 메서드(현재 주석 위에 있는 메서드)를 찾아 반환형이 MemberInfo(예:
userId와 role 등을 포함하는 객체)의 목록임을 명시하도록 변경하세요 — 예: "그룹 멤버 목록 반환 (MemberInfo: userId
및 role 포함)" 및 `@return에` List<MemberInfo> 설명을 추가해 실제 동작과 주석이 일치하도록 만드세요.
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupRepositoryAdapter.java`:
- Around line 60-63: The method
GroupRepositoryAdapter.findAllByCursorAndCreatedUserId currently calls itself
causing a StackOverflowError; update the implementation to delegate to the
injected repository (groupRepository.findAllByCursorAndCreatedUserId) using the
same parameters (cursorId, category, size, userId) instead of calling
this.findAllByCursorAndCreatedUserId; ensure you reference the groupRepository
field and return its result.
In
`@src/main/java/flipnote/group/adapter/out/persistence/mapper/GroupMemberMapper.java`:
- Around line 24-35: The toDomain method in GroupMemberMapper is missing mapping
for userId, causing GroupMember.userId to be null; update the
GroupMember.builder() call inside toDomain(GroupMemberEntity entity) to include
.userId(entity.getUserId()) (using GroupMemberEntity.getUserId() and the
GroupMember builder) so the domain object properly preserves the userId field.
- Around line 47-52: The mapping pipeline in GroupMemberMapper calls
entity.getRole().getRole() which can NPE if entity.getRole() is null; update the
stream mapping to guard against null role by either filtering out entities with
null getRole() before mapping or by supplying a safe fallback when building
MemberInfo (e.g., use a default role string or enum constant) and include a
clear comment/log or throw an IllegalStateException with context if null should
be impossible; change the lambda in the map (the MemberInfo.builder() call) to
reference entity.getRole() safely (null-check or Optional) instead of calling
getRole() directly.
In `@src/main/java/flipnote/group/api/dto/request/CursorPagingRequest.java`:
- Around line 18-20: The size field in CursorPagingRequest is declared as boxed
Integer with a default of 10, so if a caller sets it to null the
PageRequest.of(0, size) call (used in the class) will NPE on unboxing; fix by
making size a primitive int (private int size = 10) or, if boxed is required,
add `@NotNull` to the field and/or make the accessor defensive (e.g., getSize()
returns size == null ? 10 : size) and ensure any setter enforces non-null to
prevent null being passed into PageRequest.of; update references to
CursorPagingRequest.size/getSize accordingly.
In `@src/main/java/flipnote/group/api/dto/response/CursorPagingResponseDto.java`:
- Around line 13-15: The static factory method CursorPagingResponseDto.of should
defensively handle a null content list to avoid a NullPointerException: update
CursorPagingResponseDto.of(List<T> content, boolean hasNext, String nextCursor)
to treat a null content as an empty list (e.g., use Collections.emptyList() or
new ArrayList<>()) and compute size with a null-safe expression (content == null
? 0 : content.size()), then pass the normalized list, hasNext, and (hasNext ?
nextCursor : null) into the constructor so content is never null and size is
correct.
In
`@src/main/java/flipnote/group/application/port/in/result/FindAllGroupResult.java`:
- Around line 1-8: Remove the unused record FindAllGroupResult by deleting the
record declaration (FindAllGroupResult) and its file; ensure there are no
remaining imports or references (the service layer already returns
CursorPagingResponseDto<GroupInfo>), and run a project build/IDE search for
FindAllGroupResult to confirm no usages remain before committing.
In
`@src/main/java/flipnote/group/application/port/out/GroupMemberRepositoryPort.java`:
- Line 5: The port interface GroupMemberRepositoryPort currently imports and
uses the persistence type RoleEntity (violating hexagonal boundaries); change
the save(...) signature to accept the domain type GroupMemberRole instead and
remove the RoleEntity import, then propagate this change by updating
GroupRoleRepositoryPort.create() return type and any imports/usages in
CreateGroupService to use GroupMemberRole; implement the conversion from
GroupMemberRole to RoleEntity inside the adapter GroupMemberRepositoryAdapter
(adapter should map domain GroupMemberRole -> RoleEntity before persisting) so
no persistence types leak into the application.port.out layer.
In
`@src/main/java/flipnote/group/application/port/out/GroupRoleRepositoryPort.java`:
- Around line 3-7: The port GroupRoleRepositoryPort currently returns the
adapter-level RoleEntity which breaks hexagonal layering; change the create(Long
groupId) signature to return the domain type GroupMemberRole (or a new domain
VO) instead of RoleEntity, then update the out-adapter implementation to map
RoleEntity <-> GroupMemberRole when persisting/reading so the
application.port.out layer depends only on domain models (ensure implementations
of GroupRoleRepositoryPort perform the conversion).
In `@src/main/java/flipnote/group/application/service/CreateGroupService.java`:
- Line 6: CreateGroupService currently depends on persistence layer type
RoleEntity (import flipnote.group.adapter.out.entity.RoleEntity) which breaks
hexagonal boundaries; change the port method signatures
GroupRoleRepositoryPort.create(...) and GroupMemberRepositoryPort.save(...) to
use the domain enum/type GroupMemberRole instead of RoleEntity, update
CreateGroupService to pass GroupMemberRole values (and remove the RoleEntity
import), and implement mapping in the adapter implementations (convert
GroupMemberRole ↔ RoleEntity within the adapter) so infrastructure handles JPA
types while application and domain layers remain free of RoleEntity.
In
`@src/main/java/flipnote/group/application/service/FindGroupMemberService.java`:
- Line 28: The repository method
groupMemberRepository.existsUserInGroup(cmd.groupId(), cmd.userId()) is defined
to return void while its name implies a boolean; either rename the repository
method to assertUserInGroup or validateUserMembership (to keep the current
throw-on-missing behavior) and update FindGroupMemberService to call that
renamed method, or change existsUserInGroup to return boolean and update
FindGroupMemberService to check the boolean and throw the appropriate exception
itself; locate usages in FindGroupMemberService and the GroupMemberRepository
interface (existsUserInGroup) and make the corresponding renames/return-type
change so the method name matches its behavior.
In `@src/main/java/flipnote/group/domain/model/member/GroupMember.java`:
- Line 3: GroupMember currently depends on the JPA entity RoleEntity; remove
that dependency by replacing any RoleEntity references in GroupMember with a
domain-level representation (e.g., a new GroupMemberRole enum or a Role value
object) and update GroupMember's fields, constructors, getters/setters to use
GroupMemberRole instead of RoleEntity; then move conversion logic between
RoleEntity and GroupMemberRole into the adapter/mapper (e.g., GroupMemberMapper
or the repository/mapper in the adapter.out layer) so the domain model
(GroupMember) no longer imports or references RoleEntity.
In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java`:
- Around line 11-12: The interface name GroupMemberRepositoryRepository is
duplicated and should be renamed; update the interface declaration (currently
extending JpaRepository<GroupMemberEntity, Long>) to a clearer name such as
GroupMemberJpaRepository or GroupMemberRepository, and adjust any imports/usages
referencing GroupMemberRepositoryRepository to the new interface name so
compilation and references to JpaRepository<GroupMemberEntity, Long> remain
correct.
In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupRepository.java`:
- Around line 6-8: GroupRepository 인터페이스와 GroupRepositoryImpl 구현이 서로 다른 패키지(jpa
vs querydsl)에 있어 Spring Data JPA가 Impl을 못찾을 수 있습니다; 해결하려면 설정 클래스를 열어
`@EnableJpaRepositories`(basePackages =
"flipnote.group.infrastructure.persistence") 어노테이션을 추가해 리포지토리 스캔 범위를 명시하거나, 더
간단히 GroupRepositoryImpl을 GroupRepository와 동일한 패키지인
flipnote.group.infrastructure.persistence.jpa로 이동해 Spring이 기본 규칙으로 Impl을 자동으로
탐지하게 만드세요; 대상 심볼: GroupRepository, GroupRepositoryImpl, `@EnableJpaRepositories`,
패키지 flipnote.group.infrastructure.persistence(.jpa / .querydsl).
In
`@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryImpl.java`:
- Around line 105-107: findAllByCursorAndCreatedUserId의 Javadoc이 실제 동작과 맞지 않으니
주석을 실제 기능으로 고치세요: 현재 메서드(findAllByCursorAndCreatedUserId, 클래스
GroupRepositoryImpl)는 "내가 생성한(OWNER) 그룹 목록을 커서 기반으로 조회"하므로 Javadoc 설명을 해당 동작으로
변경하고 파라미터 설명(예: lastId, size, createdUserId 등)과 반환값 설명을 정확히 기재하여 메서드의 목적과 입력·출력
정보를 일치시켜 주세요.
- Around line 73-103: In both repository methods findAllByCursorAndUserId and
findAllByCursorAndCreatedUserId, the query currently checks
group.deletedAt.isNull() but does not filter out soft-deleted group members;
update the BooleanBuilder/where clause in findAllByCursorAndUserId to include
groupMember.deletedAt.isNull() (e.g., where.and(groupMember.deletedAt.isNull()))
and likewise add the same groupMember.deletedAt.isNull() condition into the
.where(...) for findAllByCursorAndCreatedUserId so only active (non-deleted)
GroupMemberEntity rows are returned.
---
Outside diff comments:
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java`:
- Around line 33-36: In GroupMemberRepositoryAdapter, locate the Javadoc block
that documents `@param` groupId and `@param` userId for the method whose return type
was changed to void and remove the stray `@return` tag; ensure the Javadoc now
only contains the descriptive text and the `@param` tags (and update the
description if necessary) so it correctly matches the void method signature.
---
Nitpick comments:
In `@src/main/java/flipnote/group/adapter/in/web/MemberController.java`:
- Line 43: The inline TODO "하위 권한 수정" in MemberController is unresolved; either
track it as a separate issue and replace the comment with a TODO referencing
that issue ID, or implement a minimal scaffold: add a controller endpoint stub
(e.g., modifySubPermissions or updateSubPermissions) in MemberController that
validates the request DTO and delegates to a MemberService.updateSubPermissions
method (create the service method signature if missing) so the intent is
explicit and testable; include input validation and a TODO comment in the new
stub linking to the issue if full implementation is deferred.
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java`:
- Around line 39-46: Replace the use of IllegalArgumentException in
GroupMemberRepositoryAdapter.existsUserInGroup with a domain-specific runtime
exception: create a UserNotInGroupException in the domain layer (e.g., extend
RuntimeException with a default message "user not in Group"), then change the
method so that when groupMemberRepository.existsByGroupIdAndUserId(groupId,
userId) is false it throws new UserNotInGroupException(); keep the boolean check
and repository call intact but swap the thrown exception to the new domain
exception so upper layers can handle this business-rule violation consistently.
- Around line 54-60: The method findMemberInfo in GroupMemberRepositoryAdapter
unnecessarily stores the result in a local variable memberInfo; remove that
intermediate variable and return GroupMemberMapper.toMemberInfo(...) directly
(either by passing the entities list from
groupMemberRepository.findAllByGroupId(groupId) or by keeping the entities local
and returning GroupMemberMapper.toMemberInfo(entities) inline) so the method
becomes a single return statement that calls GroupMemberMapper.toMemberInfo on
the results from groupMemberRepository.findAllByGroupId.
- Line 20: The interface name GroupMemberRepositoryRepository is redundant;
rename the JPA repository interface to a consistent name (e.g.,
GroupMemberJpaRepository or GroupMemberRepository) and update all usages
(including the field in GroupMemberRepositoryAdapter and the other reference
site) to the new type name; ensure imports and constructor/injection points in
GroupMemberRepositoryAdapter reflect the renamed interface and run compilation
to catch any remaining references, aligning naming with GroupRoleRepository and
GroupRolePermissionRepository.
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupRoleRepositoryAdapter.java`:
- Around line 15-16: The import and type name GroupMemberRepositoryRepository is
duplicated ("Repository" twice); rename the interface to a clearer name such as
GroupMemberJpaRepository, update the import in GroupRoleRepositoryAdapter (and
any other files) to use the new name, and update all usages (field types,
constructor parameters, method calls) that reference
GroupMemberRepositoryRepository inside this class to the new symbol; perform the
refactor via your IDE (rename file and type) or update the interface file name
and all references so imports and types (e.g., the field/parameter where
GroupMemberRepositoryRepository is used) match the new GroupMemberJpaRepository
identifier.
In `@src/main/java/flipnote/group/api/dto/request/CursorPagingRequest.java`:
- Around line 26-41: The getCursorId method currently redundantly checks
normalized.isEmpty() after StringUtils.hasText(cursor); remove the duplicate
check (lines that return null when normalized.isEmpty()) and simply trim the
cursor and parse it, keeping the try/catch for NumberFormatException; update the
method in CursorPagingRequest to rely on StringUtils.hasText(cursor) and then do
Long.valueOf(cursor.trim()) (or use the existing normalized variable) so
behavior remains the same but without the unnecessary empty-string branch.
In `@src/main/java/flipnote/group/api/dto/request/GroupListRequestDto.java`:
- Around line 16-19: getPageRequest() currently hardcodes
Sort.by(Sort.Direction.DESC, "id") and thus ignores the inherited sortBy and
order fields from CursorPagingRequest; either remove this override if
PageRequest isn't actually used for cursor pagination, or change
getPageRequest() to respect the parent fields by building the PageRequest using
getSize() and Sort.by(convert order and sortBy into Sort.Direction and property)
(reference the getPageRequest() method, the sortBy and order fields from
CursorPagingRequest, and the PageRequest.of(...) call to locate the code).
In
`@src/main/java/flipnote/group/api/dto/response/FindGroupMemberResponseDto.java`:
- Around line 8-14: The response DTO currently exposes the domain type
MemberInfo; create a new API record MemberInfoDto (e.g., record
MemberInfoDto(Long userId, GroupMemberRole role) with a static from(MemberInfo))
and update FindGroupMemberResponseDto.from to convert result.memberInfoList()
into a List<MemberInfoDto> (mapping each MemberInfo via MemberInfoDto.from) so
FindGroupMemberResponseDto holds List<MemberInfoDto> instead of
List<MemberInfo>, leaving FindGroupMemberResult and other domain classes
untouched.
In `@src/main/java/flipnote/group/application/port/in/FindGroupUseCase.java`:
- Around line 3-4: FindGroupUseCase currently depends on web-layer DTOs
(GroupListRequestDto, CursorPagingResponseDto); replace those web DTO types with
application-level query/response objects (e.g., create FindAllGroupQuery for
input and a port-level PagedGroupResult or FindGroupResult for output) and
update the FindGroupUseCase method signature(s) to accept the new query and
return the new port response type so the inbound port no longer imports
flipnote.group.api.dto; update any adapters/controllers to map between web DTOs
and the new query/response objects.
In
`@src/main/java/flipnote/group/application/port/out/GroupMemberRepositoryPort.java`:
- Line 11: The method signature void existsUserInGroup(Long groupId, Long
userId) is ambiguous at the port level; update the contract to make behavior
explicit by either (A) changing the return type to boolean (e.g.,
existsUserInGroup returns true/false) so adapters return existence, or (B)
renaming to assertUserInGroup and document/implement it to throw a specific
unchecked exception when the user is not a member. Add Javadoc to
GroupMemberRepositoryPort describing the chosen contract (return boolean vs
throw) so adapters implementing existsUserInGroup/assertUserInGroup know whether
to return a flag or throw an exception.
In
`@src/main/java/flipnote/group/application/service/FindGroupMemberService.java`:
- Around line 25-35: The findGroupMember method in FindGroupMemberService is a
read-only operation but lacks the `@Transactional`(readOnly = true) annotation;
update the FindGroupMemberService class by annotating the
findGroupMember(FindGroupMemberCommand cmd) method with `@Transactional`(readOnly
= true) (and add the import for
org.springframework.transaction.annotation.Transactional if missing) so JPQL/ORM
read optimizations apply while leaving groupMemberRepository.existsUserInGroup
and groupMemberRepository.findMemberInfo calls unchanged.
In `@src/main/java/flipnote/group/application/service/FindGroupService.java`:
- Around line 49-57: The userId parameter in findAllGroup(Long userId,
GroupListRequestDto req) is unused; either pass it into the repository call or
remove it/comment intent. If the service should scope results by user, update
the call to groupRepository.findAllByCursor(...) to include userId (adjust
repository/findAllByCursor signature and callers accordingly) so the method uses
user context; otherwise remove the userId parameter from findAllGroup (and
update callers) or add a clear comment above the method explaining why userId is
intentionally unused for authorization/telemetry purposes.
In `@src/main/java/flipnote/group/domain/model/group/GroupInfo.java`:
- Around line 9-11: The static factory method from in the GroupInfo record is
redundant because it simply forwards to the canonical constructor; remove the
from(Long groupId, String name, String description, Category category, Long
imageRefId) method from the GroupInfo record and update all callers to use the
record's canonical constructor new GroupInfo(...) instead, or if you prefer to
keep a factory for future validation, replace the body with the intended
validation/translation logic inside GroupInfo.from before constructing the
record.
In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java`:
- Line 6: Remove the unused imports in GroupMemberRepositoryRepository: delete
the import org.springframework.data.jpa.repository.Query and the import
flipnote.group.adapter.out.entity.RoleEntity since they are not referenced in
the interface (GroupMemberRepositoryRepository); ensure no other code in that
file depends on Query or RoleEntity and run a quick compile to confirm
unused-import warnings are gone.
In
`@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryCustom.java`:
- Around line 8-13: The three methods use inconsistent parameter names; make
them consistent and explicit: use lastId for the cursor, pageSize for the page
size, and a clear user parameter name that matches the method intent (userId for
findAllByCursorAndUserId, createdUserId for findAllByCursorAndCreatedUserId).
Update the method signature of findAllByCursorAndCreatedUserId(Long cursorId,
Category category, int size, Long id) to findAllByCursorAndCreatedUserId(Long
lastId, Category category, int pageSize, Long createdUserId) and ensure
corresponding implementations/referrers use the same names.
In
`@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryImpl.java`:
- Around line 24-25: The Q-type fields are declared as mutable instance
variables; change the declarations for the Q-type references (currently named
group and groupMember) to private static final and initialize them with
QGroupEntity.groupEntity and QGroupMemberEntity.groupMemberEntity respectively
so they reuse the existing public singletons and prevent reassignment and
unintended package access.
| @RequiredArgsConstructor | ||
| @RestController | ||
| @RequestMapping("/v1/groups") | ||
| public class GroupQueryController { |
There was a problem hiding this comment.
GroupController와 동일한 /v1/groups base path 사용 — 경로 충돌 가능성
GroupController에도 @RequestMapping("/v1/groups")가 있으며, GET /v1/groups/{groupId} 템플릿이 선언되어 있습니다. Spring MVC는 리터럴 세그먼트를 템플릿보다 우선하므로 /me, /created는 정상 라우팅되나, groupId가 Long이 아닌 문자열 경로가 추가될 경우 우선순위 충돌이 발생할 수 있습니다.
두 컨트롤러가 같은 base path를 공유하는 현재 구조는 향후 엔드포인트 추가 시 실수를 유발할 수 있으므로, 목록 조회용 컨트롤러는 /v1/groups/list와 같이 별도 prefix를 사용하거나, 두 컨트롤러를 통합하는 것을 고려해 보세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/flipnote/group/adapter/in/web/GroupQueryController.java` around
lines 17 - 20, GroupQueryController shares the same
`@RequestMapping`("/v1/groups") base path as GroupController which can cause
routing conflicts; change the base path on GroupQueryController (the class
annotated with `@RestController` and `@RequiredArgsConstructor`) to a distinct
prefix such as `@RequestMapping`("/v1/groups/list") and update its handler method
mappings accordingly (or alternatively consolidate GroupQueryController into
GroupController by moving its methods into the existing GroupController class)
so that methods like the list endpoints no longer collide with GroupController's
/v1/groups/{groupId} routes.
| package flipnote.group.adapter.in.web; | ||
|
|
||
| public class InvitationController { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
@RestController 및 @RequestMapping 어노테이션 누락
@RestController 어노테이션이 없으면 Spring이 이 클래스를 컨트롤러 빈으로 등록하지 않습니다. 추후 엔드포인트를 추가하더라도 요청이 라우팅되지 않으므로, 지금 단계에서 어노테이션 기본 틀을 잡아두는 것을 권장합니다.
🔧 어노테이션 추가 제안
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/v1/invitations")
+@RequiredArgsConstructor
public class InvitationController {📝 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.
| package flipnote.group.adapter.in.web; | |
| public class InvitationController { | |
| package flipnote.group.adapter.in.web; | |
| import lombok.RequiredArgsConstructor; | |
| import org.springframework.web.bind.annotation.RequestMapping; | |
| import org.springframework.web.bind.annotation.RestController; | |
| `@RestController` | |
| `@RequestMapping`("/api/v1/invitations") | |
| `@RequiredArgsConstructor` | |
| public class InvitationController { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/flipnote/group/adapter/in/web/InvitationController.java` around
lines 1 - 3, Add Spring MVC annotations to register the controller: annotate the
InvitationController class with `@RestController` and a class-level
`@RequestMapping` (e.g. "/invitations" or another suitable base path) so Spring
will detect and route requests to it; ensure you import
org.springframework.web.bind.annotation.RestController and
org.springframework.web.bind.annotation.RequestMapping and place the annotations
directly above the public class InvitationController declaration.
| package flipnote.group.adapter.in.web; | ||
|
|
||
| public class InvitationController { | ||
| //todo 가인 신청 요청 |
There was a problem hiding this comment.
오타: 가인 → 가입
//todo 가인 신청 요청 → //todo 가입 신청 요청
✏️ 오타 수정
- //todo 가인 신청 요청
+ //todo 가입 신청 요청📝 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.
| //todo 가인 신청 요청 | |
| //todo 가입 신청 요청 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/flipnote/group/adapter/in/web/InvitationController.java` at
line 4, Fix the typo in the TODO comment inside InvitationController (class
InvitationController) by replacing "가인" with "가입" so the comment reads "//todo
가입 신청 요청"; update any matching TODO occurrences in that file to keep wording
consistent.
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "group_role_id", nullable = false) | ||
| private RoleEntity role; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. findAllByGroupId 메서드 사용 패턴 확인
echo "=== findAllByGroupId 사용 패턴 ==="
rg -n "findAllByGroupId" --type=java -C 5
# 2. GroupMemberRepository 파일 찾기
echo -e "\n=== GroupMemberRepository 파일 위치 ==="
find . -name "*GroupMemberRepository*" -type f 2>/dev/null
# 3. FindGroupMemberService 파일 찾기
echo -e "\n=== FindGroupMemberService 파일 위치 ==="
find . -name "*FindGroupMemberService*" -type f 2>/dev/nullRepository: FlipNoteTeam/FlipNote-Group
Length of output: 2742
🏁 Script executed:
#!/bin/bash
# 1. 전체 GroupMemberRepositoryRepository 내용 확인
echo "=== GroupMemberRepositoryRepository 전체 ==="
cat -n src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java
# 2. GroupMemberMapper 확인 - role이 사용되는지 확인
echo -e "\n=== GroupMemberMapper 위치 ==="
find . -name "*GroupMemberMapper*" -type f 2>/dev/null
# 3. FindGroupMemberService 전체 내용
echo -e "\n=== FindGroupMemberService 전체 ==="
cat -n src/main/java/flipnote/group/application/service/FindGroupMemberService.javaRepository: FlipNoteTeam/FlipNote-Group
Length of output: 2304
🏁 Script executed:
#!/bin/bash
# 1. GroupMemberMapper 전체 확인
echo "=== GroupMemberMapper 전체 ==="
cat -n src/main/java/flipnote/group/adapter/out/persistence/mapper/GroupMemberMapper.java
# 2. MemberInfo 도메인 모델 확인
echo -e "\n=== MemberInfo 위치 및 내용 ==="
find . -name "MemberInfo.java" -type f 2>/dev/null
find . -name "MemberInfo.java" -type f -exec cat -n {} \;Repository: FlipNoteTeam/FlipNote-Group
Length of output: 2540
🏁 Script executed:
#!/bin/bash
# Verify what RoleEntity contains (to understand the full query implications)
echo "=== RoleEntity 구조 확인 ==="
find . -name "RoleEntity.java" -type f -exec cat -n {} \;Repository: FlipNoteTeam/FlipNote-Group
Length of output: 1826
@EntityGraph를 사용하거나 fetch join 쿼리로 N+1 쿼리 문제를 해결하세요.
GroupMemberRepositoryRepository.findAllByGroupId()는 현재 fetch 전략이 없어서 N+1 쿼리가 발생합니다. GroupMemberMapper.toMemberInfo()에서 각 멤버의 entity.getRole().getRole()을 접근하면, 초기 쿼리 1개 + 각 멤버당 role 로드 쿼리 N개가 실행됩니다.
findAllByGroupId() 메서드에 @EntityGraph(attributePaths = "role")을 추가하거나, @Query로 fetch join을 사용하여 한 번에 role을 로드하세요:
`@Query`("SELECT m FROM GroupMemberEntity m JOIN FETCH m.role WHERE m.groupId = :groupId")
List<GroupMemberEntity> findAllByGroupId(Long groupId);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/flipnote/group/adapter/out/entity/GroupMemberEntity.java`
around lines 46 - 48, The repository method
GroupMemberRepositoryRepository.findAllByGroupId currently triggers N+1 when
GroupMemberMapper.toMemberInfo accesses entity.getRole().getRole(); fix by
eager-loading role in the query — either annotate findAllByGroupId with
`@EntityGraph`(attributePaths = "role") or replace it with a JPQL fetch-join query
(SELECT m FROM GroupMemberEntity m JOIN FETCH m.role WHERE m.groupId = :groupId)
so the RoleEntity referenced by GroupMemberEntity.role is loaded in the same
query.
| /** | ||
| * 그룹 멤버 아이디 조회 | ||
| * @param groupId | ||
| * @return |
There was a problem hiding this comment.
Javadoc 주석이 실제 동작과 다릅니다.
"그룹 멤버 아이디 조회"라고 되어 있지만, 실제로는 MemberInfo(userId + role 등) 목록을 반환합니다. 주석을 실제 반환 내용에 맞게 수정하세요.
📝 수정 제안
/**
- * 그룹 멤버 아이디 조회
+ * 그룹 멤버 정보 조회
* `@param` groupId
* `@return`
*/🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java`
around lines 48 - 51, Javadoc "그룹 멤버 아이디 조회"는 실제 반환값과 맞지 않습니다; 수정하려는 Javadoc을
GroupMemberRepositoryAdapter 클래스의 해당 메서드(현재 주석 위에 있는 메서드)를 찾아 반환형이 MemberInfo(예:
userId와 role 등을 포함하는 객체)의 목록임을 명시하도록 변경하세요 — 예: "그룹 멤버 목록 반환 (MemberInfo: userId
및 role 포함)" 및 `@return에` List<MemberInfo> 설명을 추가해 실제 동작과 주석이 일치하도록 만드세요.
|
|
||
| import flipnote.group.domain.model.group.GroupId; | ||
| import flipnote.group.domain.model.user.UserId; | ||
| import flipnote.group.adapter.out.entity.RoleEntity; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
도메인 모델이 JPA 엔티티(RoleEntity)에 의존하고 있습니다 — 헥사고날 아키텍처의 핵심 원칙 위반.
GroupMember는 도메인 레이어에 위치하지만 adapter.out.entity.RoleEntity(JPA 엔티티)를 직접 참조하고 있습니다. 이는 도메인 레이어가 인프라/어댑터 레이어에 의존하게 되어 헥사고날 아키텍처의 의존성 방향 규칙을 위반합니다.
도메인 모델에서는 도메인 수준의 역할 표현(예: GroupMemberRole enum, 또는 role ID + role name을 가진 도메인 VO)을 사용하고, RoleEntity ↔ 도메인 모델 간 변환은 매퍼(adapter 레이어)에서 처리해야 합니다.
♻️ 도메인 모델 개선 예시
-import flipnote.group.adapter.out.entity.RoleEntity;
+import flipnote.group.domain.model.member.GroupMemberRole;
`@Getter`
`@NoArgsConstructor`(access = AccessLevel.PRIVATE)
public class GroupMember {
private Long id;
private Long groupId;
private Long userId;
- private RoleEntity role;
+ private Long roleId;
+ private GroupMemberRole role;
`@Builder`
- private GroupMember(Long id, Long groupId, Long userId, RoleEntity role) {
+ private GroupMember(Long id, Long groupId, Long userId, Long roleId, GroupMemberRole role) {
this.id = id;
this.groupId = groupId;
this.userId = userId;
+ this.roleId = roleId;
this.role = role;
}
}Also applies to: 15-15
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/flipnote/group/domain/model/member/GroupMember.java` at line 3,
GroupMember currently depends on the JPA entity RoleEntity; remove that
dependency by replacing any RoleEntity references in GroupMember with a
domain-level representation (e.g., a new GroupMemberRole enum or a Role value
object) and update GroupMember's fields, constructors, getters/setters to use
GroupMemberRole instead of RoleEntity; then move conversion logic between
RoleEntity and GroupMemberRole into the adapter/mapper (e.g., GroupMemberMapper
or the repository/mapper in the adapter.out layer) so the domain model
(GroupMember) no longer imports or references RoleEntity.
| public interface GroupMemberRepositoryRepository | ||
| extends JpaRepository<GroupMemberEntity, Long> { |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
GroupMemberRepositoryRepository — "Repository"가 중복된 이름입니다.
명명 오류로 보입니다. GroupMemberJpaRepository 또는 기존 GroupMemberRepository를 확장/대체하는 것이 적절합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java`
around lines 11 - 12, The interface name GroupMemberRepositoryRepository is
duplicated and should be renamed; update the interface declaration (currently
extending JpaRepository<GroupMemberEntity, Long>) to a clearer name such as
GroupMemberJpaRepository or GroupMemberRepository, and adjust any imports/usages
referencing GroupMemberRepositoryRepository to the new interface name so
compilation and references to JpaRepository<GroupMemberEntity, Long> remain
correct.
| import flipnote.group.infrastructure.persistence.querydsl.GroupRepositoryCustom; | ||
|
|
||
| public interface GroupRepository extends JpaRepository<GroupEntity, Long> { | ||
| public interface GroupRepository extends JpaRepository<GroupEntity, Long>, GroupRepositoryCustom { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for GroupRepositoryImpl class definition
rg -n 'class GroupRepositoryImpl' --type=java
# Search for EnableJpaRepositories configuration
rg -n 'EnableJpaRepositories' --type=java
# Also check the full package structure to understand the layout
echo "=== Checking directory structure ==="
find . -type f -name "*GroupRepository*" | head -20
# Check if there's any Spring boot configuration that might affect component scanning
rg -n '@SpringBootApplication|@Configuration' --type=java | head -20Repository: FlipNoteTeam/FlipNote-Group
Length of output: 951
🏁 Script executed:
#!/bin/bash
# Check QuerydslConfig.java content
echo "=== QuerydslConfig.java ==="
cat src/main/java/flipnote/group/infrastructure/config/QuerydslConfig.java
echo -e "\n=== GroupApplication.java ==="
cat src/main/java/flipnote/group/GroupApplication.javaRepository: FlipNoteTeam/FlipNote-Group
Length of output: 940
Spring Data JPA의 Custom Repository Impl 탐지 문제를 해결하세요.
GroupRepository는 jpa 패키지에, GroupRepositoryImpl은 querydsl 패키지에 위치하지만, 명시적인 @EnableJpaRepositories 설정이 없습니다. Spring Data JPA는 기본적으로 리포지토리 인터페이스와 동일한 패키지에서 Impl 클래스를 찾으므로, 이 구조에서는 GroupRepositoryImpl이 제대로 발견되지 않을 수 있습니다.
해결 방안:
@EnableJpaRepositories(basePackages = "flipnote.group.infrastructure.persistence")를 설정 클래스에 추가하여 명시적으로 패키지 범위를 지정하거나,GroupRepositoryImpl을GroupRepository와 동일한jpa패키지로 이동하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupRepository.java`
around lines 6 - 8, GroupRepository 인터페이스와 GroupRepositoryImpl 구현이 서로 다른 패키지(jpa
vs querydsl)에 있어 Spring Data JPA가 Impl을 못찾을 수 있습니다; 해결하려면 설정 클래스를 열어
`@EnableJpaRepositories`(basePackages =
"flipnote.group.infrastructure.persistence") 어노테이션을 추가해 리포지토리 스캔 범위를 명시하거나, 더
간단히 GroupRepositoryImpl을 GroupRepository와 동일한 패키지인
flipnote.group.infrastructure.persistence.jpa로 이동해 Spring이 기본 규칙으로 Impl을 자동으로
탐지하게 만드세요; 대상 심볼: GroupRepository, GroupRepositoryImpl, `@EnableJpaRepositories`,
패키지 flipnote.group.infrastructure.persistence(.jpa / .querydsl).
| @Override | ||
| public List<GroupInfo> findAllByCursorAndUserId(Long lastId, Category category, int pageSize, Long userId) { | ||
|
|
||
| BooleanBuilder where = new BooleanBuilder() | ||
| .and(group.deletedAt.isNull()) | ||
| .and(groupMember.userId.eq(userId)); | ||
|
|
||
| if (lastId != null) { | ||
| where.and(group.id.lt(lastId)); | ||
| } | ||
|
|
||
| if (category != null) { | ||
| where.and(group.category.eq(category)); | ||
| } | ||
|
|
||
| return queryFactory | ||
| .select(Projections.constructor( | ||
| GroupInfo.class, | ||
| group.id, | ||
| group.name, | ||
| group.description, | ||
| group.category, | ||
| group.imageRefId | ||
| )) | ||
| .from(groupMember) | ||
| .join(group).on(group.id.eq(groupMember.groupId)) | ||
| .where(where) | ||
| .orderBy(group.id.desc()) | ||
| .limit(pageSize + 1) | ||
| .fetch(); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -e java "GroupMemberEntity.java" --exec cat {}Repository: FlipNoteTeam/FlipNote-Group
Length of output: 1826
🏁 Script executed:
fd -e java "BaseEntity.java" --exec cat {}Repository: FlipNoteTeam/FlipNote-Group
Length of output: 1058
🏁 Script executed:
fd -e java "GroupRepositoryImpl.java" --exec cat {}Repository: FlipNoteTeam/FlipNote-Group
Length of output: 3313
findAllByCursorAndUserId와 findAllByCursorAndCreatedUserId에서 탈퇴한 그룹 멤버가 필터링되지 않음
group.deletedAt.isNull()은 체크하지만 groupMember.deletedAt.isNull() 필터가 없습니다. GroupMemberEntity는 BaseEntity를 상속받아 deletedAt 필드를 가지고 있으므로, 이미 탈퇴한 멤버의 그룹도 조회 결과에 포함됩니다.
- findAllByCursorAndUserId (73-103):
where.and(groupMember.deletedAt.isNull())추가 필요 - findAllByCursorAndCreatedUserId (120+):
.where()조건에groupMember.deletedAt.isNull()추가 필요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryImpl.java`
around lines 73 - 103, In both repository methods findAllByCursorAndUserId and
findAllByCursorAndCreatedUserId, the query currently checks
group.deletedAt.isNull() but does not filter out soft-deleted group members;
update the BooleanBuilder/where clause in findAllByCursorAndUserId to include
groupMember.deletedAt.isNull() (e.g., where.and(groupMember.deletedAt.isNull()))
and likewise add the same groupMember.deletedAt.isNull() condition into the
.where(...) for findAllByCursorAndCreatedUserId so only active (non-deleted)
GroupMemberEntity rows are returned.
| /** | ||
| * 그룹 테이블에 생성한 유저 추가 | ||
| * @param lastId |
There was a problem hiding this comment.
Javadoc 주석이 메서드 동작과 다릅니다
findAllByCursorAndCreatedUserId의 Javadoc에 "그룹 테이블에 생성한 유저 추가"라고 기술되어 있으나, 이 메서드는 내가 생성한(OWNER) 그룹 목록을 조회하는 메서드입니다.
📝 수정 제안
/**
- * 그룹 테이블에 생성한 유저 추가
+ * 내가 생성한 그룹 전체 조회
* `@param` lastId
* `@param` category
* `@param` pageSize
* `@param` userId
* `@return`
*/📝 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.
| /** | |
| * 그룹 테이블에 생성한 유저 추가 | |
| * @param lastId | |
| /** | |
| * 내가 생성한 그룹 전체 조회 | |
| * `@param` lastId |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/flipnote/group/infrastructure/persistence/querydsl/GroupRepositoryImpl.java`
around lines 105 - 107, findAllByCursorAndCreatedUserId의 Javadoc이 실제 동작과 맞지 않으니
주석을 실제 기능으로 고치세요: 현재 메서드(findAllByCursorAndCreatedUserId, 클래스
GroupRepositoryImpl)는 "내가 생성한(OWNER) 그룹 목록을 커서 기반으로 조회"하므로 Javadoc 설명을 해당 동작으로
변경하고 파라미터 설명(예: lastId, size, createdUserId 등)과 반환값 설명을 정확히 기재하여 메서드의 목적과 입력·출력
정보를 일치시켜 주세요.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항