Skip to content

Feat: [FN-307] 그룹 내 유저 조회 API 추가#7

Open
stoneTiger0912 wants to merge 4 commits intomainfrom
feat/find-user-in-group
Open

Feat: [FN-307] 그룹 내 유저 조회 API 추가#7
stoneTiger0912 wants to merge 4 commits intomainfrom
feat/find-user-in-group

Conversation

@stoneTiger0912
Copy link
Member

@stoneTiger0912 stoneTiger0912 commented Feb 18, 2026

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 그룹 멤버 조회 기능 추가 - 그룹 내 멤버 목록을 조회할 수 있습니다.
  • 예정

    • 그룹 초대 및 요청 관리 기능 개발 중입니다.
  • 개선사항

    • 그룹 멤버 및 역할 관리 시스템을 최적화했습니다.
    • 내부 아키텍처를 개선하여 안정성을 강화했습니다.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📋 개요

Walkthrough

이 변경사항은 그룹 멤버 조회 기능을 추가하고, 역할 관리를 Long 기반 ID에서 RoleEntity 엔티티 기반으로 리팩토링하며, GroupId/UserId 값 객체를 제거하고 GroupMember 도메인 모델을 재설계합니다. Querydsl 설정이 추가되고 새로운 저장소 추상화가 도입됩니다.

Changes

Cohort / File(s) Summary
컨트롤러 및 API 엔드포인트
GroupController.java, InvitationController.java, MemberController.java
GroupController에서 import 정리 및 형식 수정; InvitationController는 미래 구현을 위한 TODO 자리표시자 추가; MemberController에 새로운 GET /v1/groups/{groupId}/members 엔드포인트 구현
DTO 및 포트 정의
FindGroupMemberResponseDto.java, FindGroupMemberUseCase.java, FindGroupMemberCommand.java, FindGroupMemberResult.java
그룹 멤버 조회 기능을 위한 새로운 응답 DTO, 사용 사례 인터페이스, 명령 및 결과 레코드 추가
엔티티 및 매퍼
GroupMemberEntity.java, GroupMemberMapper.java, RoleEntity (참조)
GroupMemberEntity의 groupRoleId를 RoleEntity 연관관계로 변경; 매퍼에 toDomain과 toMemberInfo 메서드 추가
저장소 계층
GroupMemberRepositoryAdapter.java, GroupRoleRepositoryAdapter.java, GroupMemberRepositoryPort.java, GroupRoleRepositoryPort.java, GroupMemberRepository.java, GroupMemberRepositoryRepository.java
저장소 어댑터 리팩토링: save 메서드가 RoleEntity 매개변수 수락, existsUserInGroup이 boolean 반환에서 void로 변경되고 예외 발생, findMemberInfo 메서드 추가; 포트 서명 업데이트; 레거시 GroupMemberRepository 제거, 새로운 GroupMemberRepositoryRepository 추가
서비스 계층
CreateGroupService.java, FindGroupMemberService.java, FindGroupService.java
CreateGroupService가 RoleEntity를 처리하도록 업데이트; 새로운 FindGroupMemberService 구현; FindGroupService에서 사용자 멤버십 검증 로직 제거
도메인 모델
GroupMember.java, MemberInfo.java, GroupId.java, UserId.java
GroupMember를 Long 필드 및 RoleEntity로 리팩토링, 정적 팩토리 메서드 제거, 빌더 패턴 추가; 새로운 MemberInfo 값 객체 추가; GroupId 및 UserId 값 객체 파일 삭제
인프라 구성
QuerydslConfig.java
JPAQueryFactory 빈을 위한 새로운 Querydsl 설정 클래스 추가

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Feat: [FN-302] 그룹 권한 추가 #6 — 이 PR과 마찬가지로 그룹 역할/권한 도메인 및 지속성 계층(RoleEntity, PermissionEntity, GroupMemberEntity, 저장소 및 매퍼)을 수정합니다.
  • Feat: [FN-297] 그룹 생성 API 구현  #4 — 그룹 멤버 지속성/매핑 계층(GroupMemberEntity, 저장소, 매퍼)을 수정하며, 초기 그룹 생성/멤버 지속성 아티팩트를 도입했습니다.

Poem

🐰 역할의 실체로, 아이디에서 벗어나
멤버들을 찾는 새 길이 열렸고,
RoleEntity의 품에서 권한이 자라나,
Querydsl의 마법으로 데이터가 춤을 춘다네!
값 객체는 안녕, 도메인은 변신했으니!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 변경 사항의 주요 내용을 명확하게 요약하고 있습니다. '그룹 내 유저 조회 API 추가'는 이번 PR의 핵심인 MemberController에 새로운 GET 엔드포인트 추가와 관련 도메인 로직 구현을 정확히 반영하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/find-user-in-group

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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

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/GroupRoleRepositoryAdapter.java (1)

88-93: ⚠️ Potential issue | 🔴 Critical

findByGroupIdAndRolenull을 반환할 경우 NPE 발생

findByGroupIdAndRole의 반환 타입이 RoleEntity이므로 (Optional이 아님), 해당 역할이 존재하지 않을 때 null이 반환될 수 있습니다. 92번 줄에서 null 체크 없이 roleEntity.getId()를 호출하면 NullPointerException이 발생합니다.

제안 수정안
 public boolean checkRole(Long userId, Long groupId, GroupMemberRole groupMemberRole) {
     RoleEntity roleEntity = groupRoleRepository.findByGroupIdAndRole(groupId, groupMemberRole);
-    
-    return groupMemberRepository.existsByUserIdAndGroupRoleId(userId, roleEntity.getId());
+    if (roleEntity == null) {
+        return false;
+    }
+    return groupMemberRepository.existsByUserIdAndGroupRoleId(userId, roleEntity.getId());
 }

또는 repository 메서드 반환 타입을 Optional<RoleEntity>로 변경하는 것을 권장합니다.

