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

import com.slatto.domain.project.dto.ProjectNoticeCreateRequest;
import com.slatto.domain.project.dto.ProjectNoticeListResponse;
import com.slatto.domain.project.dto.ProjectNoticeResponse;
import com.slatto.domain.project.dto.ProjectNoticeUpdateRequest;
import com.slatto.domain.project.service.ProjectNoticeService;
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 Notice", description = "프로젝트 공지 API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/projects/{projectId}/notices")
public class ProjectNoticeController {

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

private final ProjectNoticeService projectNoticeService;

@Operation(summary = "프로젝트 공지 목록 조회")
@GetMapping
public ApiResponse<ProjectNoticeListResponse> getProjectNotices(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@PathVariable Long projectId,
@RequestParam(required = false) Long cursor,
@RequestParam(defaultValue = "20") int size
) {
ProjectNoticeListResponse response = projectNoticeService.getProjectNotices(
projectId,
currentUserId,
cursor,
size
);

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

@Operation(summary = "프로젝트 공지 등록")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<ProjectNoticeResponse> createProjectNotice(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@PathVariable Long projectId,
@Valid @RequestBody ProjectNoticeCreateRequest request
) {
ProjectNoticeResponse response = projectNoticeService.createProjectNotice(
projectId,
currentUserId,
request
);

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

@Operation(summary = "프로젝트 공지 수정")
@PatchMapping("/{noticeId}")
public ApiResponse<ProjectNoticeResponse> updateProjectNotice(
@RequestHeader(CURRENT_USER_ID_HEADER) Long currentUserId,
@PathVariable Long projectId,
@PathVariable Long noticeId,
@Valid @RequestBody ProjectNoticeUpdateRequest request
) {
ProjectNoticeResponse response = projectNoticeService.updateProjectNotice(
projectId,
noticeId,
currentUserId,
request
);

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

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

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

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@NotBlank(message = "공지 제목은 필수입니다.")
@Size(max = 100, message = "공지 제목은 최대 100자까지 입력할 수 있습니다.")
private String title;

@NotBlank(message = "공지 내용은 필수입니다.")
private String content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.slatto.domain.project.dto;

import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
public class ProjectNoticeListResponse {

private List<ProjectNoticeResponse> items;

private Long nextCursor;

private Boolean hasNext;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.slatto.domain.project.dto;

import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@Builder
public class ProjectNoticeResponse {

private Long id;

private String title;

private String content;

private WriterSummary writer;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;

@Getter
@Builder
public static class WriterSummary {

private Long id;

private String nickname;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.slatto.domain.project.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

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

@NotBlank(message = "공지 제목은 필수입니다.")
@Size(max = 100, message = "공지 제목은 최대 100자까지 입력할 수 있습니다.")
private String title;

@NotBlank(message = "공지 내용은 필수입니다.")
private String content;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class ProjectNotice extends BaseEntity {
@JoinColumn(name = "writer_id", nullable = false)
private Users writer;

@Column(name = "title", nullable = false, length = 255)
@Column(name = "title", nullable = false, length = 100)
private String title;

@Column(name = "content", nullable = false, columnDefinition = "TEXT")
Expand All @@ -37,4 +37,27 @@ public class ProjectNotice extends BaseEntity {
@Column(name = "deleted_at", nullable = true)
private LocalDateTime deletedAt;

private ProjectNotice(Project project, Users writer, String title, String content) {
this.project = project;
this.writer = writer;
this.title = title;
this.content = content;
}

public static ProjectNotice create(Project project, Users writer, String title, String content) {
return new ProjectNotice(project, writer, title, content);
}

public boolean isWrittenBy(Long userId) {
return writer.getId().equals(userId);
}

public void update(String title, String content) {
this.title = title;
this.content = content;
}

public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum ProjectErrorCode implements BaseCode {
INVALID_PROJECT_PERIOD(HttpStatus.BAD_REQUEST, "PROJECT400", "프로젝트 마감일은 시작일보다 이전일 수 없습니다."),
PROJECT_NOT_FOUND(HttpStatus.NOT_FOUND, "PROJECT404", "프로젝트를 찾을 수 없습니다."),
PROJECT_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "PROJECT_MEMBER404", "프로젝트 멤버를 찾을 수 없습니다."),
PROJECT_NOTICE_NOT_FOUND(HttpStatus.NOT_FOUND, "PROJECT_NOTICE404", "프로젝트 공지를 찾을 수 없습니다."),
PROJECT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "PROJECT403", "프로젝트 접근 권한이 없습니다."),
PROJECT_ADMIN_REQUIRED(HttpStatus.FORBIDDEN, "PROJECT_ADMIN403", "프로젝트 관리자 권한이 필요합니다."),
PROJECT_LIMIT_EXCEEDED(HttpStatus.CONFLICT, "PROJECT409", "무료 계정은 최대 5개의 프로젝트를 생성할 수 있습니다.");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.slatto.domain.project.repository;

import com.slatto.domain.project.entity.ProjectNotice;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface ProjectNoticeRepository extends JpaRepository<ProjectNotice, Long> {

@Query("""
select pn
from ProjectNotice pn
join fetch pn.writer w
where pn.project.id = :projectId
and pn.deletedAt is null
and (:cursor is null or pn.id < :cursor)
order by pn.id desc
""")
List<ProjectNotice> findActiveNoticesByCursor(
@Param("projectId") Long projectId,
@Param("cursor") Long cursor,
Pageable pageable
);

@Query("""
select pn
from ProjectNotice pn
join fetch pn.writer w
where pn.project.id = :projectId
and pn.id = :noticeId
and pn.deletedAt is null
""")
Optional<ProjectNotice> findActiveNoticeByProjectIdAndNoticeId(
@Param("projectId") Long projectId,
@Param("noticeId") Long noticeId
);
}
Loading
Loading