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,11 +1,97 @@
package com.slatto.domain.project.controller;

import com.slatto.domain.project.dto.ProjectCreateRequest;
import com.slatto.domain.project.dto.ProjectDetailResponse;
import com.slatto.domain.project.dto.ProjectListResponse;
import com.slatto.domain.project.dto.ProjectResponse;
import com.slatto.domain.project.dto.ProjectUpdateRequest;
import com.slatto.domain.project.enums.ProjectStatus;
import com.slatto.domain.project.service.ProjectService;
import com.slatto.global.response.ApiResponse;
import com.slatto.global.response.code.CommonSuccessCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Project", description = "프로젝트 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/projects")
public class ProjectController {

private static final String CURRENT_USER_ID_HEADER = "X-USER-ID";

private final ProjectService projectService;

@Operation(summary = "프로젝트 목록 조회")
@GetMapping
public ApiResponse<ProjectListResponse> getProjects(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@RequestParam(required = false) ProjectStatus status,
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "20") int size
) {
ProjectListResponse response = projectService.getProjects(currentUserId, status, cursor, size);

return ApiResponse.success(CommonSuccessCode.OK, response);
}

@Operation(summary = "프로젝트 생성")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<ProjectResponse> createProject(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@Valid @RequestBody ProjectCreateRequest request
) {
ProjectResponse response = projectService.createProject(currentUserId, request);

return ApiResponse.success(CommonSuccessCode.CREATED, response);
}

@Operation(summary = "프로젝트 상세 조회")
@GetMapping("/{projectId}")
public ApiResponse<ProjectDetailResponse> getProject(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@PathVariable Long projectId
) {
ProjectDetailResponse response = projectService.getProject(projectId, currentUserId);

return ApiResponse.success(CommonSuccessCode.OK, response);
}

@Operation(summary = "프로젝트 수정")
@PatchMapping("/{projectId}")
public ApiResponse<ProjectResponse> updateProject(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@PathVariable Long projectId,
@Valid @RequestBody ProjectUpdateRequest request
) {
ProjectResponse response = projectService.updateProject(projectId, currentUserId, request);

return ApiResponse.success(CommonSuccessCode.OK, response);
}