🤖 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 88 - 93, checkRole currently calls
groupRoleRepository.findByGroupIdAndRole which can return null (RoleEntity),
then immediately calls roleEntity.getId() causing NPE; update checkRole (in
GroupRoleRepositoryAdapter) to handle a missing role: call findByGroupIdAndRole
and if it returns null either return false (no such role -> user can't have it)
or change the repository signature to Optional<RoleEntity> and unwrap it safely
(e.g.,
roleOpt.map(role->groupMemberRepository.existsByUserIdAndGroupRoleId(userId,
role.getId())).orElse(false)); ensure references: checkRole,
groupRoleRepository.findByGroupIdAndRole, RoleEntity, and
groupMemberRepository.existsByUserIdAndGroupRoleId are used to locate the fix.
🧹 Nitpick comments (10)
src/main/java/flipnote/group/infrastructure/config/QuerydslConfig.java (2)

11-19: @PersistenceContext 필드 주입 사용을 고려해 보세요 (선택 사항)

현재 코드는 정상 동작합니다. Spring 컨테이너는 EntityManager 주입 시 단순 인스턴스 대신 특수 프록시를 주입하며, Spring은 SharedEntityManagerCreator 타입의 프록시를 주입합니다. 컨테이너는 각 EntityManager가 하나의 스레드에 한정되도록 보장하므로, 생성자 주입으로 받아도 스레드 안전성은 유지됩니다.

다만 @PersistenceContext의 기본 영속성 컨텍스트 타입은 PersistenceContextType.TRANSACTION이며, @PersistenceContext로 어노테이션을 붙이면 트랜잭션 영속성 컨텍스트를 사용함을 명시할 수 있습니다. 이 방식이 JPA 표준 관례에 더 부합하고 의도를 명확히 드러냅니다.

♻️ `@PersistenceContext` 적용 예시
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
 import com.querydsl.jpa.impl.JPAQueryFactory;
 
 import jakarta.persistence.EntityManager;
-import lombok.RequiredArgsConstructor;
+import jakarta.persistence.PersistenceContext;
 
 `@Configuration`
-@RequiredArgsConstructor
 public class QuerydslConfig {
-	private final EntityManager entityManager;
+	`@PersistenceContext`
+	private EntityManager entityManager;
 
 	`@Bean`
 	public JPAQueryFactory jpaQueryFactory() {
 		return new JPAQueryFactory(entityManager);
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/group/infrastructure/config/QuerydslConfig.java`
around lines 11 - 19, Replace the constructor-injected final EntityManager with
a field-injected persistence context: remove `@RequiredArgsConstructor` and the
final EntityManager constructor parameter, add a non-final EntityManager field
annotated with `@PersistenceContext` in class QuerydslConfig, and keep the
jpaQueryFactory() method using that field so the bean uses a transaction-scoped
persistence context as intended.

11-19: @PersistenceContext 사용을 고려해 보세요 (선택 사항)

현재 @RequiredArgsConstructor를 통한 생성자 주입도 완전히 정상 동작합니다. Spring Boot의 JPA 자동 설정이 EntityManager를 트랜잭션 범위의 프록시(SharedEntityManagerCreator)로 등록하기 때문에 스레드 안전성에는 문제가 없습니다.

@PersistenceContext로 변경하는 것은 JPA 표준 방식이며, 다음과 같은 이점이 있습니다:

  • JPA 포터빌리티: 표준 JPA 어노테이션으로 더 이식 가능한 코드
  • 고급 기능 지원: 필요시 type=EXTENDED 같은 영속성 컨텍스트 타입을 명시 가능
  • 명시적 의도 표현: EntityManager가 컨테이너 관리 프록시임을 코드에서 명확히 드러냄
♻️ `@PersistenceContext` 적용 예시
-import lombok.RequiredArgsConstructor;
+import jakarta.persistence.PersistenceContext;
 
 `@Configuration`
-@RequiredArgsConstructor
 public class QuerydslConfig {
-	private final EntityManager entityManager;
+	`@PersistenceContext`
+	private EntityManager entityManager;
 
 	`@Bean`
 	public JPAQueryFactory jpaQueryFactory() {
 		return new JPAQueryFactory(entityManager);
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/flipnote/group/infrastructure/config/QuerydslConfig.java`
around lines 11 - 19, Replace constructor-injected EntityManager with a
container-managed one by annotating the EntityManager field with
`@PersistenceContext` in QuerydslConfig: remove the final field and
RequiredArgsConstructor usage, declare a non-final EntityManager annotated with
`@PersistenceContext`, and keep the jpaQueryFactory() bean returning new
JPAQueryFactory(entityManager) so the bean uses the JPA-standard,
container-managed EntityManager proxy.
src/main/java/flipnote/group/adapter/in/web/InvitationController.java (1)

1-18: 플레이스홀더 컨트롤러에 Spring 어노테이션이 누락되어 있습니다.

향후 구현 시 @RestController, @RequestMapping 등의 어노테이션이 필요합니다. 구현 전이라면 빈 클래스를 커밋하기보다는 이슈 트래커에서 관리하는 것이 깔끔할 수 있습니다.

🤖 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 - 18, InvitationController currently lacks Spring web annotations;
either annotate the class with `@RestController` and a base `@RequestMapping` (e.g.,
"/invitations") and add appropriate imports so it becomes a valid Spring bean,
or if you want to keep it as a placeholder remove the empty class from the
commit and track the work in the issue tracker instead; locate the
InvitationController class to apply the annotation change or delete it as
described.
src/main/java/flipnote/group/adapter/in/web/GroupController.java (1)

18-28: FindGroupMemberResponseDtoFindGroupMemberCommand import가 이 컨트롤러에서 사용되지 않습니다.

그룹 멤버 조회 엔드포인트가 MemberController에 구현되어 있다면, 이 import들은 불필요합니다. 실제로 이 컨트롤러에 해당 엔드포인트를 추가할 계획이 없다면 제거하는 것이 좋습니다.

🤖 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/GroupController.java` around
lines 18 - 28, Remove the unused imports FindGroupMemberResponseDto and
FindGroupMemberCommand from GroupController.java since there is no group-member
endpoint in this controller (the member lookup is implemented in
MemberController); locate the import lines referencing
FindGroupMemberResponseDto and FindGroupMemberCommand and delete them so the
file only imports types actually used (e.g., FindGroupResponseDto,
FindGroupCommand, CreateGroupCommand, ChangeGroupCommand, DeleteGroupCommand,
and their use-case interfaces).
src/main/java/flipnote/group/api/dto/response/FindGroupMemberResponseDto.java (1)

8-14: API DTO가 도메인 모델 MemberInfo를 직접 노출합니다.

FindGroupMemberResponseDto가 도메인 모델인 MemberInfo를 API 응답에 직접 사용하고 있습니다. 이렇게 하면 도메인 모델 변경 시 API 계약이 의도치 않게 변경될 수 있습니다. API 전용 내부 record(예: MemberInfoResponse)를 정의하고 매핑하는 것이 계층 분리에 더 적합합니다.

🤖 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, FindGroupMemberResponseDto currently exposes the domain
model MemberInfo directly; instead create an API-only record (e.g.,
MemberInfoResponse) that contains only the fields needed by the API and update
FindGroupMemberResponseDto to use List<MemberInfoResponse> for memberInfoList;
in the static factory from(FindGroupMemberResult) map each domain MemberInfo
from result.memberInfoList() to a MemberInfoResponse (using a converter method
or constructor on MemberInfoResponse) and return the DTO with the mapped list to
avoid leaking domain types in the API layer.
src/main/java/flipnote/group/application/service/FindGroupMemberService.java (1)

28-28: existsUserInGroup이 void를 반환하면서 예외로 검증하는 패턴은 의도를 파악하기 어렵습니다.

메서드 이름 existsUserInGroupboolean 반환을 암시하지만, 실제로는 void를 반환하고 내부에서 예외를 던집니다. 호출부에서 반환값도 확인하지 않고 결과도 사용하지 않으므로, 메서드명을 validateUserInGroup 또는 checkUserInGroup으로 변경하면 side-effect 기반 검증임을 명확히 전달할 수 있습니다.

이 변경은 GroupMemberRepositoryPort 인터페이스와 해당 구현체에도 적용되어야 합니다.

🤖 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`
at line 28, The method existsUserInGroup currently returns void and signals
invalid state via exceptions which is confusing given its name; rename
existsUserInGroup to a side-effect-validating name (e.g., validateUserInGroup or
checkUserInGroup) across the API and implementations, update the
GroupMemberRepositoryPort interface and all concrete implementations to the new
void method, and update the caller in FindGroupMemberService to invoke the new
method name (replace groupMemberRepository.existsUserInGroup(...) with
groupMemberRepository.validateUserInGroup(...) or chosen name) so the intent
(validation via exception) is clear and consistent.
src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java (1)

12-12: existsByUserIdAndGroupRoleId의 두 번째 파라미터명 id가 모호합니다.

Spring Data 쿼리 메서드의 파라미터명 id는 어떤 엔티티의 ID인지 불분명합니다. roleId 또는 groupRoleId로 변경하면 가독성이 향상됩니다.

♻️ 수정 제안
-	boolean existsByUserIdAndGroupRoleId(Long userId, Long id);
+	boolean existsByUserIdAndGroupRoleId(Long userId, Long groupRoleId);
🤖 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 12, Rename the ambiguous parameter `id` in the repository method
existsByUserIdAndGroupRoleId to a descriptive name like `roleId` or
`groupRoleId` in the GroupMemberRepositoryRepository interface and update any
callers/implementations to use the new parameter name; ensure the method
signature reads existsByUserIdAndGroupRoleId(Long userId, Long roleId) (or Long
groupRoleId) so the intent is clear and compilation references are adjusted
accordingly.
src/main/java/flipnote/group/adapter/out/entity/GroupMemberEntity.java (1)

57-69: Javadoc의 @param@return 태그를 업데이트하세요.

role 파라미터가 추가되었지만 Javadoc에는 반영되지 않았고, @return 태그는 설명 없이 비어있습니다.

📝 수정 제안
 	/**
 	 * 멤버 생성
 	 * `@param` groupId
 	 * `@param` userId
+	 * `@param` role
-	 * `@return`
+	 * `@return` 생성된 GroupMemberEntity
 	 */
🤖 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 57 - 69, Update the Javadoc for GroupMemberEntity.create to reflect
the current signature: add a `@param` tag for the new role parameter (RoleEntity
role) describing its purpose, ensure `@param` tags for groupId and userId are
meaningful, and replace the empty `@return` tag with a brief description like "the
created GroupMemberEntity" so the documentation matches the create(Long groupId,
Long userId, RoleEntity role) factory method.
src/main/java/flipnote/group/adapter/out/persistence/GroupRoleRepositoryAdapter.java (1)

53-62: Collectors.toMap 내에서 save() 호출은 부수효과(side-effect)로 적절하지 않습니다.

Stream의 collector value mapper에서 groupRoleRepository.save()를 호출하는 것은 부수효과입니다. 순차 스트림에서는 동작하지만, 스트림 연산은 순서를 보장하지 않으며 향후 .parallel() 전환 시 예측 불가능한 동작이 발생할 수 있습니다. 명시적 반복문이나 각 역할을 먼저 저장한 후 Map으로 수집하는 방식이 더 안전합니다.

♻️ 수정 제안
-		Map<GroupMemberRole, RoleEntity> roleEntityByRole =
-			Arrays.stream(new GroupMemberRole[]{
-				GroupMemberRole.OWNER,
-				GroupMemberRole.HEAD_MANAGER,
-				GroupMemberRole.MANAGER,
-				GroupMemberRole.MEMBER
-			}).collect(Collectors.toMap(
-				role -> role,
-				role -> groupRoleRepository.save(RoleEntity.create(groupId, role))
-			));
+		List<RoleEntity> savedRoles = Arrays.stream(GroupMemberRole.values())
+			.map(role -> RoleEntity.create(groupId, role))
+			.map(groupRoleRepository::save)
+			.toList();
+
+		Map<GroupMemberRole, RoleEntity> roleEntityByRole = savedRoles.stream()
+			.collect(Collectors.toMap(RoleEntity::getRole, entity -> entity));
🤖 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 53 - 62, The collector is invoking groupRoleRepository.save(...) as
a side-effect inside Collectors.toMap which is unsafe; change
GroupRoleRepositoryAdapter so you first iterate the GroupMemberRole values
(OWNER, HEAD_MANAGER, MANAGER, MEMBER), call
groupRoleRepository.save(RoleEntity.create(groupId, role)) for each in an
explicit loop or stream map-to-list, collect the saved RoleEntity results, then
build the Map<GroupMemberRole, RoleEntity> (roleEntityByRole) by mapping each
saved entity back to its GroupMemberRole; ensure all saves happen before
creating the final map and reference groupRoleRepository.save,
RoleEntity.create, GroupMemberRole and roleEntityByRole when locating the code
to change.
src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java (1)

39-46: existsUserInGroup 메서드명이 실제 동작과 불일치합니다.

메서드명이 exists...로 시작하여 boolean 반환을 암시하지만, 실제로는 void를 반환하고 예외를 던집니다. 또한 IllegalArgumentException은 너무 범용적이어서 상위 레이어에서 적절한 에러 핸들링이 어렵습니다.

♻️ 수정 제안
  1. 메서드명을 validateUserInGroup 등으로 변경하여 검증 동작임을 명확히 하세요.
  2. 커스텀 도메인 예외(예: UserNotInGroupException)를 사용하면 @ExceptionHandler에서 적절한 HTTP 상태 코드를 반환할 수 있습니다.
-	public void existsUserInGroup(Long groupId, Long userId) {
+	public void validateUserInGroup(Long groupId, Long userId) {
 
 		boolean isMember = groupMemberRepository.existsByGroupIdAndUserId(groupId, userId);
 
 		if(!isMember) {
-			throw new IllegalArgumentException("user not in Group");
+			throw new UserNotInGroupException(groupId, userId);
 		}
 	}
🤖 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, Rename and change the semantics of existsUserInGroup in
GroupMemberRepositoryAdapter to a validation-style method (e.g.,
validateUserInGroup) rather than an existence-query: keep it void but throw a
specific domain exception instead of IllegalArgumentException; create a custom
UserNotInGroupException (or similar) and throw that when
groupMemberRepository.existsByGroupIdAndUserId(groupId, userId) is false, and
update all callers to use the new method name/exception so higher layers can
handle it via `@ExceptionHandler`.
🤖 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/out/persistence/mapper/GroupMemberMapper.java`:
- Around line 47-53: The mapping in GroupMemberMapper that builds MemberInfo
uses entity.getRole().getRole() which can NPE if Role is null; update the map in
GroupMemberMapper (the stream mapping that calls MemberInfo.builder()) to
defensively handle a null role—either by checking entity.getRole() and using a
default (e.g., an Optional.empty/unknown role) or by skipping/throwing a clearer
exception—and ensure any behaviour aligns with callers like findAllByGroupId;
alternatively, document the not-null invariant for Role if you choose not to add
runtime guarding.
- Around line 24-35: The toDomain method in GroupMemberMapper is missing mapping
for userId, causing GroupMember.userId to be null; update
GroupMemberMapper.toDomain to set the userId on the returned GroupMember by
reading entity.getUserId() (i.e., add the .userId(entity.getUserId()) call to
the GroupMember.builder() chain so GroupMemberEntity.userId is propagated into
the GroupMember domain object).

In
`@src/main/java/flipnote/group/application/port/out/GroupMemberRepositoryPort.java`:
- Around line 5-9: The port GroupMemberRepositoryPort incorrectly depends on
adapter-layer RoleEntity; change its save signature to accept a domain type
(e.g., replace RoleEntity with a domain enum/value object such as
GroupMemberRole or use existing MemberInfo) and move conversion logic into the
adapter implementation (GroupMemberRepositoryAdapter) so the adapter maps
GroupMemberRole -> RoleEntity before persisting; update CreateGroupService to
pass the domain role type to GroupMemberRepositoryPort and ensure only domain
types appear in the port API.

In
`@src/main/java/flipnote/group/application/port/out/GroupRoleRepositoryPort.java`:
- Around line 3-7: The port GroupRoleRepositoryPort currently exposes the
persistence entity RoleEntity; change its method signature to use the domain
model GroupMemberRole (e.g., RoleMemberRole create(Long groupId) ->
GroupMemberRole create(Long groupId)) so the application port only depends on
domain types, and likewise update GroupMemberRepositoryPort.save(...) to accept
the domain GroupMemberRole (not RoleEntity). Implement entity<->domain mapping
inside the adapter layer (the adapter that implements GroupRoleRepositoryPort
and GroupMemberRepositoryPort should convert between RoleEntity and
GroupMemberRole when persisting/returning). Ensure GroupRepositoryPort remains
unchanged as it already uses the domain Group model.

In `@src/main/java/flipnote/group/application/service/CreateGroupService.java`:
- Line 6: CreateGroupService currently depends on adapter.out.entity.RoleEntity
which violates hexagonal boundaries; change CreateGroupService to use domain
types (e.g., GroupMemberRole or GroupMember) instead of RoleEntity, update
usages of GroupRoleRepositoryPort.create() to return a domain role type, and
modify GroupMemberRepositoryPort.save() to accept a domain GroupMember;
implement the entity↔domain mapping inside adapter implementations (converter
methods) so adapter classes handle RoleEntity <-> domain conversions and the
application service only references domain ports and models (symbols to change:
CreateGroupService, RoleEntity, GroupRoleRepositoryPort.create(),
GroupMemberRepositoryPort.save(), GroupMember/GroupMemberRole).

In
`@src/main/java/flipnote/group/application/service/FindGroupMemberService.java`:
- Around line 25-35: The findGroupMember method performs only reads
(existsUserInGroup and findMemberInfo) and must be annotated with
`@Transactional`(readOnly = true) to prevent TOCTOU inconsistencies and enable JPA
read optimizations; add the annotation to FindGroupMemberService.findGroupMember
(or the service class) and import
org.springframework.transaction.annotation.Transactional, and apply the same
change to FindGroupService.findGroup for consistency.

In `@src/main/java/flipnote/group/domain/model/member/GroupMember.java`:
- Line 3: GroupMember currently depends on JPA RoleEntity; change GroupMember to
use the domain enum GroupMemberRole (or a new domain value object carrying
roleId+roleType) instead of RoleEntity: remove the import
flipnote.group.adapter.out.entity.RoleEntity and replace any RoleEntity-typed
fields/constructors/getters in the GroupMember class with GroupMemberRole (or
the new VO) and update constructors/builders accordingly; then implement
conversion logic in the persistence adapter (the repository implementation that
maps between GroupMemberEntity/RoleEntity and GroupMember) to translate
RoleEntity <-> GroupMemberRole (or RoleVO) so the domain model stays
persistence-agnostic.

In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java`:
- Around line 10-11: Rename the interface GroupMemberRepositoryRepository to
GroupMemberRepository to match the project's naming convention, and update the
method signature existsByUserIdAndGroupRoleId(Long userId, Long id) by renaming
the second parameter from id to groupRoleId so it reads
existsByUserIdAndGroupRoleId(Long userId, Long groupRoleId); adjust any
imports/usages accordingly to the new interface name.

---

Outside diff comments:
In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupRoleRepositoryAdapter.java`:
- Around line 88-93: checkRole currently calls
groupRoleRepository.findByGroupIdAndRole which can return null (RoleEntity),
then immediately calls roleEntity.getId() causing NPE; update checkRole (in
GroupRoleRepositoryAdapter) to handle a missing role: call findByGroupIdAndRole
and if it returns null either return false (no such role -> user can't have it)
or change the repository signature to Optional<RoleEntity> and unwrap it safely
(e.g.,
roleOpt.map(role->groupMemberRepository.existsByUserIdAndGroupRoleId(userId,
role.getId())).orElse(false)); ensure references: checkRole,
groupRoleRepository.findByGroupIdAndRole, RoleEntity, and
groupMemberRepository.existsByUserIdAndGroupRoleId are used to locate the fix.

---

Nitpick comments:
In `@src/main/java/flipnote/group/adapter/in/web/GroupController.java`:
- Around line 18-28: Remove the unused imports FindGroupMemberResponseDto and
FindGroupMemberCommand from GroupController.java since there is no group-member
endpoint in this controller (the member lookup is implemented in
MemberController); locate the import lines referencing
FindGroupMemberResponseDto and FindGroupMemberCommand and delete them so the
file only imports types actually used (e.g., FindGroupResponseDto,
FindGroupCommand, CreateGroupCommand, ChangeGroupCommand, DeleteGroupCommand,
and their use-case interfaces).

In `@src/main/java/flipnote/group/adapter/in/web/InvitationController.java`:
- Around line 1-18: InvitationController currently lacks Spring web annotations;
either annotate the class with `@RestController` and a base `@RequestMapping` (e.g.,
"/invitations") and add appropriate imports so it becomes a valid Spring bean,
or if you want to keep it as a placeholder remove the empty class from the
commit and track the work in the issue tracker instead; locate the
InvitationController class to apply the annotation change or delete it as
described.

In `@src/main/java/flipnote/group/adapter/out/entity/GroupMemberEntity.java`:
- Around line 57-69: Update the Javadoc for GroupMemberEntity.create to reflect
the current signature: add a `@param` tag for the new role parameter (RoleEntity
role) describing its purpose, ensure `@param` tags for groupId and userId are
meaningful, and replace the empty `@return` tag with a brief description like "the
created GroupMemberEntity" so the documentation matches the create(Long groupId,
Long userId, RoleEntity role) factory method.

In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupMemberRepositoryAdapter.java`:
- Around line 39-46: Rename and change the semantics of existsUserInGroup in
GroupMemberRepositoryAdapter to a validation-style method (e.g.,
validateUserInGroup) rather than an existence-query: keep it void but throw a
specific domain exception instead of IllegalArgumentException; create a custom
UserNotInGroupException (or similar) and throw that when
groupMemberRepository.existsByGroupIdAndUserId(groupId, userId) is false, and
update all callers to use the new method name/exception so higher layers can
handle it via `@ExceptionHandler`.

In
`@src/main/java/flipnote/group/adapter/out/persistence/GroupRoleRepositoryAdapter.java`:
- Around line 53-62: The collector is invoking groupRoleRepository.save(...) as
a side-effect inside Collectors.toMap which is unsafe; change
GroupRoleRepositoryAdapter so you first iterate the GroupMemberRole values
(OWNER, HEAD_MANAGER, MANAGER, MEMBER), call
groupRoleRepository.save(RoleEntity.create(groupId, role)) for each in an
explicit loop or stream map-to-list, collect the saved RoleEntity results, then
build the Map<GroupMemberRole, RoleEntity> (roleEntityByRole) by mapping each
saved entity back to its GroupMemberRole; ensure all saves happen before
creating the final map and reference groupRoleRepository.save,
RoleEntity.create, GroupMemberRole and roleEntityByRole when locating the code
to change.

In
`@src/main/java/flipnote/group/api/dto/response/FindGroupMemberResponseDto.java`:
- Around line 8-14: FindGroupMemberResponseDto currently exposes the domain
model MemberInfo directly; instead create an API-only record (e.g.,
MemberInfoResponse) that contains only the fields needed by the API and update
FindGroupMemberResponseDto to use List<MemberInfoResponse> for memberInfoList;
in the static factory from(FindGroupMemberResult) map each domain MemberInfo
from result.memberInfoList() to a MemberInfoResponse (using a converter method
or constructor on MemberInfoResponse) and return the DTO with the mapped list to
avoid leaking domain types in the API layer.

In
`@src/main/java/flipnote/group/application/service/FindGroupMemberService.java`:
- Line 28: The method existsUserInGroup currently returns void and signals
invalid state via exceptions which is confusing given its name; rename
existsUserInGroup to a side-effect-validating name (e.g., validateUserInGroup or
checkUserInGroup) across the API and implementations, update the
GroupMemberRepositoryPort interface and all concrete implementations to the new
void method, and update the caller in FindGroupMemberService to invoke the new
method name (replace groupMemberRepository.existsUserInGroup(...) with
groupMemberRepository.validateUserInGroup(...) or chosen name) so the intent
(validation via exception) is clear and consistent.

In `@src/main/java/flipnote/group/infrastructure/config/QuerydslConfig.java`:
- Around line 11-19: Replace the constructor-injected final EntityManager with a
field-injected persistence context: remove `@RequiredArgsConstructor` and the
final EntityManager constructor parameter, add a non-final EntityManager field
annotated with `@PersistenceContext` in class QuerydslConfig, and keep the
jpaQueryFactory() method using that field so the bean uses a transaction-scoped
persistence context as intended.
- Around line 11-19: Replace constructor-injected EntityManager with a
container-managed one by annotating the EntityManager field with
`@PersistenceContext` in QuerydslConfig: remove the final field and
RequiredArgsConstructor usage, declare a non-final EntityManager annotated with
`@PersistenceContext`, and keep the jpaQueryFactory() bean returning new
JPAQueryFactory(entityManager) so the bean uses the JPA-standard,
container-managed EntityManager proxy.

In
`@src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java`:
- Line 12: Rename the ambiguous parameter `id` in the repository method
existsByUserIdAndGroupRoleId to a descriptive name like `roleId` or
`groupRoleId` in the GroupMemberRepositoryRepository interface and update any
callers/implementations to use the new parameter name; ensure the method
signature reads existsByUserIdAndGroupRoleId(Long userId, Long roleId) (or Long
groupRoleId) so the intent is clear and compilation references are adjusted
accordingly.

Comment on lines +24 to 35
public static GroupMember toDomain(GroupMemberEntity entity) {

if (entity == null) {
return null;
}

return GroupMember.builder()
.id(entity.getId())
.groupId(entity.getGroupId())
.role(entity.getRole())
.build();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

toDomain()에서 userId 매핑이 누락되었습니다 — 도메인 객체의 userId가 항상 null이 됩니다.

GroupMemberEntity에는 userId 필드가 있고, GroupMember 도메인 모델에도 userId 필드가 있지만, 빌더에서 .userId(entity.getUserId()) 호출이 빠져 있습니다.

🐛 수정 제안
 	return GroupMember.builder()
 		.id(entity.getId())
 		.groupId(entity.getGroupId())
+		.userId(entity.getUserId())
 		.role(entity.getRole())
 		.build();
📝 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.

Suggested change
public static GroupMember toDomain(GroupMemberEntity entity) {
if (entity == null) {
return null;
}
return GroupMember.builder()
.id(entity.getId())
.groupId(entity.getGroupId())
.role(entity.getRole())
.build();
}
public static GroupMember toDomain(GroupMemberEntity entity) {
if (entity == null) {
return null;
}
return GroupMember.builder()
.id(entity.getId())
.groupId(entity.getGroupId())
.userId(entity.getUserId())
.role(entity.getRole())
.build();
}
🤖 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/mapper/GroupMemberMapper.java`
around lines 24 - 35, The toDomain method in GroupMemberMapper is missing
mapping for userId, causing GroupMember.userId to be null; update
GroupMemberMapper.toDomain to set the userId on the returned GroupMember by
reading entity.getUserId() (i.e., add the .userId(entity.getUserId()) call to
the GroupMember.builder() chain so GroupMemberEntity.userId is propagated into
the GroupMember domain object).

Comment on lines +47 to +53
return entities.stream()
.map(entity -> MemberInfo.builder()
.userId(entity.getUserId())
.role(entity.getRole().getRole())
.build()
)
.toList();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

entity.getRole()null일 경우 NPE가 발생할 수 있습니다.

Line 50의 entity.getRole().getRole()rolenull이면 NullPointerException을 발생시킵니다. 현재 findAllByGroupId에서 join fetch를 사용하고 있지만, 이 mapper가 다른 경로에서 호출될 경우를 대비해 방어 코드를 추가하거나, role이 항상 not-null임을 전제로 한다면 해당 전제를 문서화하는 것이 좋습니다.

🤖 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/mapper/GroupMemberMapper.java`
around lines 47 - 53, The mapping in GroupMemberMapper that builds MemberInfo
uses entity.getRole().getRole() which can NPE if Role is null; update the map in
GroupMemberMapper (the stream mapping that calls MemberInfo.builder()) to
defensively handle a null role—either by checking entity.getRole() and using a
default (e.g., an Optional.empty/unknown role) or by skipping/throwing a clearer
exception—and ensure any behaviour aligns with callers like findAllByGroupId;
alternatively, document the not-null invariant for Role if you choose not to add
runtime guarding.

Comment on lines +5 to +9
import flipnote.group.adapter.out.entity.RoleEntity;
import flipnote.group.domain.model.member.MemberInfo;

public interface GroupMemberRepositoryPort {
void save(Long groupId, Long userId, Long roleId);
void save(Long groupId, Long userId, RoleEntity role);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Port 인터페이스가 RoleEntity(adapter 계층)에 의존합니다 — Hexagonal Architecture 위반의 근본 원인입니다.

GroupMemberRepositoryPort는 application 계층의 port로서 domain 타입만 사용해야 합니다. RoleEntity를 domain 타입(예: GroupMemberRole enum 또는 새로운 도메인 값 객체)으로 교체하고, entity 변환 책임은 adapter 구현체(GroupMemberRepositoryAdapter)에 위임해야 합니다.

CreateGroupService에서 언급한 것과 동일한 문제의 근본 원인이 이 인터페이스 정의에 있습니다.

♻️ 수정 방향 제안
-import flipnote.group.adapter.out.entity.RoleEntity;
 import flipnote.group.domain.model.member.MemberInfo;
+import flipnote.group.domain.model.member.GroupMemberRole;

 public interface GroupMemberRepositoryPort {
-    void save(Long groupId, Long userId, RoleEntity role);
+    void save(Long groupId, Long userId, GroupMemberRole role);

     void existsUserInGroup(Long groupId, Long userId);

     List<MemberInfo> findMemberInfo(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/application/port/out/GroupMemberRepositoryPort.java`
around lines 5 - 9, The port GroupMemberRepositoryPort incorrectly depends on
adapter-layer RoleEntity; change its save signature to accept a domain type
(e.g., replace RoleEntity with a domain enum/value object such as
GroupMemberRole or use existing MemberInfo) and move conversion logic into the
adapter implementation (GroupMemberRepositoryAdapter) so the adapter maps
GroupMemberRole -> RoleEntity before persisting; update CreateGroupService to
pass the domain role type to GroupMemberRepositoryPort and ensure only domain
types appear in the port API.

Comment on lines +3 to +7
import flipnote.group.adapter.out.entity.RoleEntity;
import flipnote.group.domain.model.member.GroupMemberRole;

public interface GroupRoleRepositoryPort {
Long create(Long groupId);
RoleEntity create(Long groupId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/main/java/flipnote/group/application/port/out/GroupRoleRepositoryPort.java

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 459


🏁 Script executed:

rg -n 'import flipnote\.group\.adapter\.' --glob '*/application/port/**'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 219


🏁 Script executed:

fd -t f 'GroupMemberRole\.java' -o 'RoleEntity\.java'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 222


🏁 Script executed:

# Find all imports of adapter layer within application/port directory
rg 'import flipnote\.group\.adapter\.' --type java -A 1 | head -50

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 4890


🏁 Script executed:

# Find RoleEntity definition to confirm it's a JPA entity
fd -t f 'RoleEntity.java'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 134


🏁 Script executed:

# Check GroupMemberRole definition
fd -t f 'GroupMemberRole.java'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 140


🏁 Script executed:

# Look for other repositories in application/port to check pattern consistency
fd -t f -name '*RepositoryPort.java' | head -10

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 301


🏁 Script executed:

cat -n src/main/java/flipnote/group/adapter/out/entity/RoleEntity.java

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 1801


🏁 Script executed:

cat -n src/main/java/flipnote/group/domain/model/member/GroupMemberRole.java

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 220


🏁 Script executed:

fd -t f -name '*RepositoryPort.java'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 301


🏁 Script executed:

cat -n src/main/java/flipnote/group/application/port/out/GroupMemberRepositoryPort.java

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 560


🏁 Script executed:

fd -t f -path '*/application/port/out/*RepositoryPort.java'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 239


🏁 Script executed:

cat -n src/main/java/flipnote/group/application/port/out/GroupRepositoryPort.java

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 376


🏁 Script executed:

# Check if there's a Role domain aggregate or value object
fd -t f -path '*/domain/model/*' -name '*Role*'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 239


애플리케이션 포트에서 어댑터 레이어의 엔티티(RoleEntity)를 직접 참조하고 있습니다.

헥사고날 아키텍처에서 application.port.out 인터페이스는 도메인 모델만 참조해야 합니다. 현재 adapter.out.entity.RoleEntity(JPA 엔티티)를 반환 타입과 파라미터로 사용하면 애플리케이션 레이어가 영속성 어댑터에 의존하게 되어 의존성 방향이 역전됩니다.

이와 동일한 문제가 GroupMemberRepositoryPort에서도 발견됩니다(save() 메서드 파라미터). 대조적으로 GroupRepositoryPort는 도메인 모델(Group)을 올바르게 사용하고 있습니다.

도메인 모델(기존 GroupMemberRole 또는 Role 도메인 애그리거트)을 포트의 반환/파라미터 타입으로 사용하고, 어댑터에서만 엔티티↔도메인 변환을 처리하도록 리팩토링해야 합니다.

🤖 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/GroupRoleRepositoryPort.java`
around lines 3 - 7, The port GroupRoleRepositoryPort currently exposes the
persistence entity RoleEntity; change its method signature to use the domain
model GroupMemberRole (e.g., RoleMemberRole create(Long groupId) ->
GroupMemberRole create(Long groupId)) so the application port only depends on
domain types, and likewise update GroupMemberRepositoryPort.save(...) to accept
the domain GroupMemberRole (not RoleEntity). Implement entity<->domain mapping
inside the adapter layer (the adapter that implements GroupRoleRepositoryPort
and GroupMemberRepositoryPort should convert between RoleEntity and
GroupMemberRole when persisting/returning). Ensure GroupRepositoryPort remains
unchanged as it already uses the domain Group model.

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import flipnote.group.adapter.out.entity.RoleEntity;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Application 서비스가 Adapter 계층의 RoleEntity에 직접 의존합니다.

Hexagonal Architecture에서 application 서비스는 domain/port에만 의존해야 합니다. RoleEntityadapter.out.entity 패키지의 JPA 엔티티이므로, application 계층에서 직접 참조하면 의존성 방향이 역전됩니다.

GroupRoleRepositoryPort.create()가 domain 타입(예: GroupMemberRole 또는 새 도메인 모델)을 반환하고, GroupMemberRepositoryPort.save()도 domain 타입을 받도록 리팩토링하는 것이 좋습니다. Adapter 구현체에서 entity ↔ domain 변환을 담당해야 합니다.

이 문제는 GroupMemberRepositoryPort, GroupMember 도메인 모델 등 PR 전반에 영향을 미칩니다.

Also applies to: 41-44

🤖 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/CreateGroupService.java` at
line 6, CreateGroupService currently depends on adapter.out.entity.RoleEntity
which violates hexagonal boundaries; change CreateGroupService to use domain
types (e.g., GroupMemberRole or GroupMember) instead of RoleEntity, update
usages of GroupRoleRepositoryPort.create() to return a domain role type, and
modify GroupMemberRepositoryPort.save() to accept a domain GroupMember;
implement the entity↔domain mapping inside adapter implementations (converter
methods) so adapter classes handle RoleEntity <-> domain conversions and the
application service only references domain ports and models (symbols to change:
CreateGroupService, RoleEntity, GroupRoleRepositoryPort.create(),
GroupMemberRepositoryPort.save(), GroupMember/GroupMemberRole).

Comment on lines +25 to +35
@Override
public FindGroupMemberResult findGroupMember(FindGroupMemberCommand cmd) {

groupMemberRepository.existsUserInGroup(cmd.groupId(), cmd.userId());

List<MemberInfo> memberInfoList = groupMemberRepository.findMemberInfo(cmd.groupId());

//todo 멤버 정보에서 유저 아이디를 grpc를 통해 가져온 후 반환

return new FindGroupMemberResult(memberInfoList);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

읽기 전용 트랜잭션 어노테이션이 누락되었습니다.

findGroupMemberexistsUserInGroupfindMemberInfo 두 개의 DB 조회를 수행합니다. @Transactional(readOnly = true) 없이는 두 호출 사이에 데이터 불일치가 발생할 수 있으며 (TOCTOU), JPA 더티 체킹 최적화 기회도 놓칩니다.

참고로, 기존 FindGroupService.findGroup()에도 동일한 패턴이 보이므로 함께 개선하는 것이 좋습니다.

♻️ 수정 제안
+import org.springframework.transaction.annotation.Transactional;
+
 `@Service`
 `@RequiredArgsConstructor`
 public class FindGroupMemberService implements FindGroupMemberUseCase {
 
 	private final GroupMemberRepositoryPort groupMemberRepository;
 
 	`@Override`
+	`@Transactional`(readOnly = true)
 	public FindGroupMemberResult findGroupMember(FindGroupMemberCommand cmd) {
📝 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.

Suggested change
@Override
public FindGroupMemberResult findGroupMember(FindGroupMemberCommand cmd) {
groupMemberRepository.existsUserInGroup(cmd.groupId(), cmd.userId());
List<MemberInfo> memberInfoList = groupMemberRepository.findMemberInfo(cmd.groupId());
//todo 멤버 정보에서 유저 아이디를 grpc를 통해 가져온 후 반환
return new FindGroupMemberResult(memberInfoList);
}
`@Override`
`@Transactional`(readOnly = true)
public FindGroupMemberResult findGroupMember(FindGroupMemberCommand cmd) {
groupMemberRepository.existsUserInGroup(cmd.groupId(), cmd.userId());
List<MemberInfo> memberInfoList = groupMemberRepository.findMemberInfo(cmd.groupId());
//todo 멤버 정보에서 유저 아이디를 grpc를 통해 가져온 후 반환
return new FindGroupMemberResult(memberInfoList);
}
🤖 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 performs only reads
(existsUserInGroup and findMemberInfo) and must be annotated with
`@Transactional`(readOnly = true) to prevent TOCTOU inconsistencies and enable JPA
read optimizations; add the annotation to FindGroupMemberService.findGroupMember
(or the service class) and import
org.springframework.transaction.annotation.Transactional, and apply the same
change to FindGroupService.findGroup for consistency.


import flipnote.group.domain.model.group.GroupId;
import flipnote.group.domain.model.user.UserId;
import flipnote.group.adapter.out.entity.RoleEntity;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

도메인 모델 GroupMember가 JPA 엔티티 RoleEntity에 의존합니다.

도메인 모델은 인프라/persistence 계층과 독립적이어야 합니다. RoleEntity@Entity, @ManyToOne 등 JPA 어노테이션이 포함된 영속성 엔티티이므로, 도메인 모델에서 직접 참조하면 도메인이 JPA에 종속됩니다.

GroupMemberRepositoryPort 리뷰 코멘트에서 제안한 대로 RoleEntity 대신 GroupMemberRole enum(이미 도메인 패키지에 존재)을 사용하거나, 필요 시 role ID + role type을 포함하는 도메인 값 객체를 도입하는 것이 적합합니다.

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 JPA RoleEntity; change GroupMember to use the
domain enum GroupMemberRole (or a new domain value object carrying
roleId+roleType) instead of RoleEntity: remove the import
flipnote.group.adapter.out.entity.RoleEntity and replace any RoleEntity-typed
fields/constructors/getters in the GroupMember class with GroupMemberRole (or
the new VO) and update constructors/builders accordingly; then implement
conversion logic in the persistence adapter (the repository implementation that
maps between GroupMemberEntity/RoleEntity and GroupMember) to translate
RoleEntity <-> GroupMemberRole (or RoleVO) so the domain model stays
persistence-agnostic.

Comment on lines +10 to +11
public interface GroupMemberRepositoryRepository
extends JpaRepository<GroupMemberEntity, Long> {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the old GroupMemberRepository interface still exists alongside the new one
fd "GroupMemberRepository.java" --type f
echo "---"
# Check which repository is used by the adapter
rg -n "GroupMemberRepository" --type java -g '!**/GroupMemberRepositoryRepository.java'

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 2063


🏁 Script executed:

#!/bin/bash
# Read the GroupMemberRepositoryRepository file
cat -n src/main/java/flipnote/group/infrastructure/persistence/jpa/GroupMemberRepositoryRepository.java

echo "---"
# Check naming patterns for other JPA repositories
fd "Repository.java" --path "*/infrastructure/persistence/jpa/*" --type f | head -10

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 1128


🏁 Script executed:

#!/bin/bash
# Check naming patterns for other JPA repositories
find src/main/java/flipnote/group/infrastructure/persistence/jpa -name "*Repository*.java" -type f
echo "---"
# Show content of a few other repositories to check naming patterns
find src/main/java/flipnote/group/infrastructure/persistence/jpa -name "*Repository*.java" -type f | head -5 | while read f; do echo "=== $f ==="; head -15 "$f"; echo; done

Repository: FlipNoteTeam/FlipNote-Group

Length of output: 2230


GroupMemberRepositoryRepository 인터페이스 이름의 "Repository" 중복을 수정하세요.

프로젝트의 다른 JPA 저장소들은 GroupRepository, GroupRoleRepository, GroupRolePermissionRepository 등으로 명명되어 있습니다. GroupMemberRepositoryRepository는 이 명명 규칙에 맞지 않으며, 단순히 GroupMemberRepository로 변경해야 합니다.

추가로 12번 줄의 existsByUserIdAndGroupRoleId(Long userId, Long id) 메서드에서 두 번째 매개변수 이름이 id로 되어 있어 모호합니다. Spring Data 명명 규칙에 맞게 groupRoleId로 변경하는 것이 좋습니다.

🤖 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 10 - 11, Rename the interface GroupMemberRepositoryRepository to
GroupMemberRepository to match the project's naming convention, and update the
method signature existsByUserIdAndGroupRoleId(Long userId, Long id) by renaming
the second parameter from id to groupRoleId so it reads
existsByUserIdAndGroupRoleId(Long userId, Long groupRoleId); adjust any
imports/usages accordingly to the new interface name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments