diff --git a/nect-api/src/main/java/com/nect/api/domain/dm/service/DmService.java b/nect-api/src/main/java/com/nect/api/domain/dm/service/DmService.java index 5121de4e..ff0e48a5 100644 --- a/nect-api/src/main/java/com/nect/api/domain/dm/service/DmService.java +++ b/nect-api/src/main/java/com/nect/api/domain/dm/service/DmService.java @@ -100,6 +100,8 @@ public DmMessageListResponse getMessages(Long userId, Long otherUserId, Long cur .map(DirectMessageDto::fromDm) .toList(); + list.forEach( dto -> dto.setImageUrl( s3Service.getPresignedGetUrl(dto.getSenderProfileImage()) ) ); + // 응답값 생성 후 반환 return DmMessageListResponse.builder() .messages(list) diff --git a/nect-api/src/main/java/com/nect/api/domain/home/facade/MainHomeFacade.java b/nect-api/src/main/java/com/nect/api/domain/home/facade/MainHomeFacade.java index 29cf876f..af6858c0 100644 --- a/nect-api/src/main/java/com/nect/api/domain/home/facade/MainHomeFacade.java +++ b/nect-api/src/main/java/com/nect/api/domain/home/facade/MainHomeFacade.java @@ -7,10 +7,14 @@ import com.nect.api.domain.home.service.HomeStatisticsQueryService; import com.nect.api.domain.mypage.dto.MyProjectsResponseDto; import com.nect.api.domain.mypage.service.MyPageProjectQueryService; +import com.nect.api.domain.mypage.service.UserTeamRoleService; import com.nect.api.domain.team.project.service.ProjectMemberQueryService; +import com.nect.api.domain.team.project.service.ProjectService; +import com.nect.api.domain.team.project.service.ProjectUserService; import com.nect.api.global.infra.S3Service; import com.nect.core.entity.team.Project; import com.nect.core.entity.team.ProjectInterest; +import com.nect.core.entity.team.enums.MemberMatchable; import com.nect.core.entity.user.User; import com.nect.core.entity.user.enums.InterestField; import com.nect.core.entity.user.enums.Role; @@ -37,6 +41,7 @@ public class MainHomeFacade { private final S3Service s3Service; private final MyPageProjectQueryService myPageProjectQueryService; private final ProjectMemberQueryService projectMemberQueryService; + private final ProjectUserService projectUserService; // 모집 중인 프로젝트 public HomeProjectResponse getRecruitingProjects(Long userId, int count, Role role, InterestField interest){ @@ -119,7 +124,10 @@ public HomeMembersResponse getMatchableMembers(Long userId, int count, Role role users = homeMemberQueryService.getAllUsersWithoutUser(userId, safeCount); } - return buildMemberResponse(users); + List projectIds = projectUserService.getProjectByLeader(userId).stream() + .map(Project::getId) + .toList(); + return buildMemberResponse(projectIds, users); } // 홈화면 추천 넥터 @@ -127,7 +135,10 @@ public HomeMembersResponse getRecommendedMembers(Long userId, int count) { int safeCount = safeCount(count); List users = homeMemberQueryService.getAllUsersWithoutUser(userId, safeCount); - List items = new ArrayList<>(responsesFromMembers(users)); + List projectIds = projectUserService.getProjectByLeader(userId).stream() + .map(Project::getId) + .toList(); + List items = new ArrayList<>(responsesFromMembersWithMatchable(projectIds, users, false)); Collections.shuffle(items); return HomeMembersResponse.of(items); @@ -183,13 +194,16 @@ private List responsesFromProjects(List projects) { } // List users -> List - private List responsesFromMembers(List users) { + private List responsesFromMembersWithMatchable(List projectIds, List users, boolean filterMatchableOnly) { Map> partsByUserId = homeMemberQueryService.partsByUsers(users); return users.stream() .map(user -> { List parts = partsByUserId.getOrDefault(user.getUserId(), List.of()); - + MemberMatchable matchable = homeMemberQueryService.getMemberMatchable(projectIds, user.getUserId()); + if (filterMatchableOnly && matchable != MemberMatchable.MATCHABLE) { + return null; + } return HomeMemberItem.of( user.getUserId(), s3Service.getPresignedGetUrl(user.getProfileImageName()), @@ -197,14 +211,16 @@ private List responsesFromMembers(List users) { user.getRole() != null ? user.getRole().name() : null, user.getBio(), user.getCoreCompetencies(), - user.getUserStatus() != null ? user.getUserStatus().name() : null, + matchable.getDescription(), false, parts ); }) + .filter(item -> item != null) .toList(); } + private HomeProjectResponse buildProjectResponse(List projects) { if (projects.isEmpty()) { return HomeProjectResponse.of(List.of()); @@ -213,8 +229,8 @@ private HomeProjectResponse buildProjectResponse(List projects) { return HomeProjectResponse.of(responsesFromProjects(projects)); } - private HomeMembersResponse buildMemberResponse(List users) { - return HomeMembersResponse.of(responsesFromMembers(users)); + private HomeMembersResponse buildMemberResponse(List projectIds, List users) { + return HomeMembersResponse.of(responsesFromMembersWithMatchable(projectIds, users, true)); } private int safeCount(int count) { diff --git a/nect-api/src/main/java/com/nect/api/domain/home/service/HomeMemberQueryService.java b/nect-api/src/main/java/com/nect/api/domain/home/service/HomeMemberQueryService.java index 2fc6ba04..48cdd666 100644 --- a/nect-api/src/main/java/com/nect/api/domain/home/service/HomeMemberQueryService.java +++ b/nect-api/src/main/java/com/nect/api/domain/home/service/HomeMemberQueryService.java @@ -3,11 +3,17 @@ import com.nect.api.domain.home.dto.HomeHeaderResponse; import com.nect.api.domain.user.exception.UserNotFoundException; import com.nect.api.global.infra.S3Service; +import com.nect.core.entity.team.enums.MemberMatchable; +import com.nect.core.entity.team.enums.ProjectMemberStatus; import com.nect.core.entity.user.User; import com.nect.core.entity.user.UserRole; +import com.nect.core.entity.user.UserTeamRole; import com.nect.core.entity.user.enums.InterestField; import com.nect.core.entity.user.enums.Role; import com.nect.core.entity.user.enums.RoleField; +import com.nect.core.repository.user.ProjectUserRepositoryComplete; +import com.nect.core.repository.user.UserTeamRoleRepository; +import com.nect.core.repository.team.ProjectUserRepository; import com.nect.core.repository.user.UserInterestRepository; import com.nect.core.repository.user.UserRepository; import com.nect.core.repository.user.UserRoleRepository; @@ -29,6 +35,9 @@ public class HomeMemberQueryService { private final UserRepository userRepository; private final UserRoleRepository userRoleRepository; private final UserInterestRepository userInterestRepository; + private final ProjectUserRepository projectUserRepository; + private final ProjectUserRepositoryComplete projectUserRepositoryComplete; + private final UserTeamRoleRepository userTeamRoleRepository; private final S3Service s3Service; public List getFilteredMembers(Long userId, int count, Role role, InterestField interest) { @@ -89,4 +98,98 @@ public HomeHeaderResponse getHeaderProfile(Long userId) { ); } + + // 매칭 가능 판단 후 MemberMatchable 반환 + public MemberMatchable getMemberMatchable(List projectIds, Long targetUserId) { + if (projectIds == null || projectIds.isEmpty()) { + return MemberMatchable.MATCH_COMPLETE; + } + + int activeProjectCount = projectUserRepository.findActiveProjectsByUserId(targetUserId).size(); + if (activeProjectCount < 2) { +// User targetUser = userRepository.findById(targetUserId) +// .orElseThrow(() -> new UserNotFoundException("유저를 찾을 수 없습니다.")); +// +// List userRoles = userRoleRepository.findByUser(targetUser); +// if (userRoles.isEmpty()) { +// return MemberMatchable.MATCH_COMPLETE; +// } +// +// List projectRoles = userTeamRoleRepository.findByProjectIdIn(projectIds); +// Map> rolesByProjectId = new HashMap<>(); +// for (UserTeamRole role : projectRoles) { +// if (role.isDeleted()) { +// continue; +// } +// Long projectId = role.getProject().getId(); +// rolesByProjectId.computeIfAbsent(projectId, k -> new java.util.ArrayList<>()).add(role); +// } +// +// var activeMembers = projectUserRepositoryComplete.findByProjectIdInAndMemberStatus(projectIds, ProjectMemberStatus.ACTIVE); +// Map> activeCountsByProjectId = new HashMap<>(); +// for (var member : activeMembers) { +// Long projectId = member.getProject().getId(); +// Map activeCounts = activeCountsByProjectId.computeIfAbsent(projectId, k -> new HashMap<>()); +// String key = roleKey(member.getRoleField(), member.getCustomRoleFieldName()); +// activeCounts.put(key, activeCounts.getOrDefault(key, 0) + 1); +// } +// +// for (Long projectId : projectIds) { +// List roles = rolesByProjectId.get(projectId); +// if (roles == null || roles.isEmpty()) { +// continue; +// } +// +// Map activeCounts = activeCountsByProjectId.getOrDefault(projectId, Map.of()); +// for (UserTeamRole projectRole : roles) { +// if (projectRole.getRequiredCount() == null || projectRole.getRequiredCount() < 1) { +// continue; +// } +// +// if (!userHasRole(userRoles, projectRole.getRoleField(), projectRole.getCustomRoleFieldName())) { +// continue; +// } +// +// String key = roleKey(projectRole.getRoleField(), projectRole.getCustomRoleFieldName()); +// int currentCount = activeCounts.getOrDefault(key, 0); +// if (currentCount < projectRole.getRequiredCount()) { +// return MemberMatchable.MATCHABLE; +// } +// } +// } + return MemberMatchable.MATCHABLE; + } + + return MemberMatchable.MATCH_COMPLETE; + + } + + private static String roleKey(RoleField roleField, String customRoleFieldName) { + String custom = (customRoleFieldName == null) ? "" : customRoleFieldName.trim().toLowerCase(); + return roleField.name() + ":" + custom; + } + + private static boolean userHasRole(List userRoles, RoleField roleField, String customRoleFieldName) { + if (roleField == RoleField.CUSTOM) { + String custom = (customRoleFieldName == null) ? "" : customRoleFieldName.trim().toLowerCase(); + for (UserRole userRole : userRoles) { + if (userRole.getRoleField() != RoleField.CUSTOM) { + continue; + } + String userCustom = (userRole.getCustomField() == null) ? "" : userRole.getCustomField().trim().toLowerCase(); + if (custom.equals(userCustom)) { + return true; + } + } + return false; + } + + for (UserRole userRole : userRoles) { + if (userRole.getRoleField() == roleField) { + return true; + } + } + return false; + } + } diff --git a/nect-api/src/main/java/com/nect/api/domain/mypage/dto/UserTeamRolesResDto.java b/nect-api/src/main/java/com/nect/api/domain/mypage/dto/UserTeamRolesResDto.java index 2fa76ffd..f988c987 100644 --- a/nect-api/src/main/java/com/nect/api/domain/mypage/dto/UserTeamRolesResDto.java +++ b/nect-api/src/main/java/com/nect/api/domain/mypage/dto/UserTeamRolesResDto.java @@ -23,6 +23,22 @@ public record PartDto( String label, @JsonProperty("required_count") - Integer requiredCount + Integer requiredCount, + + @JsonProperty("members") + List members + ) {} + + public record PartUserInfo( + @JsonProperty("user_id") + Long userId, + @JsonProperty("profile_image") + String profileImage, + @JsonProperty("name") + String name, + @JsonProperty("part") + String part + ) {} + } diff --git a/nect-api/src/main/java/com/nect/api/domain/mypage/service/UserTeamRoleQueryService.java b/nect-api/src/main/java/com/nect/api/domain/mypage/service/UserTeamRoleQueryService.java index 45ea027b..82f3e973 100644 --- a/nect-api/src/main/java/com/nect/api/domain/mypage/service/UserTeamRoleQueryService.java +++ b/nect-api/src/main/java/com/nect/api/domain/mypage/service/UserTeamRoleQueryService.java @@ -7,14 +7,20 @@ import com.nect.api.domain.mypage.exception.UserTeamRoleException; import com.nect.api.domain.team.project.enums.code.ProjectErrorCode; import com.nect.api.domain.team.project.exception.ProjectException; +import com.nect.api.domain.team.project.service.ProjectMemberQueryService; +import com.nect.api.domain.team.project.service.ProjectUserService; +import com.nect.api.global.infra.S3Service; import com.nect.core.entity.team.Project; import com.nect.core.entity.team.ProjectTeamRole; +import com.nect.core.entity.team.ProjectUser; import com.nect.core.entity.team.enums.ProjectMemberStatus; import com.nect.core.entity.team.enums.ProjectMemberType; +import com.nect.core.entity.user.User; import com.nect.core.entity.user.UserTeamRole; import com.nect.core.entity.user.enums.RoleField; import com.nect.core.repository.team.ProjectRepository; import com.nect.core.repository.team.ProjectUserRepository; +import com.nect.core.repository.user.UserRepository; import com.nect.core.repository.user.UserTeamRoleRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -22,6 +28,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @Service @@ -31,7 +38,10 @@ public class UserTeamRoleQueryService { private final UserTeamRoleRepository userTeamRoleRepository; private final ProjectUserRepository projectUserRepository; private final ProjectRepository projectRepository; - + private final UserRepository userRepository; + private final S3Service s3Service; + private final ProjectUserService projectUserService; + private final ProjectMemberQueryService projectMemberQueryService; // 마이페이지 팀 파트 조회(UserTeamRole 사용), 프로젝트 멤버면 조회 가능 @Transactional(readOnly = true) @@ -53,13 +63,36 @@ public UserTeamRolesResDto readMyPageParts(Long projectId, Long requesterUserId) userTeamRoleRepository.findAllByProject_IdAndDeletedAtIsNullOrderByIdAsc(projectId); List parts = roles.stream() - .map(r -> new UserTeamRolesResDto.PartDto( + .map(r -> { + + List projectUsers = projectUserRepository.findByProjectIdAndRoleField(r.getProject().getId(), r.getRoleField()); + List userIds = projectUsers.stream() + .map(ProjectUser::getUserId) + .toList(); + + List users = userIds.stream() + .map(userRepository::findById) // Optional + .flatMap(Optional::stream) // 존재하는 것만 User로 변환 + .toList(); + + List userInfos = users.stream() + .map(u -> new UserTeamRolesResDto.PartUserInfo( + u.getUserId(), + s3Service.getPresignedGetUrl(u.getProfileImageName()), + u.getNickname(), + projectMemberQueryService.roleFieldInProject(projectId, u.getUserId()).getLabelEn() + )).toList(); + + return new UserTeamRolesResDto.PartDto( r.getId(), r.getRoleField(), r.getCustomRoleFieldName(), r.getLabel(), - r.getRequiredCount() - )) + r.getRequiredCount(), + userInfos + ); + + }) .toList(); return new UserTeamRolesResDto(parts); diff --git a/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectMemberQueryService.java b/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectMemberQueryService.java index e02bcda8..9af9bea8 100644 --- a/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectMemberQueryService.java +++ b/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectMemberQueryService.java @@ -3,8 +3,11 @@ import com.nect.api.domain.home.dto.HomeProjectMembersResponse; import com.nect.api.domain.team.project.dto.ProjectUsersResDto; import com.nect.api.domain.team.project.enums.code.ProjectErrorCode; +import com.nect.api.domain.team.project.enums.code.ProjectUserErrorCode; import com.nect.api.domain.team.project.exception.ProjectException; +import com.nect.api.domain.team.project.exception.ProjectUserException; import com.nect.api.global.infra.S3Service; +import com.nect.core.entity.team.ProjectUser; import com.nect.core.entity.team.enums.ProjectMemberStatus; import com.nect.core.entity.user.User; import com.nect.core.entity.user.enums.RoleField; @@ -131,6 +134,13 @@ public HomeProjectMembersResponse homeReadProjectUsers(Long projectId) { return new HomeProjectMembersResponse(users); } + // 특정 사람이 특정 프로젝트에서 어떤 역할인지 ( RoleField ) + public RoleField roleFieldInProject(Long projectId, Long userId) { + ProjectUser projectUser = projectUserRepository.findByUserIdAndProjectId(userId, projectId) + .orElseThrow(() -> new ProjectUserException(ProjectUserErrorCode.PROJECT_USER_NOT_FOUND)); + return projectUser.getRoleField(); + } + private void assertActiveProjectMember(Long projectId, Long userId) { boolean ok = projectUserRepository.existsByProjectIdAndUserIdAndMemberStatus( projectId, userId, ProjectMemberStatus.ACTIVE diff --git a/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectUserService.java b/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectUserService.java index b4169110..ba0e7c08 100644 --- a/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectUserService.java +++ b/nect-api/src/main/java/com/nect/api/domain/team/project/service/ProjectUserService.java @@ -32,6 +32,14 @@ public class ProjectUserService { private final ProjectUserRepository projectUserRepository; private final ProjectService projectService; + // 내가 리더로 속한 프로젝트 조회 + public List getProjectByLeader(Long userId){ + List projectUser = projectUserRepository.findByUserIdAndMemberType(userId, ProjectMemberType.LEADER); + return projectUser.stream() + .map(ProjectUser::getProject) + .toList(); + } + public ProjectUser addProjectUser(Long userId, Project project, RoleField field){ ProjectUser projectUser = ProjectUser.builder() .project(project) diff --git a/nect-api/src/main/java/com/nect/api/domain/user/service/UserEnumService.java b/nect-api/src/main/java/com/nect/api/domain/user/service/UserEnumService.java index a1d94191..c60970b0 100644 --- a/nect-api/src/main/java/com/nect/api/domain/user/service/UserEnumService.java +++ b/nect-api/src/main/java/com/nect/api/domain/user/service/UserEnumService.java @@ -37,7 +37,7 @@ public List getRoles() { public RoleFieldsResponseDto getRoleFields(Role role) { List fields = Arrays.stream(RoleField.values()) .filter(field -> field.getRole() == null || field.getRole().equals(role)) - .map(field -> new EnumValueDto(field.name(), field.getLabelEn(), field.getLabelEn())) + .map(field -> new EnumValueDto(field.name(), field.getDescription(), field.getLabelEn())) .collect(Collectors.toList()); return new RoleFieldsResponseDto( diff --git a/nect-api/src/test/java/com/nect/api/domain/mypage/controller/UserTeamRoleControllerTest.java b/nect-api/src/test/java/com/nect/api/domain/mypage/controller/UserTeamRoleControllerTest.java index 8de61239..ed6f09a9 100644 --- a/nect-api/src/test/java/com/nect/api/domain/mypage/controller/UserTeamRoleControllerTest.java +++ b/nect-api/src/test/java/com/nect/api/domain/mypage/controller/UserTeamRoleControllerTest.java @@ -253,14 +253,36 @@ void readMyPageParts() throws Exception { RoleField.BACKEND, null, "Backend", - 1 + 1, + List.of( + new UserTeamRolesResDto.PartUserInfo( + 1L, + "https://example.com/profile/1.png", + "홍길동", + "BACKEND" + ) + ) ), new UserTeamRolesResDto.PartDto( 11L, RoleField.CUSTOM, "데이터", "데이터", - 2 + 2, + List.of( + new UserTeamRolesResDto.PartUserInfo( + 2L, + "https://example.com/profile/2.png", + "이영희", + "FRONTEND" + ), + new UserTeamRolesResDto.PartUserInfo( + 3L, + "https://example.com/profile/3.png", + "박민수", + "FRONTEND" + ) + ) ) )); @@ -298,7 +320,12 @@ void readMyPageParts() throws Exception { fieldWithPath("body.parts[].role_field").type(STRING).description("파트 타입(RoleField)"), fieldWithPath("body.parts[].custom_role_field_name").optional().type(STRING).description("CUSTOM 파트명"), fieldWithPath("body.parts[].label").type(STRING).description("표시 라벨(label)"), - fieldWithPath("body.parts[].required_count").type(NUMBER).description("모집 인원") + fieldWithPath("body.parts[].required_count").type(NUMBER).description("모집 인원"), + fieldWithPath("body.parts[].members").type(ARRAY).description("파트 멤버 목록"), + fieldWithPath("body.parts[].members[].user_id").type(NUMBER).description("멤버 유저 ID"), + fieldWithPath("body.parts[].members[].profile_image").type(STRING).description("멤버 프로필 이미지").optional(), + fieldWithPath("body.parts[].members[].name").type(STRING).description("멤버 이름"), + fieldWithPath("body.parts[].members[].part").type(STRING).description("멤버 파트 라벨") ) .build() ) diff --git a/nect-core/src/main/java/com/nect/core/entity/team/enums/MemberMatchable.java b/nect-core/src/main/java/com/nect/core/entity/team/enums/MemberMatchable.java new file mode 100644 index 00000000..174955e9 --- /dev/null +++ b/nect-core/src/main/java/com/nect/core/entity/team/enums/MemberMatchable.java @@ -0,0 +1,17 @@ +package com.nect.core.entity.team.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MemberMatchable { + + MATCHABLE("매칭 가능"), + MATCH_COMPLETE("매칭 완료"), + + ; + + private final String description; + +} diff --git a/nect-core/src/main/java/com/nect/core/entity/user/enums/RoleField.java b/nect-core/src/main/java/com/nect/core/entity/user/enums/RoleField.java index 65c9deb0..c6d480c9 100644 --- a/nect-core/src/main/java/com/nect/core/entity/user/enums/RoleField.java +++ b/nect-core/src/main/java/com/nect/core/entity/user/enums/RoleField.java @@ -39,14 +39,15 @@ public enum RoleField { AD_VIRAL("광고/바이럴", "Ads/Viral", Role.MARKETER), LIVE_COMMERCE("라이브커머스", "Live Commerce", Role.MARKETER), DATA_ANALYSIS("데이터 분석", "Data Analysis", Role.MARKETER), - MARKETING_OTHER("기타", "Other", Role.MARKETER), - OPERATIONS_CS("운영/CS", "Operations/CS", Role.MARKETER), - SALES_PARTNERSHIP("영업/제휴", "Sales/Partnership", Role.MARKETER), - VIDEO_MUSIC_DIRECTING("영상/음악 감독", "Video/Music Directing", Role.MARKETER), - TRANSLATION_INTERPRETATION("번역/통역", "Translation/Interpretation", Role.MARKETER), - MANUSCRIPT_CONSULTING("원고 컨설턴트", "Manuscript Consulting", Role.MARKETER), - ACCOUNTING_LAW_HR("세무/법무/노무", "Accounting/Law/HR", Role.MARKETER), - STARTUP_CONSULTING("창업 컨설팅", "Startup Consulting", Role.MARKETER), +// MARKETING_OTHER("기타", "Other", Role.MARKETER), + + OPERATIONS_CS("운영/CS", "Operations/CS", Role.OTHER), + SALES_PARTNERSHIP("영업/제휴", "Sales/Partnership", Role.OTHER), + VIDEO_MUSIC_DIRECTING("영상/음악 감독", "Video/Music Directing", Role.OTHER), + TRANSLATION_INTERPRETATION("번역/통역", "Translation/Interpretation", Role.OTHER), + MANUSCRIPT_CONSULTING("원고 컨설턴트", "Manuscript Consulting", Role.OTHER), + ACCOUNTING_LAW_HR("세무/법무/노무", "Accounting/Law/HR", Role.OTHER), + STARTUP_CONSULTING("창업 컨설팅", "Startup Consulting", Role.OTHER), // 직접입력 (모든 Role에서 가능) CUSTOM("직접입력", "Custom",null); diff --git a/nect-core/src/main/java/com/nect/core/repository/team/ProjectUserRepository.java b/nect-core/src/main/java/com/nect/core/repository/team/ProjectUserRepository.java index 3482a6db..6641264c 100644 --- a/nect-core/src/main/java/com/nect/core/repository/team/ProjectUserRepository.java +++ b/nect-core/src/main/java/com/nect/core/repository/team/ProjectUserRepository.java @@ -19,6 +19,7 @@ public interface ProjectUserRepository extends JpaRepository { Optional findByUserIdAndProject(Long userId, Project project); + Optional findByUserIdAndProjectId(Long userId, Long projectId); @Query(""" SELECT pu.userId @@ -287,6 +288,10 @@ Optional findActiveUserByProjectIdAndUserId( Optional findByProjectIdAndUserId(Long projectId, Long userId); + List findByUserIdAndMemberType(Long userId, ProjectMemberType memberType); + + List findByProjectIdAndRoleField(Long projectId, RoleField roleField); + boolean existsByProjectIdAndUserIdAndMemberType(Long projectId, Long userId, ProjectMemberType memberType);