@Operation(summary = "프로젝트 삭제")
@DeleteMapping("/{projectId}")
public ApiResponse<Void> deleteProject(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@PathVariable Long projectId
) {
projectService.deleteProject(projectId, currentUserId);

return ApiResponse.success(CommonSuccessCode.OK, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package com.slatto.domain.project.converter;

import com.slatto.domain.project.dto.ProjectCreateRequest;
import com.slatto.domain.project.dto.ProjectDetailResponse;
import com.slatto.domain.project.dto.ProjectListResponse;
import com.slatto.domain.project.dto.ProjectResponse;
import com.slatto.domain.project.entity.Project;
import com.slatto.domain.project.entity.ProjectMember;
import com.slatto.domain.user.entity.Users;
import com.slatto.domain.user.enums.RoleName;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;

@Component
public class ProjectConverter {

private static final int MIN_PROGRESS_PERCENT = 0;
private static final int MAX_PROGRESS_PERCENT = 100;

public Project toProject(Users ownerUser, ProjectCreateRequest request) {
return Project.create(
ownerUser,
request.getTitle(),
request.getType(),
request.getCustomTypeName(),
request.getLengthType(),
request.getDescription(),
request.getEndDate(),
request.getClientName(),
request.getKind()
);
}

public ProjectResponse toResponse(Project project) {
return ProjectResponse.builder()
.id(project.getId())
.title(project.getTitle())
.status(project.getStatus())
.createdAt(project.getCreatedAt())
.updatedAt(project.getUpdatedAt())
.build();
}

public ProjectListResponse toListResponse(
List<ProjectListResponse.ProjectSummary> items,
Long nextCursor,
Boolean hasNext
) {
return ProjectListResponse.builder()
.items(items)
.nextCursor(nextCursor)
.hasNext(hasNext)
.build();
}

public ProjectListResponse.ProjectSummary toSummary(
Project project,
Long memberCount,
List<String> memberPreviewImageUrls,
LocalDateTime lastActivityAt
) {
return ProjectListResponse.ProjectSummary.builder()
.id(project.getId())
.title(project.getTitle())
.type(project.getType())
.customTypeName(project.getCustomTypeName())
.lengthType(project.getLengthType())
.status(project.getStatus())
.kind(project.getKind())
.startDate(project.getStartDate())
.endDate(project.getEndDate())
.deadlineProgressPercent(calculateDeadlineProgressPercent(project.getStartDate(), project.getEndDate()))
.lastActivityAt(lastActivityAt)
.memberPreviewImageUrls(memberPreviewImageUrls)
.memberCount(memberCount)
.createdAt(project.getCreatedAt())
.updatedAt(project.getUpdatedAt())
.build();
}

public ProjectDetailResponse toDetailResponse(
Project project,
ProjectMember currentMember,
List<RoleName> myRoles,
Long memberCount
) {
boolean admin = currentMember.isAdmin();

return ProjectDetailResponse.builder()
.id(project.getId())
.title(project.getTitle())
.type(project.getType())
.customTypeName(project.getCustomTypeName())
.lengthType(project.getLengthType())
.description(project.getDescription())
.startDate(project.getStartDate())
.endDate(project.getEndDate())
.clientName(project.getClientName())
.status(project.getStatus())
.kind(project.getKind())
.owner(toOwnerSummary(project.getOwnerUser()))
.myPermission(currentMember.getPermission())
.myRoles(myRoles)
.memberCount(memberCount)
.canEdit(admin)
.canDelete(admin)
.createdAt(project.getCreatedAt())
.updatedAt(project.getUpdatedAt())
.build();
}

private ProjectDetailResponse.OwnerSummary toOwnerSummary(Users ownerUser) {
return ProjectDetailResponse.OwnerSummary.builder()
.id(ownerUser.getId())
.nickname(ownerUser.getNickname())
.profileImageUrl(ownerUser.getProfileImageUrl())
.build();
}

private int calculateDeadlineProgressPercent(LocalDate startDate, LocalDate endDate) {
long totalDays = ChronoUnit.DAYS.between(startDate, endDate);
if (totalDays <= 0) {
return MAX_PROGRESS_PERCENT;
}

long elapsedDays = ChronoUnit.DAYS.between(startDate, LocalDate.now());
long boundedElapsedDays = Math.max(0, Math.min(elapsedDays, totalDays));
long progressPercent = boundedElapsedDays * MAX_PROGRESS_PERCENT / totalDays;

return Math.max(MIN_PROGRESS_PERCENT, Math.min((int) progressPercent, MAX_PROGRESS_PERCENT));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,48 @@
package com.slatto.domain.project.dto;

import com.slatto.domain.project.enums.LengthType;
import com.slatto.domain.user.enums.CategoryName;
import com.slatto.domain.user.enums.Kind;
import com.slatto.domain.user.enums.RoleName;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDate;
import java.util.List;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ProjectCreateRequest {

@NotBlank(message = "프로젝트명은 필수입니다.")
@Size(max = 255, message = "프로젝트명은 최대 255자까지 입력할 수 있습니다.")
private String title;

@NotNull(message = "프로젝트 유형은 필수입니다.")
private CategoryName type;

@Size(max = 100, message = "직접 입력한 프로젝트 유형은 최대 100자까지 입력할 수 있습니다.")
private String customTypeName;

@NotNull(message = "영상 길이는 필수입니다.")
private LengthType lengthType;

@NotBlank(message = "프로젝트 설명은 필수입니다.")
private String description;

@NotNull(message = "프로젝트 마감일은 필수입니다.")
private LocalDate endDate;

@Size(max = 255, message = "클라이언트명은 최대 255자까지 입력할 수 있습니다.")
private String clientName;

private Kind kind;

@NotEmpty(message = "프로젝트에서의 역할은 1개 이상 선택해야 합니다.")
private List<RoleName> roleNames;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.slatto.domain.project.dto;

import com.slatto.domain.project.enums.LengthType;
import com.slatto.domain.project.enums.Permission;
import com.slatto.domain.project.enums.ProjectStatus;
import com.slatto.domain.user.enums.CategoryName;
import com.slatto.domain.user.enums.Kind;
import com.slatto.domain.user.enums.RoleName;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

@Getter
@Builder
public class ProjectDetailResponse {

private Long id;

private String title;

private CategoryName type;

private String customTypeName;

private LengthType lengthType;

private String description;

private LocalDate startDate;

private LocalDate endDate;

private String clientName;

private ProjectStatus status;

private Kind kind;

private OwnerSummary owner;

private Permission myPermission;

private List<RoleName> myRoles;

private Long memberCount;

private Boolean canEdit;

private Boolean canDelete;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;

@Getter
@Builder
public static class OwnerSummary {

private Long id;

private String nickname;

private String profileImageUrl;
}
}
Loading
Loading