Skip to content

Commit

Permalink
[FEAT] Project API 구현 완료 (#88)
Browse files Browse the repository at this point in the history
* feat: create controller, repository, request dto, service file

* feat: add FileRepository

* chore: add AllArgsConstructor in Member Entity

* chore: add AllArgsConstructor and cascade option in Project Entity

* feat: add MemberRequest Dto

* feat: add ProjectRequest Dto

* feat: add createProject controller

* feat: add createProject Service

* feat: add getProjects controller

* feat: add getProjects repository

* feat: add getProjects response dto

* feat: add getProjects service

* feat: change builder to of

* feat: pageable 오타 수정

* chore: change studentNames, professorNames type and name

* chore: change studentNames, professorNames type and name

* chore: change thumbnail type

* add: project 도메인 관련 에러 코드 추가

* add: memberRequeset dto 객체 작성

* add: projectRequeset dto 객체 작성

* add: projectDetailResponse dto 객체 작성

* feat: projectService 코드 작성

* feat: projectController 작성

* add: project domain entity cascade 옵션 추가

* add: createProject, getProject, updateProject, deleteProject restdocs 코드 작성

* chore: @PathVariable("projectId")로 변경

* chore: @transactional 일괄적으로 적용

* chore: file table에 고아객체 삭제 옵션 추가

* add: MemberRequest 객체 검증 로직 작성

* add: 인증 설정 관련 ToDo 작성

* add: ProjectRequest validation 로직 수정

* add: ProjectService @transactional(readOnly = true) 추가

* add: adoc 파일 추가해서 rest docs 생성

* fix: ProjectRequest에서 MemberRequest Validation 로직 코드 수정

* fix: import 정리

* fix: member type nullable 타입으로 변경

* add: getProjects test 추가

* fix: File entity를 FileResponse Dto로 변경

* add: git cheery-pick으로 auth 관련 부분 가져오기

* add: git cheery-pick으로 auth 관련 부분 가져오기

* feat: createProjectFavorite, deleteProjectFavorite api 구현

* feat: createProjectLike, deleteProjectLike api 구현

* feat: createProjectComment, deleteProjectComment api 구현

* add: likes api 사용기간 설정을 위한 EventPeriodRepository 생성

* fix: user 이중 검증되는 부분 수정

* add: favorite, like, comment project api (6개) restdocs 작성

* fix: restdocs 수정

* chore: 주석 제거

* chore: techStackList

* chore: ExceptionCode 좋아요 message 변경

* chore: deleteProjectLike Exception 변경

* chore: 현재 eventPeriod 찾는 로직 변경

* feat: getProject 반환 json에 댓글 추가

* add: getProject, getProjects 로그인 유저 좋아요 및 북마크 json 맴버에 추

* add: createProject, updateProject, deleteProject @authuser 추가

* add: FileResponse id 추가

* add: restdocs 정보 수정

* chore: JsonFieldType class -> OBJECT 변경

* [FEAT] Application 관련 API 개발 완료 (#69)

* chore: domain/project -> project로 폴더 이동

* chore: domain/project -> project로 폴더 이동

* chore: ProjectControllerTest 폴더 구조 변경

* chore: getAwardProjects API 구현

* chore: findByYear type Optional로 변경

* chore: like, favoriteProject 동시성 문제 해결을 위한 {userId, projectId} unique 설정 및 DataIntegrityViolationException 예외 처리

* add: getAwardProjects restdocs 추가

* fix: ProjectResponse, ProjectDetailResponse 중복 제거

* fix: getProject를 다른 Service 메서드에서 사용하는 코드 제거

* chore: 코드 사이 공백 수정 및 설명 주석 추가

* chore: createProject, updateProject when 인자 수정

* merge: develop branch 재처리

* fix: getProjects, getProject, getAwardProjects user OPTIONAL 처리

* add: index.adoc에 project.adoc추가 및 --- 지우기

* add: pageable queryParameters 추가

* fix: 오타 수정

* fix: createProjectLike, createProjectFavorite 지연 로딩 관련 에러 수정

* fix: user null인 경우 예외처리

* fix: userRepository.save(user)로 영속성 컨텍스트에 user 로드

* chore: 상단에 --- 추가

---------

Co-authored-by: chanyeong <[email protected]>
Co-authored-by: yesjuhee <[email protected]>
  • Loading branch information
3 people authored Sep 4, 2024
1 parent 50d824d commit b2b310b
Show file tree
Hide file tree
Showing 34 changed files with 1,812 additions and 58 deletions.
3 changes: 2 additions & 1 deletion src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ include::notice.adoc[]
include::eventNotice.adoc[]
include::eventPeriod.adoc[]
include::gallery.adoc[]
include::application.adoc[]
include::application.adoc[]
include::project.adoc[]
75 changes: 75 additions & 0 deletions src/docs/asciidoc/project.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
== 프로젝트 API
:source-highlighter: highlightjs

---
=== 프로젝트 조회 (GET /projects)
==== 200 OK
====
operation::project-controller-test/get-projects[snippets="http-request,http-response,query-parameters,response-fields"]
====

=== 프로젝트 생성 (POST /projects)
==== 201 Created
====
operation::project-controller-test/create-project[snippets="http-request,http-response,request-fields,response-fields"]
====

=== 프로젝트 조회 (GET /projects/{projectId})
==== 200 OK
====
operation::project-controller-test/get-project[snippets="http-request,http-response,path-parameters,response-fields"]
====

=== 프로젝트 수정 (PUT /projects/{projectId})
==== 200 OK
====
operation::project-controller-test/update-project[snippets="http-request,http-response,path-parameters,request-fields,response-fields"]
====

=== 프로젝트 삭제 (DELETE /projects/{projectId})
==== 204 No Content
====
operation::project-controller-test/delete-project[snippets="http-request,http-response,path-parameters"]
====

=== 관심 프로젝트 등록 (POST /projects/{projectId}/favorite)
==== 201 Created
====
operation::project-controller-test/create-project-favorite[snippets="http-request,http-response,path-parameters"]
====

=== 관심 프로젝트 삭제 (DELETE /projects/{projectId}/favorite)
==== 204 No Content
====
operation::project-controller-test/delete-project-favorite[snippets="http-request,http-response,path-parameters"]
====

=== 프로젝트 좋아요 등록 (POST /projects/{projectId}/like)
==== 201 Created
====
operation::project-controller-test/create-project-like[snippets="http-request,http-response,path-parameters"]
====

=== 프로젝트 좋아요 삭제 (DELETE /projects/{projectId}/like)
==== 204 No Content
====
operation::project-controller-test/delete-project-like[snippets="http-request,http-response,path-parameters"]
====

=== 프로젝트 댓글 등록 (POST /projects/{projectId}/comment)
==== 201 Created
====
operation::project-controller-test/create-project-comment[snippets="http-request,http-response,path-parameters,request-fields,response-fields"]
====

=== 프로젝트 댓글 삭제 (DELETE /projects/{projectId}/comment)
==== 204 No Content
====
operation::project-controller-test/delete-project-comment[snippets="http-request,http-response,path-parameters"]
====

=== 수상 프로젝트 조회 (GET /projects/award?year={year})
==== 200 No Content
====
operation::project-controller-test/get-award-projects[snippets="http-request,http-response,query-parameters,response-fields"]
====
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import com.scg.stop.event.domain.EventPeriod;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface EventPeriodRepository extends JpaRepository<EventPeriod, Long> {

Boolean existsByYear(Integer year);
EventPeriod findByYear(Integer year);
Optional<EventPeriod> findByYear(Integer year);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public EventPeriodResponse createEventPeriod(EventPeriodRequest request) {
@Transactional(readOnly = true)
public EventPeriodResponse getEventPeriod() {
int currentYear = LocalDateTime.now().getYear();
EventPeriod eventPeriod = eventPeriodRepository.findByYear(currentYear);
EventPeriod eventPeriod = eventPeriodRepository.findByYear(currentYear)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_EVENT_PERIOD));
return EventPeriodResponse.from(eventPeriod);
}

Expand All @@ -52,10 +53,8 @@ public List<EventPeriodResponse> getEventPeriods() {

public EventPeriodResponse updateEventPeriod(EventPeriodRequest request) {
int currentYear = LocalDateTime.now().getYear();
EventPeriod currentEventPeriod = eventPeriodRepository.findByYear(currentYear);
if (currentEventPeriod == null) {
throw new BadRequestException(NOT_FOUND_EVENT_PERIOD);
}
EventPeriod currentEventPeriod = eventPeriodRepository.findByYear(currentYear)
.orElseThrow(() -> new BadRequestException(NOT_FOUND_EVENT_PERIOD));
currentEventPeriod.update(request.getStart(), request.getEnd());
return EventPeriodResponse.from(currentEventPeriod);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import com.scg.stop.file.domain.File;
import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

public interface FileRepository extends JpaRepository<File, Long> {

Optional<File> findById(Long id);
List<File> findByIdIn(List<Long> ids);
}
13 changes: 13 additions & 0 deletions src/main/java/com/scg/stop/global/exception/ExceptionCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,19 @@ public enum ExceptionCode {
NOT_FOUND_APPLICATION_ID(4010, "ID에 해당하는 인증 신청 정보가 존재하지 않습니다."),
ALREADY_VERIFIED_USER(4011, "이미 인증 된 회원입니다."),

// project domain
NOT_FOUND_PROJECT(77000, "프로젝트를 찾을 수 없습니다."),
NOT_FOUND_PROJECT_THUMBNAIL(77001, "프로젝트 썸네일을 찾을 수 없습니다"),
NOT_FOUND_PROJECT_POSTER(77002, "프로젝트 포스터를 찾을 수 없습니다"),
INVALID_MEMBER(77003, "멤버 정보가 올바르지 않습니다."),
INVALID_TECHSTACK(77004, "기술 스택 정보가 올바르지 않습니다."),
ALREADY_FAVORITE_PROJECT(77005, "관심 표시한 프로젝트가 이미 존재합니다"),
NOT_FOUND_FAVORITE_PROJECT(77007, "관심 표시한 프로젝트를 찾을 수 없습니다."),
ALREADY_LIKE_PROJECT(77008, "이미 좋아요 한 프로젝트입니다."),
NOT_FOUND_LIKE_PROJECT(77009, "좋아요 표시한 프로젝트가 존재하지 않습니다"),
NOT_FOUND_COMMENT(77010, "댓글을 찾을 수 없습니다"),
NOT_MATCH_USER(77011, "유저 정보가 일치하지 않습니다"),

// file domain
FAILED_TO_UPLOAD_FILE(5000, "파일 업로드를 실패했습니다."),
FAILED_TO_GET_FILE(5001, "파일 가져오기를 실패했습니다."),
Expand Down
144 changes: 144 additions & 0 deletions src/main/java/com/scg/stop/project/controller/ProjectController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.scg.stop.project.controller;


import com.scg.stop.auth.annotation.AuthUser;
import com.scg.stop.project.domain.ProjectCategory;
import com.scg.stop.project.dto.request.CommentRequest;
import com.scg.stop.project.dto.request.ProjectRequest;
import com.scg.stop.project.dto.response.CommentResponse;
import com.scg.stop.project.dto.response.ProjectDetailResponse;
import com.scg.stop.project.dto.response.ProjectResponse;
import com.scg.stop.project.service.ProjectService;
import com.scg.stop.user.domain.AccessType;
import com.scg.stop.user.domain.User;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/projects")
public class ProjectController {

private final ProjectService projectService;

@GetMapping
public ResponseEntity<Page<ProjectResponse>> getProjects(
@RequestParam(value = "title", required = false) String title,
@RequestParam(value = "year", required = false) Integer year,
@RequestParam(value = "category", required = false) ProjectCategory category,
@PageableDefault(page = 0, size = 10) Pageable pageable,
@AuthUser(accessType = {AccessType.OPTIONAL}) User user
){
Page<ProjectResponse> pageProjectResponse = projectService.getProjects(title, year, category, pageable, user);
return ResponseEntity.status(HttpStatus.OK).body(pageProjectResponse);
}

@PostMapping
public ResponseEntity<ProjectDetailResponse> createProject(
@RequestBody @Valid ProjectRequest projectRequest,
@AuthUser(accessType = {AccessType.ADMIN}) User user
) {
ProjectDetailResponse projectDetailResponse = projectService.createProject(projectRequest, user);
return ResponseEntity.status(HttpStatus.CREATED).body(projectDetailResponse);
}

@GetMapping("/{projectId}")
public ResponseEntity<ProjectDetailResponse> getProject(
@PathVariable("projectId") Long projectId,
@AuthUser(accessType = {AccessType.OPTIONAL}) User user
) {
ProjectDetailResponse projectDetailResponse = projectService.getProject(projectId, user);
return ResponseEntity.status(HttpStatus.OK).body(projectDetailResponse);
}

@PutMapping("/{projectId}")
public ResponseEntity<ProjectDetailResponse> updateProject(
@PathVariable("projectId") Long projectId,
@RequestBody @Valid ProjectRequest projectRequest,
@AuthUser(accessType = {AccessType.ADMIN}) User user
) {
ProjectDetailResponse projectDetailResponse = projectService.updateProject(projectId, projectRequest, user);
return ResponseEntity.status(HttpStatus.OK).body(projectDetailResponse);
}

@DeleteMapping("/{projectId}")
public ResponseEntity<Void> deleteProject(
@PathVariable("projectId") Long projectId,
@AuthUser(accessType = {AccessType.ADMIN}) User user
) {
projectService.deleteProject(projectId);
return ResponseEntity.noContent().build();
}

@PostMapping("/{projectId}/favorite")
public ResponseEntity<Void> createProjectFavorite(
@PathVariable("projectId") Long projectId,
@AuthUser(accessType = {AccessType.ALL}) User user
){
projectService.createProjectFavorite(projectId, user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@DeleteMapping("/{projectId}/favorite")
public ResponseEntity<Void> deleteProjectFavorite(
@PathVariable("projectId") Long projectId,
@AuthUser(accessType = {AccessType.ALL}) User user
){
projectService.deleteProjectFavorite(projectId, user);
return ResponseEntity.noContent().build();
}

@PostMapping("/{projectId}/like")
public ResponseEntity<Void> createProjectLike(
@PathVariable("projectId") Long projectId,
@AuthUser(accessType = {AccessType.ALL}) User user
){
projectService.createProjectLike(projectId, user);
return ResponseEntity.status(HttpStatus.CREATED).build();
}

@DeleteMapping("/{projectId}/like")
public ResponseEntity<Void> deleteProjectLike(
@PathVariable("projectId") Long projectId,
@AuthUser(accessType = {AccessType.ALL}) User user
){
projectService.deleteProjectLike(projectId, user);
return ResponseEntity.noContent().build();
}

@PostMapping("/{projectId}/comment")
public ResponseEntity<CommentResponse> createProjectComment(
@PathVariable("projectId") Long projectId,
@RequestBody @Valid CommentRequest commentRequest,
@AuthUser(accessType = {AccessType.ALL}) User user
){
CommentResponse commentResponse = projectService.createProjectComment(projectId, user, commentRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(commentResponse);
}

@DeleteMapping("/{projectId}/comment/{commentId}")
public ResponseEntity<Void> deleteProjectComment(
@PathVariable("projectId") Long projectId,
@PathVariable("commentId") Long commentId,
@AuthUser(accessType = {AccessType.ALL}) User user
){
projectService.deleteProjectComment(projectId, commentId, user);
return ResponseEntity.noContent().build();
}

@GetMapping("/award")
public ResponseEntity<Page<ProjectResponse>> getAwardProjects(
@RequestParam(value = "year", required = true) Integer year,
@PageableDefault(page = 0, size = 10) Pageable pageable,
@AuthUser(accessType = {AccessType.OPTIONAL}) User user
){
Page<ProjectResponse> pageProjectResponse = projectService.getAwardProjects(year, pageable, user);
return ResponseEntity.status(HttpStatus.OK).body(pageProjectResponse);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.scg.stop.domain.project.domain;
package com.scg.stop.project.domain;

public enum AwardStatus {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.scg.stop.domain.project.domain;
package com.scg.stop.project.domain;

import static jakarta.persistence.FetchType.LAZY;
import static jakarta.persistence.GenerationType.IDENTITY;
Expand All @@ -12,11 +12,13 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = PROTECTED)
public class Comment extends BaseTimeEntity {

Expand All @@ -28,7 +30,7 @@ public class Comment extends BaseTimeEntity {
private String content;

@Column(nullable = false, columnDefinition = "TINYINT(1)")
private boolean isAnonymous;
private Boolean isAnonymous;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "project_id")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package com.scg.stop.domain.project.domain;
package com.scg.stop.project.domain;

import static jakarta.persistence.FetchType.LAZY;
import static jakarta.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

import com.scg.stop.user.domain.User;
import com.scg.stop.global.domain.BaseTimeEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Getter
@AllArgsConstructor
@NoArgsConstructor(access = PROTECTED)
@Table(
name = "favorite_projects",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"project_id", "user_id"})
}
)
public class FavoriteProject extends BaseTimeEntity {

@Id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.scg.stop.domain.project.domain;
package com.scg.stop.project.domain;

import static jakarta.persistence.FetchType.LAZY;
import static jakarta.persistence.GenerationType.IDENTITY;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.scg.stop.domain.project.domain;
package com.scg.stop.project.domain;

import static jakarta.persistence.FetchType.LAZY;
import static jakarta.persistence.GenerationType.IDENTITY;
Expand Down
Loading

0 comments on commit b2b310b

Please sign in to comment.