Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.nect.api.domain.home.controller;

import com.nect.api.domain.home.dto.HomeHeaderResponse;
import com.nect.api.domain.home.dto.HomeMembersResponse;
import com.nect.api.domain.home.dto.HomeProjectResponse;
import com.nect.api.domain.home.dto.HomeStatisticResponse;
import com.nect.api.domain.home.dto.*;
import com.nect.api.domain.home.facade.MainHomeFacade;
import com.nect.api.domain.mypage.dto.ProfileSettingsDto;
import com.nect.api.domain.mypage.service.MypageService;
import com.nect.api.global.response.ApiResponse;
import com.nect.api.global.security.UserDetailsImpl;
import com.nect.core.entity.user.enums.InterestField;
Expand All @@ -19,6 +18,7 @@
public class HomeController {

private final MainHomeFacade mainHomeFacade;
private final MypageService mypageService;

// 모집 중인 프로젝트 조회, role, interest 필수 x
@GetMapping("/projects")
Expand All @@ -33,6 +33,15 @@ public ApiResponse<HomeProjectResponse> recruitingProjects(
return ApiResponse.ok(projects);
}

// 모집 중인 프로젝트 상세 조회
@GetMapping("/projects/{projectId}")
public ApiResponse<HomeProjectDetailResponse> recruitingProjectDetails(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long projectId
) {
return ApiResponse.ok(mainHomeFacade.getRecruitingProjectsDetails(projectId));
}

// 홈화면 프로젝트 추천
@GetMapping("/recommendations/projects")
public ApiResponse<HomeProjectResponse> recommendedProjects(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam("count") int count){
Expand All @@ -54,9 +63,19 @@ public ApiResponse<HomeMembersResponse> matchableMembers(
return ApiResponse.ok(members);
}

// 홈화면 매칭 가능한 넥터 - 세부정보
@GetMapping("/members/{userId}")
public ApiResponse<ProfileSettingsDto.ProfileSettingsResponseDto> getMemberInfo(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@PathVariable Long userId
){
return ApiResponse.ok(mypageService.getProfile(userId));
}

// 홈화면 팀원 추천
@GetMapping("/recommendations/members")
public ApiResponse<HomeMembersResponse> recommendedMembers(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam("count") int count){
public ApiResponse<HomeMembersResponse> recommendedMembers(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam("count") int count) {

Long userId = resolveUserId(userDetails);
HomeMembersResponse members = mainHomeFacade.getRecommendedMembers(userId, count);
return ApiResponse.ok(members);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class HomeMemberItem {
private String name;
private String part;
private String introduction;
private String coreCompetencies;
private String status;
private Boolean isScrapped;
private List<String> roles;
Expand All @@ -28,6 +29,7 @@ public static HomeMemberItem of(
String name,
String part,
String introduction,
String coreCompetencies,
String status,
Boolean isScrapped,
List<String> roles
Expand All @@ -38,6 +40,7 @@ public static HomeMemberItem of(
.name(name)
.part(part)
.introduction(introduction)
.coreCompetencies(coreCompetencies)
.status(status)
.isScrapped(isScrapped)
.roles(roles)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.nect.api.domain.home.dto;

import com.nect.api.domain.mypage.dto.MyProjectsResponseDto;
import lombok.*;

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class HomeProjectDetailResponse {

private MyProjectsResponseDto.ProjectInfo defaultInfo; // 기본정보
private MyProjectsResponseDto.ProjectFieldResponse fields; // 프로젝트 분야
// private stack...; // TODO 필수스택
private MyProjectsResponseDto.StringListResponse purposes; // 프로젝트 목표
private MyProjectsResponseDto.StringListResponse functions; // 주요기능
private MyProjectsResponseDto.StringListResponse serviceUsers; // 서비스 사용자
private MyProjectsResponseDto.ProjectPlanFilesResponse planFiles; // 기획 파일

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.nect.api.domain.home.service.HomeMemberQueryService;
import com.nect.api.domain.home.service.HomeProjectQueryService;
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.global.infra.S3Service;
import com.nect.core.entity.team.Project;
import com.nect.core.entity.user.User;
Expand All @@ -31,6 +33,7 @@ public class MainHomeFacade {
private final HomeMemberQueryService homeMemberQueryService;
private final HomeStatisticsQueryService statisticsQueryService;
private final S3Service s3Service;
private final MyPageProjectQueryService myPageProjectQueryService;

// 모집 중인 프로젝트
public HomeProjectResponse getRecruitingProjects(Long userId, int count, Role role, InterestField interest){
Expand All @@ -40,26 +43,40 @@ public HomeProjectResponse getRecruitingProjects(Long userId, int count, Role ro
// 페이징 정보
PageRequest pageRequest = PageRequest.of(0, safeCount);

// List<Project> 미리 생성
// List<Project> projects = new ArrayList<>();

// // 둘 중 하나가 null일 수는 없음
// if ((role == null && interest != null) || (role != null && interest == null)) {
// throw new HomeInvalidParametersException("role과 interest 중 하나만 null일 수 없습니다.");
// }
//
// // role이 null일 때
// if (role == null) {
//
// }else{
//
// }
// 둘 중 하나가 null일 수는 없음
if ((role == null && interest != null) || (role != null && interest == null)) {
throw new HomeInvalidParametersException("role과 interest 중 하나만 null일 수 없습니다.");
}

List<Project> projects = homeProjectQueryService.getProjects(userId, pageRequest);
List<Project> projects;
if (role != null) {
projects = homeProjectQueryService.getFilteredProjects(userId, pageRequest, role, interest);
} else {
projects = homeProjectQueryService.getProjects(userId, pageRequest);
}

return buildProjectResponse(projects);
}

// 모집 중인 프로젝트
public HomeProjectDetailResponse getRecruitingProjectsDetails(Long projectId) {

MyProjectsResponseDto.ProjectInfo defaultInfo = homeProjectQueryService.getProject(projectId);
MyProjectsResponseDto.ProjectFieldResponse fields = myPageProjectQueryService.getProjectFields(projectId);
MyProjectsResponseDto.StringListResponse purposes = myPageProjectQueryService.getPurposes(projectId);
MyProjectsResponseDto.StringListResponse functions = myPageProjectQueryService.getFunctions(projectId);
MyProjectsResponseDto.StringListResponse serviceUsers = myPageProjectQueryService.getServiceUsers(projectId);
MyProjectsResponseDto.ProjectPlanFilesResponse planFiles = myPageProjectQueryService.getPlanFiles(projectId);
return HomeProjectDetailResponse.builder()
.defaultInfo(defaultInfo)
.fields(fields)
.purposes(purposes)
.functions(functions)
.serviceUsers(serviceUsers)
.planFiles(planFiles)
.build();
}

// 홈화면 추천 프로젝트들
public HomeProjectResponse getRecommendedProjects(Long userId, int count) {
int safeCount = safeCount(count);
Expand Down Expand Up @@ -168,7 +185,8 @@ private List<HomeMemberItem> responsesFromMembers(List<User> users) {
s3Service.getPresignedGetUrl(user.getProfileImageName()),
user.getName(),
user.getRole() != null ? user.getRole().name() : null,
null,
user.getBio(),
user.getCoreCompetencies(),
user.getUserStatus() != null ? user.getUserStatus().name() : null,
false,
parts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
package com.nect.api.domain.home.service;

import com.nect.api.domain.mypage.dto.MyProjectsResponseDto;
import com.nect.api.domain.team.project.enums.code.ProjectErrorCode;
import com.nect.api.domain.team.project.exception.ProjectException;
import com.nect.api.global.infra.S3Service;
import com.nect.core.entity.matching.Recruitment;
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.enums.InterestField;
import com.nect.core.entity.user.enums.Role;
import com.nect.core.entity.user.enums.RoleField;
import com.nect.core.entity.team.enums.RecruitmentStatus;
import com.nect.core.entity.user.User;
import com.nect.core.repository.matching.RecruitmentRepository;
import com.nect.core.repository.team.ProjectRepository;
import com.nect.core.repository.team.ProjectTeamRoleRepository;
import com.nect.core.repository.user.ProjectUserRepositoryComplete;
import com.nect.core.repository.team.ProjectUserRepository;
import com.nect.core.repository.user.UserRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -32,6 +44,9 @@ public class HomeProjectQueryService {
private final ProjectUserRepository projectUserRepository;
private final RecruitmentRepository recruitmentRepository;
private final UserRepository userRepository;
private final ProjectUserRepositoryComplete projectUserRepositoryComplete;
private final ProjectTeamRoleRepository projectTeamRoleRepository;
private final S3Service s3Service;

public record HomeProjectBatch(
Map<Long, User> authorByProjectId,
Expand Down Expand Up @@ -113,19 +128,108 @@ public List<Project> getProjects(Long userId, PageRequest pageRequest){
: projectRepository.findHomeProjects(userId, RecruitmentStatus.OPEN, pageRequest);
}

public List<Project> getFilteredProjects(Long userId, PageRequest pageRequest, Role role, InterestField interest) {
List<RoleField> roleFields = Arrays.stream(RoleField.getFieldsByRole(role))
.filter(field -> field.getRole() == role)
.toList();

if (roleFields.isEmpty()) {
return List.of();
}

return projectRepository.findHomeProjectsByRoleAndInterest(
userId,
RecruitmentStatus.OPEN,
interest,
roleFields,
pageRequest
);
}

public List<Project> getProjects(Long userId) {
return (userId == null)
? projectRepository.findHomeProjectsWithoutUser(RecruitmentStatus.OPEN)
: projectRepository.findHomeProjects(userId, RecruitmentStatus.OPEN);
}

public MyProjectsResponseDto.ProjectInfo getProject(Long projectId) {
if (projectId == null) {
throw new ProjectException(ProjectErrorCode.INVALID_REQUEST, "projectId is required");
}

Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectException(ProjectErrorCode.PROJECT_NOT_FOUND));

List<ProjectTeamRole> teamRoles = projectTeamRoleRepository.findByProjectId(projectId);
List<MyProjectsResponseDto.TeamRoleInfo> roleInfos = teamRoles.stream()
.map(role -> MyProjectsResponseDto.TeamRoleInfo.builder()
.roleField(role.getRoleField())
.requiredCount(role.getRequiredCount())
.build())
.toList();

MyProjectsResponseDto.LeaderInfo leaderInfo = projectUserRepositoryComplete
.findByProjectIdAndMemberType(projectId, ProjectMemberType.LEADER)
.map(ProjectUser::getUserId)
.flatMap(userRepository::findById)
.map(leader -> MyProjectsResponseDto.LeaderInfo.builder()
.userId(leader.getUserId())
.name(leader.getName())
.profileImageUrl(s3Service.getPresignedGetUrl(leader.getProfileImageName()))
.build())
.orElse(null);

List<ProjectUser> activeMembers = projectUserRepositoryComplete
.findByProjectIdAndMemberStatus(projectId, ProjectMemberStatus.ACTIVE);
List<MyProjectsResponseDto.TeamMemberProjectInfo> teamMemberProjects =
getTeamMemberProjectsByProject(activeMembers, projectId);

return MyProjectsResponseDto.ProjectInfo.builder()
.projectId(projectId)
.projectTitle(project.getTitle())
.description(project.getDescription())
.imageName(s3Service.getPresignedGetUrl(project.getImageName()))
.plannedStartedOn(project.getPlannedStartedOn())
.plannedEndedOn(project.getPlannedEndedOn())
.teamRoles(roleInfos)
.leader(leaderInfo)
.teamMemberProjects(teamMemberProjects)
.build();
}

public Integer getDDay(Project project) {
LocalDateTime endedAt = project.getEndedAt();
LocalDate today = LocalDate.now();
LocalDate endDate = endedAt.toLocalDate();
return (int) ChronoUnit.DAYS.between(today, endDate);
}
}

private List<MyProjectsResponseDto.TeamMemberProjectInfo> getTeamMemberProjectsByProject(List<ProjectUser> activeMembers, Long projectId) {

List<Long> teamMemberIds = activeMembers.stream()
.map(ProjectUser::getUserId)
.distinct()
.toList();

if (teamMemberIds.isEmpty()) {
return List.of();
}

List<ProjectUser> teamMemberProjects = projectUserRepositoryComplete
.findByUserIdInAndMemberStatus(teamMemberIds, ProjectMemberStatus.ACTIVE);

return teamMemberProjects.stream()
.map(ProjectUser::getProject)
.filter(project -> !project.getId().equals(projectId))
.distinct()
.map(project -> MyProjectsResponseDto.TeamMemberProjectInfo.builder()
.projectId(project.getId())
.title(project.getTitle())
.description(project.getDescription())
.imageName(s3Service.getPresignedGetUrl(project.getImageName()))
.createdAt(project.getCreatedAt())
.endedAt(project.getEndedAt())
.build())
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public ApiResponse<MyProjectsResponseDto.StringListResponse> getFunctions(
@PathVariable Long projectId,
@AuthenticationPrincipal UserDetailsImpl userDetails
) {
return ApiResponse.ok(projectQueryService.getPurposes(projectId));
return ApiResponse.ok(projectQueryService.getFunctions(projectId));
}

// 주요기능 작성
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ public MyProjectsResponseDto.StringListResponse getPurposes(Long projectId) {
return new MyProjectsResponseDto.StringListResponse(projectId, slicedPurpose);
}

public MyProjectsResponseDto.StringListResponse getFunctions(Long projectId) {
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectException(ProjectErrorCode.PROJECT_NOT_FOUND));
String functions = project.getMainFunctions();
List<String> slicedPurpose = projectListConverter.convertToEntityAttribute(functions);
return new MyProjectsResponseDto.StringListResponse(projectId, slicedPurpose);
}

public MyProjectsResponseDto.StringListResponse getServiceUsers(Long projectId) {
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new ProjectException(ProjectErrorCode.PROJECT_NOT_FOUND));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@


public class FileValidator {
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024;
private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
private static final List<String> ALLOWED_IMAGE_TYPES = Arrays.asList(
"image/jpeg",
"image/jpg",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
public final class FileUploadValidator {
private static final long MB = 1024L * 1024L;

private static final long MAX_5MB = 5L * MB;
private static final long MAX_10MB = 10L * MB;
private static final long MAX_20MB = 20L * MB;

private static final Set<FileExt> LIMIT_5MB = EnumSet.of(FileExt.JPG, FileExt.PNG, FileExt.SVG);
private static final Set<FileExt> LIMIT_10MB = EnumSet.of(FileExt.JPG, FileExt.PNG, FileExt.SVG);
private static final Set<FileExt> LIMIT_20MB = EnumSet.of(FileExt.PDF, FileExt.DOCS, FileExt.PPTX, FileExt.FIG, FileExt.ZIP);

private FileUploadValidator() {
Expand All @@ -33,8 +33,8 @@ public static void validateNotEmpty(MultipartFile file) {
public static void validateSizeOrThrow(FileExt ext, long fileSize) {
long max;

if (LIMIT_5MB.contains(ext)) {
max = MAX_5MB;
if (LIMIT_10MB.contains(ext)) {
max = MAX_10MB;
} else if (LIMIT_20MB.contains(ext)) {
max = MAX_20MB;
} else {
Expand Down
Loading