From ea4c486e653d0be561f5eaae499e1bf14f2ddbeb Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 19 Dec 2024 21:07:40 +0900 Subject: [PATCH 01/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4,=20?= =?UTF-8?q?=ED=95=B4=EC=BB=A4=ED=86=A4=ED=8C=80,=20=ED=95=B4=EC=BB=A4?= =?UTF-8?q?=ED=86=A4=20=ED=8C=80=EC=9B=90=20=EA=B4=80=EB=A0=A8=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/main/resources/schema.sql | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 91f3297b..23e5eb2d 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -6,6 +6,9 @@ drop table if exists sw_css.college; drop table if exists sw_css.milestone; drop table if exists sw_css.milestone_category; drop table if exists sw_css.milestone_history; +drop table if exists sw_css.hackathon; +drop table if exists sw_css.hackathon_team; +drop table if exists sw_css.hackathon_team_member; create table member ( @@ -85,3 +88,43 @@ create table milestone_history is_deleted boolean not null, created_at datetime(6) not null default current_timestamp(6) ); + +create table hackathon +( + id bigint auto_increment primary key, + name varchar(255) not null, + description text not null, + password varchar(255) not null, + apply_start_date date not null, + apply_end_date date not null, + hackathon_start_date date not null, + hackathon_end_date date not null, + image_url varchar(255) not null, + visible_status boolean not null, + is_deleted boolean not null, + created_at datetime(6) not null default current_timestamp(6) +); + +create table hackathon_team +( + id bigint auto_increment primary key, + hackathon_id bigint not null, + name varchar(255) not null, + image_url varchar(255) not null, + work varchar(255) not null, + github_url varchar(255) not null, + vote int not null, + prize varchar(255), + is_deleted boolean not null, + created_at datetime(6) not null default current_timestamp(6) +); + +create table hackathon_team_member +( + id bigint auto_increment primary key, + team_id bigint not null, + student_id bigint not null, + role varchar(255) not null, + is_deleted boolean not null, + created_at datetime(6) not null default current_timestamp(6) +); From ee755c44544ba454905d9eadea6e960c8da87071 Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 19 Dec 2024 21:08:14 +0900 Subject: [PATCH 02/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4,=20?= =?UTF-8?q?=ED=95=B4=EC=BB=A4=ED=86=A4=ED=8C=80,=20=ED=95=B4=EC=BB=A4?= =?UTF-8?q?=ED=86=A4=ED=8C=80=EC=9B=90=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../sw_css/hackathon/domain/Hackathon.java | 66 +++++++++++++++++++ .../hackathon/domain/HackathonTeam.java | 49 ++++++++++++++ .../hackathon/domain/HackathonTeamMember.java | 38 +++++++++++ 3 files changed, 153 insertions(+) create mode 100644 backend/src/main/java/sw_css/hackathon/domain/Hackathon.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java diff --git a/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java new file mode 100644 index 00000000..6dc6a657 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java @@ -0,0 +1,66 @@ +package sw_css.hackathon.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDate; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLRestriction; +import sw_css.milestone.domain.MilestoneCategory; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") +public class Hackathon { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String description; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private LocalDate applyStartDate; + + @Column(nullable = false) + private LocalDate applyEndDate; + + @Column(nullable = false) + private LocalDate hackathonStartDate; + + @Column(nullable = false) + private LocalDate hackathonEndDate; + + @Column + private String imageUrl; + + @Column(nullable = false) + private boolean visibleStatus; + + @Column(nullable = false) + private Boolean isDeleted; + + public Hackathon(final String name, final String description, final String password, final LocalDate applyStartDate, final LocalDate applyEndDate, final LocalDate hackathonStartDate, final LocalDate hackathonEndDate, final String imageUrl) { + this(null, name, description, password, applyStartDate, applyEndDate, hackathonStartDate, hackathonEndDate, imageUrl, false, false); + } + + public void delete() { + isDeleted = true; + } +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java new file mode 100644 index 00000000..e27d9784 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -0,0 +1,49 @@ +package sw_css.hackathon.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLRestriction; + + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") +public class HackathonTeam { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "hackathon_id", nullable = false) + private Hackathon hackathon; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String imageUrl; + + @Column(nullable = false) + private String work; + + @Column(nullable = false) + private String githubUrl; + + @Column(nullable = false) + private int vote; + + @Column(nullable = false) + private String prize; +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java new file mode 100644 index 00000000..c71c7e71 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java @@ -0,0 +1,38 @@ +package sw_css.hackathon.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLRestriction; +import sw_css.member.domain.StudentMember; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") +public class HackathonTeamMember { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id", nullable = false) + private HackathonTeam team; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "student_id", nullable = false) + private StudentMember studentMember; + + @Column(nullable = false) + private String role; +} From 18dd50d3c97ba84123666f9aad1cd0f821ffd415 Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 19 Dec 2024 21:08:34 +0900 Subject: [PATCH 03/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4,=20?= =?UTF-8?q?=ED=95=B4=EC=BB=A4=ED=86=A4=ED=8C=80,=20=ED=95=B4=EC=BB=A4?= =?UTF-8?q?=ED=86=A4=ED=8C=80=EC=9B=90=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/domain/repository/HackathonRepository.java | 7 +++++++ .../domain/repository/HackathonTeamMemberRepository.java | 7 +++++++ .../domain/repository/HackathonTeamRepository.java | 7 +++++++ 3 files changed, 21 insertions(+) create mode 100644 backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java new file mode 100644 index 00000000..cb88d6ea --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -0,0 +1,7 @@ +package sw_css.hackathon.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import sw_css.hackathon.domain.Hackathon; + +public interface HackathonRepository extends JpaRepository{ +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java new file mode 100644 index 00000000..51a2710b --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java @@ -0,0 +1,7 @@ +package sw_css.hackathon.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import sw_css.hackathon.domain.HackathonTeamMember; + +public interface HackathonTeamMemberRepository extends JpaRepository { +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java new file mode 100644 index 00000000..058500c8 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -0,0 +1,7 @@ +package sw_css.hackathon.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import sw_css.hackathon.domain.HackathonTeam; + +public interface HackathonTeamRepository extends JpaRepository { +} From c6fe4f7e09e48bd9184b9dbb683481b5d908974e Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 19 Dec 2024 21:09:09 +0900 Subject: [PATCH 04/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EB=B0=8F=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=ED=83=80=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../exception/HackathonException.java | 16 ++++++++++ .../exception/HackathonExceptionType.java | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java new file mode 100644 index 00000000..bed0cb79 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java @@ -0,0 +1,16 @@ +package sw_css.admin.hackathon.exception; + +import sw_css.base.BaseException; +import sw_css.base.BaseExceptionType; + +public class HackathonException extends BaseException { + private final HackathonExceptionType exceptionType; + + public HackathonException(final HackathonExceptionType exceptionType) { + super((exceptionType.errorMessage())); + this.exceptionType = exceptionType; + } + + @Override + public BaseExceptionType exceptionType() { return exceptionType; } +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java new file mode 100644 index 00000000..dd6d168f --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java @@ -0,0 +1,29 @@ +package sw_css.admin.hackathon.exception; + +import org.springframework.http.HttpStatus; +import sw_css.base.BaseExceptionType; + +public enum HackathonExceptionType implements BaseExceptionType { + NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), + INVALID_APPLY_DATE(HttpStatus.BAD_REQUEST, "신청 시작일이 신청 마지막날 보다 이후일 수 없습니다."), + INVALID_HACKATHON_DATE(HttpStatus.BAD_REQUEST, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), + UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "파일 유형이 png, jpg, jpeg 가 아닙니다."); + + private final HttpStatus httpStatus; + private final String errorMessage; + + HackathonExceptionType(final HttpStatus httpStatus, final String errorMessage) { + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String errorMessage() { + return errorMessage; + } +} From d903d7f7ff29cebb0d362c6aee6d5c0b0e2dbd07 Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 19 Dec 2024 21:09:40 +0900 Subject: [PATCH 05/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #209 --- .../hackathon/api/HackathonController.java | 48 ++++++++++ .../application/HackathonCommandService.java | 92 +++++++++++++++++++ .../dto/request/HackathonCreateRequest.java | 28 ++++++ 3 files changed, 168 insertions(+) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java new file mode 100644 index 00000000..f70f919c --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -0,0 +1,48 @@ +package sw_css.admin.hackathon.api; + +import jakarta.validation.Valid; +import java.net.URI; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import sw_css.admin.hackathon.application.HackathonCommandService; +import sw_css.admin.hackathon.application.dto.request.HackathonCreateRequest; +import sw_css.member.domain.FacultyMember; +import sw_css.utils.annotation.AdminInterface; + +@Validated +@RequestMapping("/admin/hackathons") +@RestController +@RequiredArgsConstructor +public class HackathonController { + private final HackathonCommandService hackathonCommandService; + + // TODO: 목록 조회 + + // TODO: 상세 조회 + + // TODO: 해커톤 등록 + @PostMapping + public ResponseEntity registerHackathon( + @AdminInterface FacultyMember facultyMember, + @RequestPart(value = "file", required = false) final MultipartFile file, + @RequestPart(value = "request") @Valid final HackathonCreateRequest request) { + final Long registeredHackathonId = hackathonCommandService.registerHackathon(file, request); + return ResponseEntity.created(URI.create("/admin/hackathon/" + registeredHackathonId)).build(); + } + + // TODO: 해커톤 수정 + + // TODO: 해커톤 삭제 + + // TODO: 해커톤 투표 결과 다운로드 + + // TODO: 해커톤 활성 여부 수정 + + // TODO: 해커톤 등수 수정 +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java new file mode 100644 index 00000000..8ea2af36 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java @@ -0,0 +1,92 @@ +package sw_css.admin.hackathon.application; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.time.LocalDate; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import sw_css.admin.hackathon.application.dto.request.HackathonCreateRequest; +import sw_css.admin.hackathon.exception.HackathonException; +import sw_css.admin.hackathon.exception.HackathonExceptionType; +import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.milestone.exception.MilestoneHistoryException; +import sw_css.milestone.exception.MilestoneHistoryExceptionType; + +@Service +@RequiredArgsConstructor +@Transactional +public class HackathonCommandService { + + @Value("${data.file-path-prefix}") + private String filePathPrefix; + + private final HackathonRepository hackathonRepository; + + public Long registerHackathon(final MultipartFile file, final HackathonCreateRequest request) { + validateFileType(file); + validateDate(request.applyStartDate(), request.applyEndDate(), HackathonExceptionType.INVALID_APPLY_DATE); + validateDate(request.hackathonStartDate(), request.hackathonEndDate(), HackathonExceptionType.INVALID_HACKATHON_DATE); + + final String newFilePath = generateFilePath(file); + final Hackathon newHackathon = new Hackathon(request.name(), request.description(), request.password(), request.applyStartDate(), request.applyEndDate(), request.hackathonStartDate(), request.hackathonEndDate(), newFilePath); + + final Long newHackathonId = hackathonRepository.save(newHackathon).getId(); + uploadFile(file, newFilePath); + return newHackathonId; + } + + private void validateDate(LocalDate startDate, LocalDate endDate, HackathonExceptionType exceptionType) { + if (startDate.isAfter(endDate)) throw new HackathonException(exceptionType); + } + + private void validateFileType(final MultipartFile file) { + System.out.println(file.getOriginalFilename()); + if (file == null) { + throw new HackathonException(HackathonExceptionType.NOT_EXIST_FILE); + } + final String contentType = file.getContentType(); + if (!isSupportedContentType(contentType)) { + throw new HackathonException(HackathonExceptionType.UNSUPPORTED_FILE_TYPE); + } + } + + private boolean isSupportedContentType(final String contentType) { + return contentType != null && ( + contentType.equals("image/png") || + contentType.equals("image/jpeg") || + contentType.equals("image/jpg") + ); + } + + private String generateFilePath(final MultipartFile file) { + if (file == null) { + return null; + } + return UUID.randomUUID() + "_" + file.getOriginalFilename().replaceAll("\\[|\\]", ""); + } + + private void uploadFile(final MultipartFile file, final String newFilePath) { + if (file == null) { + return; + } + final Path filePath = Paths.get(System.getProperty("user.dir") + filePathPrefix) + .resolve(Paths.get(newFilePath)).normalize().toAbsolutePath(); + try (final InputStream inputStream = file.getInputStream()) { + if (Files.notExists(filePath.getParent())) { + Files.createDirectories(filePath.getParent()); + } + Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException e) { + throw new MilestoneHistoryException(MilestoneHistoryExceptionType.CANNOT_OPEN_FILE); + } + } +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java new file mode 100644 index 00000000..88f193db --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java @@ -0,0 +1,28 @@ +package sw_css.admin.hackathon.application.dto.request; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; + +public record HackathonCreateRequest( + @NotBlank(message = "해커톤 명을 기재해주세요.") + String name, + @NotBlank(message = "해커톤 상세 내용를 기재해주세요.") + String description, + @NotBlank(message = "해커톤 비밀번호를 기재해주세요.") + String password, + @NotNull(message = "해커톤 신청 시작일을 기재해주세요") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyStartDate, + @NotNull(message = "해커톤 신청 마지막일을 기재해주세요") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyEndDate, + @NotNull(message = "해커톤 대회 시작일을 기재해주세요") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonStartDate, + @NotNull(message = "해커톤 대회 마지막일을 기재해주세요") + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonEndDate +) { +} From 597162da5bfc7ec0813b6bbc419243158bf7a797 Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 09:28:31 +0900 Subject: [PATCH 06/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=EC=9D=98=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/api/HackathonController.java | 19 ++++++++++ .../application/HackathonQueryService.java | 38 +++++++++++++++++++ .../dto/response/HackathonResponse.java | 31 +++++++++++++++ .../repository/HackathonRepository.java | 6 +++ 4 files changed, 94 insertions(+) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index f70f919c..60bfec99 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -2,16 +2,23 @@ import jakarta.validation.Valid; import java.net.URI; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import sw_css.admin.hackathon.application.HackathonCommandService; +import sw_css.admin.hackathon.application.HackathonQueryService; import sw_css.admin.hackathon.application.dto.request.HackathonCreateRequest; +import sw_css.admin.hackathon.application.dto.response.HackathonResponse; import sw_css.member.domain.FacultyMember; import sw_css.utils.annotation.AdminInterface; @@ -21,8 +28,20 @@ @RequiredArgsConstructor public class HackathonController { private final HackathonCommandService hackathonCommandService; + private final HackathonQueryService hackathonQueryService; // TODO: 목록 조회 + @GetMapping + public ResponseEntity> findAllHackathons( + final Pageable pageable, + @AdminInterface FacultyMember facultyMember, + @RequestParam(value = "name", required = false) final String name, + @RequestParam(value = "visibleStatus", required = false) final String visibleStatus + ) { + return ResponseEntity.ok( + hackathonQueryService.findAllHackathons(pageable, name, visibleStatus) + ); + } // TODO: 상세 조회 diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java new file mode 100644 index 00000000..ed7c85a3 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java @@ -0,0 +1,38 @@ +package sw_css.admin.hackathon.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.admin.hackathon.application.dto.response.HackathonResponse; +import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.repository.HackathonRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HackathonQueryService { + private final HackathonRepository hackathonRepository; + + public Page findAllHackathons(final Pageable pageable, + final String name, + final String visibleStatus) { + if(name != null && visibleStatus != null) { + Page hackathons = hackathonRepository.findByNameContainingAndVisibleStatus(name, visibleStatus.equals("ACTIVE"), pageable); + return HackathonResponse.from(hackathons); + } + if(name != null) { + Page hackathons = hackathonRepository.findByNameContaining(name, pageable); + return HackathonResponse.from(hackathons); + } + if(visibleStatus != null) { + Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals("ACTIVE"), pageable); + return HackathonResponse.from(hackathons); + } + + Page hackathons = hackathonRepository.findAll(pageable); + return HackathonResponse.from(hackathons); + } + +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java new file mode 100644 index 00000000..3b75d300 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java @@ -0,0 +1,31 @@ +package sw_css.admin.hackathon.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import sw_css.hackathon.domain.Hackathon; + +public record HackathonResponse( + Long id, + String name, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonEndDate, + String password, + Boolean visibleStatus +) { + public static Page from(Page hackathons) { + return new PageImpl<>(hackathons.stream() + .map(hackathon -> new HackathonResponse( + hackathon.getId(), + hackathon.getName(), + hackathon.getHackathonStartDate(), + hackathon.getHackathonEndDate(), + hackathon.getPassword(), + hackathon.isVisibleStatus() + )) + .toList(), hackathons.getPageable(), hackathons.getTotalElements()); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java index cb88d6ea..04589289 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -1,7 +1,13 @@ package sw_css.hackathon.domain.repository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import sw_css.hackathon.domain.Hackathon; public interface HackathonRepository extends JpaRepository{ + Page findAll(Pageable pageable); + Page findByNameContaining(String name, Pageable pageable); + Page findByVisibleStatus(boolean visibleStatus, Pageable pageable); + Page findByNameContainingAndVisibleStatus(String name, boolean visibleStatus, Pageable pageable); } From 028d57d571687e542c34127b06a8604a7306b7ff Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 09:41:11 +0900 Subject: [PATCH 07/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/api/HackathonController.java | 12 +++++-- .../application/HackathonQueryService.java | 10 ++++++ .../dto/response/HackathonDetailResponse.java | 34 +++++++++++++++++++ .../exception/HackathonExceptionType.java | 1 + 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index 60bfec99..ef23116c 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -9,6 +9,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -18,6 +19,7 @@ import sw_css.admin.hackathon.application.HackathonCommandService; import sw_css.admin.hackathon.application.HackathonQueryService; import sw_css.admin.hackathon.application.dto.request.HackathonCreateRequest; +import sw_css.admin.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.HackathonResponse; import sw_css.member.domain.FacultyMember; import sw_css.utils.annotation.AdminInterface; @@ -30,7 +32,6 @@ public class HackathonController { private final HackathonCommandService hackathonCommandService; private final HackathonQueryService hackathonQueryService; - // TODO: 목록 조회 @GetMapping public ResponseEntity> findAllHackathons( final Pageable pageable, @@ -44,8 +45,15 @@ public ResponseEntity> findAllHackathons( } // TODO: 상세 조회 + @GetMapping("/{hackathonId}") + public ResponseEntity findHackathonById( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId + ){ + return ResponseEntity.ok( + hackathonQueryService.findHackathonById(hackathonId)); + } - // TODO: 해커톤 등록 @PostMapping public ResponseEntity registerHackathon( @AdminInterface FacultyMember facultyMember, diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java index ed7c85a3..136d9ad6 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java @@ -5,7 +5,10 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import sw_css.admin.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.HackathonResponse; +import sw_css.admin.hackathon.exception.HackathonException; +import sw_css.admin.hackathon.exception.HackathonExceptionType; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.repository.HackathonRepository; @@ -35,4 +38,11 @@ public Page findAllHackathons(final Pageable pageable, return HackathonResponse.from(hackathons); } + public HackathonDetailResponse findHackathonById(final Long id) { + Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + return HackathonDetailResponse.of(hackathon); + } + } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java new file mode 100644 index 00000000..94aef630 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java @@ -0,0 +1,34 @@ +package sw_css.admin.hackathon.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import sw_css.hackathon.domain.Hackathon; + +public record HackathonDetailResponse( + Long id, + String name, + String description, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyEndDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonEndDate, + String password, + Boolean visibleStatus) { + public static HackathonDetailResponse of(Hackathon hackathon) { + return new HackathonDetailResponse( + hackathon.getId(), + hackathon.getName(), + hackathon.getDescription(), + hackathon.getApplyStartDate(), + hackathon.getApplyEndDate(), + hackathon.getHackathonStartDate(), + hackathon.getHackathonEndDate(), + hackathon.getPassword(), + hackathon.isVisibleStatus() + ); + } +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java index dd6d168f..3a5fa83e 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java @@ -4,6 +4,7 @@ import sw_css.base.BaseExceptionType; public enum HackathonExceptionType implements BaseExceptionType { + NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), INVALID_APPLY_DATE(HttpStatus.BAD_REQUEST, "신청 시작일이 신청 마지막날 보다 이후일 수 없습니다."), INVALID_HACKATHON_DATE(HttpStatus.BAD_REQUEST, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), From e6e47776f1197e3590e670daf4ce8f1c04f37086 Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 09:59:47 +0900 Subject: [PATCH 08/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/api/HackathonController.java | 18 ++++++++++--- .../application/HackathonCommandService.java | 27 +++++++++++++++++-- ...eateRequest.java => HackathonRequest.java} | 2 +- .../dto/response/HackathonDetailResponse.java | 2 ++ .../sw_css/hackathon/domain/Hackathon.java | 2 ++ 5 files changed, 44 insertions(+), 7 deletions(-) rename backend/src/main/java/sw_css/admin/hackathon/application/dto/request/{HackathonCreateRequest.java => HackathonRequest.java} (96%) diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index ef23116c..074a034b 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -2,15 +2,16 @@ import jakarta.validation.Valid; import java.net.URI; -import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; @@ -18,7 +19,7 @@ import org.springframework.web.multipart.MultipartFile; import sw_css.admin.hackathon.application.HackathonCommandService; import sw_css.admin.hackathon.application.HackathonQueryService; -import sw_css.admin.hackathon.application.dto.request.HackathonCreateRequest; +import sw_css.admin.hackathon.application.dto.request.HackathonRequest; import sw_css.admin.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.HackathonResponse; import sw_css.member.domain.FacultyMember; @@ -44,7 +45,6 @@ public ResponseEntity> findAllHackathons( ); } - // TODO: 상세 조회 @GetMapping("/{hackathonId}") public ResponseEntity findHackathonById( @AdminInterface FacultyMember facultyMember, @@ -58,12 +58,22 @@ public ResponseEntity findHackathonById( public ResponseEntity registerHackathon( @AdminInterface FacultyMember facultyMember, @RequestPart(value = "file", required = false) final MultipartFile file, - @RequestPart(value = "request") @Valid final HackathonCreateRequest request) { + @RequestPart(value = "request") @Valid final HackathonRequest request) { final Long registeredHackathonId = hackathonCommandService.registerHackathon(file, request); return ResponseEntity.created(URI.create("/admin/hackathon/" + registeredHackathonId)).build(); } // TODO: 해커톤 수정 + @PatchMapping("/{hackathonId}") + public ResponseEntity updateHackathon( + @AdminInterface FacultyMember facultyMember, + @RequestPart(value = "file", required = false) final MultipartFile file, + @RequestPart(value = "request") @Valid final HackathonRequest request, + @PathVariable final Long hackathonId + ) { + hackathonCommandService.updateHackathon(hackathonId, file, request); + return ResponseEntity.noContent().build(); + } // TODO: 해커톤 삭제 diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java index 8ea2af36..4312c772 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java @@ -13,7 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import sw_css.admin.hackathon.application.dto.request.HackathonCreateRequest; +import sw_css.admin.hackathon.application.dto.request.HackathonRequest; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; import sw_css.hackathon.domain.Hackathon; @@ -31,7 +31,7 @@ public class HackathonCommandService { private final HackathonRepository hackathonRepository; - public Long registerHackathon(final MultipartFile file, final HackathonCreateRequest request) { + public Long registerHackathon(final MultipartFile file, final HackathonRequest request) { validateFileType(file); validateDate(request.applyStartDate(), request.applyEndDate(), HackathonExceptionType.INVALID_APPLY_DATE); validateDate(request.hackathonStartDate(), request.hackathonEndDate(), HackathonExceptionType.INVALID_HACKATHON_DATE); @@ -44,6 +44,29 @@ public Long registerHackathon(final MultipartFile file, final HackathonCreateReq return newHackathonId; } + public void updateHackathon(final Long hackathonId, final MultipartFile file, final HackathonRequest request) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + if(file != null) { + validateFileType(file); + final String newFilePath = generateFilePath(file); + uploadFile(file, newFilePath); + hackathon.setImageUrl(newFilePath); + } + validateDate(request.applyStartDate(), request.applyEndDate(), HackathonExceptionType.INVALID_APPLY_DATE); + validateDate(request.hackathonStartDate(), request.hackathonEndDate(), HackathonExceptionType.INVALID_HACKATHON_DATE); + + hackathon.setName(request.name()); + hackathon.setDescription(request.description()); + hackathon.setPassword(request.password()); + hackathon.setApplyStartDate(request.applyStartDate()); + hackathon.setApplyEndDate(request.applyEndDate()); + hackathon.setHackathonStartDate(request.hackathonStartDate()); + hackathon.setHackathonEndDate(request.hackathonEndDate()); + hackathonRepository.save(hackathon); + } + private void validateDate(LocalDate startDate, LocalDate endDate, HackathonExceptionType exceptionType) { if (startDate.isAfter(endDate)) throw new HackathonException(exceptionType); } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonRequest.java similarity index 96% rename from backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java rename to backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonRequest.java index 88f193db..1a188e60 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonCreateRequest.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -public record HackathonCreateRequest( +public record HackathonRequest( @NotBlank(message = "해커톤 명을 기재해주세요.") String name, @NotBlank(message = "해커톤 상세 내용를 기재해주세요.") diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java index 94aef630..40c6dfa4 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java @@ -8,6 +8,7 @@ public record HackathonDetailResponse( Long id, String name, String description, + String imageUrl, @JsonFormat(pattern = "yyyy-MM-dd") LocalDate applyStartDate, @JsonFormat(pattern = "yyyy-MM-dd") @@ -23,6 +24,7 @@ public static HackathonDetailResponse of(Hackathon hackathon) { hackathon.getId(), hackathon.getName(), hackathon.getDescription(), + hackathon.getImageUrl(), hackathon.getApplyStartDate(), hackathon.getApplyEndDate(), hackathon.getHackathonStartDate(), diff --git a/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java index 6dc6a657..638b1832 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java +++ b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java @@ -13,11 +13,13 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.SQLRestriction; import sw_css.milestone.domain.MilestoneCategory; @Entity @Getter +@Setter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @SQLRestriction("is_deleted = false") From a4c82381685f739cebeff147c01fc790bfd43e14 Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 10:03:35 +0900 Subject: [PATCH 09/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../admin/hackathon/api/HackathonController.java | 10 +++++++++- .../hackathon/application/HackathonCommandService.java | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index 074a034b..9c5d4eb4 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +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; @@ -63,7 +64,6 @@ public ResponseEntity registerHackathon( return ResponseEntity.created(URI.create("/admin/hackathon/" + registeredHackathonId)).build(); } - // TODO: 해커톤 수정 @PatchMapping("/{hackathonId}") public ResponseEntity updateHackathon( @AdminInterface FacultyMember facultyMember, @@ -76,6 +76,14 @@ public ResponseEntity updateHackathon( } // TODO: 해커톤 삭제 + @DeleteMapping("/{hackathonId}") + public ResponseEntity deleteHackathon( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId + ){ + hackathonCommandService.deleteHackathon(hackathonId); + return ResponseEntity.noContent().build(); + } // TODO: 해커톤 투표 결과 다운로드 diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java index 4312c772..fd3c42c0 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java @@ -67,6 +67,13 @@ public void updateHackathon(final Long hackathonId, final MultipartFile file, fi hackathonRepository.save(hackathon); } + public void deleteHackathon(final Long hackathonId) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + hackathon.delete(); + hackathonRepository.save(hackathon); + } + private void validateDate(LocalDate startDate, LocalDate endDate, HackathonExceptionType exceptionType) { if (startDate.isAfter(endDate)) throw new HackathonException(exceptionType); } From 38c05bebccab51c3175a255392a16548ea64b38e Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 10:58:58 +0900 Subject: [PATCH 10/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EA=B2=B0=EA=B3=BC=20=EC=97=91=EC=85=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20API?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/api/HackathonController.java | 34 +++-- .../application/HackathonCommandService.java | 6 +- .../application/HackathonQueryService.java | 133 ++++++++++++++++-- ...equest.java => AdminHackathonRequest.java} | 2 +- ...java => AdminHackathonDetailResponse.java} | 6 +- ...ponse.java => AdminHackathonResponse.java} | 6 +- .../exception/HackathonExceptionType.java | 1 + .../dto/response/HackathonTeamResponse.java | 12 ++ .../repository/HackathonTeamRepository.java | 16 +++ backend/src/main/resources/test-data.sql | 39 ++++- 10 files changed, 221 insertions(+), 34 deletions(-) rename backend/src/main/java/sw_css/admin/hackathon/application/dto/request/{HackathonRequest.java => AdminHackathonRequest.java} (96%) rename backend/src/main/java/sw_css/admin/hackathon/application/dto/response/{HackathonDetailResponse.java => AdminHackathonDetailResponse.java} (86%) rename backend/src/main/java/sw_css/admin/hackathon/application/dto/response/{HackathonResponse.java => AdminHackathonResponse.java} (84%) create mode 100644 backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index 9c5d4eb4..9520e18e 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -2,9 +2,13 @@ import jakarta.validation.Valid; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; @@ -12,7 +16,6 @@ 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; @@ -20,9 +23,9 @@ import org.springframework.web.multipart.MultipartFile; import sw_css.admin.hackathon.application.HackathonCommandService; import sw_css.admin.hackathon.application.HackathonQueryService; -import sw_css.admin.hackathon.application.dto.request.HackathonRequest; -import sw_css.admin.hackathon.application.dto.response.HackathonDetailResponse; -import sw_css.admin.hackathon.application.dto.response.HackathonResponse; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.member.domain.FacultyMember; import sw_css.utils.annotation.AdminInterface; @@ -35,7 +38,7 @@ public class HackathonController { private final HackathonQueryService hackathonQueryService; @GetMapping - public ResponseEntity> findAllHackathons( + public ResponseEntity> findAllHackathons( final Pageable pageable, @AdminInterface FacultyMember facultyMember, @RequestParam(value = "name", required = false) final String name, @@ -47,7 +50,7 @@ public ResponseEntity> findAllHackathons( } @GetMapping("/{hackathonId}") - public ResponseEntity findHackathonById( + public ResponseEntity findHackathonById( @AdminInterface FacultyMember facultyMember, @PathVariable final Long hackathonId ){ @@ -59,7 +62,7 @@ public ResponseEntity findHackathonById( public ResponseEntity registerHackathon( @AdminInterface FacultyMember facultyMember, @RequestPart(value = "file", required = false) final MultipartFile file, - @RequestPart(value = "request") @Valid final HackathonRequest request) { + @RequestPart(value = "request") @Valid final AdminHackathonRequest request) { final Long registeredHackathonId = hackathonCommandService.registerHackathon(file, request); return ResponseEntity.created(URI.create("/admin/hackathon/" + registeredHackathonId)).build(); } @@ -68,14 +71,13 @@ public ResponseEntity registerHackathon( public ResponseEntity updateHackathon( @AdminInterface FacultyMember facultyMember, @RequestPart(value = "file", required = false) final MultipartFile file, - @RequestPart(value = "request") @Valid final HackathonRequest request, + @RequestPart(value = "request") @Valid final AdminHackathonRequest request, @PathVariable final Long hackathonId ) { hackathonCommandService.updateHackathon(hackathonId, file, request); return ResponseEntity.noContent().build(); } - // TODO: 해커톤 삭제 @DeleteMapping("/{hackathonId}") public ResponseEntity deleteHackathon( @AdminInterface FacultyMember facultyMember, @@ -85,7 +87,19 @@ public ResponseEntity deleteHackathon( return ResponseEntity.noContent().build(); } - // TODO: 해커톤 투표 결과 다운로드 + @GetMapping("/{hackathonId}/download/votes") + public ResponseEntity downloadVotes( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId + ){ + final String filename = "해커톤_투표_결과.xlsx"; + final String encodedFilename = URLEncoder.encode(filename, StandardCharsets.UTF_8); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(hackathonQueryService.downloadHackathonVotesById(hackathonId)); + + } // TODO: 해커톤 활성 여부 수정 diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java index fd3c42c0..2b2f8549 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java @@ -13,7 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import sw_css.admin.hackathon.application.dto.request.HackathonRequest; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; import sw_css.hackathon.domain.Hackathon; @@ -31,7 +31,7 @@ public class HackathonCommandService { private final HackathonRepository hackathonRepository; - public Long registerHackathon(final MultipartFile file, final HackathonRequest request) { + public Long registerHackathon(final MultipartFile file, final AdminHackathonRequest request) { validateFileType(file); validateDate(request.applyStartDate(), request.applyEndDate(), HackathonExceptionType.INVALID_APPLY_DATE); validateDate(request.hackathonStartDate(), request.hackathonEndDate(), HackathonExceptionType.INVALID_HACKATHON_DATE); @@ -44,7 +44,7 @@ public Long registerHackathon(final MultipartFile file, final HackathonRequest r return newHackathonId; } - public void updateHackathon(final Long hackathonId, final MultipartFile file, final HackathonRequest request) { + public void updateHackathon(final Long hackathonId, final MultipartFile file, final AdminHackathonRequest request) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java index 136d9ad6..6d61fb1e 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java @@ -1,48 +1,159 @@ package sw_css.admin.hackathon.application; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFCellStyle; +import org.apache.poi.xssf.usermodel.XSSFColor; +import org.apache.poi.xssf.usermodel.XSSFFont; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import sw_css.admin.hackathon.application.dto.response.HackathonDetailResponse; -import sw_css.admin.hackathon.application.dto.response.HackathonResponse; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonTeam; import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.milestone.exception.MilestoneHistoryException; +import sw_css.milestone.exception.MilestoneHistoryExceptionType; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class HackathonQueryService { private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; - public Page findAllHackathons(final Pageable pageable, - final String name, - final String visibleStatus) { + public Page findAllHackathons(final Pageable pageable, + final String name, + final String visibleStatus) { if(name != null && visibleStatus != null) { Page hackathons = hackathonRepository.findByNameContainingAndVisibleStatus(name, visibleStatus.equals("ACTIVE"), pageable); - return HackathonResponse.from(hackathons); + return AdminHackathonResponse.from(hackathons); } if(name != null) { Page hackathons = hackathonRepository.findByNameContaining(name, pageable); - return HackathonResponse.from(hackathons); + return AdminHackathonResponse.from(hackathons); } if(visibleStatus != null) { Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals("ACTIVE"), pageable); - return HackathonResponse.from(hackathons); + return AdminHackathonResponse.from(hackathons); } Page hackathons = hackathonRepository.findAll(pageable); - return HackathonResponse.from(hackathons); + return AdminHackathonResponse.from(hackathons); } - public HackathonDetailResponse findHackathonById(final Long id) { + public AdminHackathonDetailResponse findHackathonById(final Long id) { Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); - return HackathonDetailResponse.of(hackathon); + return AdminHackathonDetailResponse.of(hackathon); + } + + public byte[] downloadHackathonVotesById(final Long id) { + Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + final List hackathonTeams = hackathonTeamRepository.findByHackathonIdSorted(hackathon.getId()); + hackathonTeams.stream().forEach(team -> { + System.out.println(team.getName()); + }); + + return generateHackathonVoteExcelFile(hackathonTeams); + } + + private byte[] generateHackathonVoteExcelFile(final List hackathonTeams){ + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("해커톤 투표 현황"); + sheet.setDefaultColumnWidth(20); + + XSSFFont headerXSSFFont = createHeaderFont(workbook); + XSSFCellStyle headerXssfCellStyle = createHeaderStyle(workbook, headerXSSFFont); + XSSFCellStyle bodyXssfCellStyle = createBodyStyle(workbook); + + int rowCount = 0; // 데이터가 저장될 행 + List headerNames = new ArrayList<>(List.of("순위", "득표수", "팀명", "서비스명")); + + Row headerRow = sheet.createRow(rowCount++); + Cell headerCell; + for (int i = 0; i < headerNames.size(); i++) { + headerCell = headerRow.createCell(i); + headerCell.setCellValue(headerNames.get(i)); // 데이터 추가 + headerCell.setCellStyle(headerXssfCellStyle); // 스타일 추가 + } + + Row bodyRow; + Cell bodyCell; + for (int i = 0; i < hackathonTeams.size(); i++) { + bodyRow = sheet.createRow(rowCount++); + + for (int j = 0; j < headerNames.size(); j++) { + bodyCell = bodyRow.createCell(j); + bodyCell.setCellStyle(bodyXssfCellStyle); + } + bodyRow.getCell(0).setCellValue(i+1); + bodyRow.getCell(1).setCellValue(hackathonTeams.get(i).getVote()); + bodyRow.getCell(2).setCellValue(hackathonTeams.get(i).getName()); + bodyRow.getCell(3).setCellValue(hackathonTeams.get(i).getWork()); + } + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + workbook.write(bos); + workbook.close(); + return bos.toByteArray(); + } catch (IOException e) { + throw new HackathonException(HackathonExceptionType.CANNOT_OPEN_FILE); + } + } + + private XSSFFont createHeaderFont(Workbook workbook) { + XSSFFont headerXSSFFont = (XSSFFont) workbook.createFont(); + headerXSSFFont.setColor(new XSSFColor(new byte[]{(byte) 255, (byte) 255, (byte) 255})); + return headerXSSFFont; + } + + private XSSFCellStyle createHeaderStyle(Workbook workbook, XSSFFont headerXSSFFont) { + XSSFCellStyle headerXssfCellStyle = (XSSFCellStyle) workbook.createCellStyle(); + + // 테두리 설정 + headerXssfCellStyle.setBorderLeft(BorderStyle.THIN); + headerXssfCellStyle.setBorderRight(BorderStyle.THIN); + headerXssfCellStyle.setBorderTop(BorderStyle.THIN); + headerXssfCellStyle.setBorderBottom(BorderStyle.THIN); + + headerXssfCellStyle.setFillForegroundColor(new XSSFColor(new byte[]{(byte) 34, (byte) 37, (byte) 41})); + headerXssfCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + headerXssfCellStyle.setFont(headerXSSFFont); + + return headerXssfCellStyle; + } + + private XSSFCellStyle createBodyStyle(Workbook workbook) { + XSSFCellStyle bodyXssfCellStyle = (XSSFCellStyle) workbook.createCellStyle(); + + // 테두리 설정 + bodyXssfCellStyle.setBorderLeft(BorderStyle.THIN); + bodyXssfCellStyle.setBorderRight(BorderStyle.THIN); + bodyXssfCellStyle.setBorderTop(BorderStyle.THIN); + bodyXssfCellStyle.setBorderBottom(BorderStyle.THIN); + return bodyXssfCellStyle; } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonRequest.java similarity index 96% rename from backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonRequest.java rename to backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonRequest.java index 1a188e60..455aaabf 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/HackathonRequest.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.NotNull; import java.time.LocalDate; -public record HackathonRequest( +public record AdminHackathonRequest( @NotBlank(message = "해커톤 명을 기재해주세요.") String name, @NotBlank(message = "해커톤 상세 내용를 기재해주세요.") diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonDetailResponse.java similarity index 86% rename from backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java rename to backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonDetailResponse.java index 40c6dfa4..743d84ec 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonDetailResponse.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonDetailResponse.java @@ -4,7 +4,7 @@ import java.time.LocalDate; import sw_css.hackathon.domain.Hackathon; -public record HackathonDetailResponse( +public record AdminHackathonDetailResponse( Long id, String name, String description, @@ -19,8 +19,8 @@ public record HackathonDetailResponse( LocalDate hackathonEndDate, String password, Boolean visibleStatus) { - public static HackathonDetailResponse of(Hackathon hackathon) { - return new HackathonDetailResponse( + public static AdminHackathonDetailResponse of(Hackathon hackathon) { + return new AdminHackathonDetailResponse( hackathon.getId(), hackathon.getName(), hackathon.getDescription(), diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonResponse.java similarity index 84% rename from backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java rename to backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonResponse.java index 3b75d300..bb75b4ce 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/HackathonResponse.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonResponse.java @@ -6,7 +6,7 @@ import org.springframework.data.domain.PageImpl; import sw_css.hackathon.domain.Hackathon; -public record HackathonResponse( +public record AdminHackathonResponse( Long id, String name, @JsonFormat(pattern = "yyyy-MM-dd") @@ -16,9 +16,9 @@ public record HackathonResponse( String password, Boolean visibleStatus ) { - public static Page from(Page hackathons) { + public static Page from(Page hackathons) { return new PageImpl<>(hackathons.stream() - .map(hackathon -> new HackathonResponse( + .map(hackathon -> new AdminHackathonResponse( hackathon.getId(), hackathon.getName(), hackathon.getHackathonStartDate(), diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java index 3a5fa83e..a8afdddd 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java @@ -6,6 +6,7 @@ public enum HackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), + CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), INVALID_APPLY_DATE(HttpStatus.BAD_REQUEST, "신청 시작일이 신청 마지막날 보다 이후일 수 없습니다."), INVALID_HACKATHON_DATE(HttpStatus.BAD_REQUEST, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "파일 유형이 png, jpg, jpeg 가 아닙니다."); diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java new file mode 100644 index 00000000..3b2163d2 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java @@ -0,0 +1,12 @@ +package sw_css.hackathon.application.dto.response; + +public record HackathonTeamResponse( + Long id, + String name, + String imageUrl, + String work, + String githubUrl, + Long vote, + String prize +) { +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 058500c8..4f0284e2 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -1,7 +1,23 @@ package sw_css.hackathon.domain.repository; +import io.lettuce.core.dynamic.annotation.Param; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import sw_css.hackathon.domain.HackathonTeam; public interface HackathonTeamRepository extends JpaRepository { + @Query("SELECT h FROM HackathonTeam h " + + "WHERE h.hackathon.id = :hackathonId " + + "ORDER BY " + + "CASE h.prize " + + " WHEN 'GRAND_PRIZE' THEN 1 " + + " WHEN 'EXCELLENCE_PRIZE' THEN 2 " + + " WHEN 'MERIT_PRIZE' THEN 3 " + + " WHEN 'ENCOURAGEMENT_PRIZE' THEN 4 " + + " WHEN 'NONE_PRIZE' THEN 5 " + + " ELSE 6 " + + "END, " + + "h.vote DESC") + List findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); } diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index 63754c40..ee0db920 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -2,17 +2,50 @@ insert into member (email, name, password, phone_number, is_deleted) values ('admin@pusan.ac.kr', '관리자', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '01000000000', false); - insert into faculty_member (member_id) values (1); - insert into member (email, name, password, phone_number, is_deleted) values ( 'ddang@pusan.ac.kr', '이다은', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '01000000000' , false); - insert into student_member (id, member_id, major_id, minor_id, double_major_id, career, career_detail) values (202055555, 2, 1, null, null, 'GRADUATE_SCHOOL', 'IT 기업 개발자'); + +## hackathon +insert into hackathon (name, description, password, apply_start_date, apply_end_date, hackathon_start_date, hackathon_end_date, image_url, visible_status, is_deleted) +values('제5회 PNU 창의융합 소프트웨어해커톤', ' +# Heading 1 +## Heading 2 +### Heading 3 + +This is a **bold** text with some *italic* and [a link](https://example.com). +- ㅁ렁ㄹㄴㄹ +1. ㄹㄴㅇㄹㅁㄹ', '1234', '2024-05-22', '2024-05-29', '2024-05-22', '2024-09-07', '1.png', true, false); +insert into hackathon (name, description, password, apply_start_date, apply_end_date, hackathon_start_date, hackathon_end_date, image_url, visible_status, is_deleted) +values('제4회 PNU 창의융합 소프트웨어해커톤', ' +# 제목 1 +## 제목 2 +### 제목 3 +**bold** *italic* [a link](https://example.com). +- asdf +1. qwer', '1234', '2022-03-22', '2022-05-29', '2022-12-22', '2023-03-07', '1.png', true, false); + +## hackathon team +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) +values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', '28', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) +values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', '52', 'EXCELLENCE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) +values(1, '코드플레이스', '1.png', '코드 플레이스 코드 플레이스', 'https://github.com/pnu-code-place/code-place', '92', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) +values(1, 'Next JS', '1.png', '암어 넥스트 레블 절대로', 'https://github.com/vercel/next.js', '18', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) +values(1, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', '22', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) +values(2, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', '22', 'NONE_PRIZE', false); + + + ## milestone histories INSERT INTO sw_css.milestone_history (id, milestone_id, student_id, description, file_url, status, reject_reason, count, activated_at, is_deleted, created_at) From c0a5df014393eb56ddc4ff67fdcb91cf3052bad9 Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 11:13:34 +0900 Subject: [PATCH 11/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=99=9C=EC=84=B1=20=EC=97=AC=EB=B6=80=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../admin/hackathon/api/HackathonController.java | 12 +++++++++++- .../application/HackathonCommandService.java | 12 ++++++++++++ .../hackathon/application/HackathonQueryService.java | 3 ++- .../dto/request/AdminHackathonActiveRequest.java | 9 +++++++++ .../admin/hackathon/domain/HackathonStatus.java | 5 +++++ .../hackathon/exception/HackathonExceptionType.java | 1 + 6 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonActiveRequest.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/domain/HackathonStatus.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index 9520e18e..5d1a2e06 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -16,6 +16,7 @@ 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestPart; @@ -23,6 +24,7 @@ import org.springframework.web.multipart.MultipartFile; import sw_css.admin.hackathon.application.HackathonCommandService; import sw_css.admin.hackathon.application.HackathonQueryService; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonActiveRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; @@ -101,7 +103,15 @@ public ResponseEntity downloadVotes( } - // TODO: 해커톤 활성 여부 수정 + @PatchMapping("/{hackathonId}/active") + public ResponseEntity patchHackathon( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId, + @RequestBody @Valid AdminHackathonActiveRequest request + ){ + hackathonCommandService.activeHackathon(hackathonId, request.visibleStatus()); + return ResponseEntity.noContent().build(); + } // TODO: 해커톤 등수 수정 } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java index 2b2f8549..ce7453f9 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java @@ -14,6 +14,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; +import sw_css.admin.hackathon.domain.HackathonStatus; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; import sw_css.hackathon.domain.Hackathon; @@ -74,6 +75,17 @@ public void deleteHackathon(final Long hackathonId) { hackathonRepository.save(hackathon); } + public void activeHackathon(final Long hackathonId, final String visibleStatus) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + if(visibleStatus.equals(HackathonStatus.ACTIVE.toString())) hackathon.setVisibleStatus(true); + else if(visibleStatus.equals(HackathonStatus.INACTIVE.toString())) hackathon.setVisibleStatus(false); + else throw new HackathonException(HackathonExceptionType.INVALID_ACTIVE_STATUS); + + hackathonRepository.save(hackathon); + } + private void validateDate(LocalDate startDate, LocalDate endDate, HackathonExceptionType exceptionType) { if (startDate.isAfter(endDate)) throw new HackathonException(exceptionType); } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java index 6d61fb1e..1c50d181 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java @@ -23,6 +23,7 @@ import org.springframework.transaction.annotation.Transactional; import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; +import sw_css.admin.hackathon.domain.HackathonStatus; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; @@ -52,7 +53,7 @@ public Page findAllHackathons(final Pageable pageable, return AdminHackathonResponse.from(hackathons); } if(visibleStatus != null) { - Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals("ACTIVE"), pageable); + Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageable); return AdminHackathonResponse.from(hackathons); } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonActiveRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonActiveRequest.java new file mode 100644 index 00000000..a0c26a35 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonActiveRequest.java @@ -0,0 +1,9 @@ +package sw_css.admin.hackathon.application.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record AdminHackathonActiveRequest( + @NotBlank(message="활성 여부를 기재해주세요.") + String visibleStatus +) { +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/domain/HackathonStatus.java b/backend/src/main/java/sw_css/admin/hackathon/domain/HackathonStatus.java new file mode 100644 index 00000000..670c38ec --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/domain/HackathonStatus.java @@ -0,0 +1,5 @@ +package sw_css.admin.hackathon.domain; + +public enum HackathonStatus { + ACTIVE, INACTIVE +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java index a8afdddd..cc7d854b 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java @@ -7,6 +7,7 @@ public enum HackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), + INVALID_ACTIVE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 활성 형식입니다."), INVALID_APPLY_DATE(HttpStatus.BAD_REQUEST, "신청 시작일이 신청 마지막날 보다 이후일 수 없습니다."), INVALID_HACKATHON_DATE(HttpStatus.BAD_REQUEST, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "파일 유형이 png, jpg, jpeg 가 아닙니다."); From 0f25feb7de2d7ed3a9ee829a16259f2cf71fca07 Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 11:39:34 +0900 Subject: [PATCH 12/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=EB=93=B1=EC=88=98=20=EC=88=98=EC=A0=95=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/api/HackathonController.java | 11 ++++++- .../application/HackathonCommandService.java | 32 +++++++++++++++++++ .../request/AdminHackathonPrizeRequest.java | 7 ++++ .../exception/HackathonExceptionType.java | 1 + .../hackathon/domain/HackathonPrize.java | 5 +++ .../hackathon/domain/HackathonTeam.java | 2 ++ .../repository/HackathonTeamRepository.java | 2 ++ 7 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonPrizeRequest.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/HackathonPrize.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java index 5d1a2e06..abc2ff2e 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java @@ -25,6 +25,7 @@ import sw_css.admin.hackathon.application.HackathonCommandService; import sw_css.admin.hackathon.application.HackathonQueryService; import sw_css.admin.hackathon.application.dto.request.AdminHackathonActiveRequest; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; @@ -113,5 +114,13 @@ public ResponseEntity patchHackathon( return ResponseEntity.noContent().build(); } - // TODO: 해커톤 등수 수정 + @PatchMapping("/{hackathonId}/prize") + public ResponseEntity patchHackathonPrize( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId, + @RequestBody @Valid AdminHackathonPrizeRequest request + ){ + hackathonCommandService.hackathonChangePrize(hackathonId, request.teams()); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java index ce7453f9..46f3958e 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java @@ -7,18 +7,24 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.domain.HackathonStatus; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonPrize; +import sw_css.hackathon.domain.HackathonTeam; import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; import sw_css.milestone.exception.MilestoneHistoryException; import sw_css.milestone.exception.MilestoneHistoryExceptionType; @@ -27,6 +33,7 @@ @Transactional public class HackathonCommandService { + private final HackathonTeamRepository hackathonTeamRepository; @Value("${data.file-path-prefix}") private String filePathPrefix; @@ -86,6 +93,31 @@ public void activeHackathon(final Long hackathonId, final String visibleStatus) hackathonRepository.save(hackathon); } + public void hackathonChangePrize(final Long hackathonId, List teams) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + List hackathonTeams = hackathonTeamRepository.findByHackathonId(hackathon.getId()); + + for(AdminHackathonPrizeRequest.AdminTeam team : teams) { + for(HackathonTeam hackathonTeam : hackathonTeams) { + if( !hackathonTeam.getId().equals(team.id()) ) continue; + + validatePrize(team.prize()); + hackathonTeam.setPrize(team.prize()); + hackathonTeamRepository.save(hackathonTeam); + } + } + } + + private void validatePrize(String prize){ + try { + HackathonPrize.valueOf(prize); + } catch (IllegalArgumentException e) { + throw new HackathonException(HackathonExceptionType.INVALID_PRIZE_STATUS); + } + } + private void validateDate(LocalDate startDate, LocalDate endDate, HackathonExceptionType exceptionType) { if (startDate.isAfter(endDate)) throw new HackathonException(exceptionType); } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonPrizeRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonPrizeRequest.java new file mode 100644 index 00000000..a2a5c006 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonPrizeRequest.java @@ -0,0 +1,7 @@ +package sw_css.admin.hackathon.application.dto.request; + +import java.util.List; + +public record AdminHackathonPrizeRequest(List teams) { + public record AdminTeam(Long id, String prize){} +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java index cc7d854b..60c7f6b8 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java @@ -7,6 +7,7 @@ public enum HackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), + INVALID_PRIZE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 상장 형식입니다."), INVALID_ACTIVE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 활성 형식입니다."), INVALID_APPLY_DATE(HttpStatus.BAD_REQUEST, "신청 시작일이 신청 마지막날 보다 이후일 수 없습니다."), INVALID_HACKATHON_DATE(HttpStatus.BAD_REQUEST, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonPrize.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonPrize.java new file mode 100644 index 00000000..10d57c94 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonPrize.java @@ -0,0 +1,5 @@ +package sw_css.hackathon.domain; + +public enum HackathonPrize { + GRAND_PRIZE, EXCELLENCE_PRIZE , MERIT_PRIZE, ENCOURAGEMENT_PRIZE, NONE_PRIZE +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java index e27d9784..a67e09d2 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -12,11 +12,13 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.SQLRestriction; @Entity @Getter +@Setter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @SQLRestriction("is_deleted = false") diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 4f0284e2..ea284986 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -20,4 +20,6 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); + + List findByHackathonId(Long hackathonId); } From 0ddae47db8382159d568cf8c13f1f025f6f2d50e Mon Sep 17 00:00:00 2001 From: llddang Date: Fri, 20 Dec 2024 11:44:09 +0900 Subject: [PATCH 13/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20=EB=8C=80?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=EC=9E=91=EC=9D=BC=20=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=82=B4=EB=A6=BC=EC=B0=A8=EC=88=9C=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../hackathon/application/HackathonQueryService.java | 10 +++++----- .../domain/repository/HackathonRepository.java | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java index 1c50d181..defcc314 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java @@ -44,20 +44,20 @@ public class HackathonQueryService { public Page findAllHackathons(final Pageable pageable, final String name, final String visibleStatus) { + Sort sort = Sort.by(Sort.Order.desc("hackathonStartDate")); if(name != null && visibleStatus != null) { - Page hackathons = hackathonRepository.findByNameContainingAndVisibleStatus(name, visibleStatus.equals("ACTIVE"), pageable); + Page hackathons = hackathonRepository.findByNameContainingAndVisibleStatus(name, visibleStatus.equals("ACTIVE"), pageable, sort); return AdminHackathonResponse.from(hackathons); } if(name != null) { - Page hackathons = hackathonRepository.findByNameContaining(name, pageable); + Page hackathons = hackathonRepository.findByNameContaining(name, pageable, sort); return AdminHackathonResponse.from(hackathons); } if(visibleStatus != null) { - Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageable); + Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageable, sort); return AdminHackathonResponse.from(hackathons); } - - Page hackathons = hackathonRepository.findAll(pageable); + Page hackathons = hackathonRepository.findAll(pageable, sort); return AdminHackathonResponse.from(hackathons); } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java index 04589289..d1f5d37c 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -2,12 +2,13 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import sw_css.hackathon.domain.Hackathon; public interface HackathonRepository extends JpaRepository{ - Page findAll(Pageable pageable); - Page findByNameContaining(String name, Pageable pageable); - Page findByVisibleStatus(boolean visibleStatus, Pageable pageable); - Page findByNameContainingAndVisibleStatus(String name, boolean visibleStatus, Pageable pageable); + Page findAll(Pageable pageable, Sort sort); + Page findByNameContaining(String name, Pageable pageable, Sort sort); + Page findByVisibleStatus(boolean visibleStatus, Pageable pageable, Sort sort); + Page findByNameContainingAndVisibleStatus(String name, boolean visibleStatus, Pageable pageable, Sort sort); } From 1445a75654b69cdfa3fc186de4760dcde1aa3794 Mon Sep 17 00:00:00 2001 From: llddang Date: Sun, 29 Dec 2024 23:32:01 +0900 Subject: [PATCH 14/35] =?UTF-8?q?feat:=20hackathon=20vote=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- ...ler.java => AdminHackathonController.java} | 26 ++++++------- ...java => AdminHackathonCommandService.java} | 3 +- ...e.java => AdminHackathonQueryService.java} | 24 ++++++------ .../hackathon/domain/HackathonTeam.java | 3 -- .../hackathon/domain/HackathonTeamMember.java | 4 ++ .../hackathon/domain/HackathonTeamVote.java | 37 +++++++++++++++++++ .../repository/HackathonRepository.java | 8 ++-- .../repository/HackathonTeamRepository.java | 23 ++++++------ .../HackathonTeamVoteRepository.java | 8 ++++ backend/src/main/resources/schema.sql | 25 +++++++++---- backend/src/main/resources/test-data.sql | 37 +++++++++++++------ 11 files changed, 135 insertions(+), 63 deletions(-) rename backend/src/main/java/sw_css/admin/hackathon/api/{HackathonController.java => AdminHackathonController.java} (82%) rename backend/src/main/java/sw_css/admin/hackathon/application/{HackathonCommandService.java => AdminHackathonCommandService.java} (99%) rename backend/src/main/java/sw_css/admin/hackathon/application/{HackathonQueryService.java => AdminHackathonQueryService.java} (88%) create mode 100644 backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java similarity index 82% rename from backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java rename to backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java index abc2ff2e..1f9ce822 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java @@ -22,8 +22,8 @@ import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import sw_css.admin.hackathon.application.HackathonCommandService; -import sw_css.admin.hackathon.application.HackathonQueryService; +import sw_css.admin.hackathon.application.AdminHackathonCommandService; +import sw_css.admin.hackathon.application.AdminHackathonQueryService; import sw_css.admin.hackathon.application.dto.request.AdminHackathonActiveRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; @@ -36,9 +36,9 @@ @RequestMapping("/admin/hackathons") @RestController @RequiredArgsConstructor -public class HackathonController { - private final HackathonCommandService hackathonCommandService; - private final HackathonQueryService hackathonQueryService; +public class AdminHackathonController { + private final AdminHackathonCommandService adminHackathonCommandService; + private final AdminHackathonQueryService adminHackathonQueryService; @GetMapping public ResponseEntity> findAllHackathons( @@ -48,7 +48,7 @@ public ResponseEntity> findAllHackathons( @RequestParam(value = "visibleStatus", required = false) final String visibleStatus ) { return ResponseEntity.ok( - hackathonQueryService.findAllHackathons(pageable, name, visibleStatus) + adminHackathonQueryService.findAllHackathons(pageable, name, visibleStatus) ); } @@ -58,7 +58,7 @@ public ResponseEntity findHackathonById( @PathVariable final Long hackathonId ){ return ResponseEntity.ok( - hackathonQueryService.findHackathonById(hackathonId)); + adminHackathonQueryService.findHackathonById(hackathonId)); } @PostMapping @@ -66,7 +66,7 @@ public ResponseEntity registerHackathon( @AdminInterface FacultyMember facultyMember, @RequestPart(value = "file", required = false) final MultipartFile file, @RequestPart(value = "request") @Valid final AdminHackathonRequest request) { - final Long registeredHackathonId = hackathonCommandService.registerHackathon(file, request); + final Long registeredHackathonId = adminHackathonCommandService.registerHackathon(file, request); return ResponseEntity.created(URI.create("/admin/hackathon/" + registeredHackathonId)).build(); } @@ -77,7 +77,7 @@ public ResponseEntity updateHackathon( @RequestPart(value = "request") @Valid final AdminHackathonRequest request, @PathVariable final Long hackathonId ) { - hackathonCommandService.updateHackathon(hackathonId, file, request); + adminHackathonCommandService.updateHackathon(hackathonId, file, request); return ResponseEntity.noContent().build(); } @@ -86,7 +86,7 @@ public ResponseEntity deleteHackathon( @AdminInterface FacultyMember facultyMember, @PathVariable final Long hackathonId ){ - hackathonCommandService.deleteHackathon(hackathonId); + adminHackathonCommandService.deleteHackathon(hackathonId); return ResponseEntity.noContent().build(); } @@ -100,7 +100,7 @@ public ResponseEntity downloadVotes( return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFilename + "\"; filename*=UTF-8''" + encodedFilename) .contentType(MediaType.APPLICATION_OCTET_STREAM) - .body(hackathonQueryService.downloadHackathonVotesById(hackathonId)); + .body(adminHackathonQueryService.downloadHackathonVotesById(hackathonId)); } @@ -110,7 +110,7 @@ public ResponseEntity patchHackathon( @PathVariable final Long hackathonId, @RequestBody @Valid AdminHackathonActiveRequest request ){ - hackathonCommandService.activeHackathon(hackathonId, request.visibleStatus()); + adminHackathonCommandService.activeHackathon(hackathonId, request.visibleStatus()); return ResponseEntity.noContent().build(); } @@ -120,7 +120,7 @@ public ResponseEntity patchHackathonPrize( @PathVariable final Long hackathonId, @RequestBody @Valid AdminHackathonPrizeRequest request ){ - hackathonCommandService.hackathonChangePrize(hackathonId, request.teams()); + adminHackathonCommandService.hackathonChangePrize(hackathonId, request.teams()); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java similarity index 99% rename from backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java rename to backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java index 46f3958e..0b2f633c 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java @@ -7,7 +7,6 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.time.LocalDate; -import java.util.Arrays; import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -31,7 +30,7 @@ @Service @RequiredArgsConstructor @Transactional -public class HackathonCommandService { +public class AdminHackathonCommandService { private final HackathonTeamRepository hackathonTeamRepository; @Value("${data.file-path-prefix}") diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java similarity index 88% rename from backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java rename to backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java index defcc314..21c70acd 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java @@ -2,7 +2,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; @@ -17,6 +16,7 @@ import org.apache.poi.xssf.usermodel.XSSFFont; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; @@ -26,38 +26,38 @@ import sw_css.admin.hackathon.domain.HackathonStatus; import sw_css.admin.hackathon.exception.HackathonException; import sw_css.admin.hackathon.exception.HackathonExceptionType; -import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.HackathonTeam; import sw_css.hackathon.domain.repository.HackathonRepository; import sw_css.hackathon.domain.repository.HackathonTeamRepository; -import sw_css.milestone.exception.MilestoneHistoryException; -import sw_css.milestone.exception.MilestoneHistoryExceptionType; +import sw_css.hackathon.domain.repository.HackathonTeamVoteRepository; @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class HackathonQueryService { +public class AdminHackathonQueryService { private final HackathonRepository hackathonRepository; private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamVoteRepository hackathonTeamVoteRepository; - public Page findAllHackathons(final Pageable pageable, + public Page findAllHackathons(Pageable pageable, final String name, final String visibleStatus) { Sort sort = Sort.by(Sort.Order.desc("hackathonStartDate")); + Pageable pageableWithSort = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); if(name != null && visibleStatus != null) { - Page hackathons = hackathonRepository.findByNameContainingAndVisibleStatus(name, visibleStatus.equals("ACTIVE"), pageable, sort); + Page hackathons = hackathonRepository.findByNameContainingAndVisibleStatus(name, visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageableWithSort); return AdminHackathonResponse.from(hackathons); } if(name != null) { - Page hackathons = hackathonRepository.findByNameContaining(name, pageable, sort); + Page hackathons = hackathonRepository.findByNameContaining(name, pageableWithSort); return AdminHackathonResponse.from(hackathons); } if(visibleStatus != null) { - Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageable, sort); + Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageableWithSort); return AdminHackathonResponse.from(hackathons); } - Page hackathons = hackathonRepository.findAll(pageable, sort); + Page hackathons = hackathonRepository.findAll(pageableWithSort); return AdminHackathonResponse.from(hackathons); } @@ -110,7 +110,9 @@ private byte[] generateHackathonVoteExcelFile(final List hackatho bodyCell.setCellStyle(bodyXssfCellStyle); } bodyRow.getCell(0).setCellValue(i+1); - bodyRow.getCell(1).setCellValue(hackathonTeams.get(i).getVote()); + Long voteCount = hackathonTeamVoteRepository.countByHackathonIdAndTeamId( + hackathonTeams.get(i).getHackathon().getId(), hackathonTeams.get(i).getId()); + bodyRow.getCell(1).setCellValue(voteCount); bodyRow.getCell(2).setCellValue(hackathonTeams.get(i).getName()); bodyRow.getCell(3).setCellValue(hackathonTeams.get(i).getWork()); } diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java index a67e09d2..b1ecbe4a 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -43,9 +43,6 @@ public class HackathonTeam { @Column(nullable = false) private String githubUrl; - @Column(nullable = false) - private int vote; - @Column(nullable = false) private String prize; } diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java index c71c7e71..9477cc20 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java @@ -25,6 +25,10 @@ public class HackathonTeamMember { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "hackathon_id", nullable = false) + private Hackathon hackathon; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id", nullable = false) private HackathonTeam team; diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java new file mode 100644 index 00000000..7e7fb585 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java @@ -0,0 +1,37 @@ +package sw_css.hackathon.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLRestriction; +import sw_css.member.domain.StudentMember; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class HackathonTeamVote { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "hackathon_id", nullable = false) + private Hackathon hackathon; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id", nullable = false) + private HackathonTeam team; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "student_id", nullable = false) + private StudentMember studentMember; +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java index d1f5d37c..6d437c4f 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -7,8 +7,8 @@ import sw_css.hackathon.domain.Hackathon; public interface HackathonRepository extends JpaRepository{ - Page findAll(Pageable pageable, Sort sort); - Page findByNameContaining(String name, Pageable pageable, Sort sort); - Page findByVisibleStatus(boolean visibleStatus, Pageable pageable, Sort sort); - Page findByNameContainingAndVisibleStatus(String name, boolean visibleStatus, Pageable pageable, Sort sort); + Page findAll(Pageable pageable); + Page findByNameContaining(String name, Pageable pageable); + Page findByVisibleStatus(boolean visibleStatus, Pageable pageable); + Page findByNameContainingAndVisibleStatus(String name, boolean visibleStatus, Pageable pageable); } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index ea284986..31dcac96 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -7,18 +7,19 @@ import sw_css.hackathon.domain.HackathonTeam; public interface HackathonTeamRepository extends JpaRepository { - @Query("SELECT h FROM HackathonTeam h " + - "WHERE h.hackathon.id = :hackathonId " + + @Query("SELECT ht.id AS team_id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, COUNT(htv.id) AS vote_count " + + "FROM HackathonTeam ht " + + "LEFT JOIN HackathonTeamVote htv ON ht.id = htv.team.id AND ht.hackathon.id = htv.hackathon.id " + + "GROUP BY ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, ht.hackathon.id " + "ORDER BY " + - "CASE h.prize " + - " WHEN 'GRAND_PRIZE' THEN 1 " + - " WHEN 'EXCELLENCE_PRIZE' THEN 2 " + - " WHEN 'MERIT_PRIZE' THEN 3 " + - " WHEN 'ENCOURAGEMENT_PRIZE' THEN 4 " + - " WHEN 'NONE_PRIZE' THEN 5 " + - " ELSE 6 " + - "END, " + - "h.vote DESC") + "CASE ht.prize " + + "WHEN 'GRAND_PRIZE' THEN 1 " + + "WHEN 'EXCELLENCE_PRIZE' THEN 2 " + + "WHEN 'MERIT_PRIZE' THEN 3 " + + "WHEN 'ENCOURAGEMENT_PRIZE' THEN 4 " + + "WHEN 'NONE_PRIZE' THEN 5 " + + "ELSE 6 END, " + + "COUNT(htv.id) DESC") List findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); List findByHackathonId(Long hackathonId); diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java new file mode 100644 index 00000000..9f7d415f --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java @@ -0,0 +1,8 @@ +package sw_css.hackathon.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import sw_css.hackathon.domain.HackathonTeamVote; + +public interface HackathonTeamVoteRepository extends JpaRepository { + Long countByHackathonIdAndTeamId(Long hackathonId, Long teamId); +} diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 23e5eb2d..09e8f64b 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -8,6 +8,7 @@ drop table if exists sw_css.milestone_category; drop table if exists sw_css.milestone_history; drop table if exists sw_css.hackathon; drop table if exists sw_css.hackathon_team; +drop table if exists sw_css.hackathon_team_vote; drop table if exists sw_css.hackathon_team_member; create table member @@ -113,18 +114,28 @@ create table hackathon_team image_url varchar(255) not null, work varchar(255) not null, github_url varchar(255) not null, - vote int not null, prize varchar(255), is_deleted boolean not null, created_at datetime(6) not null default current_timestamp(6) ); +create table hackathon_team_vote +( + id bigint auto_increment primary key, + hackathon_id bigint not null, + team_id bigint not null, + student_id bigint not null, + is_deleted boolean not null, + created_at datetime(6) not null default current_timestamp(6) +); + create table hackathon_team_member ( - id bigint auto_increment primary key, - team_id bigint not null, - student_id bigint not null, - role varchar(255) not null, - is_deleted boolean not null, - created_at datetime(6) not null default current_timestamp(6) + id bigint auto_increment primary key, + hackathon_id bigint not null, + team_id bigint not null, + student_id bigint not null, + role varchar(255) not null, + is_deleted boolean not null, + created_at datetime(6) not null default current_timestamp(6) ); diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index ee0db920..580a7643 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -31,19 +31,32 @@ values('제4회 PNU 창의융합 소프트웨어해커톤', ' 1. qwer', '1234', '2022-03-22', '2022-05-29', '2022-12-22', '2023-03-07', '1.png', true, false); ## hackathon team -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) -values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', '28', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) -values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', '52', 'EXCELLENCE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) -values(1, '코드플레이스', '1.png', '코드 플레이스 코드 플레이스', 'https://github.com/pnu-code-place/code-place', '92', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) -values(1, 'Next JS', '1.png', '암어 넥스트 레블 절대로', 'https://github.com/vercel/next.js', '18', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) -values(1, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', '22', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, vote, prize, is_deleted) -values(2, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', '22', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '코드플레이스', '1.png', '코드 플레이스 코드 플레이스', 'https://github.com/pnu-code-place/code-place', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, 'Next JS', '1.png', '암어 넥스트 레블 절대로', 'https://github.com/vercel/next.js', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(2, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', 'NONE_PRIZE', false); +## hackathon team vote +insert into hackathon_team_vote (hackathon_id, team_id, student_id) +values(1, 2, 202012341); +insert into hackathon_team_vote (hackathon_id, team_id, student_id) +values(1, 2, 202012342); +insert into hackathon_team_vote (hackathon_id, team_id, student_id) +values(1, 2, 202012343); +insert into hackathon_team_vote (hackathon_id, team_id, student_id) +values(1, 3, 202012344); +insert into hackathon_team_vote (hackathon_id, team_id, student_id) +values(1, 3, 202012343); +insert into hackathon_team_vote (hackathon_id, team_id, student_id) +values(1, 1, 202012345); ## milestone histories From 6ee51dc2b97eb7fb592dd4ab24f43f2a0cb063e1 Mon Sep 17 00:00:00 2001 From: llddang Date: Sun, 29 Dec 2024 23:37:46 +0900 Subject: [PATCH 15/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20test=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 13 ++ .../java/sw_css/restdocs/RestDocsTest.java | 8 ++ .../docs/admin/AdminHackathonApiDocsTest.java | 120 ++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 7225f58f..bee9c46e 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -349,3 +349,16 @@ include::{snippets}/admin-auth-delete/request-body.adoc[] .HTTP Response include::{snippets}/admin-auth-delete/http-response.adoc[] + +== 관리자 - 해커톤 + +=== `GET`: 관리자의 해커톤 전체 목록 조회 + +.HTTP Request +include::{snippets}/admin-hackathon-find-all/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-find-all/query-parameters.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-find-all/http-response.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index 9b382b26..e8c497f2 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -15,6 +15,8 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; import sw_css.admin.auth.application.AdminAuthCommandService; +import sw_css.admin.hackathon.application.AdminHackathonCommandService; +import sw_css.admin.hackathon.application.AdminHackathonQueryService; import sw_css.admin.member.application.MemberAdminQueryService; import sw_css.admin.milestone.application.MilestoneHistoryAdminCommandService; import sw_css.admin.milestone.application.MilestoneHistoryAdminQueryService; @@ -63,6 +65,12 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected MemberAdminQueryService memberAdminQueryService; + @MockBean + protected AdminHackathonQueryService adminHackathonQueryService; + + @MockBean + protected AdminHackathonCommandService adminHackathonCommandService; + @MockBean protected FileService fileService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java new file mode 100644 index 00000000..b22b4b3b --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -0,0 +1,120 @@ +package sw_css.restdocs.docs.admin; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; +import sw_css.admin.hackathon.api.AdminHackathonController; +import sw_css.admin.hackathon.application.AdminHackathonQueryService; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; +import sw_css.hackathon.domain.Hackathon; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(AdminHackathonController.class) +public class AdminHackathonApiDocsTest extends RestDocsTest { + + @Test + @DisplayName("[성공] 관리자가 해커톤 전체 목록 조회 가능") + public void findAllHackathons() throws Exception { + // given + final QueryParametersSnippet queryParameters = queryParameters( + parameterWithName("page").optional().description("조회할 해커톤의 페이지 번호"), + parameterWithName("size").optional().description("조회할 해커톤의 페이지 당 데이터 수"), + parameterWithName("name").optional().description("조죄할 해커톤 명"), + parameterWithName("visibleStatus").optional().description("조회할 해커톤 상태 (ACTIVE / INACTIVE)") + ); + + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("totalElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("size").type(JsonFieldType.NUMBER).description("페이지 내 데이터 수"), + fieldWithPath("number").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("sort.empty").type(JsonFieldType.BOOLEAN).description("정렬 속성의 존재 여부"), + fieldWithPath("sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬 여부"), + fieldWithPath("sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬여부"), + fieldWithPath("first").type(JsonFieldType.BOOLEAN).description("첫 페이지인지 여부"), + fieldWithPath("last").type(JsonFieldType.BOOLEAN).description("마지막 페이지인지 여부"), + fieldWithPath("pageable.pageNumber").type(JsonFieldType.NUMBER).description("요청한 페이지번호"), + fieldWithPath("pageable.pageSize").type(JsonFieldType.NUMBER).description("요청한 페이지크기"), + fieldWithPath("pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("요청한 데이터가 비었는지 여부"), + fieldWithPath("pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.offset").type(JsonFieldType.NUMBER).description("요청한 페이지오프셋"), + fieldWithPath("pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("pageable.unpaged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("numberOfElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("empty").type(JsonFieldType.BOOLEAN).description("데이터의 존재 여부"), + fieldWithPath("content[].id").type(JsonFieldType.NUMBER).description("해커톤 id"), + fieldWithPath("content[].name").type(JsonFieldType.STRING).description("해커톤 명"), + fieldWithPath("content[].hackathonStartDate").type(JsonFieldType.STRING).description("해커톤 시작 날(yyyy-MM-dd)"), + fieldWithPath("content[].hackathonEndDate").type(JsonFieldType.STRING).description("해커톤 마지막 날(yyyy-MM-dd)"), + fieldWithPath("content[].password").type(JsonFieldType.STRING).description("해커톤 비밀번호"), + fieldWithPath("content[].visibleStatus").type(JsonFieldType.BOOLEAN).description("해커톤 활성화 상태") + ); + + final Page hackathonPage = new PageImpl<>(List.of( + new Hackathon(1L, "제5회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1234", LocalDate.parse("2024-05-22"), LocalDate.parse("2024-05-29"), LocalDate.parse("2024-05-22"), LocalDate.parse("2024-09-07"), "1.png", true, false), + new Hackathon(2L, "제4회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1234", LocalDate.parse("2022-03-22"), LocalDate.parse("2022-05-29"), LocalDate.parse("2022-12-22"), LocalDate.parse("2023-03-07"), "1.png", true, false)), + PageRequest.of(0, 10), + 2 + ); + + final Page response = AdminHackathonResponse.from(hackathonPage); + + final String hackathonName = "해커톤"; + final String visibleStatus = "ACTIVE"; + + final String token = "Bearer AccessToken"; + + // when + when(adminHackathonQueryService.findAllHackathons(any(), any(), any())).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/admin/hackathons") + .param("name", hackathonName) + .param("visibleStatus", visibleStatus) + .param("page", "0") + .param("size", "10") + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isOk()) + .andDo(document("admin-hackathon-find-all",queryParameters, responseBodySnippet)); + } + + // 해커톤 상세 조회 + + // 해커톤 생성 + + // 해커톤 수정 + + // 해커톤 삭제 + + // 해커톤 투표 결과 다운로드 + + // 해커톤 활성화 수정 + + // 해커톤 상장 수정 + + +} From 2b62c1092e7be8126ae18f3b273194fadaa2bf80 Mon Sep 17 00:00:00 2001 From: llddang Date: Sun, 29 Dec 2024 23:53:07 +0900 Subject: [PATCH 16/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20API=20test=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 14 ++++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 34 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index bee9c46e..234aec0c 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -362,3 +362,17 @@ include::{snippets}/admin-hackathon-find-all/query-parameters.adoc[] .HTTP Response include::{snippets}/admin-hackathon-find-all/http-response.adoc[] + +.Response Body +include::{snippets}/admin-hackathon-find-all/response-fields.adoc[] + +=== `GET`: 관리자의 특정 해커톤 조회 + +.HTTP Request +include::{snippets}/admin-hackathon-find/http-request.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-find/http-response.adoc[] + +.Response Body +include::{snippets}/admin-hackathon-find/response-fields.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index b22b4b3b..36266439 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -27,6 +27,7 @@ import org.springframework.restdocs.request.QueryParametersSnippet; import sw_css.admin.hackathon.api.AdminHackathonController; import sw_css.admin.hackathon.application.AdminHackathonQueryService; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.hackathon.domain.Hackathon; import sw_css.restdocs.RestDocsTest; @@ -103,6 +104,39 @@ public void findAllHackathons() throws Exception { } // 해커톤 상세 조회 + @Test + @DisplayName("[성공] 관리자가 해커톤 상세 조회 가능") + public void findHackathon() throws Exception { + // given + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("해커톤 id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("해커톤 명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("해커톤 내용"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("해커톤 배너 이미지"), + fieldWithPath("applyStartDate").type(JsonFieldType.STRING).description("해커톤 지원 시작일"), + fieldWithPath("applyEndDate").type(JsonFieldType.STRING).description("해커톤 지원 마지막날"), + fieldWithPath("hackathonStartDate").type(JsonFieldType.STRING).description("해커톤 대회 시작일"), + fieldWithPath("hackathonEndDate").type(JsonFieldType.STRING).description("해커톤 대회 마지막날"), + fieldWithPath("password").type(JsonFieldType.STRING).description("해커톤 비밀번호"), + fieldWithPath("visibleStatus").type(JsonFieldType.BOOLEAN).description("해커톤 활성화 상태") + ); + + final AdminHackathonDetailResponse response = new AdminHackathonDetailResponse( + 1L, "제5회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1.png", LocalDate.parse("2024-05-22"), LocalDate.parse("2024-05-29"), LocalDate.parse("2024-05-22"), LocalDate.parse("2024-09-07"), "1234", true + ); + final Long hackathonId = 1L; + final String token = "Bearer AccessToken"; + + // when + when(adminHackathonQueryService.findHackathonById(hackathonId)).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/admin/hackathons/{hackathonId}", hackathonId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isOk()) + .andDo(document("admin-hackathon-find", responseBodySnippet)); + } // 해커톤 생성 From 3eea364b8c0b68295c2f5d97249b7c112d8139ca Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 00:05:53 +0900 Subject: [PATCH 17/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?API=20Test=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 11 +++++ .../docs/admin/AdminHackathonApiDocsTest.java | 40 ++++++++++++++++++- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 234aec0c..b006eba3 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -376,3 +376,14 @@ include::{snippets}/admin-hackathon-find/http-response.adoc[] .Response Body include::{snippets}/admin-hackathon-find/response-fields.adoc[] + +=== `POST`: 관리자의 해커톤 생성 + +.HTTP Request +include::{snippets}/admin-hackathon-register/http-request.adoc[] + +.Request Body +include::{snippets}/admin-hackathon-register/request-parts.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-register/http-response.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index 36266439..454828bd 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -1,12 +1,16 @@ package sw_css.restdocs.docs.admin; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -21,12 +25,17 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.QueryParametersSnippet; +import org.springframework.restdocs.request.RequestPartsSnippet; import sw_css.admin.hackathon.api.AdminHackathonController; import sw_css.admin.hackathon.application.AdminHackathonQueryService; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.hackathon.domain.Hackathon; @@ -103,7 +112,6 @@ public void findAllHackathons() throws Exception { .andDo(document("admin-hackathon-find-all",queryParameters, responseBodySnippet)); } - // 해커톤 상세 조회 @Test @DisplayName("[성공] 관리자가 해커톤 상세 조회 가능") public void findHackathon() throws Exception { @@ -138,7 +146,35 @@ public void findHackathon() throws Exception { .andDo(document("admin-hackathon-find", responseBodySnippet)); } - // 해커톤 생성 + @Test + @DisplayName("[성공] 관리자의 해커톤 생성") + public void registerHackathon() throws Exception { + // given + final RequestPartsSnippet requestPartsSnippet = requestParts( + partWithName("request").description( + "해커톤 정보( name: 해커톤 명, description: 해커톤 내용, password: 해커톤 비밀번호, applyStartDate: 해커톤 지원 시작일(yyy-MM-dd), applyEndDate: 해커톤 지원 미자믹일(yyy-MM-dd), hackathonStartDate: 해커돈 대회 시작일(yyy-MM-dd), hackathonEndDate: 해커톤 대회 마지막일(yyy-MM-dd))"), + partWithName("file").description("해커톤 배너 이미지")); + + final MockMultipartFile file = new MockMultipartFile("file", "test.png", "multipart/form-data", "example".getBytes()); + final AdminHackathonRequest request = new AdminHackathonRequest("제5회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1234", LocalDate.parse("2024-05-22"), LocalDate.parse("2024-05-29"), LocalDate.parse("2024-05-22"), LocalDate.parse("2024-09-07")); + final MockMultipartFile requestFile = new MockMultipartFile("request", null, "application/json", objectMapper.writeValueAsString(request).getBytes()); + final String token = "Bearer AccessToken"; + final Long hackathonId = 1L; + + // when + when(adminHackathonCommandService.registerHackathon(file, request)).thenReturn(hackathonId); + + // then + mockMvc.perform( + multipart("/admin/hackathons") + .file(file) + .file(requestFile) + .contentType(MediaType.MULTIPART_MIXED) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isCreated()) + .andDo(document("admin-hackathon-register", requestPartsSnippet)); + } // 해커톤 수정 From f8b5a16f65e5fbf17ddaa907c8dbabb1aa2d0cf3 Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 00:18:20 +0900 Subject: [PATCH 18/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?API=20test=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 11 ++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 35 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index b006eba3..d6ca8059 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -387,3 +387,14 @@ include::{snippets}/admin-hackathon-register/request-parts.adoc[] .HTTP Response include::{snippets}/admin-hackathon-register/http-response.adoc[] + +=== `PATCH`: 관리자의 해커톤 수정 + +.HTTP Request +include::{snippets}/admin-hackathon-update/http-request.adoc[] + +.Request Body +include::{snippets}/admin-hackathon-update/request-parts.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-update/http-response.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index 454828bd..c65afd50 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -176,7 +176,40 @@ public void registerHackathon() throws Exception { .andDo(document("admin-hackathon-register", requestPartsSnippet)); } - // 해커톤 수정 + @Test + @DisplayName("[성공] 관리자의 해커톤 수정") + public void updateHackathon() throws Exception { + // given + final RequestPartsSnippet requestPartsSnippet = requestParts( + partWithName("request").description( + "해커톤 정보( name: 해커톤 명, description: 해커톤 내용, password: 해커톤 비밀번호, applyStartDate: 해커톤 지원 시작일(yyy-MM-dd), applyEndDate: 해커톤 지원 미자믹일(yyy-MM-dd), hackathonStartDate: 해커돈 대회 시작일(yyy-MM-dd), hackathonEndDate: 해커톤 대회 마지막일(yyy-MM-dd))"), + partWithName("file").optional().description("해커톤 배너 이미지: optional")); + + final MockMultipartFile file = new MockMultipartFile("file", "test.png", "multipart/form-data", "example".getBytes()); + final AdminHackathonRequest request = new AdminHackathonRequest("제5회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1234", LocalDate.parse("2024-05-22"), LocalDate.parse("2024-05-29"), LocalDate.parse("2024-05-22"), LocalDate.parse("2024-09-07")); + final MockMultipartFile requestFile = new MockMultipartFile("request", null, "application/json", objectMapper.writeValueAsString(request).getBytes()); + final String token = "Bearer AccessToken"; + final Long hackathonId = 1L; + + // when + doNothing().when(adminHackathonCommandService).updateHackathon(hackathonId, file, request); + + // then + mockMvc.perform( + multipart("/admin/hackathons/{hackathonId}", hackathonId) + .file(file) + .file(requestFile) + .contentType(MediaType.MULTIPART_MIXED) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token) + .with(request1 -> { + request1.setMethod("PATCH"); + return request1; + })) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-update", requestPartsSnippet)); + } + // 해커톤 삭제 From fab10cc02bf1f6cea331ec3b46129e29fe11c6aa Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 00:26:41 +0900 Subject: [PATCH 19/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?API=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 17 ++++++++ .../api/AdminHackathonController.java | 2 +- .../docs/admin/AdminHackathonApiDocsTest.java | 42 +++++++++++++++---- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index d6ca8059..9d1e5adf 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -374,6 +374,9 @@ include::{snippets}/admin-hackathon-find/http-request.adoc[] .HTTP Response include::{snippets}/admin-hackathon-find/http-response.adoc[] +.Path Parameters +include::{snippets}/admin-hackathon-find/path-parameters.adoc[] + .Response Body include::{snippets}/admin-hackathon-find/response-fields.adoc[] @@ -396,5 +399,19 @@ include::{snippets}/admin-hackathon-update/http-request.adoc[] .Request Body include::{snippets}/admin-hackathon-update/request-parts.adoc[] +.Path Parameters +include::{snippets}/admin-hackathon-update/path-parameters.adoc[] + .HTTP Response include::{snippets}/admin-hackathon-update/http-response.adoc[] + +=== `DELETE`: 관리자의 해커톤 삭제 + +.HTTP Request +include::{snippets}/admin-hackathon-delete/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-delete/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-delete/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java index 1f9ce822..34508cac 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java @@ -64,7 +64,7 @@ public ResponseEntity findHackathonById( @PostMapping public ResponseEntity registerHackathon( @AdminInterface FacultyMember facultyMember, - @RequestPart(value = "file", required = false) final MultipartFile file, + @RequestPart(value = "file") final MultipartFile file, @RequestPart(value = "request") @Valid final AdminHackathonRequest request) { final Long registeredHackathonId = adminHackathonCommandService.registerHackathon(file, request); return ResponseEntity.created(URI.create("/admin/hackathon/" + registeredHackathonId)).build(); diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index c65afd50..9a137556 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -9,6 +9,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -31,6 +32,7 @@ import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.PathParametersSnippet; import org.springframework.restdocs.request.QueryParametersSnippet; import org.springframework.restdocs.request.RequestPartsSnippet; import sw_css.admin.hackathon.api.AdminHackathonController; @@ -45,7 +47,7 @@ public class AdminHackathonApiDocsTest extends RestDocsTest { @Test - @DisplayName("[성공] 관리자가 해커톤 전체 목록 조회 가능") + @DisplayName("[성공] 관리자가 해커톤 전체 목록을 조회할 수 있다.") public void findAllHackathons() throws Exception { // given final QueryParametersSnippet queryParameters = queryParameters( @@ -113,9 +115,12 @@ public void findAllHackathons() throws Exception { } @Test - @DisplayName("[성공] 관리자가 해커톤 상세 조회 가능") + @DisplayName("[성공] 관리자가 해커톤 상세 조회할 수 있다.") public void findHackathon() throws Exception { // given + final PathParametersSnippet pathParameters = pathParameters( + parameterWithName("hackathonId").description("해커톤 id") + ); final ResponseFieldsSnippet responseBodySnippet = responseFields( fieldWithPath("id").type(JsonFieldType.NUMBER).description("해커톤 id"), fieldWithPath("name").type(JsonFieldType.STRING).description("해커톤 명"), @@ -143,11 +148,11 @@ public void findHackathon() throws Exception { RestDocumentationRequestBuilders.get("/admin/hackathons/{hackathonId}", hackathonId) .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isOk()) - .andDo(document("admin-hackathon-find", responseBodySnippet)); + .andDo(document("admin-hackathon-find", pathParameters, responseBodySnippet)); } @Test - @DisplayName("[성공] 관리자의 해커톤 생성") + @DisplayName("[성공] 관리자의 해커톤 생성할 수 있다.") public void registerHackathon() throws Exception { // given final RequestPartsSnippet requestPartsSnippet = requestParts( @@ -177,9 +182,13 @@ public void registerHackathon() throws Exception { } @Test - @DisplayName("[성공] 관리자의 해커톤 수정") + @DisplayName("[성공] 관리자의 해커톤 수정할 수 있다.") public void updateHackathon() throws Exception { // given + final PathParametersSnippet pathParameters = pathParameters( + parameterWithName("hackathonId").description("해커톤 id") + ); + final RequestPartsSnippet requestPartsSnippet = requestParts( partWithName("request").description( "해커톤 정보( name: 해커톤 명, description: 해커톤 내용, password: 해커톤 비밀번호, applyStartDate: 해커톤 지원 시작일(yyy-MM-dd), applyEndDate: 해커톤 지원 미자믹일(yyy-MM-dd), hackathonStartDate: 해커돈 대회 시작일(yyy-MM-dd), hackathonEndDate: 해커톤 대회 마지막일(yyy-MM-dd))"), @@ -207,11 +216,30 @@ public void updateHackathon() throws Exception { return request1; })) .andExpect(status().isNoContent()) - .andDo(document("admin-hackathon-update", requestPartsSnippet)); + .andDo(document("admin-hackathon-update", pathParameters, requestPartsSnippet)); } - // 해커톤 삭제 + @Test + @DisplayName("[성공] 관리자는 해커톤을 삭제할 수 있다.") + public void deleteHackathon() throws Exception { + // given + final PathParametersSnippet pathParameters = pathParameters( + parameterWithName("hackathonId").description("해커톤 id") + ); + + final Long hackathonId = 1L; + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminHackathonCommandService).deleteHackathon(hackathonId); + + // then + mockMvc.perform(RestDocumentationRequestBuilders.delete("/admin/hackathons/{hackathonId}", hackathonId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-delete", pathParameters)); + } // 해커톤 투표 결과 다운로드 From 34c96e0ef4ff0b2bdeb7f0c720177d077ce9d081 Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 00:31:56 +0900 Subject: [PATCH 20/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20API?= =?UTF-8?q?=20test=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 11 +++++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 23 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 9d1e5adf..91187991 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -415,3 +415,14 @@ include::{snippets}/admin-hackathon-delete/path-parameters.adoc[] .HTTP Response include::{snippets}/admin-hackathon-delete/http-response.adoc[] + +=== `GET`: 관리자의 해커톤 투표 결과 다운로드 + +.HTTP Request +include::{snippets}/admin-hackathon-download-vote/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-download-vote/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-download-vote/http-response.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index 9a137556..9af73a40 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -219,7 +219,6 @@ public void updateHackathon() throws Exception { .andDo(document("admin-hackathon-update", pathParameters, requestPartsSnippet)); } - // 해커톤 삭제 @Test @DisplayName("[성공] 관리자는 해커톤을 삭제할 수 있다.") public void deleteHackathon() throws Exception { @@ -242,6 +241,28 @@ public void deleteHackathon() throws Exception { } // 해커톤 투표 결과 다운로드 + @Test + @DisplayName("[성공] 관리자는 해커톤 투표 결과를 다운로드 받을 수 있다.") + public void downloadHackathonVote() throws Exception { + // given + final PathParametersSnippet pathParameters = pathParameters( + parameterWithName("hackathonId").description("해커톤 id") + ); + + final byte[] response = new byte[]{}; + final Long hackathonId = 1L; + final String token = "Bearer AccessToken"; + + // when + when(adminHackathonQueryService.downloadHackathonVotesById(hackathonId)).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/admin/hackathons/{hackathonId}", hackathonId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isOk()) + .andDo(document("admin-hackathon-download-vote", pathParameters)); + } // 해커톤 활성화 수정 From cbf6c4e57a54fef7488491320033e016d362fcd2 Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 00:40:52 +0900 Subject: [PATCH 21/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=20=EC=83=81=ED=83=9C=20=EC=88=98=EC=A0=95=20API=20tes?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 14 ++++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 33 ++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 91187991..84cebf3a 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -426,3 +426,17 @@ include::{snippets}/admin-hackathon-download-vote/path-parameters.adoc[] .HTTP Response include::{snippets}/admin-hackathon-download-vote/http-response.adoc[] + +=== `PATCH`: 관리자의 해커톤 활성화 수정 + +.HTTP Request +include::{snippets}/admin-hackathon-update-active/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-update-active/path-parameters.adoc[] + +.Request Body +include::{snippets}/admin-hackathon-update-active/request-fields.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-update-active/http-response.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index 9af73a40..b8274536 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -3,9 +3,11 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; @@ -37,6 +39,7 @@ import org.springframework.restdocs.request.RequestPartsSnippet; import sw_css.admin.hackathon.api.AdminHackathonController; import sw_css.admin.hackathon.application.AdminHackathonQueryService; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonActiveRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; @@ -240,7 +243,6 @@ public void deleteHackathon() throws Exception { .andDo(document("admin-hackathon-delete", pathParameters)); } - // 해커톤 투표 결과 다운로드 @Test @DisplayName("[성공] 관리자는 해커톤 투표 결과를 다운로드 받을 수 있다.") public void downloadHackathonVote() throws Exception { @@ -258,13 +260,36 @@ public void downloadHackathonVote() throws Exception { // then mockMvc.perform( - RestDocumentationRequestBuilders.get("/admin/hackathons/{hackathonId}", hackathonId) - .header(HttpHeaders.AUTHORIZATION, token)) + RestDocumentationRequestBuilders.get("/admin/hackathons/{hackathonId}/download/votes", hackathonId) + .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isOk()) .andDo(document("admin-hackathon-download-vote", pathParameters)); } - // 해커톤 활성화 수정 + @Test + @DisplayName("[성공] 관리자는 해커톤의 활성화를 수정할 수 있다.") + public void updateHackathonActive() throws Exception { + // given + final PathParametersSnippet pathParameters = pathParameters(parameterWithName("hackathonId").description("해커톤 id")); + final RequestFieldsSnippet requestFieldsSnippet = requestFields(fieldWithPath("visibleStatus").type(JsonFieldType.STRING).description("해커톤 활성화 상태 (ACTIVE / INACTIVE)")); + + final AdminHackathonActiveRequest request = new AdminHackathonActiveRequest("ACTIVE"); + final Long hackathonId = 1L; + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminHackathonCommandService).activeHackathon(hackathonId, request.visibleStatus()); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.patch("/admin/hackathons/{hackathonId}/active", hackathonId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-update-active", pathParameters, requestFieldsSnippet)); + } + // 해커톤 상장 수정 From 93bfcc50f5799e3f5913de73a738fb2ad2f4d209 Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 00:54:00 +0900 Subject: [PATCH 22/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=ED=8C=80=20=EC=83=81?= =?UTF-8?q?=EC=9E=A5=20=EC=88=98=EC=A0=95=20API=20test=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 14 +++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 40 +++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 84cebf3a..e7bc496e 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -440,3 +440,17 @@ include::{snippets}/admin-hackathon-update-active/request-fields.adoc[] .HTTP Response include::{snippets}/admin-hackathon-update-active/http-response.adoc[] + +=== `PATCH`: 관리자의 해커톤 팀의 상장 수정 + +.HTTP Request +include::{snippets}/admin-hackathon-change-prize/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-change-prize/path-parameters.adoc[] + +.Request Body +include::{snippets}/admin-hackathon-change-prize/request-fields.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-change-prize/http-response.adoc[] diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index b8274536..aeded855 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -5,7 +5,10 @@ import static org.mockito.Mockito.when; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -40,10 +43,13 @@ import sw_css.admin.hackathon.api.AdminHackathonController; import sw_css.admin.hackathon.application.AdminHackathonQueryService; import sw_css.admin.hackathon.application.dto.request.AdminHackathonActiveRequest; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest.AdminTeam; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonPrize; import sw_css.restdocs.RestDocsTest; @WebMvcTest(AdminHackathonController.class) @@ -237,7 +243,7 @@ public void deleteHackathon() throws Exception { doNothing().when(adminHackathonCommandService).deleteHackathon(hackathonId); // then - mockMvc.perform(RestDocumentationRequestBuilders.delete("/admin/hackathons/{hackathonId}", hackathonId) + mockMvc.perform(delete("/admin/hackathons/{hackathonId}", hackathonId) .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isNoContent()) .andDo(document("admin-hackathon-delete", pathParameters)); @@ -282,7 +288,7 @@ public void updateHackathonActive() throws Exception { // then mockMvc.perform( - RestDocumentationRequestBuilders.patch("/admin/hackathons/{hackathonId}/active", hackathonId) + patch("/admin/hackathons/{hackathonId}/active", hackathonId) .contentType(APPLICATION_JSON) .content(objectMapper.writeValueAsString(request)) .header(HttpHeaders.AUTHORIZATION, token)) @@ -290,8 +296,36 @@ public void updateHackathonActive() throws Exception { .andDo(document("admin-hackathon-update-active", pathParameters, requestFieldsSnippet)); } + @Test + @DisplayName("[성공] 관리자는 해커톤 팀에게 상을 부여할 수 있다.") + public void updateHackathonTeamPrize() throws Exception { + // given + final PathParametersSnippet pathParameters = pathParameters(parameterWithName("hackathonId").description("해커톤 id")); + final RequestFieldsSnippet requestFieldsSnippet = requestFields( + fieldWithPath("teams[].id").type(JsonFieldType.NUMBER).description("해커톤 팀의 id"), + fieldWithPath("teams[].prize").type(JsonFieldType.STRING).description("해커톤 팀의 상 (GRAND_PRIZE: 대상, EXCELLENCE_PRIZE: 최우수상, MERIT_PRIZE: 우수상, ENCOURAGEMENT_PRIZE: 장려상, NONE_PRIZE: 상없음)") + ); - // 해커톤 상장 수정 + final List AdminTeams = List.of( + new AdminTeam(1L, HackathonPrize.GRAND_PRIZE.toString()), + new AdminTeam(2L, HackathonPrize.NONE_PRIZE.toString()) + ); + final AdminHackathonPrizeRequest request = new AdminHackathonPrizeRequest(AdminTeams); + final Long hackathonId = 1L; + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminHackathonCommandService).hackathonChangePrize(hackathonId, request.teams()); + + // then + mockMvc.perform( + patch("/admin/hackathons/{hackathonId}/prize", hackathonId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-change-prize", pathParameters, requestFieldsSnippet)); + } } From cbb70b443e05c79b6f3beba033dad708207f0b3a Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 03:01:38 +0900 Subject: [PATCH 23/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=ED=8C=80=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20test=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 16 +++ .../api/AdminHackathonTeamController.java | 39 ++++++ .../AdminHackathonTeamCommandService.java | 123 ++++++++++++++++++ .../request/AdminHackathonTeamRequest.java | 25 ++++ .../exception/HackathonExceptionType.java | 5 +- .../hackathon/domain/HackathonRole.java | 5 + .../hackathon/domain/HackathonTeamMember.java | 23 +++- .../hackathon/domain/HackathonTeamVote.java | 1 + .../HackathonTeamMemberRepository.java | 4 + .../repository/HackathonTeamRepository.java | 3 + backend/src/main/resources/schema.sql | 1 + backend/src/main/resources/test-data.sql | 30 ++--- .../java/sw_css/restdocs/RestDocsTest.java | 4 + .../admin/AdminHackathonTeamApiDocsTest.java | 71 ++++++++++ 14 files changed, 326 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java create mode 100644 backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java create mode 100644 backend/src/main/java/sw_css/hackathon/domain/HackathonRole.java create mode 100644 backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index e7bc496e..01b069ab 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -454,3 +454,19 @@ include::{snippets}/admin-hackathon-change-prize/request-fields.adoc[] .HTTP Response include::{snippets}/admin-hackathon-change-prize/http-response.adoc[] + + +== 관리자 - 해커톤 팀 +=== `PATCH`: 관리자의 해커톤 팀 수정 + +.HTTP Request +include::{snippets}/admin-hackathon-team-update/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-team-update/path-parameters.adoc[] + +.Request Body +include::{snippets}/admin-hackathon-team-update/request-fields.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-team-update/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java new file mode 100644 index 00000000..54231a83 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java @@ -0,0 +1,39 @@ +package sw_css.admin.hackathon.api; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sw_css.admin.hackathon.application.AdminHackathonTeamCommandService; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; +import sw_css.member.domain.FacultyMember; +import sw_css.utils.annotation.AdminInterface; + +@Validated +@RequestMapping("admin/hackathons/{hackathonId}/teams") +@RestController +@RequiredArgsConstructor +@Transactional +public class AdminHackathonTeamController { + private final AdminHackathonTeamCommandService adminHackathonTeamCommandService; + + // TODO: 팀 수정 + @PatchMapping("{teamId}") + public ResponseEntity updateHackathonTeam( + @AdminInterface FacultyMember facultyMember, + @PathVariable Long hackathonId, + @PathVariable Long teamId, + @RequestBody @Valid AdminHackathonTeamRequest request + ) { + adminHackathonTeamCommandService.updateHackathonTeam(hackathonId, teamId, request); + return ResponseEntity.noContent().build(); + } + + // TODO: 팀 삭제 +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java new file mode 100644 index 00000000..8c210ffa --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java @@ -0,0 +1,123 @@ +package sw_css.admin.hackathon.application; + + +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest.TeamMember; +import sw_css.admin.hackathon.exception.HackathonException; +import sw_css.admin.hackathon.exception.HackathonExceptionType; +import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonRole; +import sw_css.hackathon.domain.HackathonTeam; +import sw_css.hackathon.domain.HackathonTeamMember; +import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamMemberRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.member.domain.embedded.StudentId; + +@Service +@RequiredArgsConstructor +@Transactional +public class AdminHackathonTeamCommandService { + private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamMemberRepository hackathonTeamMemberRepository; + + public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTeamRequest request) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + hackathonTeam.setName(request.name()); + hackathonTeam.setWork(request.work()); + hackathonTeam.setGithubUrl(request.githubUrl()); + + hackathonTeamRepository.save(hackathonTeam); + + System.out.println("\n\n\n\n" + request.leader().toString() + "\n\n\n\n"); + System.out.println(request.members().toString() + "\n\n\n\n"); + + final HackathonTeamMember teamLeader = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderTrue(hackathonId, teamId); + final List teamMembers = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(hackathonId, teamId); + + final TeamMember leader = request.leader(); + final List members = request.members().stream() + .sorted(Comparator.comparingLong(AdminHackathonTeamRequest.TeamMember::id)) + .toList(); + + checkLeaderAndUpdate(teamLeader, leader, hackathon, hackathonTeam); + + for (HackathonTeamMember originMember : teamMembers) { + boolean found = false; + + Iterator iterator = members.iterator(); + while (iterator.hasNext()) { + TeamMember member = iterator.next(); + + if ( !originMember.getStudentId().equals(member.id()) ) continue; + + checkMemberAndUpdate(originMember, member); + found = true; + break; + } + + if( found ) iterator.remove(); + else { + originMember.setIsDeleted(true); + hackathonTeamMemberRepository.save(originMember); + } + } + + for (TeamMember member : members) { + validateTeamMember(member); + HackathonTeamMember newMember = new HackathonTeamMember(hackathon, hackathonTeam, member.id(), member.role()); + hackathonTeamMemberRepository.save(newMember); + } + } + + private void checkMemberAndUpdate(HackathonTeamMember originMember, TeamMember member) { + validateTeamMember(member); + if ( !originMember.getRole().equals(member.role()) ) { + originMember.setRole(member.role()); + hackathonTeamMemberRepository.save(originMember); + } + } + + private void checkLeaderAndUpdate(HackathonTeamMember originLeader, TeamMember leader, Hackathon hackathon, HackathonTeam team) { + validateTeamMember(leader); + if ( !originLeader.getStudentId().equals(leader.id()) ) { + originLeader.setIsDeleted(true); + HackathonTeamMember newLeader = new HackathonTeamMember(hackathon, team, leader.id(), leader.role(), true); + hackathonTeamMemberRepository.save(originLeader); + hackathonTeamMemberRepository.save(newLeader); + } else if ( !originLeader.getRole().equals(leader.role()) ) { + originLeader.setRole(leader.role()); + hackathonTeamMemberRepository.save(originLeader); + } + } + + private void validateTeamMember(TeamMember teamMember) { + validateStudentId(teamMember.id()); + validateRole(teamMember.role()); + } + + private void validateRole(String role){ + try { + HackathonRole.valueOf(role); + } catch (IllegalArgumentException e) { + throw new HackathonException(HackathonExceptionType.INVALID_ROLE_STATUS); + } + } + + private void validateStudentId(Long studentId){ + if ( !studentId.toString().matches(StudentId.STUDENT_ID_REGEX) ) + throw new HackathonException(HackathonExceptionType.INVALID_STUDENT_ID); + } + +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java new file mode 100644 index 00000000..a2e43d45 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java @@ -0,0 +1,25 @@ +package sw_css.admin.hackathon.application.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record AdminHackathonTeamRequest( + @NotBlank(message="팀명을 기재해주세요.") + String name, + @NotBlank(message="프로젝트명을 기재해주세요.") + String work, + @NotBlank(message="프로젝트의 깃헙 레포지토리의 url을 기재해주세요.") + String githubUrl, + @NotNull(message="팀장의 학번과 역할을 기재해주세요.") + TeamMember leader, + @NotNull(message="팀원의 정보를 넣어주세요.") + List members +) { + public record TeamMember( + @NotNull(message="팀원의 학번을 기재해주세요.") + Long id, + @NotBlank(message="팀원의 역할을 기재해주세요.") + String role + ){} +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java index 60c7f6b8..060ef43b 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java @@ -5,9 +5,12 @@ public enum HackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), + NOT_FOUND_HACKATHON_TEAM(HttpStatus.NOT_FOUND, "해당 해커톤 팀이 존재하지 않습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), - INVALID_PRIZE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 상장 형식입니다."), + INVALID_PRIZE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 상장입니다."), + INVALID_STUDENT_ID(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 학번입니다."), + INVALID_ROLE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 팀원 역할입니다."), INVALID_ACTIVE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 활성 형식입니다."), INVALID_APPLY_DATE(HttpStatus.BAD_REQUEST, "신청 시작일이 신청 마지막날 보다 이후일 수 없습니다."), INVALID_HACKATHON_DATE(HttpStatus.BAD_REQUEST, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonRole.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonRole.java new file mode 100644 index 00000000..f092f456 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonRole.java @@ -0,0 +1,5 @@ +package sw_css.hackathon.domain; + +public enum HackathonRole { + DEVELOPER, DESIGNER, PLANNER, OTHER, +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java index 9477cc20..cc73082c 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java @@ -8,10 +8,12 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.time.LocalDate; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.SQLRestriction; import sw_css.member.domain.StudentMember; @@ -33,10 +35,25 @@ public class HackathonTeamMember { @JoinColumn(name = "team_id", nullable = false) private HackathonTeam team; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "student_id", nullable = false) - private StudentMember studentMember; + @Column(nullable = false) + private Long studentId; @Column(nullable = false) + @Setter(AccessLevel.PUBLIC) private String role; + + @Column(nullable = false) + private Boolean isLeader; + + @Column(nullable = false) + @Setter(AccessLevel.PUBLIC) + private Boolean isDeleted; + + public HackathonTeamMember(Hackathon hackathon, HackathonTeam team, Long studentId, String role, Boolean isLeader) { + this(null, hackathon, team, studentId, role, isLeader, false); + } + + public HackathonTeamMember(Hackathon hackathon, HackathonTeam team, Long studentId, String role) { + this(null, hackathon, team, studentId, role, false, false); + } } diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java index 7e7fb585..037bd76d 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java @@ -18,6 +18,7 @@ @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") public class HackathonTeamVote { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java index 51a2710b..8491c590 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java @@ -1,7 +1,11 @@ package sw_css.hackathon.domain.repository; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import sw_css.hackathon.domain.HackathonTeamMember; public interface HackathonTeamMemberRepository extends JpaRepository { + List findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(Long hackathonId, Long teamId); + + HackathonTeamMember findAllByHackathonIdAndTeamIdAndIsLeaderTrue(Long hackathonId, Long teamId); } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 31dcac96..3017b6ab 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -2,6 +2,7 @@ import io.lettuce.core.dynamic.annotation.Param; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import sw_css.hackathon.domain.HackathonTeam; @@ -23,4 +24,6 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); List findByHackathonId(Long hackathonId); + + Optional findByHackathonIdAndId(Long hackathonId, Long teamId); } diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 09e8f64b..f16a14f1 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -136,6 +136,7 @@ create table hackathon_team_member team_id bigint not null, student_id bigint not null, role varchar(255) not null, + is_leader boolean not null, is_deleted boolean not null, created_at datetime(6) not null default current_timestamp(6) ); diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index 580a7643..8f63a89c 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -35,28 +35,18 @@ insert into hackathon_team (hackathon_id, name, image_url, work, github_url, pri values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'NONE_PRIZE', false); insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '코드플레이스', '1.png', '코드 플레이스 코드 플레이스', 'https://github.com/pnu-code-place/code-place', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, 'Next JS', '1.png', '암어 넥스트 레블 절대로', 'https://github.com/vercel/next.js', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(2, 'React', '1.png', '리액트 리액트 리액트', 'https://github.com/facebook/react', 'NONE_PRIZE', false); ## hackathon team vote -insert into hackathon_team_vote (hackathon_id, team_id, student_id) -values(1, 2, 202012341); -insert into hackathon_team_vote (hackathon_id, team_id, student_id) -values(1, 2, 202012342); -insert into hackathon_team_vote (hackathon_id, team_id, student_id) -values(1, 2, 202012343); -insert into hackathon_team_vote (hackathon_id, team_id, student_id) -values(1, 3, 202012344); -insert into hackathon_team_vote (hackathon_id, team_id, student_id) -values(1, 3, 202012343); -insert into hackathon_team_vote (hackathon_id, team_id, student_id) -values(1, 1, 202012345); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 2, 202012341, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 2, 202012342, false); + +## hackathon team member +insert into hackathon_team_member (hackathon_id, team_id, student_id, role, is_leader, is_deleted) +values(1, 1, 202055574, 'DEVELOPER', true, false); +insert into hackathon_team_member (hackathon_id, team_id, student_id, role, is_leader, is_deleted) +values(1, 2, 202055555, 'DEVELOPER', true, false); ## milestone histories diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index e8c497f2..a8d393a9 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -17,6 +17,7 @@ import sw_css.admin.auth.application.AdminAuthCommandService; import sw_css.admin.hackathon.application.AdminHackathonCommandService; import sw_css.admin.hackathon.application.AdminHackathonQueryService; +import sw_css.admin.hackathon.application.AdminHackathonTeamCommandService; import sw_css.admin.member.application.MemberAdminQueryService; import sw_css.admin.milestone.application.MilestoneHistoryAdminCommandService; import sw_css.admin.milestone.application.MilestoneHistoryAdminQueryService; @@ -71,6 +72,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected AdminHackathonCommandService adminHackathonCommandService; + @MockBean + protected AdminHackathonTeamCommandService adminHackathonTeamCommandService; + @MockBean protected FileService fileService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java new file mode 100644 index 00000000..6801ff17 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java @@ -0,0 +1,71 @@ +package sw_css.restdocs.docs.admin; + +import static org.mockito.Mockito.doNothing; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.request.PathParametersSnippet; +import sw_css.admin.hackathon.api.AdminHackathonTeamController; +import sw_css.admin.hackathon.application.AdminHackathonTeamCommandService; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest.TeamMember; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(AdminHackathonTeamController.class) +public class AdminHackathonTeamApiDocsTest extends RestDocsTest { + + @Autowired + private AdminHackathonTeamCommandService adminHackathonTeamCommandService; + + @Test + @DisplayName("[성공] 관리자는 해커톤 팀의 정보를 수정할 수 있다.") + public void updateHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameters = pathParameters( + parameterWithName("hackathonId").description("해커톤의 id"), + parameterWithName("teamId").description("팀의 id") + ); + final RequestFieldsSnippet requestFieldsSnippet = requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("팀명"), + fieldWithPath("work").type(JsonFieldType.STRING).description("프로젝트 명"), + fieldWithPath("githubUrl").type(JsonFieldType.STRING).description("프로젝트 명"), + fieldWithPath("leader.id").type(JsonFieldType.NUMBER).description("팀 리더의 학번"), + fieldWithPath("leader.role").type(JsonFieldType.STRING).description("팀 리더의 역할 (DEVELOPER: 개발자, DESIGNER: 디자이너, PLANNER: 기획자, OTHER: 기타)"), + fieldWithPath("members[].id").type(JsonFieldType.NUMBER).description("팀원의 학번"), + fieldWithPath("members[].role").type(JsonFieldType.STRING).description("팀원의 역할 (DEVELOPER: 개발자, DESIGNER: 디자이너, PLANNER: 기획자, OTHER: 기타)") + ); + + final Long hackathonId = 1L; + final Long teamId = 1L; + final TeamMember leader = new TeamMember(202055555L, "DEVELOPER"); + final AdminHackathonTeamRequest request = new AdminHackathonTeamRequest("팀명", "프로젝트명", "https://www.github.com", leader, List.of(new TeamMember(202012345L, "OTHER"))); + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminHackathonTeamCommandService).updateHackathonTeam(hackathonId, teamId, request); + + // then + + mockMvc.perform( + patch("/admin/hackathons/{hackathonId}/teams/{teamId}", hackathonId, teamId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-team-update", pathParameters, requestFieldsSnippet)); + } +} From 8f5550d4c36c2c67f06abc22dbf2de1883a19aab Mon Sep 17 00:00:00 2001 From: llddang Date: Mon, 30 Dec 2024 03:10:29 +0900 Subject: [PATCH 24/35] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20=ED=8C=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20test=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 11 +++++++ .../api/AdminHackathonTeamController.java | 16 +++++++--- .../AdminHackathonTeamCommandService.java | 13 +++++++-- .../hackathon/domain/HackathonTeam.java | 3 ++ .../admin/AdminHackathonTeamApiDocsTest.java | 29 +++++++++++++++---- 5 files changed, 60 insertions(+), 12 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 01b069ab..5d1d4b80 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -470,3 +470,14 @@ include::{snippets}/admin-hackathon-team-update/request-fields.adoc[] .HTTP Response include::{snippets}/admin-hackathon-team-update/http-response.adoc[] + +=== `DELETE`: 관리자의 해커톤 팀 삭제 + +.HTTP Request +include::{snippets}/admin-hackathon-team-delete/http-request.adoc[] + +.Path Parameters +include::{snippets}/admin-hackathon-team-delete/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/admin-hackathon-team-delete/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java index 54231a83..8b60ba61 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java @@ -5,6 +5,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -16,15 +17,14 @@ import sw_css.utils.annotation.AdminInterface; @Validated -@RequestMapping("admin/hackathons/{hackathonId}/teams") +@RequestMapping("admin/hackathons/{hackathonId}/teams/{teamId}") @RestController @RequiredArgsConstructor @Transactional public class AdminHackathonTeamController { private final AdminHackathonTeamCommandService adminHackathonTeamCommandService; - // TODO: 팀 수정 - @PatchMapping("{teamId}") + @PatchMapping() public ResponseEntity updateHackathonTeam( @AdminInterface FacultyMember facultyMember, @PathVariable Long hackathonId, @@ -35,5 +35,13 @@ public ResponseEntity updateHackathonTeam( return ResponseEntity.noContent().build(); } - // TODO: 팀 삭제 + @DeleteMapping() + public ResponseEntity deleteHackathonTeam( + @AdminInterface FacultyMember facultyMember, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + adminHackathonTeamCommandService.deleteHackathonTeam(hackathonId, teamId); + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java index 8c210ffa..d64757b6 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java @@ -40,9 +40,6 @@ public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTea hackathonTeamRepository.save(hackathonTeam); - System.out.println("\n\n\n\n" + request.leader().toString() + "\n\n\n\n"); - System.out.println(request.members().toString() + "\n\n\n\n"); - final HackathonTeamMember teamLeader = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderTrue(hackathonId, teamId); final List teamMembers = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(hackathonId, teamId); @@ -81,6 +78,16 @@ public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTea } } + public void deleteHackathonTeam(Long hackathonId, Long teamId) { + hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + hackathonTeam.setDeleted(true); + hackathonTeamRepository.save(hackathonTeam); + } + private void checkMemberAndUpdate(HackathonTeamMember originMember, TeamMember member) { validateTeamMember(member); if ( !originMember.getRole().equals(member.role()) ) { diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java index b1ecbe4a..57402300 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -45,4 +45,7 @@ public class HackathonTeam { @Column(nullable = false) private String prize; + + @Column(nullable = false) + private boolean isDeleted; } diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java index 6801ff17..cba686a7 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java @@ -3,6 +3,7 @@ import static org.mockito.Mockito.doNothing; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; @@ -20,7 +21,6 @@ import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import sw_css.admin.hackathon.api.AdminHackathonTeamController; -import sw_css.admin.hackathon.application.AdminHackathonTeamCommandService; import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest.TeamMember; import sw_css.restdocs.RestDocsTest; @@ -28,9 +28,6 @@ @WebMvcTest(AdminHackathonTeamController.class) public class AdminHackathonTeamApiDocsTest extends RestDocsTest { - @Autowired - private AdminHackathonTeamCommandService adminHackathonTeamCommandService; - @Test @DisplayName("[성공] 관리자는 해커톤 팀의 정보를 수정할 수 있다.") public void updateHackathonTeam() throws Exception { @@ -59,7 +56,6 @@ public void updateHackathonTeam() throws Exception { doNothing().when(adminHackathonTeamCommandService).updateHackathonTeam(hackathonId, teamId, request); // then - mockMvc.perform( patch("/admin/hackathons/{hackathonId}/teams/{teamId}", hackathonId, teamId) .contentType(APPLICATION_JSON) @@ -68,4 +64,27 @@ public void updateHackathonTeam() throws Exception { .andExpect(status().isNoContent()) .andDo(document("admin-hackathon-team-update", pathParameters, requestFieldsSnippet)); } + + @Test + @DisplayName("[성공] 관리자는 해커톤의 팀을 삭제할 수 있다.") + public void deleteHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameters = pathParameters( + parameterWithName("hackathonId").description("해커톤의 id"), + parameterWithName("teamId").description("팀의 id") + ); + final Long hackathonId = 1L; + final Long teamId = 1L; + final String token = "Bearer AccessToken"; + + // when + doNothing().when(adminHackathonTeamCommandService).deleteHackathonTeam(hackathonId, teamId); + + // then + mockMvc.perform( + delete("/admin/hackathons/{hackathonId}/teams/{teamId}", hackathonId, teamId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-team-delete", pathParameters)); + } } From a449dfce3ab7c4aa37b1291fd8d321f3de04f02a Mon Sep 17 00:00:00 2001 From: llddang Date: Tue, 31 Dec 2024 08:40:55 +0900 Subject: [PATCH 25/35] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=ED=95=B4?= =?UTF-8?q?=EC=BB=A4=ED=86=A4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 17 ++++ .../hackathon/api/HackathonController.java | 36 +++++++ .../application/HackathonQueryService.java | 34 +++++++ .../dto/response/HackathonResponse.java | 36 +++++++ .../repository/HackathonRepository.java | 3 + .../java/sw_css/restdocs/RestDocsTest.java | 4 + .../restdocs/docs/HackathonApiDocsTest.java | 95 +++++++++++++++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 8 +- 8 files changed, 227 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/api/HackathonController.java create mode 100644 backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java create mode 100644 backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonResponse.java create mode 100644 backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 5d1d4b80..624cc118 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -481,3 +481,20 @@ include::{snippets}/admin-hackathon-team-delete/path-parameters.adoc[] .HTTP Response include::{snippets}/admin-hackathon-team-delete/http-response.adoc[] + +== 일반 - 해커톤 + +=== `GET`: 해커톤 전체 조회 + +.HTTP Request +include::{snippets}/hackathon-find-all/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-find-all/query-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-find-all/http-response.adoc[] + +.Response Body +include::{snippets}/hackathon-find-all/response-fields.adoc[] + diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java new file mode 100644 index 00000000..a2a17f98 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java @@ -0,0 +1,36 @@ +package sw_css.hackathon.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import sw_css.hackathon.application.HackathonQueryService; +import sw_css.hackathon.application.dto.response.HackathonResponse; + +@Validated +@RequestMapping("/hackathons") +@RestController +@RequiredArgsConstructor +public class HackathonController { + + private final HackathonQueryService hackathonQueryService; + + @GetMapping + public ResponseEntity> findAllHackathons( + final Pageable pageable, + @RequestParam(value = "name", required = false) final String name + ) { + return ResponseEntity.ok( + hackathonQueryService.findAllHackathon(pageable, name) + ); + } + + // TODO: 해커톤 상세 조회 + + // TODO: 수상 내역 조회 +} diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java new file mode 100644 index 00000000..243b49fe --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java @@ -0,0 +1,34 @@ +package sw_css.hackathon.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; +import sw_css.admin.hackathon.domain.HackathonStatus; +import sw_css.hackathon.application.dto.response.HackathonResponse; +import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.repository.HackathonRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HackathonQueryService { + + private final HackathonRepository hackathonRepository; + + public Page findAllHackathon(Pageable pageable, + final String name) { + Sort sort = Sort.by(Sort.Order.desc("hackathonStartDate")); + Pageable pageableWithSort = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort); + if(name != null) { + Page hackathons = hackathonRepository.findAllByNameContainingAndVisibleStatusIsTrue(name, pageableWithSort); + return HackathonResponse.from(hackathons); + } + Page hackathons = hackathonRepository.findAllByVisibleStatusIsTrue(pageableWithSort); + return HackathonResponse.from(hackathons); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonResponse.java new file mode 100644 index 00000000..7c98b434 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonResponse.java @@ -0,0 +1,36 @@ +package sw_css.hackathon.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; +import sw_css.hackathon.domain.Hackathon; + +public record HackathonResponse( + Long id, + String name, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyEndDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonEndDate, + String imageUrl +) { + public static Page from(Page hackathons) { + return new PageImpl<>(hackathons.stream() + .map(hackathon -> new HackathonResponse( + hackathon.getId(), + hackathon.getName(), + hackathon.getApplyStartDate(), + hackathon.getApplyEndDate(), + hackathon.getHackathonStartDate(), + hackathon.getHackathonEndDate(), + hackathon.getImageUrl() + )) + .toList(), hackathons.getPageable(), hackathons.getTotalElements()); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java index 6d437c4f..cde0e259 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -11,4 +11,7 @@ public interface HackathonRepository extends JpaRepository{ Page findByNameContaining(String name, Pageable pageable); Page findByVisibleStatus(boolean visibleStatus, Pageable pageable); Page findByNameContainingAndVisibleStatus(String name, boolean visibleStatus, Pageable pageable); + + Page findAllByVisibleStatusIsTrue(Pageable pageable); + Page findAllByNameContainingAndVisibleStatusIsTrue(String name, Pageable pageable); } diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index a8d393a9..b2b81208 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -26,6 +26,7 @@ import sw_css.auth.application.AuthSignInService; import sw_css.auth.application.AuthSignUpService; import sw_css.file.application.FileService; +import sw_css.hackathon.application.HackathonQueryService; import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; import sw_css.member.application.MemberQueryService; @@ -75,6 +76,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected AdminHackathonTeamCommandService adminHackathonTeamCommandService; + @MockBean + protected HackathonQueryService hackathonQueryService; + @MockBean protected FileService fileService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java new file mode 100644 index 00000000..f1a8c954 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java @@ -0,0 +1,95 @@ +package sw_css.restdocs.docs; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.LocalDate; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; +import sw_css.hackathon.api.HackathonController; +import sw_css.hackathon.application.dto.response.HackathonResponse; +import sw_css.hackathon.domain.Hackathon; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(HackathonController.class) +public class HackathonApiDocsTest extends RestDocsTest { + + @Test + @DisplayName("[성공] 모든 사람은 해커톤 목록을 조회할 수 있다.") + public void findAllHackathons() throws Exception { + // given + final QueryParametersSnippet queryParametersSnippet = queryParameters( + parameterWithName("page").optional().description("조회할 해커톤의 페이지 번호"), + parameterWithName("size").optional().description("조회할 해커톤의 페이지 당 데이터 수"), + parameterWithName("name").optional().description("조죄할 해커톤 명") + ); + + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("totalElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("size").type(JsonFieldType.NUMBER).description("페이지 내 데이터 수"), + fieldWithPath("number").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("sort.empty").type(JsonFieldType.BOOLEAN).description("정렬 속성의 존재 여부"), + fieldWithPath("sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬 여부"), + fieldWithPath("sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬여부"), + fieldWithPath("first").type(JsonFieldType.BOOLEAN).description("첫 페이지인지 여부"), + fieldWithPath("last").type(JsonFieldType.BOOLEAN).description("마지막 페이지인지 여부"), + fieldWithPath("pageable.pageNumber").type(JsonFieldType.NUMBER).description("요청한 페이지번호"), + fieldWithPath("pageable.pageSize").type(JsonFieldType.NUMBER).description("요청한 페이지크기"), + fieldWithPath("pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("요청한 데이터가 비었는지 여부"), + fieldWithPath("pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.offset").type(JsonFieldType.NUMBER).description("요청한 페이지오프셋"), + fieldWithPath("pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("pageable.unpaged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("numberOfElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("empty").type(JsonFieldType.BOOLEAN).description("데이터의 존재 여부"), + fieldWithPath("content[].id").type(JsonFieldType.NUMBER).description("해커톤 id"), + fieldWithPath("content[].name").type(JsonFieldType.STRING).description("해커톤 명"), + fieldWithPath("content[].applyStartDate").type(JsonFieldType.STRING).description("해커톤 지원 시작 날(yyyy-MM-dd)"), + fieldWithPath("content[].applyEndDate").type(JsonFieldType.STRING).description("해커톤 지원 마지막 날(yyyy-MM-dd)"), + fieldWithPath("content[].hackathonStartDate").type(JsonFieldType.STRING).description("해커톤 대회 시작 날(yyyy-MM-dd)"), + fieldWithPath("content[].hackathonEndDate").type(JsonFieldType.STRING).description("해커톤 대회 마지막 날(yyyy-MM-dd)"), + fieldWithPath("content[].imageUrl").type(JsonFieldType.STRING).description("해커톤 배너 이미지") + ); + + final Page hackathonPage = new PageImpl<>(List.of( + new Hackathon(1L, "제5회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1234", LocalDate.parse("2024-05-22"), LocalDate.parse("2024-05-29"), LocalDate.parse("2024-05-22"), LocalDate.parse("2024-09-07"), "1.png", true, false), + new Hackathon(2L, "제4회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", "1234", LocalDate.parse("2022-03-22"), LocalDate.parse("2022-05-29"), LocalDate.parse("2022-12-22"), LocalDate.parse("2023-03-07"), "1.png", true, false)), + PageRequest.of(0, 10), + 2 + ); + final Page response = HackathonResponse.from(hackathonPage); + + final String hackathonName = "해커톤"; + + // when + when(hackathonQueryService.findAllHackathon(any(), any())).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/hackathons") + .param("name", hackathonName) + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andDo(document("hackathon-find-all", queryParametersSnippet, responseBodySnippet)); + } +} diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index aeded855..c2ebd845 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -8,7 +8,6 @@ import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; @@ -24,12 +23,10 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; @@ -41,7 +38,6 @@ import org.springframework.restdocs.request.QueryParametersSnippet; import org.springframework.restdocs.request.RequestPartsSnippet; import sw_css.admin.hackathon.api.AdminHackathonController; -import sw_css.admin.hackathon.application.AdminHackathonQueryService; import sw_css.admin.hackathon.application.dto.request.AdminHackathonActiveRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest.AdminTeam; @@ -59,7 +55,7 @@ public class AdminHackathonApiDocsTest extends RestDocsTest { @DisplayName("[성공] 관리자가 해커톤 전체 목록을 조회할 수 있다.") public void findAllHackathons() throws Exception { // given - final QueryParametersSnippet queryParameters = queryParameters( + final QueryParametersSnippet queryParametersSnippet = queryParameters( parameterWithName("page").optional().description("조회할 해커톤의 페이지 번호"), parameterWithName("size").optional().description("조회할 해커톤의 페이지 당 데이터 수"), parameterWithName("name").optional().description("조죄할 해커톤 명"), @@ -120,7 +116,7 @@ public void findAllHackathons() throws Exception { .param("size", "10") .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isOk()) - .andDo(document("admin-hackathon-find-all",queryParameters, responseBodySnippet)); + .andDo(document("admin-hackathon-find-all", queryParametersSnippet, responseBodySnippet)); } @Test From 043cd1e62d2fc2669e905ca91207beb02674939a Mon Sep 17 00:00:00 2001 From: llddang Date: Tue, 31 Dec 2024 09:03:27 +0900 Subject: [PATCH 26/35] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EC=9D=98=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 20 ++++++++-- .../AdminHackathonCommandService.java | 32 +++++++-------- .../AdminHackathonQueryService.java | 10 ++--- .../AdminHackathonTeamCommandService.java | 16 ++++---- .../exception/AdminHackathonException.java | 16 ++++++++ ....java => AdminHackathonExceptionType.java} | 4 +- .../hackathon/api/HackathonController.java | 11 ++++- .../application/HackathonQueryService.java | 13 +++++- .../dto/response/HackathonDetailResponse.java | 33 +++++++++++++++ .../repository/HackathonRepository.java | 4 +- .../exception/HackathonException.java | 3 +- .../exception/HackathonExceptionType.java | 26 ++++++++++++ .../restdocs/docs/HackathonApiDocsTest.java | 40 +++++++++++++++++++ .../docs/admin/AdminHackathonApiDocsTest.java | 4 +- 14 files changed, 191 insertions(+), 41 deletions(-) create mode 100644 backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonException.java rename backend/src/main/java/sw_css/admin/hackathon/exception/{HackathonExceptionType.java => AdminHackathonExceptionType.java} (90%) create mode 100644 backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonDetailResponse.java rename backend/src/main/java/sw_css/{admin => }/hackathon/exception/HackathonException.java (91%) create mode 100644 backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 624cc118..cf5b4851 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -371,12 +371,12 @@ include::{snippets}/admin-hackathon-find-all/response-fields.adoc[] .HTTP Request include::{snippets}/admin-hackathon-find/http-request.adoc[] -.HTTP Response -include::{snippets}/admin-hackathon-find/http-response.adoc[] - .Path Parameters include::{snippets}/admin-hackathon-find/path-parameters.adoc[] +.HTTP Response +include::{snippets}/admin-hackathon-find/http-response.adoc[] + .Response Body include::{snippets}/admin-hackathon-find/response-fields.adoc[] @@ -498,3 +498,17 @@ include::{snippets}/hackathon-find-all/http-response.adoc[] .Response Body include::{snippets}/hackathon-find-all/response-fields.adoc[] +=== `GET`: 해커톤 상세 조회 + +.HTTP Request +include::{snippets}/hackathon-find/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-find/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-find/http-response.adoc[] + +.Response Body +include::{snippets}/hackathon-find/response-fields.adoc[] + diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java index 0b2f633c..2937e713 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java @@ -17,8 +17,8 @@ import sw_css.admin.hackathon.application.dto.request.AdminHackathonPrizeRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.admin.hackathon.domain.HackathonStatus; -import sw_css.admin.hackathon.exception.HackathonException; -import sw_css.admin.hackathon.exception.HackathonExceptionType; +import sw_css.admin.hackathon.exception.AdminHackathonException; +import sw_css.admin.hackathon.exception.AdminHackathonExceptionType; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.HackathonPrize; import sw_css.hackathon.domain.HackathonTeam; @@ -40,8 +40,8 @@ public class AdminHackathonCommandService { public Long registerHackathon(final MultipartFile file, final AdminHackathonRequest request) { validateFileType(file); - validateDate(request.applyStartDate(), request.applyEndDate(), HackathonExceptionType.INVALID_APPLY_DATE); - validateDate(request.hackathonStartDate(), request.hackathonEndDate(), HackathonExceptionType.INVALID_HACKATHON_DATE); + validateDate(request.applyStartDate(), request.applyEndDate(), AdminHackathonExceptionType.INVALID_APPLY_DATE); + validateDate(request.hackathonStartDate(), request.hackathonEndDate(), AdminHackathonExceptionType.INVALID_HACKATHON_DATE); final String newFilePath = generateFilePath(file); final Hackathon newHackathon = new Hackathon(request.name(), request.description(), request.password(), request.applyStartDate(), request.applyEndDate(), request.hackathonStartDate(), request.hackathonEndDate(), newFilePath); @@ -53,7 +53,7 @@ public Long registerHackathon(final MultipartFile file, final AdminHackathonRequ public void updateHackathon(final Long hackathonId, final MultipartFile file, final AdminHackathonRequest request) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); if(file != null) { validateFileType(file); @@ -61,8 +61,8 @@ public void updateHackathon(final Long hackathonId, final MultipartFile file, fi uploadFile(file, newFilePath); hackathon.setImageUrl(newFilePath); } - validateDate(request.applyStartDate(), request.applyEndDate(), HackathonExceptionType.INVALID_APPLY_DATE); - validateDate(request.hackathonStartDate(), request.hackathonEndDate(), HackathonExceptionType.INVALID_HACKATHON_DATE); + validateDate(request.applyStartDate(), request.applyEndDate(), AdminHackathonExceptionType.INVALID_APPLY_DATE); + validateDate(request.hackathonStartDate(), request.hackathonEndDate(), AdminHackathonExceptionType.INVALID_HACKATHON_DATE); hackathon.setName(request.name()); hackathon.setDescription(request.description()); @@ -76,25 +76,25 @@ public void updateHackathon(final Long hackathonId, final MultipartFile file, fi public void deleteHackathon(final Long hackathonId) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); hackathon.delete(); hackathonRepository.save(hackathon); } public void activeHackathon(final Long hackathonId, final String visibleStatus) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); if(visibleStatus.equals(HackathonStatus.ACTIVE.toString())) hackathon.setVisibleStatus(true); else if(visibleStatus.equals(HackathonStatus.INACTIVE.toString())) hackathon.setVisibleStatus(false); - else throw new HackathonException(HackathonExceptionType.INVALID_ACTIVE_STATUS); + else throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_ACTIVE_STATUS); hackathonRepository.save(hackathon); } public void hackathonChangePrize(final Long hackathonId, List teams) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); List hackathonTeams = hackathonTeamRepository.findByHackathonId(hackathon.getId()); @@ -113,22 +113,22 @@ private void validatePrize(String prize){ try { HackathonPrize.valueOf(prize); } catch (IllegalArgumentException e) { - throw new HackathonException(HackathonExceptionType.INVALID_PRIZE_STATUS); + throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_PRIZE_STATUS); } } - private void validateDate(LocalDate startDate, LocalDate endDate, HackathonExceptionType exceptionType) { - if (startDate.isAfter(endDate)) throw new HackathonException(exceptionType); + private void validateDate(LocalDate startDate, LocalDate endDate, AdminHackathonExceptionType exceptionType) { + if (startDate.isAfter(endDate)) throw new AdminHackathonException(exceptionType); } private void validateFileType(final MultipartFile file) { System.out.println(file.getOriginalFilename()); if (file == null) { - throw new HackathonException(HackathonExceptionType.NOT_EXIST_FILE); + throw new AdminHackathonException(AdminHackathonExceptionType.NOT_EXIST_FILE); } final String contentType = file.getContentType(); if (!isSupportedContentType(contentType)) { - throw new HackathonException(HackathonExceptionType.UNSUPPORTED_FILE_TYPE); + throw new AdminHackathonException(AdminHackathonExceptionType.UNSUPPORTED_FILE_TYPE); } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java index 21c70acd..75e29343 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java @@ -24,8 +24,8 @@ import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.admin.hackathon.domain.HackathonStatus; -import sw_css.admin.hackathon.exception.HackathonException; -import sw_css.admin.hackathon.exception.HackathonExceptionType; +import sw_css.admin.hackathon.exception.AdminHackathonException; +import sw_css.admin.hackathon.exception.AdminHackathonExceptionType; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.HackathonTeam; import sw_css.hackathon.domain.repository.HackathonRepository; @@ -63,14 +63,14 @@ public Page findAllHackathons(Pageable pageable, public AdminHackathonDetailResponse findHackathonById(final Long id) { Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); return AdminHackathonDetailResponse.of(hackathon); } public byte[] downloadHackathonVotesById(final Long id) { Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); final List hackathonTeams = hackathonTeamRepository.findByHackathonIdSorted(hackathon.getId()); hackathonTeams.stream().forEach(team -> { @@ -122,7 +122,7 @@ private byte[] generateHackathonVoteExcelFile(final List hackatho workbook.close(); return bos.toByteArray(); } catch (IOException e) { - throw new HackathonException(HackathonExceptionType.CANNOT_OPEN_FILE); + throw new AdminHackathonException(AdminHackathonExceptionType.CANNOT_OPEN_FILE); } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java index d64757b6..2b043470 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java @@ -9,8 +9,8 @@ import org.springframework.transaction.annotation.Transactional; import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest.TeamMember; -import sw_css.admin.hackathon.exception.HackathonException; -import sw_css.admin.hackathon.exception.HackathonExceptionType; +import sw_css.admin.hackathon.exception.AdminHackathonException; +import sw_css.admin.hackathon.exception.AdminHackathonExceptionType; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.HackathonRole; import sw_css.hackathon.domain.HackathonTeam; @@ -30,9 +30,9 @@ public class AdminHackathonTeamCommandService { public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTeamRequest request) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); hackathonTeam.setName(request.name()); hackathonTeam.setWork(request.work()); @@ -80,9 +80,9 @@ public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTea public void deleteHackathonTeam(Long hackathonId, Long teamId) { hackathonRepository.findById(hackathonId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( - () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); hackathonTeam.setDeleted(true); hackathonTeamRepository.save(hackathonTeam); @@ -118,13 +118,13 @@ private void validateRole(String role){ try { HackathonRole.valueOf(role); } catch (IllegalArgumentException e) { - throw new HackathonException(HackathonExceptionType.INVALID_ROLE_STATUS); + throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_ROLE_STATUS); } } private void validateStudentId(Long studentId){ if ( !studentId.toString().matches(StudentId.STUDENT_ID_REGEX) ) - throw new HackathonException(HackathonExceptionType.INVALID_STUDENT_ID); + throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_STUDENT_ID); } } diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonException.java b/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonException.java new file mode 100644 index 00000000..6a5df7cb --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonException.java @@ -0,0 +1,16 @@ +package sw_css.admin.hackathon.exception; + +import sw_css.base.BaseException; +import sw_css.base.BaseExceptionType; + +public class AdminHackathonException extends BaseException { + private final AdminHackathonExceptionType exceptionType; + + public AdminHackathonException(final AdminHackathonExceptionType exceptionType) { + super((exceptionType.errorMessage())); + this.exceptionType = exceptionType; + } + + @Override + public BaseExceptionType exceptionType() { return exceptionType; } +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonExceptionType.java similarity index 90% rename from backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java rename to backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonExceptionType.java index 060ef43b..12e9e89f 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonExceptionType.java @@ -3,7 +3,7 @@ import org.springframework.http.HttpStatus; import sw_css.base.BaseExceptionType; -public enum HackathonExceptionType implements BaseExceptionType { +public enum AdminHackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), NOT_FOUND_HACKATHON_TEAM(HttpStatus.NOT_FOUND, "해당 해커톤 팀이 존재하지 않습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), @@ -19,7 +19,7 @@ public enum HackathonExceptionType implements BaseExceptionType { private final HttpStatus httpStatus; private final String errorMessage; - HackathonExceptionType(final HttpStatus httpStatus, final String errorMessage) { + AdminHackathonExceptionType(final HttpStatus httpStatus, final String errorMessage) { this.httpStatus = httpStatus; this.errorMessage = errorMessage; } diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java index a2a17f98..0dd97f5e 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java @@ -6,10 +6,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import sw_css.hackathon.application.HackathonQueryService; +import sw_css.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.hackathon.application.dto.response.HackathonResponse; @Validated @@ -30,7 +32,14 @@ public ResponseEntity> findAllHackathons( ); } - // TODO: 해커톤 상세 조회 + @GetMapping("{hackathonId}") + public ResponseEntity findHackathonById( + final @PathVariable Long hackathonId + ) { + return ResponseEntity.ok( + hackathonQueryService.findHackathon(hackathonId) + ); + } // TODO: 수상 내역 조회 } diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java index 243b49fe..1654889f 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java @@ -7,11 +7,13 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; -import sw_css.admin.hackathon.domain.HackathonStatus; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; +import sw_css.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.hackathon.application.dto.response.HackathonResponse; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.exception.HackathonException; +import sw_css.hackathon.exception.HackathonExceptionType; @Service @RequiredArgsConstructor @@ -31,4 +33,11 @@ public Page findAllHackathon(Pageable pageable, Page hackathons = hackathonRepository.findAllByVisibleStatusIsTrue(pageableWithSort); return HackathonResponse.from(hackathons); } + + public HackathonDetailResponse findHackathon(final Long id) { + Hackathon hackathon = hackathonRepository.findByIdAndVisibleStatusIsTrue(id).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + return HackathonDetailResponse.of(hackathon); + } } diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonDetailResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonDetailResponse.java new file mode 100644 index 00000000..04bd833f --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonDetailResponse.java @@ -0,0 +1,33 @@ +package sw_css.hackathon.application.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; +import sw_css.hackathon.domain.Hackathon; + +public record HackathonDetailResponse( + Long id, + String name, + String description, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate applyEndDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonStartDate, + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate hackathonEndDate, + String imageUrl +) { + public static HackathonDetailResponse of(Hackathon hackathon) { + return new HackathonDetailResponse( + hackathon.getId(), + hackathon.getName(), + hackathon.getDescription(), + hackathon.getApplyStartDate(), + hackathon.getApplyEndDate(), + hackathon.getHackathonStartDate(), + hackathon.getHackathonEndDate(), + hackathon.getImageUrl() + ); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java index cde0e259..37cdbb16 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -1,8 +1,8 @@ package sw_css.hackathon.domain.repository; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.data.jpa.repository.JpaRepository; import sw_css.hackathon.domain.Hackathon; @@ -14,4 +14,6 @@ public interface HackathonRepository extends JpaRepository{ Page findAllByVisibleStatusIsTrue(Pageable pageable); Page findAllByNameContainingAndVisibleStatusIsTrue(String name, Pageable pageable); + + Optional findByIdAndVisibleStatusIsTrue(long id); } diff --git a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonException.java similarity index 91% rename from backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java rename to backend/src/main/java/sw_css/hackathon/exception/HackathonException.java index bed0cb79..00a7dab5 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/exception/HackathonException.java +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonException.java @@ -1,4 +1,5 @@ -package sw_css.admin.hackathon.exception; +package sw_css.hackathon.exception; + import sw_css.base.BaseException; import sw_css.base.BaseExceptionType; diff --git a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java new file mode 100644 index 00000000..6726a09f --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -0,0 +1,26 @@ +package sw_css.hackathon.exception; + +import org.springframework.http.HttpStatus; +import sw_css.base.BaseExceptionType; + +public enum HackathonExceptionType implements BaseExceptionType { + NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."); + + private final HttpStatus httpStatus; + private final String errorMessage; + + HackathonExceptionType(final HttpStatus httpStatus, final String errorMessage) { + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String errorMessage() { + return errorMessage; + } +} diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java index f1a8c954..1f2ddd1a 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java @@ -6,6 +6,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -21,9 +22,12 @@ import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.PathParametersSnippet; import org.springframework.restdocs.request.QueryParametersSnippet; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.hackathon.api.HackathonController; +import sw_css.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.hackathon.application.dto.response.HackathonResponse; import sw_css.hackathon.domain.Hackathon; import sw_css.restdocs.RestDocsTest; @@ -92,4 +96,40 @@ public void findAllHackathons() throws Exception { .andExpect(status().isOk()) .andDo(document("hackathon-find-all", queryParametersSnippet, responseBodySnippet)); } + + @Test + @DisplayName("[성공] 모든 사람은 해커톤 상세 조회를 할 수 있다.") + public void findHackathonById() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해커톤 id") + ); + + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("해커톤 id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("해커톤 명"), + fieldWithPath("description").type(JsonFieldType.STRING).description("해커톤 내용"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("해커톤 배너 이미지"), + fieldWithPath("applyStartDate").type(JsonFieldType.STRING).description("해커톤 지원 시작일"), + fieldWithPath("applyEndDate").type(JsonFieldType.STRING).description("해커톤 지원 마지막날"), + fieldWithPath("hackathonStartDate").type(JsonFieldType.STRING).description("해커톤 대회 시작일"), + fieldWithPath("hackathonEndDate").type(JsonFieldType.STRING).description("해커톤 대회 마지막날") + ); + + + final HackathonDetailResponse response = new HackathonDetailResponse( + 1L, "제5회 PNU 창의융합 소프트웨어해커톤", "# 해커톤 설명 **bold**", LocalDate.parse("2024-05-22"), LocalDate.parse("2024-05-29"), LocalDate.parse("2024-05-22"), LocalDate.parse("2024-09-07"), "1.png" + ); + final Long hackathonId = 1L; + + // when + when(hackathonQueryService.findHackathon(hackathonId)).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/hackathons/{hackathonId}", hackathonId)) + .andExpect(status().isOk()) + .andDo(document("hackathon-find", pathParameterSnippet, responseBodySnippet)); + + } } diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java index c2ebd845..d46c69dd 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -123,7 +123,7 @@ public void findAllHackathons() throws Exception { @DisplayName("[성공] 관리자가 해커톤 상세 조회할 수 있다.") public void findHackathon() throws Exception { // given - final PathParametersSnippet pathParameters = pathParameters( + final PathParametersSnippet pathParameterSnippet = pathParameters( parameterWithName("hackathonId").description("해커톤 id") ); final ResponseFieldsSnippet responseBodySnippet = responseFields( @@ -153,7 +153,7 @@ public void findHackathon() throws Exception { RestDocumentationRequestBuilders.get("/admin/hackathons/{hackathonId}", hackathonId) .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isOk()) - .andDo(document("admin-hackathon-find", pathParameters, responseBodySnippet)); + .andDo(document("admin-hackathon-find", pathParameterSnippet, responseBodySnippet)); } @Test From 5c9c637cc929a9d831a67b6c48a0459e385435c3 Mon Sep 17 00:00:00 2001 From: llddang Date: Tue, 31 Dec 2024 09:59:04 +0900 Subject: [PATCH 27/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=EC=83=81=EC=9E=A5=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 14 +++++++ .../AdminHackathonCommandService.java | 1 - .../AdminHackathonQueryService.java | 3 -- .../MilestoneHistoryAdminQueryService.java | 4 -- .../hackathon/api/HackathonController.java | 11 ++++- .../application/HackathonQueryService.java | 38 ++++++++++++++++- .../dto/response/HackathonPrizeResponse.java | 15 +++++++ .../HackathonTeamMemberRepository.java | 2 + .../repository/HackathonTeamRepository.java | 2 + backend/src/main/resources/test-data.sql | 4 +- .../restdocs/docs/HackathonApiDocsTest.java | 41 +++++++++++++++++++ 11 files changed, 123 insertions(+), 12 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonPrizeResponse.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index cf5b4851..816632da 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -512,3 +512,17 @@ include::{snippets}/hackathon-find/http-response.adoc[] .Response Body include::{snippets}/hackathon-find/response-fields.adoc[] + +=== `GET`: 해커톤 상장 조회 + +.HTTP Request +include::{snippets}/hackathon-find-prize/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-find-prize/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-find-prize/http-response.adoc[] + +.Response Body +include::{snippets}/hackathon-find-prize/response-fields.adoc[] diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java index 2937e713..1ead72a3 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java @@ -122,7 +122,6 @@ private void validateDate(LocalDate startDate, LocalDate endDate, AdminHackathon } private void validateFileType(final MultipartFile file) { - System.out.println(file.getOriginalFilename()); if (file == null) { throw new AdminHackathonException(AdminHackathonExceptionType.NOT_EXIST_FILE); } diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java index 75e29343..d9569e40 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java @@ -73,9 +73,6 @@ public byte[] downloadHackathonVotesById(final Long id) { () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); final List hackathonTeams = hackathonTeamRepository.findByHackathonIdSorted(hackathon.getId()); - hackathonTeams.stream().forEach(team -> { - System.out.println(team.getName()); - }); return generateHackathonVoteExcelFile(hackathonTeams); } diff --git a/backend/src/main/java/sw_css/admin/milestone/application/MilestoneHistoryAdminQueryService.java b/backend/src/main/java/sw_css/admin/milestone/application/MilestoneHistoryAdminQueryService.java index fe0db93a..0000fc0d 100644 --- a/backend/src/main/java/sw_css/admin/milestone/application/MilestoneHistoryAdminQueryService.java +++ b/backend/src/main/java/sw_css/admin/milestone/application/MilestoneHistoryAdminQueryService.java @@ -179,8 +179,6 @@ public Page findAllMilestoneHistoryScores(final String s final List milestoneHistoryInfos = milestoneScoreRepository.findMilestoneScoresWithStudentInfoByPeriod( parsedStartDate, parsedEndDate, pageable.getPageNumber() * pageable.getPageSize() * 1L, pageable.getPageSize() * 1L); - System.out.println(milestoneHistoryInfos); - final Long totalMilestoneHistoryInfoCount = milestoneScoreRepository.countAllMilestoneScoresWithStudentInfoByPeriod(); final List content = milestoneHistoryInfos.stream().map(entry -> { @@ -199,8 +197,6 @@ public Page findAllMilestoneHistoryScores(final String s ); }).toList(); - System.out.println(content); - return new PageImpl<>(content, pageable, totalMilestoneHistoryInfoCount); } diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java index 0dd97f5e..de7ac2ca 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java @@ -1,5 +1,6 @@ package sw_css.hackathon.api; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.RestController; import sw_css.hackathon.application.HackathonQueryService; import sw_css.hackathon.application.dto.response.HackathonDetailResponse; +import sw_css.hackathon.application.dto.response.HackathonPrizeResponse; import sw_css.hackathon.application.dto.response.HackathonResponse; @Validated @@ -41,5 +43,12 @@ public ResponseEntity findHackathonById( ); } - // TODO: 수상 내역 조회 + @GetMapping("{hackathonId}/prize") + public ResponseEntity> findHackathonPrize( + final @PathVariable Long hackathonId + ) { + return ResponseEntity.ok( + hackathonQueryService.findHackathonPrizes(hackathonId) + ); + } } diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java index 1654889f..3f5c5d19 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java @@ -1,5 +1,7 @@ package sw_css.hackathon.application; +import java.util.ArrayList; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -7,11 +9,16 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; import sw_css.hackathon.application.dto.response.HackathonDetailResponse; +import sw_css.hackathon.application.dto.response.HackathonPrizeResponse; +import sw_css.hackathon.application.dto.response.HackathonPrizeResponse.HackathonTeamPrize; import sw_css.hackathon.application.dto.response.HackathonResponse; import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonPrize; +import sw_css.hackathon.domain.HackathonTeam; import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamMemberRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; import sw_css.hackathon.exception.HackathonException; import sw_css.hackathon.exception.HackathonExceptionType; @@ -21,6 +28,8 @@ public class HackathonQueryService { private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamMemberRepository hackathonTeamMemberRepository; public Page findAllHackathon(Pageable pageable, final String name) { @@ -40,4 +49,31 @@ public HackathonDetailResponse findHackathon(final Long id) { return HackathonDetailResponse.of(hackathon); } + + public List findHackathonPrizes(final Long id) { + List grandTeams = findHackathonTeamPrizeByIdAndPrizeName(id, HackathonPrize.GRAND_PRIZE.toString()); + List excellenceTeams = findHackathonTeamPrizeByIdAndPrizeName(id, HackathonPrize.EXCELLENCE_PRIZE.toString()); + List meritTeams = findHackathonTeamPrizeByIdAndPrizeName(id, HackathonPrize.MERIT_PRIZE.toString()); + List encouragementTeams = findHackathonTeamPrizeByIdAndPrizeName(id, HackathonPrize.ENCOURAGEMENT_PRIZE.toString()); + + List prizes = new ArrayList<>(); + prizes.add(new HackathonPrizeResponse(HackathonPrize.GRAND_PRIZE.toString(), grandTeams)); + prizes.add(new HackathonPrizeResponse(HackathonPrize.EXCELLENCE_PRIZE.toString(), excellenceTeams)); + prizes.add(new HackathonPrizeResponse(HackathonPrize.MERIT_PRIZE.toString(), meritTeams)); + prizes.add(new HackathonPrizeResponse(HackathonPrize.ENCOURAGEMENT_PRIZE.toString(), encouragementTeams)); + + return prizes; + } + + private List findHackathonTeamPrizeByIdAndPrizeName(Long id, String prize) { + List teams = hackathonTeamRepository.findByHackathonIdAndPrizeEquals(id, prize); + return convertHackathonTeamToHackathonTeamPrize(teams); + } + + private List convertHackathonTeamToHackathonTeamPrize(List teams) { + return teams.stream().map(team -> { + Long teamCount = hackathonTeamMemberRepository.countByHackathonIdAndTeamId(team.getId(), team.getId()); + return new HackathonTeamPrize(team.getId(), team.getName(), teamCount, team.getWork()); + }).toList(); + } } diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonPrizeResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonPrizeResponse.java new file mode 100644 index 00000000..f858cc6a --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonPrizeResponse.java @@ -0,0 +1,15 @@ +package sw_css.hackathon.application.dto.response; + +import java.util.List; + +public record HackathonPrizeResponse( + String prize, + List teams +) { + public record HackathonTeamPrize( + Long id, + String name, + Long memberCount, + String work + ){} +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java index 8491c590..879742ad 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java @@ -8,4 +8,6 @@ public interface HackathonTeamMemberRepository extends JpaRepository findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(Long hackathonId, Long teamId); HackathonTeamMember findAllByHackathonIdAndTeamIdAndIsLeaderTrue(Long hackathonId, Long teamId); + + Long countByHackathonIdAndTeamId(Long hackathonId, Long teamId); } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 3017b6ab..9a7a1867 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -26,4 +26,6 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonId(Long hackathonId); Optional findByHackathonIdAndId(Long hackathonId, Long teamId); + + List findByHackathonIdAndPrizeEquals(Long hackathonId, String prize); } diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index 8f63a89c..80ab961b 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -32,9 +32,9 @@ values('제4회 PNU 창의융합 소프트웨어해커톤', ' ## hackathon team insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'NONE_PRIZE', false); +values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'GRAND_PRIZE', false); insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'NONE_PRIZE', false); +values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'EXCELLENCE_PRIZE', false); ## hackathon team vote insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java index 1f2ddd1a..c6302ba4 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java @@ -11,6 +11,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.LocalDate; +import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,6 +29,8 @@ import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.hackathon.api.HackathonController; import sw_css.hackathon.application.dto.response.HackathonDetailResponse; +import sw_css.hackathon.application.dto.response.HackathonPrizeResponse; +import sw_css.hackathon.application.dto.response.HackathonPrizeResponse.HackathonTeamPrize; import sw_css.hackathon.application.dto.response.HackathonResponse; import sw_css.hackathon.domain.Hackathon; import sw_css.restdocs.RestDocsTest; @@ -130,6 +133,44 @@ public void findHackathonById() throws Exception { RestDocumentationRequestBuilders.get("/hackathons/{hackathonId}", hackathonId)) .andExpect(status().isOk()) .andDo(document("hackathon-find", pathParameterSnippet, responseBodySnippet)); + } + + @Test + @DisplayName("[성공] 모든 사용자는 해커톤의 수상 내역을 조회할 수 있다.") + public void findHackathonPrize() throws Exception { + // given + PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해커톤 id") + ); + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("[].prize").type(JsonFieldType.STRING).description("상장 타입 (GRAND_PRIZE, EXCELLENCE_PRIZE, MERIT_PRIZE, ENCOURAGEMENT_PRIZE)"), + fieldWithPath("[].teams[].id").type(JsonFieldType.NUMBER).description("해커톤 팀 id"), + fieldWithPath("[].teams[].name").type(JsonFieldType.STRING).description("해커톤 팀명"), + fieldWithPath("[].teams[].work").type(JsonFieldType.STRING).description("해커톤 팀의 프로젝트 명"), + fieldWithPath("[].teams[].memberCount").type(JsonFieldType.NUMBER).description("해커톤 팀의 팀원 수") + ); + + final List teams = List.of( + new HackathonTeamPrize(1L, "팀명입니다", 4L, "프로젝트명입니다"), + new HackathonTeamPrize(2L, "팀명2입니다", 4L, "프로젝트명2입니다") + ); + final List response = List.of( + new HackathonPrizeResponse("GRAND_PRIZE", teams), + new HackathonPrizeResponse("EXCELLENCE_PRIZE", teams), + new HackathonPrizeResponse("MERIT_PRIZE", teams), + new HackathonPrizeResponse("ENCOURAGEMENT_PRIZE", teams) + ); + + final Long hackathonId = 1L; + + // when + when(hackathonQueryService.findHackathonPrizes(hackathonId)).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/hackathons/{hackathonId}/prize", hackathonId)) + .andExpect(status().isOk()) + .andDo(document("hackathon-find-prize", pathParameterSnippet, responseBodySnippet)); } } From b5763b3c36ae555ba8402c2f9b9b2eeb3f43ef69 Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 01:47:19 +0900 Subject: [PATCH 28/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=EC=9D=98=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20test=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 17 ++++ .../api/HackathonTeamController.java | 40 ++++++++ .../HackathonTeamQueryService.java | 27 +++++ .../dto/response/HackathonTeamResponse.java | 2 +- .../repository/HackathonTeamRepository.java | 21 +++- backend/src/main/resources/test-data.sql | 20 ++++ .../java/sw_css/restdocs/RestDocsTest.java | 4 + .../restdocs/docs/HackathonApiDocsTest.java | 4 - .../docs/HackathonTeamApiDocsTest.java | 99 +++++++++++++++++++ 9 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java create mode 100644 backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java create mode 100644 backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 816632da..6f7ca2bd 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -526,3 +526,20 @@ include::{snippets}/hackathon-find-prize/http-response.adoc[] .Response Body include::{snippets}/hackathon-find-prize/response-fields.adoc[] + + +== 일반 - 해커톤 팀 + +=== `GET`: 해커톤 팀 목록 조회 + +.HTTP Request +include::{snippets}/hackathon-team-find-all/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-find-all/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-find-all/http-response.adoc[] + +.Response Body +include::{snippets}/hackathon-team-find-all/response-fields.adoc[] diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java new file mode 100644 index 00000000..5e30cece --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -0,0 +1,40 @@ +package sw_css.hackathon.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sw_css.hackathon.application.HackathonTeamQueryService; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse; + +@Validated +@RequestMapping("/hackathons/{hackathonId}/teams") +@RestController +@RequiredArgsConstructor +public class HackathonTeamController { + + private final HackathonTeamQueryService hackathonTeamQueryService; + + // TODO: 해커톤 팀 목록 조회 + @GetMapping + public ResponseEntity> findAllTeams( + Pageable pageable, + @PathVariable Long hackathonId + ) { + return ResponseEntity.ok(hackathonTeamQueryService.findAllHackathonTeam(pageable, hackathonId)); + } + + // TODO: 해커톤 팀 상세 조회 + + // TODO: 해커톤 팀 등록 + + // TODO: 해커톤 팀 수정 + + // TODO: 해커톤 투표 + +} diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java new file mode 100644 index 00000000..87a9941b --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java @@ -0,0 +1,27 @@ +package sw_css.hackathon.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse; +import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.hackathon.exception.HackathonException; +import sw_css.hackathon.exception.HackathonExceptionType; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HackathonTeamQueryService { + + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonRepository hackathonRepository; + + public Page findAllHackathonTeam(final Pageable pageable, final Long hackathonId) { + hackathonRepository.findById(hackathonId).orElseThrow(() -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + return hackathonTeamRepository.findByHackathonIdWithPageable(hackathonId, pageable); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java index 3b2163d2..872dde56 100644 --- a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java @@ -3,9 +3,9 @@ public record HackathonTeamResponse( Long id, String name, - String imageUrl, String work, String githubUrl, + String imageUrl, Long vote, String prize ) { diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 9a7a1867..5bc55d7b 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -3,12 +3,15 @@ import io.lettuce.core.dynamic.annotation.Param; import java.util.List; import java.util.Optional; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.domain.HackathonTeam; public interface HackathonTeamRepository extends JpaRepository { - @Query("SELECT ht.id AS team_id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, COUNT(htv.id) AS vote_count " + + @Query("SELECT ht.id AS team_id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, COUNT(htv.id) AS vote " + "FROM HackathonTeam ht " + "LEFT JOIN HackathonTeamVote htv ON ht.id = htv.team.id AND ht.hackathon.id = htv.hackathon.id " + "GROUP BY ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, ht.hackathon.id " + @@ -23,6 +26,22 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); + @Query("SELECT new sw_css.hackathon.application.dto.response.HackathonTeamResponse(" + + "ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, COUNT(htv.id), ht.prize) " + + "FROM HackathonTeam ht " + + "LEFT JOIN HackathonTeamVote htv ON ht.id = htv.team.id AND ht.hackathon.id = htv.hackathon.id " + + "GROUP BY ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, ht.hackathon.id " + + "ORDER BY " + + "CASE ht.prize " + + "WHEN 'GRAND_PRIZE' THEN 1 " + + "WHEN 'EXCELLENCE_PRIZE' THEN 2 " + + "WHEN 'MERIT_PRIZE' THEN 3 " + + "WHEN 'ENCOURAGEMENT_PRIZE' THEN 4 " + + "WHEN 'NONE_PRIZE' THEN 5 " + + "ELSE 6 END, " + + "COUNT(htv.id) DESC") + Page findByHackathonIdWithPageable(@Param("hackathonId") Long hackathonId, Pageable pageable); + List findByHackathonId(Long hackathonId); Optional findByHackathonIdAndId(Long hackathonId, Long teamId); diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index 80ab961b..4442b7fc 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -35,12 +35,32 @@ insert into hackathon_team (hackathon_id, name, image_url, work, github_url, pri values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'GRAND_PRIZE', false); insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'EXCELLENCE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '팀명1', '1.png', '프로젝트명1', 'https://www.naver.com', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '팀명2', '1.png', '프로젝트명2', 'https://www.naver.com', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '팀명3', '1.png', '프로젝트명3', 'https://www.naver.com', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) +values(1, '팀명4', '1.png', '프로젝트명4', 'https://www.naver.com', 'NONE_PRIZE', false); ## hackathon team vote insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) values(1, 2, 202012341, false); insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) values(1, 2, 202012342, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 2, 202012341, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 2, 202012342, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 3, 202012341, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 3, 202012342, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 6, 202012341, false); +insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +values(1, 5, 202012342, false); ## hackathon team member insert into hackathon_team_member (hackathon_id, team_id, student_id, role, is_leader, is_deleted) diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index b2b81208..f3cf11b9 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -27,6 +27,7 @@ import sw_css.auth.application.AuthSignUpService; import sw_css.file.application.FileService; import sw_css.hackathon.application.HackathonQueryService; +import sw_css.hackathon.application.HackathonTeamQueryService; import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; import sw_css.member.application.MemberQueryService; @@ -79,6 +80,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected HackathonQueryService hackathonQueryService; + @MockBean + protected HackathonTeamQueryService hackathonTeamQueryService; + @MockBean protected FileService fileService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java index c6302ba4..5bfd7b62 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java @@ -11,7 +11,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,14 +18,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; -import org.springframework.http.HttpHeaders; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import org.springframework.restdocs.request.QueryParametersSnippet; -import sw_css.admin.hackathon.application.dto.response.AdminHackathonDetailResponse; -import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; import sw_css.hackathon.api.HackathonController; import sw_css.hackathon.application.dto.response.HackathonDetailResponse; import sw_css.hackathon.application.dto.response.HackathonPrizeResponse; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java new file mode 100644 index 00000000..daacb5f9 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -0,0 +1,99 @@ +package sw_css.restdocs.docs; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; +import org.springframework.restdocs.request.PathParametersSnippet; +import org.springframework.restdocs.request.QueryParametersSnippet; +import sw_css.hackathon.api.HackathonTeamController; +import sw_css.hackathon.application.HackathonTeamQueryService; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse; +import sw_css.hackathon.domain.HackathonPrize; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(HackathonTeamController.class) +public class HackathonTeamApiDocsTest extends RestDocsTest { + + @Autowired + private HackathonTeamQueryService hackathonTeamQueryService; + + @Test + @DisplayName("[성공] 모든 사용자는 해커톤 팀 목록을 조회할 수 있다.") + public void findAllHackathonTeams() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해커톤 id"), + parameterWithName("page").optional().description("조회할 해커톤 팀의 페이지"), + parameterWithName("size").optional().description("조회할 해커톤의 페이지 당 데이터 수") + ); + + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("totalElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("size").type(JsonFieldType.NUMBER).description("페이지 내 데이터 수"), + fieldWithPath("number").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("sort.empty").type(JsonFieldType.BOOLEAN).description("정렬 속성의 존재 여부"), + fieldWithPath("sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬 여부"), + fieldWithPath("sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬여부"), + fieldWithPath("first").type(JsonFieldType.BOOLEAN).description("첫 페이지인지 여부"), + fieldWithPath("last").type(JsonFieldType.BOOLEAN).description("마지막 페이지인지 여부"), + fieldWithPath("pageable.pageNumber").type(JsonFieldType.NUMBER).description("요청한 페이지번호"), + fieldWithPath("pageable.pageSize").type(JsonFieldType.NUMBER).description("요청한 페이지크기"), + fieldWithPath("pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("요청한 데이터가 비었는지 여부"), + fieldWithPath("pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("요청한 데이터 정렬 기준 존재 여부"), + fieldWithPath("pageable.offset").type(JsonFieldType.NUMBER).description("요청한 페이지오프셋"), + fieldWithPath("pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("pageable.unpaged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("numberOfElements").type(JsonFieldType.NUMBER).description("총 데이터 수"), + fieldWithPath("empty").type(JsonFieldType.BOOLEAN).description("데이터의 존재 여부"), + fieldWithPath("content[].id").type(JsonFieldType.NUMBER).description("팀의 id"), + fieldWithPath("content[].name").type(JsonFieldType.STRING).description("팀의 이름"), + fieldWithPath("content[].work").type(JsonFieldType.STRING).description("팀의 프로젝트 명"), + fieldWithPath("content[].githubUrl").type(JsonFieldType.STRING).description("팀의 깃헙 레포 url"), + fieldWithPath("content[].imageUrl").type(JsonFieldType.STRING).description("팀의 썸네일 이미지"), + fieldWithPath("content[].prize").type(JsonFieldType.STRING).description("팀의 상장 타입 (GRAND_PRIZE, EXCELLENCE_PRIZE, MERIT_PRIZE, ENCOURAGEMENT_PRIZE, NONE_PRIZE)"), + fieldWithPath("content[].vote").type(JsonFieldType.NUMBER).description("팀의 투표 득표수") + ); + + final Long hackathonId = 1L; + final Pageable pageable = PageRequest.of(0, 10); + Page response = new PageImpl<>( + List.of( + new HackathonTeamResponse(1L, "팀명1", "프로젝트명1", "https://github.com/SW-CSS/sw-css", "1.png", 98L, HackathonPrize.GRAND_PRIZE.toString()), + new HackathonTeamResponse(2L, "팀명2", "프로젝트명2", "https://github.com/SW-CSS/sw-css", "1.png", 78L, HackathonPrize.EXCELLENCE_PRIZE.toString())), + pageable, 2); + + // when + when(hackathonTeamQueryService.findAllHackathonTeam(pageable, hackathonId)).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/hackathons/{hackathonId}/teams", hackathonId) + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andDo(document("hackathon-team-find-all", pathParameterSnippet, responseBodySnippet)); + } + +} From 66131b4e9a50fd5035baa993f11f538a146e52c8 Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 02:52:20 +0900 Subject: [PATCH 29/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C=20response=EC=97=90?= =?UTF-8?q?=20=ED=8C=80=EC=9B=90=20=EB=AA=A9=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- .../api/HackathonTeamController.java | 1 - .../HackathonTeamQueryService.java | 43 ++++++++++++++++++- .../dto/response/HackathonTeamResponse.java | 19 +++++++- .../domain/HackathonTeamWithVote.java | 12 ++++++ .../HackathonTeamMemberRepository.java | 2 + .../repository/HackathonTeamRepository.java | 5 ++- .../docs/HackathonTeamApiDocsTest.java | 40 ++++++++++++++--- 7 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java index 5e30cece..1f8e75f7 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -20,7 +20,6 @@ public class HackathonTeamController { private final HackathonTeamQueryService hackathonTeamQueryService; - // TODO: 해커톤 팀 목록 조회 @GetMapping public ResponseEntity> findAllTeams( Pageable pageable, diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java index 87a9941b..a9486903 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java @@ -1,15 +1,26 @@ package sw_css.hackathon.application; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse.HackathonTeamMemberResponse; +import sw_css.hackathon.domain.HackathonRole; +import sw_css.hackathon.domain.HackathonTeamMember; +import sw_css.hackathon.domain.HackathonTeamWithVote; import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamMemberRepository; import sw_css.hackathon.domain.repository.HackathonTeamRepository; import sw_css.hackathon.exception.HackathonException; import sw_css.hackathon.exception.HackathonExceptionType; +import sw_css.member.domain.StudentMember; +import sw_css.member.domain.repository.StudentMemberRepository; @Service @RequiredArgsConstructor @@ -18,10 +29,40 @@ public class HackathonTeamQueryService { private final HackathonTeamRepository hackathonTeamRepository; private final HackathonRepository hackathonRepository; + private final HackathonTeamMemberRepository hackathonTeamMemberRepository; + private final StudentMemberRepository studentMemberRepository; public Page findAllHackathonTeam(final Pageable pageable, final Long hackathonId) { hackathonRepository.findById(hackathonId).orElseThrow(() -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); - return hackathonTeamRepository.findByHackathonIdWithPageable(hackathonId, pageable); + Page teams = hackathonTeamRepository.findByHackathonIdWithPageable(hackathonId, pageable); + + List teamResponses = teams.getContent().stream().map(team -> { + List developers = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.DEVELOPER.toString()); + List designers = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.DESIGNER.toString()); + List planners = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.PLANNER.toString()); + List others = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.OTHER.toString()); + + Map> members = new HashMap<>(); + members.put(HackathonRole.DEVELOPER.toString(), developers); + members.put(HackathonRole.DESIGNER.toString(), designers); + members.put(HackathonRole.PLANNER.toString(), planners); + members.put(HackathonRole.OTHER.toString(), others); + + return HackathonTeamResponse.of(team, members); + }).toList(); + + return new PageImpl<>(teamResponses, teams.getPageable(), teams.getTotalElements()); + } + + private List findTeamMemberByRole(Long hackathonId, Long teamId, String role){ + List teamMember = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndRole(hackathonId, teamId, role); + return teamMember.stream().map(this::convertHackathonTeamMemberToHackathonTeamMemberResponse).toList(); + } + + private HackathonTeamMemberResponse convertHackathonTeamMemberToHackathonTeamMemberResponse(HackathonTeamMember teamMember) { + StudentMember member = studentMemberRepository.findById(teamMember.getStudentId()).orElse(null); + if(member == null) return new HackathonTeamMemberResponse(teamMember.getStudentId(), "", "", teamMember.getIsLeader()); + return new HackathonTeamMemberResponse(member.getId(), member.getMember().getName(), member.getMajor().getName(), teamMember.getIsLeader()); } } diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java index 872dde56..f708f283 100644 --- a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java @@ -1,5 +1,9 @@ package sw_css.hackathon.application.dto.response; +import java.util.List; +import java.util.Map; +import sw_css.hackathon.domain.HackathonTeamWithVote; + public record HackathonTeamResponse( Long id, String name, @@ -7,6 +11,19 @@ public record HackathonTeamResponse( String githubUrl, String imageUrl, Long vote, - String prize + String prize, + Map> members ) { + public record HackathonTeamMemberResponse( + Long id, + String name, + String major, + boolean isLeader + ) {} + + static public HackathonTeamResponse of(HackathonTeamWithVote team, Map> members) { + return new HackathonTeamResponse( + team.id(), team.name(), team.work(), team.githubUrl(), team.imageUrl(), team.vote(), team.prize(), members + ); + } } diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java new file mode 100644 index 00000000..daa83d52 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java @@ -0,0 +1,12 @@ +package sw_css.hackathon.domain; + +public record HackathonTeamWithVote( + Long id, + String name, + String work, + String githubUrl, + String imageUrl, + Long vote, + String prize +) { +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java index 879742ad..26bb3004 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java @@ -10,4 +10,6 @@ public interface HackathonTeamMemberRepository extends JpaRepository findAllByHackathonIdAndTeamIdAndRole(Long hackathonId, Long teamId, String role); } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 5bc55d7b..0734e070 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.Query; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.domain.HackathonTeam; +import sw_css.hackathon.domain.HackathonTeamWithVote; public interface HackathonTeamRepository extends JpaRepository { @Query("SELECT ht.id AS team_id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, COUNT(htv.id) AS vote " + @@ -26,7 +27,7 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); - @Query("SELECT new sw_css.hackathon.application.dto.response.HackathonTeamResponse(" + + @Query("SELECT new sw_css.hackathon.domain.HackathonTeamWithVote(" + "ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, COUNT(htv.id), ht.prize) " + "FROM HackathonTeam ht " + "LEFT JOIN HackathonTeamVote htv ON ht.id = htv.team.id AND ht.hackathon.id = htv.hackathon.id " + @@ -40,7 +41,7 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonIdWithPageable(@Param("hackathonId") Long hackathonId, Pageable pageable); + Page findByHackathonIdWithPageable(@Param("hackathonId") Long hackathonId, Pageable pageable); List findByHackathonId(Long hackathonId); diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java index daacb5f9..50f0f0db 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -11,7 +11,9 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -24,11 +26,12 @@ import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; -import org.springframework.restdocs.request.QueryParametersSnippet; import sw_css.hackathon.api.HackathonTeamController; import sw_css.hackathon.application.HackathonTeamQueryService; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; +import sw_css.hackathon.application.dto.response.HackathonTeamResponse.HackathonTeamMemberResponse; import sw_css.hackathon.domain.HackathonPrize; +import sw_css.hackathon.domain.HackathonRole; import sw_css.restdocs.RestDocsTest; @WebMvcTest(HackathonTeamController.class) @@ -73,16 +76,43 @@ public void findAllHackathonTeams() throws Exception { fieldWithPath("content[].githubUrl").type(JsonFieldType.STRING).description("팀의 깃헙 레포 url"), fieldWithPath("content[].imageUrl").type(JsonFieldType.STRING).description("팀의 썸네일 이미지"), fieldWithPath("content[].prize").type(JsonFieldType.STRING).description("팀의 상장 타입 (GRAND_PRIZE, EXCELLENCE_PRIZE, MERIT_PRIZE, ENCOURAGEMENT_PRIZE, NONE_PRIZE)"), - fieldWithPath("content[].vote").type(JsonFieldType.NUMBER).description("팀의 투표 득표수") + fieldWithPath("content[].vote").type(JsonFieldType.NUMBER).description("팀의 투표 득표수"), + fieldWithPath("content[].members.DEVELOPER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("content[].members.DEVELOPER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("content[].members.DEVELOPER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("content[].members.DEVELOPER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부"), + fieldWithPath("content[].members.DESIGNER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("content[].members.DESIGNER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("content[].members.DESIGNER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("content[].members.DESIGNER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부"), + fieldWithPath("content[].members.PLANNER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("content[].members.PLANNER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("content[].members.PLANNER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("content[].members.PLANNER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부"), + fieldWithPath("content[].members.OTHER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("content[].members.OTHER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("content[].members.OTHER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("content[].members.OTHER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부") ); final Long hackathonId = 1L; final Pageable pageable = PageRequest.of(0, 10); + + final List members = List.of( + new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", true), + new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", false)); + final Map> memberMap = new HashMap<>(); + memberMap.put(HackathonRole.DEVELOPER.toString(), members); + memberMap.put(HackathonRole.DESIGNER.toString(), members); + memberMap.put(HackathonRole.PLANNER.toString(), members); + memberMap.put(HackathonRole.OTHER.toString(), members); + Page response = new PageImpl<>( List.of( - new HackathonTeamResponse(1L, "팀명1", "프로젝트명1", "https://github.com/SW-CSS/sw-css", "1.png", 98L, HackathonPrize.GRAND_PRIZE.toString()), - new HackathonTeamResponse(2L, "팀명2", "프로젝트명2", "https://github.com/SW-CSS/sw-css", "1.png", 78L, HackathonPrize.EXCELLENCE_PRIZE.toString())), - pageable, 2); + new HackathonTeamResponse(1L, "팀명1", "프로젝트명1", "https://github.com/SW-CSS/sw-css", "1.png", 98L, HackathonPrize.GRAND_PRIZE.toString(), memberMap), + new HackathonTeamResponse(2L, "팀명2", "프로젝트명2", "https://github.com/SW-CSS/sw-css", "2.png", 60L, HackathonPrize.GRAND_PRIZE.toString(), memberMap)), + pageable, 2 + ); // when when(hackathonTeamQueryService.findAllHackathonTeam(pageable, hackathonId)).thenReturn(response); From 7fbb35d4a6dae127bd580790a8fd5290cd9f6c8b Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 03:11:15 +0900 Subject: [PATCH 30/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=EB=B0=8F=20test=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 15 +++++ .../api/HackathonTeamController.java | 8 ++- .../HackathonTeamQueryService.java | 48 +++++++++----- .../domain/HackathonTeamWithVote.java | 4 ++ .../repository/HackathonTeamRepository.java | 2 +- .../exception/HackathonExceptionType.java | 3 +- .../docs/HackathonTeamApiDocsTest.java | 63 ++++++++++++++++++- 7 files changed, 125 insertions(+), 18 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 6f7ca2bd..c0553d82 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -543,3 +543,18 @@ include::{snippets}/hackathon-team-find-all/http-response.adoc[] .Response Body include::{snippets}/hackathon-team-find-all/response-fields.adoc[] + + +=== `GET`: 해커톤 팀 상세 조회 + +.HTTP Request +include::{snippets}/hackathon-team-find/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-find/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-find/http-response.adoc[] + +.Response Body +include::{snippets}/hackathon-team-find/response-fields.adoc[] diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java index 1f8e75f7..20dd29e7 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -28,7 +28,13 @@ public ResponseEntity> findAllTeams( return ResponseEntity.ok(hackathonTeamQueryService.findAllHackathonTeam(pageable, hackathonId)); } - // TODO: 해커톤 팀 상세 조회 + @GetMapping("{teamId}") + public ResponseEntity findTeamById( + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + return ResponseEntity.ok(hackathonTeamQueryService.findHackathonTeam(hackathonId, teamId)); + } // TODO: 해커톤 팀 등록 diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java index a9486903..5cde5a01 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java @@ -7,16 +7,19 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.application.dto.response.HackathonTeamResponse.HackathonTeamMemberResponse; import sw_css.hackathon.domain.HackathonRole; +import sw_css.hackathon.domain.HackathonTeam; import sw_css.hackathon.domain.HackathonTeamMember; import sw_css.hackathon.domain.HackathonTeamWithVote; import sw_css.hackathon.domain.repository.HackathonRepository; import sw_css.hackathon.domain.repository.HackathonTeamMemberRepository; import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.hackathon.domain.repository.HackathonTeamVoteRepository; import sw_css.hackathon.exception.HackathonException; import sw_css.hackathon.exception.HackathonExceptionType; import sw_css.member.domain.StudentMember; @@ -31,28 +34,45 @@ public class HackathonTeamQueryService { private final HackathonRepository hackathonRepository; private final HackathonTeamMemberRepository hackathonTeamMemberRepository; private final StudentMemberRepository studentMemberRepository; + private final HackathonTeamVoteRepository hackathonTeamVoteRepository; public Page findAllHackathonTeam(final Pageable pageable, final Long hackathonId) { - hackathonRepository.findById(hackathonId).orElseThrow(() -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + validateHackathonId(hackathonId); Page teams = hackathonTeamRepository.findByHackathonIdWithPageable(hackathonId, pageable); - List teamResponses = teams.getContent().stream().map(team -> { - List developers = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.DEVELOPER.toString()); - List designers = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.DESIGNER.toString()); - List planners = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.PLANNER.toString()); - List others = findTeamMemberByRole(hackathonId, team.id(), HackathonRole.OTHER.toString()); + List teamResponses = teams.getContent().stream().map( + this::convertHackathonTeamToHackathonTeamResponse).toList(); + + return new PageImpl<>(teamResponses, teams.getPageable(), teams.getTotalElements()); + } - Map> members = new HashMap<>(); - members.put(HackathonRole.DEVELOPER.toString(), developers); - members.put(HackathonRole.DESIGNER.toString(), designers); - members.put(HackathonRole.PLANNER.toString(), planners); - members.put(HackathonRole.OTHER.toString(), others); + public HackathonTeamResponse findHackathonTeam(final Long hackathonId, final Long teamId) { + validateHackathonId(hackathonId); - return HackathonTeamResponse.of(team, members); - }).toList(); + HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow(() -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + final Long vote = hackathonTeamVoteRepository.countByHackathonIdAndTeamId(hackathonId, teamId); - return new PageImpl<>(teamResponses, teams.getPageable(), teams.getTotalElements()); + return convertHackathonTeamToHackathonTeamResponse(HackathonTeamWithVote.of(team, vote)); + } + + private void validateHackathonId(final Long hackathonId) { + hackathonRepository.findById(hackathonId).orElseThrow(() -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + } + + private HackathonTeamResponse convertHackathonTeamToHackathonTeamResponse(HackathonTeamWithVote team) { + List developers = findTeamMemberByRole(team.hackathonId(), team.id(), HackathonRole.DEVELOPER.toString()); + List designers = findTeamMemberByRole(team.hackathonId(), team.id(), HackathonRole.DESIGNER.toString()); + List planners = findTeamMemberByRole(team.hackathonId(), team.id(), HackathonRole.PLANNER.toString()); + List others = findTeamMemberByRole(team.hackathonId(), team.id(), HackathonRole.OTHER.toString()); + + Map> members = new HashMap<>(); + members.put(HackathonRole.DEVELOPER.toString(), developers); + members.put(HackathonRole.DESIGNER.toString(), designers); + members.put(HackathonRole.PLANNER.toString(), planners); + members.put(HackathonRole.OTHER.toString(), others); + + return HackathonTeamResponse.of(team, members); } private List findTeamMemberByRole(Long hackathonId, Long teamId, String role){ diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java index daa83d52..b6ca4810 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java @@ -2,6 +2,7 @@ public record HackathonTeamWithVote( Long id, + Long hackathonId, String name, String work, String githubUrl, @@ -9,4 +10,7 @@ public record HackathonTeamWithVote( Long vote, String prize ) { + static public HackathonTeamWithVote of(HackathonTeam team, Long vote) { + return new HackathonTeamWithVote(team.getId(), team.getHackathon().getId(), team.getName(), team.getWork(), team.getGithubUrl(), team.getImageUrl(), vote, team.getPrize()); + } } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java index 0734e070..0ca49e7f 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -28,7 +28,7 @@ public interface HackathonTeamRepository extends JpaRepository findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); @Query("SELECT new sw_css.hackathon.domain.HackathonTeamWithVote(" + - "ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, COUNT(htv.id), ht.prize) " + + "ht.id, ht.hackathon.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, COUNT(htv.id), ht.prize) " + "FROM HackathonTeam ht " + "LEFT JOIN HackathonTeamVote htv ON ht.id = htv.team.id AND ht.hackathon.id = htv.hackathon.id " + "GROUP BY ht.id, ht.name, ht.imageUrl, ht.work, ht.githubUrl, ht.prize, ht.hackathon.id " + diff --git a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java index 6726a09f..f5f2123a 100644 --- a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -4,7 +4,8 @@ import sw_css.base.BaseExceptionType; public enum HackathonExceptionType implements BaseExceptionType { - NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."); + NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), + NOT_FOUND_HACKATHON_TEAM(HttpStatus.NOT_FOUND, "해당하는 팀이 존재하지 않습니다."); private final HttpStatus httpStatus; private final String errorMessage; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java index 50f0f0db..73bcc4f3 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -24,6 +24,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseBodySnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import sw_css.hackathon.api.HackathonTeamController; @@ -32,6 +33,7 @@ import sw_css.hackathon.application.dto.response.HackathonTeamResponse.HackathonTeamMemberResponse; import sw_css.hackathon.domain.HackathonPrize; import sw_css.hackathon.domain.HackathonRole; +import sw_css.hackathon.domain.HackathonTeam; import sw_css.restdocs.RestDocsTest; @WebMvcTest(HackathonTeamController.class) @@ -99,7 +101,7 @@ public void findAllHackathonTeams() throws Exception { final Pageable pageable = PageRequest.of(0, 10); final List members = List.of( - new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", true), + new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", false), new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", false)); final Map> memberMap = new HashMap<>(); memberMap.put(HackathonRole.DEVELOPER.toString(), members); @@ -126,4 +128,63 @@ public void findAllHackathonTeams() throws Exception { .andDo(document("hackathon-team-find-all", pathParameterSnippet, responseBodySnippet)); } + @Test + @DisplayName("[성공] 모든 사용자는 해커톤 팀의 상세 조회할 수 있다.") + public void findHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해커톤 id"), + parameterWithName("teamId").description("해커톤 팀 id") + ); + + final ResponseFieldsSnippet responseBodySnippet = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("팀의 id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("팀의 이름"), + fieldWithPath("work").type(JsonFieldType.STRING).description("팀의 프로젝트 명"), + fieldWithPath("githubUrl").type(JsonFieldType.STRING).description("팀의 깃헙 레포 url"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("팀의 썸네일 이미지"), + fieldWithPath("prize").type(JsonFieldType.STRING).description("팀의 상장 타입 (GRAND_PRIZE, EXCELLENCE_PRIZE, MERIT_PRIZE, ENCOURAGEMENT_PRIZE, NONE_PRIZE)"), + fieldWithPath("vote").type(JsonFieldType.NUMBER).description("팀의 투표 득표수"), + fieldWithPath("members.DEVELOPER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("members.DEVELOPER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("members.DEVELOPER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("members.DEVELOPER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부"), + fieldWithPath("members.DESIGNER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("members.DESIGNER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("members.DESIGNER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("members.DESIGNER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부"), + fieldWithPath("members.PLANNER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("members.PLANNER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("members.PLANNER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("members.PLANNER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부"), + fieldWithPath("members.OTHER[].id").type(JsonFieldType.NUMBER).description("팀원 중 개발자의 학번"), + fieldWithPath("members.OTHER[].name").type(JsonFieldType.STRING).description("팀원 중 개발자의 이름"), + fieldWithPath("members.OTHER[].major").type(JsonFieldType.STRING).description("팀원 중 개발자의 전공"), + fieldWithPath("members.OTHER[].isLeader").type(JsonFieldType.BOOLEAN).description("팀이 팀장 여부") + ); + + final Long hackathonId = 1L; + final Long teamId = 2L; + + final List members = List.of( + new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", false), + new HackathonTeamMemberResponse(202012345L, "학생 이름", "학생 전공", false)); + final Map> memberMap = new HashMap<>(); + memberMap.put(HackathonRole.DEVELOPER.toString(), members); + memberMap.put(HackathonRole.DESIGNER.toString(), members); + memberMap.put(HackathonRole.PLANNER.toString(), members); + memberMap.put(HackathonRole.OTHER.toString(), members); + + HackathonTeamResponse response = new HackathonTeamResponse(1L, "팀명1", "프로젝트명1", "https://github.com/SW-CSS/sw-css", "1.png", 98L, HackathonPrize.GRAND_PRIZE.toString(), memberMap); + + // when + when(hackathonTeamQueryService.findHackathonTeam(hackathonId, teamId)).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/hackathons/{hackathonId}/teams/{teamId}", hackathonId, teamId)) + .andExpect(status().isOk()) + .andDo(document("hackathon-team-find", pathParameterSnippet, responseBodySnippet)); + } + } From 33c27e4cf9f54ad9fb4fa11a7c1c621bfc5039ee Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 04:13:53 +0900 Subject: [PATCH 31/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=EB=93=B1=EB=A1=9D=20API=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 15 ++ .../api/AdminHackathonTeamController.java | 4 +- .../AdminHackathonTeamCommandService.java | 26 ++-- .../api/HackathonTeamController.java | 26 +++- .../HackathonTeamCommandService.java | 140 ++++++++++++++++++ .../HackathonTeamQueryService.java | 1 - .../dto/request/HackathonTeamRequest.java} | 11 +- .../sw_css/hackathon/domain/Hackathon.java | 4 - .../hackathon/domain/HackathonTeam.java | 14 +- .../exception/HackathonExceptionType.java | 5 +- .../java/sw_css/restdocs/RestDocsTest.java | 4 + .../docs/HackathonTeamApiDocsTest.java | 47 +++++- .../admin/AdminHackathonTeamApiDocsTest.java | 9 +- 13 files changed, 266 insertions(+), 40 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java rename backend/src/main/java/sw_css/{admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java => hackathon/application/dto/request/HackathonTeamRequest.java} (77%) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index c0553d82..44e2c252 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -558,3 +558,18 @@ include::{snippets}/hackathon-team-find/http-response.adoc[] .Response Body include::{snippets}/hackathon-team-find/response-fields.adoc[] + + +=== `POST`: 해커톤 팀 등록 + +.HTTP Request +include::{snippets}/hackathon-team-register/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-register/path-parameters.adoc[] + +.Request Body +include::{snippets}/hackathon-team-register/request-parts.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-register/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java index 8b60ba61..0ae2f93c 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java @@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sw_css.admin.hackathon.application.AdminHackathonTeamCommandService; -import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest; import sw_css.member.domain.FacultyMember; import sw_css.utils.annotation.AdminInterface; @@ -29,7 +29,7 @@ public ResponseEntity updateHackathonTeam( @AdminInterface FacultyMember facultyMember, @PathVariable Long hackathonId, @PathVariable Long teamId, - @RequestBody @Valid AdminHackathonTeamRequest request + @RequestBody @Valid HackathonTeamRequest request ) { adminHackathonTeamCommandService.updateHackathonTeam(hackathonId, teamId, request); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java index 2b043470..7a886c19 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java @@ -7,10 +7,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; -import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest.TeamMember; import sw_css.admin.hackathon.exception.AdminHackathonException; import sw_css.admin.hackathon.exception.AdminHackathonExceptionType; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; import sw_css.hackathon.domain.Hackathon; import sw_css.hackathon.domain.HackathonRole; import sw_css.hackathon.domain.HackathonTeam; @@ -28,7 +28,7 @@ public class AdminHackathonTeamCommandService { private final HackathonTeamRepository hackathonTeamRepository; private final HackathonTeamMemberRepository hackathonTeamMemberRepository; - public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTeamRequest request) { + public void updateHackathonTeam(Long hackathonId, Long teamId, HackathonTeamRequest request) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( @@ -43,9 +43,9 @@ public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTea final HackathonTeamMember teamLeader = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderTrue(hackathonId, teamId); final List teamMembers = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(hackathonId, teamId); - final TeamMember leader = request.leader(); - final List members = request.members().stream() - .sorted(Comparator.comparingLong(AdminHackathonTeamRequest.TeamMember::id)) + final HackathonTeamMemberRequest leader = request.leader(); + final List members = request.members().stream() + .sorted(Comparator.comparingLong(HackathonTeamRequest.HackathonTeamMemberRequest::id)) .toList(); checkLeaderAndUpdate(teamLeader, leader, hackathon, hackathonTeam); @@ -53,9 +53,9 @@ public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTea for (HackathonTeamMember originMember : teamMembers) { boolean found = false; - Iterator iterator = members.iterator(); + Iterator iterator = members.iterator(); while (iterator.hasNext()) { - TeamMember member = iterator.next(); + HackathonTeamMemberRequest member = iterator.next(); if ( !originMember.getStudentId().equals(member.id()) ) continue; @@ -71,7 +71,7 @@ public void updateHackathonTeam(Long hackathonId, Long teamId, AdminHackathonTea } } - for (TeamMember member : members) { + for (HackathonTeamMemberRequest member : members) { validateTeamMember(member); HackathonTeamMember newMember = new HackathonTeamMember(hackathon, hackathonTeam, member.id(), member.role()); hackathonTeamMemberRepository.save(newMember); @@ -84,11 +84,11 @@ public void deleteHackathonTeam(Long hackathonId, Long teamId) { final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); - hackathonTeam.setDeleted(true); + hackathonTeam.delete(); hackathonTeamRepository.save(hackathonTeam); } - private void checkMemberAndUpdate(HackathonTeamMember originMember, TeamMember member) { + private void checkMemberAndUpdate(HackathonTeamMember originMember, HackathonTeamMemberRequest member) { validateTeamMember(member); if ( !originMember.getRole().equals(member.role()) ) { originMember.setRole(member.role()); @@ -96,7 +96,7 @@ private void checkMemberAndUpdate(HackathonTeamMember originMember, TeamMember m } } - private void checkLeaderAndUpdate(HackathonTeamMember originLeader, TeamMember leader, Hackathon hackathon, HackathonTeam team) { + private void checkLeaderAndUpdate(HackathonTeamMember originLeader, HackathonTeamMemberRequest leader, Hackathon hackathon, HackathonTeam team) { validateTeamMember(leader); if ( !originLeader.getStudentId().equals(leader.id()) ) { originLeader.setIsDeleted(true); @@ -109,7 +109,7 @@ private void checkLeaderAndUpdate(HackathonTeamMember originLeader, TeamMember l } } - private void validateTeamMember(TeamMember teamMember) { + private void validateTeamMember(HackathonTeamMemberRequest teamMember) { validateStudentId(teamMember.id()); validateRole(teamMember.role()); } diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java index 20dd29e7..fb6f1036 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -1,5 +1,7 @@ package sw_css.hackathon.api; +import jakarta.validation.Valid; +import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -7,10 +9,17 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import sw_css.hackathon.application.HackathonTeamCommandService; import sw_css.hackathon.application.HackathonTeamQueryService; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; +import sw_css.member.domain.Member; +import sw_css.utils.annotation.MemberInterface; @Validated @RequestMapping("/hackathons/{hackathonId}/teams") @@ -19,9 +28,10 @@ public class HackathonTeamController { private final HackathonTeamQueryService hackathonTeamQueryService; + private final HackathonTeamCommandService hackathonTeamCommandService; @GetMapping - public ResponseEntity> findAllTeams( + public ResponseEntity> findAllHackathonTeams( Pageable pageable, @PathVariable Long hackathonId ) { @@ -29,14 +39,24 @@ public ResponseEntity> findAllTeams( } @GetMapping("{teamId}") - public ResponseEntity findTeamById( + public ResponseEntity findHackathonTeamById( @PathVariable Long hackathonId, @PathVariable Long teamId ) { return ResponseEntity.ok(hackathonTeamQueryService.findHackathonTeam(hackathonId, teamId)); } - // TODO: 해커톤 팀 등록 + @PostMapping + public ResponseEntity registerHackathonTeam( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @RequestPart(value = "file") final MultipartFile file, + @RequestPart(value = "request") @Valid final HackathonTeamRequest request + ) { + Long teamId = hackathonTeamCommandService.registerHackathonTeam(hackathonId, file, request); + + return ResponseEntity.created(URI.create("/hackathons/" + hackathonId + "/teams/" + teamId)).build(); + } // TODO: 해커톤 팀 수정 diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java new file mode 100644 index 00000000..29993c3e --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java @@ -0,0 +1,140 @@ +package sw_css.hackathon.application; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; +import sw_css.admin.hackathon.exception.AdminHackathonException; +import sw_css.admin.hackathon.exception.AdminHackathonExceptionType; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; +import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonRole; +import sw_css.hackathon.domain.HackathonTeam; +import sw_css.hackathon.domain.HackathonTeamMember; +import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamMemberRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.hackathon.exception.HackathonException; +import sw_css.hackathon.exception.HackathonExceptionType; +import sw_css.member.domain.embedded.StudentId; +import sw_css.milestone.exception.MilestoneHistoryException; +import sw_css.milestone.exception.MilestoneHistoryExceptionType; + +@Service +@RequiredArgsConstructor +@Transactional +public class HackathonTeamCommandService { + + private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamMemberRepository hackathonTeamMemberRepository; + @Value("${data.file-path-prefix}") + private String filePathPrefix; + + public Long registerHackathonTeam(final Long hackathonId, final MultipartFile file, final HackathonTeamRequest request) { + validateFileType(file); + Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow(() -> new HackathonException( + HackathonExceptionType.NOT_FOUND_HACKATHON)); + + final String newFilePath = generateFilePath(file); + final HackathonTeam team = new HackathonTeam(hackathon, request.name(), request.work(), request.githubUrl(), newFilePath); + + final HackathonTeam newTeam = hackathonTeamRepository.save(team); + uploadFile(file, newFilePath); + + validateAllHackathonTeamMember(request.leader(), request.members()); + + HackathonTeamMember leader = new HackathonTeamMember(hackathon, newTeam, request.leader().id(), request.leader().role(), true); + hackathonTeamMemberRepository.save(leader); + + request.members().forEach(member -> { + HackathonTeamMember newMember = new HackathonTeamMember(hackathon, newTeam, member.id(), member.role(), false); + hackathonTeamMemberRepository.save(newMember); + }); + + return newTeam.getId(); + } + + private void validateAllHackathonTeamMember(HackathonTeamMemberRequest leader, List members) { + Set uniqueIds = new HashSet<>(); + + uniqueIds.add(leader.id()); + validateHackathonTeamMember(leader); + + members.forEach(member -> { + validateHackathonTeamMember(member); + if( !uniqueIds.add(member.id()) ) throw new HackathonException(HackathonExceptionType.INVALID_TEAM_MEMBER); + }); + } + + private void validateHackathonTeamMember(HackathonTeamMemberRequest teamMember) { + validateHackathonTeamMemberId(teamMember.id()); + validateHackathonTeamMemberRole(teamMember.role()); + } + + private void validateHackathonTeamMemberId(Long studentId){ + if ( !studentId.toString().matches(StudentId.STUDENT_ID_REGEX) ) + throw new HackathonException(HackathonExceptionType.INVALID_STUDENT_ID); + } + + private void validateHackathonTeamMemberRole(String role){ + try { + HackathonRole.valueOf(role); + } catch (IllegalArgumentException e) { + throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_ROLE_STATUS); + } + } + + private void validateFileType(final MultipartFile file) { + if (file == null) { + throw new AdminHackathonException(AdminHackathonExceptionType.NOT_EXIST_FILE); + } + final String contentType = file.getContentType(); + if (!isSupportedContentType(contentType)) { + throw new AdminHackathonException(AdminHackathonExceptionType.UNSUPPORTED_FILE_TYPE); + } + } + + private boolean isSupportedContentType(final String contentType) { + return contentType != null && ( + contentType.equals("image/png") || + contentType.equals("image/jpeg") || + contentType.equals("image/jpg") + ); + } + + private String generateFilePath(final MultipartFile file) { + if (file == null) { + return null; + } + return UUID.randomUUID() + "_" + file.getOriginalFilename().replaceAll("\\[|\\]", ""); + } + + private void uploadFile(final MultipartFile file, final String newFilePath) { + if (file == null) { + return; + } + final Path filePath = Paths.get(System.getProperty("user.dir") + filePathPrefix) + .resolve(Paths.get(newFilePath)).normalize().toAbsolutePath(); + try (final InputStream inputStream = file.getInputStream()) { + if (Files.notExists(filePath.getParent())) { + Files.createDirectories(filePath.getParent()); + } + Files.copy(inputStream, filePath, StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException e) { + throw new MilestoneHistoryException(MilestoneHistoryExceptionType.CANNOT_OPEN_FILE); + } + } +} diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java index 5cde5a01..0467d18a 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java @@ -7,7 +7,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; -import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java b/backend/src/main/java/sw_css/hackathon/application/dto/request/HackathonTeamRequest.java similarity index 77% rename from backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java rename to backend/src/main/java/sw_css/hackathon/application/dto/request/HackathonTeamRequest.java index a2e43d45..09939f54 100644 --- a/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonTeamRequest.java +++ b/backend/src/main/java/sw_css/hackathon/application/dto/request/HackathonTeamRequest.java @@ -1,10 +1,10 @@ -package sw_css.admin.hackathon.application.dto.request; +package sw_css.hackathon.application.dto.request; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.util.List; -public record AdminHackathonTeamRequest( +public record HackathonTeamRequest( @NotBlank(message="팀명을 기재해주세요.") String name, @NotBlank(message="프로젝트명을 기재해주세요.") @@ -12,14 +12,15 @@ public record AdminHackathonTeamRequest( @NotBlank(message="프로젝트의 깃헙 레포지토리의 url을 기재해주세요.") String githubUrl, @NotNull(message="팀장의 학번과 역할을 기재해주세요.") - TeamMember leader, + HackathonTeamMemberRequest leader, @NotNull(message="팀원의 정보를 넣어주세요.") - List members + List members ) { - public record TeamMember( + public record HackathonTeamMemberRequest( @NotNull(message="팀원의 학번을 기재해주세요.") Long id, @NotBlank(message="팀원의 역할을 기재해주세요.") String role ){} } + diff --git a/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java index 638b1832..cb39ffbb 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java +++ b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java @@ -2,12 +2,9 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import java.time.LocalDate; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -15,7 +12,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLRestriction; -import sw_css.milestone.domain.MilestoneCategory; @Entity @Getter diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java index 57402300..b02d1443 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -34,18 +34,26 @@ public class HackathonTeam { @Column(nullable = false) private String name; - @Column(nullable = false) - private String imageUrl; - @Column(nullable = false) private String work; @Column(nullable = false) private String githubUrl; + @Column(nullable = false) + private String imageUrl; + @Column(nullable = false) private String prize; @Column(nullable = false) private boolean isDeleted; + + public HackathonTeam(Hackathon hackathon, String name, String work, String githubUrl, String imageUrl) { + this(null, hackathon, name, work, githubUrl, imageUrl, HackathonPrize.NONE_PRIZE.toString(), false); + } + + public void delete() { + isDeleted = true; + } } diff --git a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java index f5f2123a..44a2e1ab 100644 --- a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -5,7 +5,10 @@ public enum HackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON(HttpStatus.NOT_FOUND, "해당 해커톤이 존재하지 않습니다."), - NOT_FOUND_HACKATHON_TEAM(HttpStatus.NOT_FOUND, "해당하는 팀이 존재하지 않습니다."); + NOT_FOUND_HACKATHON_TEAM(HttpStatus.NOT_FOUND, "해당하는 팀이 존재하지 않습니다."), + INVALID_ROLE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 팀원 역할입니다."), + INVALID_STUDENT_ID(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 팀원 학번입니다."), + INVALID_TEAM_MEMBER(HttpStatus.BAD_REQUEST, "팀원이 중복으로 존재할 수 없습니다."); private final HttpStatus httpStatus; private final String errorMessage; diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index f3cf11b9..7aadd79d 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -27,6 +27,7 @@ import sw_css.auth.application.AuthSignUpService; import sw_css.file.application.FileService; import sw_css.hackathon.application.HackathonQueryService; +import sw_css.hackathon.application.HackathonTeamCommandService; import sw_css.hackathon.application.HackathonTeamQueryService; import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; @@ -83,6 +84,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected HackathonTeamQueryService hackathonTeamQueryService; + @MockBean + protected HackathonTeamCommandService hackathonTeamCommandService; + @MockBean protected FileService fileService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java index 73bcc4f3..a0d83971 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -3,14 +3,18 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.time.LocalDate; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -22,13 +26,20 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseBodySnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; +import org.springframework.restdocs.request.RequestPartsSnippet; +import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.hackathon.api.HackathonTeamController; import sw_css.hackathon.application.HackathonTeamQueryService; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.application.dto.response.HackathonTeamResponse.HackathonTeamMemberResponse; import sw_css.hackathon.domain.HackathonPrize; @@ -39,9 +50,6 @@ @WebMvcTest(HackathonTeamController.class) public class HackathonTeamApiDocsTest extends RestDocsTest { - @Autowired - private HackathonTeamQueryService hackathonTeamQueryService; - @Test @DisplayName("[성공] 모든 사용자는 해커톤 팀 목록을 조회할 수 있다.") public void findAllHackathonTeams() throws Exception { @@ -187,4 +195,37 @@ public void findHackathonTeam() throws Exception { .andDo(document("hackathon-team-find", pathParameterSnippet, responseBodySnippet)); } + @Test + @DisplayName("[성공] 회원은 해커톤의 팀을 등록할 수 있다.") + public void registerHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters(parameterWithName("hackathonId").description("해커톤 id")); + final RequestPartsSnippet requestPartsSnippet = requestParts( + partWithName("request").description( + "해커톤 정보( name: 팀명, work: 프로젝트명, githubUrl: 프로젝트 레포, leader{id: 리더의 학번, role: 리더의 역할}, members[]{id: 팀원의 학번, role: 팀원의 역할}"), + partWithName("file").description("팀의 썸네일 이미지")); + + final MockMultipartFile file = new MockMultipartFile("file", "test.png", "multipart/form-data", "example".getBytes()); + final HackathonTeamRequest request = new HackathonTeamRequest("팀명", "프로젝트명", "깃헙 url", new HackathonTeamMemberRequest(202012345L, HackathonRole.DEVELOPER.toString()), List.of(new HackathonTeamMemberRequest(202012346L, HackathonRole.DESIGNER.toString()))); + final MockMultipartFile requestJson = new MockMultipartFile("request", null, "application/json", objectMapper.writeValueAsString(request).getBytes()); + final String token = "Bearer AccessToken"; + final Long hackathonId = 1L; + final Long teamId = 1L; + + // when + when(hackathonTeamCommandService.registerHackathonTeam(hackathonId, file, request)).thenReturn(teamId); + + // then + mockMvc.perform( + multipart("/hackathons/{hackathonId}/teams", hackathonId) + .file(file) + .file(requestJson) + .contentType(MediaType.MULTIPART_MIXED) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isCreated()) + .andDo(document("hackathon-team-register", pathParameterSnippet, requestPartsSnippet)); + + } + } diff --git a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java index cba686a7..d4138c79 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java @@ -14,15 +14,14 @@ import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import sw_css.admin.hackathon.api.AdminHackathonTeamController; -import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest; -import sw_css.admin.hackathon.application.dto.request.AdminHackathonTeamRequest.TeamMember; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; import sw_css.restdocs.RestDocsTest; @WebMvcTest(AdminHackathonTeamController.class) @@ -48,8 +47,8 @@ public void updateHackathonTeam() throws Exception { final Long hackathonId = 1L; final Long teamId = 1L; - final TeamMember leader = new TeamMember(202055555L, "DEVELOPER"); - final AdminHackathonTeamRequest request = new AdminHackathonTeamRequest("팀명", "프로젝트명", "https://www.github.com", leader, List.of(new TeamMember(202012345L, "OTHER"))); + final HackathonTeamMemberRequest leader = new HackathonTeamMemberRequest(202055555L, "DEVELOPER"); + final HackathonTeamRequest request = new HackathonTeamRequest("팀명", "프로젝트명", "https://www.github.com", leader, List.of(new HackathonTeamMemberRequest(202012345L, "OTHER"))); final String token = "Bearer AccessToken"; // when From 8012f3a1ad36ec5d0bccd78da6501ae6970ebbda Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 05:07:53 +0900 Subject: [PATCH 32/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=EC=88=98=EC=A0=95=20API=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 15 +++ .../application/AdminAuthCommandService.java | 2 +- .../dto/request/RegisterFacultyRequest.java | 4 +- .../dto/request/SignUpRequest.java | 4 +- .../api/HackathonTeamController.java | 15 ++- .../HackathonTeamCommandService.java | 98 ++++++++++++++++++- .../hackathon/domain/HackathonTeam.java | 9 +- .../hackathon/domain/HackathonTeamMember.java | 4 +- .../exception/HackathonExceptionType.java | 5 +- .../java/sw_css/member/domain/Member.java | 8 +- backend/src/main/resources/schema.sql | 1 + backend/src/main/resources/test-data.sql | 29 +++--- .../docs/HackathonTeamApiDocsTest.java | 53 ++++++++-- .../restdocs/docs/MemberApiDocsTest.java | 2 +- 14 files changed, 209 insertions(+), 40 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 44e2c252..870705f6 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -573,3 +573,18 @@ include::{snippets}/hackathon-team-register/request-parts.adoc[] .HTTP Response include::{snippets}/hackathon-team-register/http-response.adoc[] + + +=== `PATCH`: 해커톤 팀 수정 + +.HTTP Request +include::{snippets}/hackathon-team-update/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-update/path-parameters.adoc[] + +.Request Body +include::{snippets}/hackathon-team-update/request-fields.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-update/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java b/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java index ee5565d2..9f1b9c3a 100644 --- a/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java +++ b/backend/src/main/java/sw_css/admin/auth/application/AdminAuthCommandService.java @@ -107,7 +107,7 @@ private Workbook generateWorkbook(final MultipartFile file, String extension) { } private void saveFaculty(final String email, final String name, final String password) { - Member member = new Member(email, name, password, "01000000000", false); + Member member = new Member(email, name, password, "01000000000"); final Member savedMember = memberRepository.save(member); diff --git a/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java b/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java index 21125da4..a3097f1b 100644 --- a/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java +++ b/backend/src/main/java/sw_css/admin/auth/application/dto/request/RegisterFacultyRequest.java @@ -15,11 +15,11 @@ public record RegisterFacultyRequest( String name) { public Member toMember(String password) { - return new Member(email, name, password, "01000000000", false); + return new Member(email, name, password, "01000000000"); } public Member toMember(Long memberId, String password) { - return new Member(memberId, email, name, password, "01000000000", false); + return new Member(memberId, email, name, password, "01000000000"); } public FacultyMember toFacultyMember(Long memberId, String password) { diff --git a/backend/src/main/java/sw_css/auth/application/dto/request/SignUpRequest.java b/backend/src/main/java/sw_css/auth/application/dto/request/SignUpRequest.java index 3f8566f1..953a7d73 100644 --- a/backend/src/main/java/sw_css/auth/application/dto/request/SignUpRequest.java +++ b/backend/src/main/java/sw_css/auth/application/dto/request/SignUpRequest.java @@ -38,11 +38,11 @@ public record SignUpRequest( String auth_code) { public Member toMember() { - return new Member(this.email, this.name, Password.encode(this.password), this.phone_number, false); + return new Member(this.email, this.name, Password.encode(this.password), this.phone_number); } public Member toMember(Long memberId) { - return new Member(memberId, this.email, this.name, Password.encode(this.password), this.phone_number, false); + return new Member(memberId, this.email, this.name, Password.encode(this.password), this.phone_number); } public StudentMember toStudentMember(Long memberId, Major major, Major minor, Major doubleMajor) { diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java index fb6f1036..6876d80a 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -8,8 +8,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; @@ -53,12 +55,21 @@ public ResponseEntity registerHackathonTeam( @RequestPart(value = "file") final MultipartFile file, @RequestPart(value = "request") @Valid final HackathonTeamRequest request ) { - Long teamId = hackathonTeamCommandService.registerHackathonTeam(hackathonId, file, request); + Long teamId = hackathonTeamCommandService.registerHackathonTeam(me, hackathonId, file, request); return ResponseEntity.created(URI.create("/hackathons/" + hackathonId + "/teams/" + teamId)).build(); } - // TODO: 해커톤 팀 수정 + @PatchMapping("{teamId}") + public ResponseEntity updateHackathonTeam( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId, + @RequestBody @Valid final HackathonTeamRequest request + ) { + hackathonTeamCommandService.updateHackathonTeam(me, hackathonId, teamId, request); + return ResponseEntity.noContent().build(); + } // TODO: 해커톤 투표 diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java index 29993c3e..33e580fc 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java @@ -6,7 +6,9 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Comparator; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.UUID; @@ -28,7 +30,10 @@ import sw_css.hackathon.domain.repository.HackathonTeamRepository; import sw_css.hackathon.exception.HackathonException; import sw_css.hackathon.exception.HackathonExceptionType; +import sw_css.member.domain.Member; +import sw_css.member.domain.StudentMember; import sw_css.member.domain.embedded.StudentId; +import sw_css.member.domain.repository.StudentMemberRepository; import sw_css.milestone.exception.MilestoneHistoryException; import sw_css.milestone.exception.MilestoneHistoryExceptionType; @@ -40,21 +45,23 @@ public class HackathonTeamCommandService { private final HackathonRepository hackathonRepository; private final HackathonTeamRepository hackathonTeamRepository; private final HackathonTeamMemberRepository hackathonTeamMemberRepository; + private final StudentMemberRepository studentMemberRepository; + @Value("${data.file-path-prefix}") private String filePathPrefix; - public Long registerHackathonTeam(final Long hackathonId, final MultipartFile file, final HackathonTeamRequest request) { + public Long registerHackathonTeam(final Member me, final Long hackathonId, final MultipartFile file, final HackathonTeamRequest request) { validateFileType(file); Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow(() -> new HackathonException( HackathonExceptionType.NOT_FOUND_HACKATHON)); final String newFilePath = generateFilePath(file); - final HackathonTeam team = new HackathonTeam(hackathon, request.name(), request.work(), request.githubUrl(), newFilePath); + final HackathonTeam team = new HackathonTeam(hackathon, request.name(), request.work(), request.githubUrl(), newFilePath, me); final HackathonTeam newTeam = hackathonTeamRepository.save(team); uploadFile(file, newFilePath); - validateAllHackathonTeamMember(request.leader(), request.members()); + validateAllHackathonTeamMember(me, request.leader(), request.members()); HackathonTeamMember leader = new HackathonTeamMember(hackathon, newTeam, request.leader().id(), request.leader().role(), true); hackathonTeamMemberRepository.save(leader); @@ -67,7 +74,86 @@ public Long registerHackathonTeam(final Long hackathonId, final MultipartFile fi return newTeam.getId(); } - private void validateAllHackathonTeamMember(HackathonTeamMemberRequest leader, List members) { + public void updateHackathonTeam(final Member me, final Long hackathonId, final Long teamId, final HackathonTeamRequest request) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + validateTeamUpdater(me, team); + + team.setName(request.name()); + team.setWork(request.work()); + team.setGithubUrl(request.githubUrl()); + + hackathonTeamRepository.save(team); + + validateAllHackathonTeamMember(me, request.leader(), request.members()); + + // 리더가 변경되었는지 확인 및 갱신 + final HackathonTeamMember leader = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderTrue(hackathonId, teamId); + checkLeaderAndUpdate(leader, request.leader(), hackathon, team); + + + final List originMembers = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(hackathonId, teamId); + final List newMembers = request.members(); + + boolean found = false; + // 기존에 존재하던 팀원인지 확인 + for (HackathonTeamMember originMember : originMembers) { + Iterator iterator = newMembers.iterator(); + while (iterator.hasNext()) { + HackathonTeamMemberRequest newMember = iterator.next(); + if ( !originMember.getStudentId().equals(newMember.id()) ) continue; + + checkMemberAndUpdate(originMember, newMember); + // 변경 확인이 끝난 팀원이므로 배열에서 삭제 + found = true; iterator.remove(); + break; + } + + // 새로운 팀원에 존재하지 않으므로, 삭제된 팀원임. + if( !found ) { + originMember.delete(); + hackathonTeamMemberRepository.save(originMember); + } + found = false; + } + + // 삭제되지 않은 회원은 추가되어야할 팀원들임. + for (HackathonTeamMemberRequest member : newMembers) { + HackathonTeamMember newMember = new HackathonTeamMember(hackathon, team, member.id(), member.role()); + hackathonTeamMemberRepository.save(newMember); + } + } + + private void checkLeaderAndUpdate(HackathonTeamMember originLeader, HackathonTeamMemberRequest leader, Hackathon hackathon, HackathonTeam team) { + if ( !originLeader.getStudentId().equals(leader.id()) ) { + originLeader.delete(); + HackathonTeamMember newLeader = new HackathonTeamMember(hackathon, team, leader.id(), leader.role(), true); + hackathonTeamMemberRepository.save(originLeader); + hackathonTeamMemberRepository.save(newLeader); + } else if ( !originLeader.getRole().equals(leader.role()) ) { + originLeader.setRole(leader.role()); + hackathonTeamMemberRepository.save(originLeader); + } + } + + private void checkMemberAndUpdate(HackathonTeamMember originMember, HackathonTeamMemberRequest member) { + if ( !originMember.getRole().equals(member.role()) ) { + originMember.setRole(member.role()); + hackathonTeamMemberRepository.save(originMember); + } + } + + private void validateTeamUpdater(final Member me, final HackathonTeam team){ + if( !team.getCreatedBy().getId().equals(me.getId()) ) + throw new HackathonException(HackathonExceptionType.INVALID_TEAM_UPDATER); + } + + private void validateAllHackathonTeamMember(Member me, HackathonTeamMemberRequest leader, List members) { + StudentMember student = studentMemberRepository.findByMemberId(me.getId()).orElseThrow(() -> new HackathonException(HackathonExceptionType.INVALID_TEAM_UPDATER)); + Set uniqueIds = new HashSet<>(); uniqueIds.add(leader.id()); @@ -77,6 +163,10 @@ private void validateAllHackathonTeamMember(HackathonTeamMemberRequest leader, L validateHackathonTeamMember(member); if( !uniqueIds.add(member.id()) ) throw new HackathonException(HackathonExceptionType.INVALID_TEAM_MEMBER); }); + + if(student != null && !uniqueIds.contains(student.getId())) { + throw new HackathonException(HackathonExceptionType.INVALID_TEAM_CREATOR); + } } private void validateHackathonTeamMember(HackathonTeamMemberRequest teamMember) { diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java index b02d1443..b70dc011 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -14,6 +14,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLRestriction; +import sw_css.member.domain.Member; @Entity @@ -46,11 +47,15 @@ public class HackathonTeam { @Column(nullable = false) private String prize; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "created_by", nullable = false) + private Member createdBy; + @Column(nullable = false) private boolean isDeleted; - public HackathonTeam(Hackathon hackathon, String name, String work, String githubUrl, String imageUrl) { - this(null, hackathon, name, work, githubUrl, imageUrl, HackathonPrize.NONE_PRIZE.toString(), false); + public HackathonTeam(Hackathon hackathon, String name, String work, String githubUrl, String imageUrl, Member createdBy) { + this(null, hackathon, name, work, githubUrl, imageUrl, HackathonPrize.NONE_PRIZE.toString(), createdBy, false); } public void delete() { diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java index cc73082c..0e298b1b 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java @@ -8,14 +8,12 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import java.time.LocalDate; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.hibernate.annotations.SQLRestriction; -import sw_css.member.domain.StudentMember; @Entity @Getter @@ -56,4 +54,6 @@ public HackathonTeamMember(Hackathon hackathon, HackathonTeam team, Long student public HackathonTeamMember(Hackathon hackathon, HackathonTeam team, Long studentId, String role) { this(null, hackathon, team, studentId, role, false, false); } + + public void delete() { this.isDeleted = true; } } diff --git a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java index 44a2e1ab..ef4dd9d3 100644 --- a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -8,7 +8,10 @@ public enum HackathonExceptionType implements BaseExceptionType { NOT_FOUND_HACKATHON_TEAM(HttpStatus.NOT_FOUND, "해당하는 팀이 존재하지 않습니다."), INVALID_ROLE_STATUS(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 팀원 역할입니다."), INVALID_STUDENT_ID(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 팀원 학번입니다."), - INVALID_TEAM_MEMBER(HttpStatus.BAD_REQUEST, "팀원이 중복으로 존재할 수 없습니다."); + INVALID_TEAM_MEMBER(HttpStatus.BAD_REQUEST, "팀원이 중복으로 존재할 수 없습니다."), + INVALID_TEAM_CREATOR(HttpStatus.BAD_REQUEST, "팀원이 아닌 사람이 팀을 만들 수 없습니다."), + INVALID_TEAM_UPDATER(HttpStatus.BAD_REQUEST, "해당 팀의 수정 권한이 없습니다."); + private final HttpStatus httpStatus; private final String errorMessage; diff --git a/backend/src/main/java/sw_css/member/domain/Member.java b/backend/src/main/java/sw_css/member/domain/Member.java index 28c6abc5..e1cc6a1f 100644 --- a/backend/src/main/java/sw_css/member/domain/Member.java +++ b/backend/src/main/java/sw_css/member/domain/Member.java @@ -41,8 +41,12 @@ public class Member extends BaseEntity { @Column(nullable = false) private boolean isDeleted; - public Member(String email, String name, String password, String phoneNumber, boolean isDeleted) { - this(null, email, name, password, phoneNumber, isDeleted); + public Member(String email, String name, String password, String phoneNumber) { + this(null, email, name, password, phoneNumber, false); + } + + public Member(Long id, String email, String name, String password, String phoneNumber) { + this(id, email, name, password, phoneNumber, false); } public boolean isWrongPassword(String rawPassword) { diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index f16a14f1..8bd1f5f0 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -115,6 +115,7 @@ create table hackathon_team work varchar(255) not null, github_url varchar(255) not null, prize varchar(255), + created_by bigint not null, is_deleted boolean not null, created_at datetime(6) not null default current_timestamp(6) ); diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index 4442b7fc..6cb17682 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -9,6 +9,11 @@ values ( 'ddang@pusan.ac.kr', '이다은', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO. , false); insert into student_member (id, member_id, major_id, minor_id, double_major_id, career, career_detail) values (202055555, 2, 1, null, null, 'GRADUATE_SCHOOL', 'IT 기업 개발자'); +insert into member (email, name, password, phone_number, is_deleted) +values ( 'asdf@pusan.ac.kr', '이다은2', '$2a$10$YyiOL/E5WjKrZPkB6eQSK.PwZtAO.z3JimFbq/Ky3u3rFf3XTGrWK', '01000000000' + , false); +insert into student_member (id, member_id, major_id, minor_id, double_major_id, career, career_detail) +values (202055574, 3, 1, null, null, 'GRADUATE_SCHOOL', 'IT 기업 개발자'); ## hackathon @@ -31,18 +36,18 @@ values('제4회 PNU 창의융합 소프트웨어해커톤', ' 1. qwer', '1234', '2022-03-22', '2022-05-29', '2022-12-22', '2023-03-07', '1.png', true, false); ## hackathon team -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'GRAND_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'EXCELLENCE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '팀명1', '1.png', '프로젝트명1', 'https://www.naver.com', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '팀명2', '1.png', '프로젝트명2', 'https://www.naver.com', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '팀명3', '1.png', '프로젝트명3', 'https://www.naver.com', 'NONE_PRIZE', false); -insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, is_deleted) -values(1, '팀명4', '1.png', '프로젝트명4', 'https://www.naver.com', 'NONE_PRIZE', false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, created_by, is_deleted) +values(1, '두레', '1.png', '두레 두레 두레 두레 두레', 'https://github.com/BDD-CLUB/01-doo-re-front', 'GRAND_PRIZE', 2, false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, created_by, is_deleted) +values(1, '키퍼', '1.png', '키퍼 키퍼 키퍼 키퍼 키퍼', 'https://github.com/KEEPER31337/Homepage-Front-R2', 'EXCELLENCE_PRIZE', 2, false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, created_by, is_deleted) +values(1, '팀명1', '1.png', '프로젝트명1', 'https://www.naver.com', 'NONE_PRIZE', 2, false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, created_by, is_deleted) +values(1, '팀명2', '1.png', '프로젝트명2', 'https://www.naver.com', 'NONE_PRIZE', 2, false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, created_by, is_deleted) +values(1, '팀명3', '1.png', '프로젝트명3', 'https://www.naver.com', 'NONE_PRIZE', 2, false); +insert into hackathon_team (hackathon_id, name, image_url, work, github_url, prize, created_by, is_deleted) +values(1, '팀명4', '1.png', '프로젝트명4', 'https://www.naver.com', 'NONE_PRIZE', 2, false); ## hackathon team vote insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java index a0d83971..eb6a7656 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -1,26 +1,26 @@ package sw_css.restdocs.docs; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import java.time.LocalDate; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -31,20 +31,18 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.restdocs.payload.ResponseBodySnippet; +import org.springframework.restdocs.payload.RequestFieldsSnippet; import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import org.springframework.restdocs.request.RequestPartsSnippet; -import sw_css.admin.hackathon.application.dto.request.AdminHackathonRequest; import sw_css.hackathon.api.HackathonTeamController; -import sw_css.hackathon.application.HackathonTeamQueryService; import sw_css.hackathon.application.dto.request.HackathonTeamRequest; import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; import sw_css.hackathon.application.dto.response.HackathonTeamResponse; import sw_css.hackathon.application.dto.response.HackathonTeamResponse.HackathonTeamMemberResponse; import sw_css.hackathon.domain.HackathonPrize; import sw_css.hackathon.domain.HackathonRole; -import sw_css.hackathon.domain.HackathonTeam; +import sw_css.member.domain.Member; import sw_css.restdocs.RestDocsTest; @WebMvcTest(HackathonTeamController.class) @@ -209,11 +207,12 @@ public void registerHackathonTeam() throws Exception { final HackathonTeamRequest request = new HackathonTeamRequest("팀명", "프로젝트명", "깃헙 url", new HackathonTeamMemberRequest(202012345L, HackathonRole.DEVELOPER.toString()), List.of(new HackathonTeamMemberRequest(202012346L, HackathonRole.DESIGNER.toString()))); final MockMultipartFile requestJson = new MockMultipartFile("request", null, "application/json", objectMapper.writeValueAsString(request).getBytes()); final String token = "Bearer AccessToken"; + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); final Long hackathonId = 1L; final Long teamId = 1L; // when - when(hackathonTeamCommandService.registerHackathonTeam(hackathonId, file, request)).thenReturn(teamId); + when(hackathonTeamCommandService.registerHackathonTeam(me, hackathonId, file, request)).thenReturn(teamId); // then mockMvc.perform( @@ -225,7 +224,43 @@ public void registerHackathonTeam() throws Exception { .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isCreated()) .andDo(document("hackathon-team-register", pathParameterSnippet, requestPartsSnippet)); + } + + @Test + @DisplayName("[성공] 팀을 생성했던 사람은 해커톤 팀을 수정할 수 있다.") + public void updateHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해당 팀이 속한 해커톤 id"), + parameterWithName("teamId").description("팀의 id")); + + final RequestFieldsSnippet requestFieldsSnippet = requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("팀명"), + fieldWithPath("work").type(JsonFieldType.STRING).description("프로젝트명"), + fieldWithPath("githubUrl").type(JsonFieldType.STRING).description("프로젝트의 깃헙 레포지토리의 URL"), + fieldWithPath("leader.id").type(JsonFieldType.NUMBER).description("팀장의 학번"), + fieldWithPath("leader.role").type(JsonFieldType.STRING).description("팀장의 역할 ( DEVELOPER, DESIGNER, PLANNER, OTHER )"), + fieldWithPath("members[].id").type(JsonFieldType.NUMBER).description("팀원의 학번"), + fieldWithPath("members[].role").type(JsonFieldType.STRING).description("팀원의 역할 ( DEVELOPER, DESIGNER, PLANNER, OTHER )") + ); + + final HackathonTeamRequest request = new HackathonTeamRequest("팀명", "프로젝트명", "깃헙 url", new HackathonTeamMemberRequest(202012345L, HackathonRole.DEVELOPER.toString()), List.of(new HackathonTeamMemberRequest(202012346L, HackathonRole.DESIGNER.toString()))); + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); + final Long hackathonId = 1L; + final Long teamId = 1L; + final String token = "Bearer AccessToken"; + + // when + doNothing().when(hackathonTeamCommandService).updateHackathonTeam(me, hackathonId, teamId, request); + // then + mockMvc.perform( + patch("/hackathons/{hackathonId}/teams/{teamId}", hackathonId, teamId) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("hackathon-team-update", pathParameterSnippet, requestFieldsSnippet)); } } diff --git a/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java index 4b01be00..2f51e396 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/MemberApiDocsTest.java @@ -72,7 +72,7 @@ public void changePassword() throws Exception { fieldWithPath("newPassword").type(JsonFieldType.STRING).description("새로운 비밀번호") ); - final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234", false); + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); final String oldPassword = "qwer1234!"; final String newPassword = "asdf1234!"; final String token = "Bearer AccessToken"; From 2d8f671e4ee644f8584d4f6617886cb0611c454b Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 05:15:33 +0900 Subject: [PATCH 33/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=EC=82=AD=EC=A0=9C=20API=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 12 +++++++++ .../api/HackathonTeamController.java | 11 ++++++++ .../HackathonTeamCommandService.java | 25 +++++++++++++------ .../exception/HackathonExceptionType.java | 5 +++- backend/src/main/resources/config | 2 +- .../docs/HackathonTeamApiDocsTest.java | 25 +++++++++++++++++++ 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 870705f6..c57c028f 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -588,3 +588,15 @@ include::{snippets}/hackathon-team-update/request-fields.adoc[] .HTTP Response include::{snippets}/hackathon-team-update/http-response.adoc[] + + +=== `DELETE`: 해커톤 팀 삭제 + +.HTTP Request +include::{snippets}/hackathon-team-delete/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-delete/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-delete/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java index 6876d80a..0b460e23 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; +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; @@ -71,6 +72,16 @@ public ResponseEntity updateHackathonTeam( return ResponseEntity.noContent().build(); } + @DeleteMapping("{teamId}") + public ResponseEntity deleteHackathonTeam( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + hackathonTeamCommandService.deleteHackathonTeam(me, hackathonId, teamId); + return ResponseEntity.noContent().build(); + } + // TODO: 해커톤 투표 } diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java index 33e580fc..6724298b 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java @@ -6,7 +6,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -17,8 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import sw_css.admin.hackathon.exception.AdminHackathonException; -import sw_css.admin.hackathon.exception.AdminHackathonExceptionType; import sw_css.hackathon.application.dto.request.HackathonTeamRequest; import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; import sw_css.hackathon.domain.Hackathon; @@ -76,9 +73,9 @@ public Long registerHackathonTeam(final Member me, final Long hackathonId, final public void updateHackathonTeam(final Member me, final Long hackathonId, final Long teamId, final HackathonTeamRequest request) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( - () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( - () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); validateTeamUpdater(me, team); @@ -126,6 +123,18 @@ public void updateHackathonTeam(final Member me, final Long hackathonId, final L hackathonTeamMemberRepository.save(newMember); } } + + public void deleteHackathonTeam(final Member me, final Long hackathonId, final Long teamId) { + hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + validateTeamUpdater(me, team); + + team.delete(); + hackathonTeamRepository.save(team); + } private void checkLeaderAndUpdate(HackathonTeamMember originLeader, HackathonTeamMemberRequest leader, Hackathon hackathon, HackathonTeam team) { if ( !originLeader.getStudentId().equals(leader.id()) ) { @@ -183,17 +192,17 @@ private void validateHackathonTeamMemberRole(String role){ try { HackathonRole.valueOf(role); } catch (IllegalArgumentException e) { - throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_ROLE_STATUS); + throw new HackathonException(HackathonExceptionType.INVALID_ROLE_STATUS); } } private void validateFileType(final MultipartFile file) { if (file == null) { - throw new AdminHackathonException(AdminHackathonExceptionType.NOT_EXIST_FILE); + throw new HackathonException(HackathonExceptionType.NOT_EXIST_FILE); } final String contentType = file.getContentType(); if (!isSupportedContentType(contentType)) { - throw new AdminHackathonException(AdminHackathonExceptionType.UNSUPPORTED_FILE_TYPE); + throw new HackathonException(HackathonExceptionType.UNSUPPORTED_FILE_TYPE); } } diff --git a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java index ef4dd9d3..150f507e 100644 --- a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -10,7 +10,10 @@ public enum HackathonExceptionType implements BaseExceptionType { INVALID_STUDENT_ID(HttpStatus.BAD_REQUEST,"올바르지 않는 형식의 팀원 학번입니다."), INVALID_TEAM_MEMBER(HttpStatus.BAD_REQUEST, "팀원이 중복으로 존재할 수 없습니다."), INVALID_TEAM_CREATOR(HttpStatus.BAD_REQUEST, "팀원이 아닌 사람이 팀을 만들 수 없습니다."), - INVALID_TEAM_UPDATER(HttpStatus.BAD_REQUEST, "해당 팀의 수정 권한이 없습니다."); + INVALID_TEAM_UPDATER(HttpStatus.BAD_REQUEST, "해당 팀의 수정 권한이 없습니다."), + NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), + CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), + UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "파일 유형이 png, jpg, jpeg 가 아닙니다."); private final HttpStatus httpStatus; diff --git a/backend/src/main/resources/config b/backend/src/main/resources/config index 15a3a493..ae9a1428 160000 --- a/backend/src/main/resources/config +++ b/backend/src/main/resources/config @@ -1 +1 @@ -Subproject commit 15a3a49390a83503f261c3b251e52b779116025a +Subproject commit ae9a1428fcd5979dd78e9ad7e7535401fbb8dde0 diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java index eb6a7656..9bb6bd1a 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.when; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; @@ -263,4 +264,28 @@ public void updateHackathonTeam() throws Exception { .andDo(document("hackathon-team-update", pathParameterSnippet, requestFieldsSnippet)); } + @Test + @DisplayName("[성공] 팀을 생성했던 사람은 해커톤 팀을 삭제할 수 있다.") + public void deleteHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해당 팀이 속한 해커톤 id"), + parameterWithName("teamId").description("팀의 id")); + + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); + final Long hackathonId = 1L; + final Long teamId = 1L; + final String token = "Bearer AccessToken"; + + // when + doNothing().when(hackathonTeamCommandService).deleteHackathonTeam(me, hackathonId, teamId); + + // then + mockMvc.perform( + delete("/hackathons/{hackathonId}/teams/{teamId}", hackathonId, teamId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("hackathon-team-delete", pathParameterSnippet)); + } + } From 58a1c659631fa64286255b34746794799ba96a8d Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 05:47:12 +0900 Subject: [PATCH 34/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=8C=80=20=ED=88=AC=ED=91=9C=20=EB=B0=8F=20=ED=88=AC=ED=91=9C?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=20API=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?test=20=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 24 +++++++ .../api/HackathonTeamController.java | 3 - .../api/HackathonTeamVoteController.java | 42 +++++++++++ .../HackathonTeamVoteCommandService.java | 54 ++++++++++++++ .../hackathon/domain/HackathonTeamVote.java | 18 ++++- .../HackathonTeamVoteRepository.java | 5 ++ .../exception/HackathonExceptionType.java | 1 + backend/src/main/resources/schema.sql | 2 +- backend/src/main/resources/test-data.sql | 16 ++--- .../java/sw_css/restdocs/RestDocsTest.java | 4 ++ .../docs/HackathonTeamVoteApiDocsTest.java | 72 +++++++++++++++++++ 11 files changed, 226 insertions(+), 15 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java create mode 100644 backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java create mode 100644 backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index c57c028f..dfae359e 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -600,3 +600,27 @@ include::{snippets}/hackathon-team-delete/path-parameters.adoc[] .HTTP Response include::{snippets}/hackathon-team-delete/http-response.adoc[] + + +== 일반 - 해커톤 팀 투표 + +=== `POST`: 헤커톤 팀 투표 +.HTTP Request +include::{snippets}/hackathon-team-vote/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-vote/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-vote/http-response.adoc[] + + +=== `DELETE`: 헤커톤 팀 투표 취소 +.HTTP Request +include::{snippets}/hackathon-team-vote-cancel/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-vote-cancel/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-vote-cancel/http-response.adoc[] diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java index 0b460e23..819313be 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -81,7 +81,4 @@ public ResponseEntity deleteHackathonTeam( hackathonTeamCommandService.deleteHackathonTeam(me, hackathonId, teamId); return ResponseEntity.noContent().build(); } - - // TODO: 해커톤 투표 - } diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java new file mode 100644 index 00000000..e5e242cf --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java @@ -0,0 +1,42 @@ +package sw_css.hackathon.api; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import sw_css.hackathon.application.HackathonTeamVoteCommandService; +import sw_css.member.domain.Member; +import sw_css.utils.annotation.MemberInterface; + +@Validated +@RequestMapping("/hackathons/{hackathonId}/teams/{teamId}/vote") +@RestController +@RequiredArgsConstructor +public class HackathonTeamVoteController { + private final HackathonTeamVoteCommandService hackathonTeamVoteCommandService; + + @PostMapping + public ResponseEntity voteHackathonTeam( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + hackathonTeamVoteCommandService.voteHackathonTeam(me, hackathonId, teamId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity cancelHackathonTeamVote( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + hackathonTeamVoteCommandService.cancelHackathonTeamVote(me, hackathonId, teamId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java new file mode 100644 index 00000000..de349a9e --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java @@ -0,0 +1,54 @@ +package sw_css.hackathon.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.hackathon.domain.Hackathon; +import sw_css.hackathon.domain.HackathonTeam; +import sw_css.hackathon.domain.HackathonTeamVote; +import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.hackathon.domain.repository.HackathonTeamVoteRepository; +import sw_css.hackathon.exception.HackathonException; +import sw_css.hackathon.exception.HackathonExceptionType; + +import sw_css.member.domain.Member; + +@Service +@RequiredArgsConstructor +@Transactional +public class HackathonTeamVoteCommandService { + + private final HackathonTeamVoteRepository hackathonTeamVoteRepository; + private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + + public void voteHackathonTeam(final Member me, final Long hackathonId, final Long teamId) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + boolean voted = hackathonTeamVoteRepository.existsByHackathonIdAndTeamIdAndMemberId(hackathonId, teamId, me.getId()); + if ( !voted ) { + HackathonTeamVote vote = new HackathonTeamVote(hackathon, team, me); + hackathonTeamVoteRepository.save(vote); + } else { + throw new HackathonException(HackathonExceptionType.CANNOT_DUPLICATE_VOTE); + } + } + + public void cancelHackathonTeamVote(final Member me, final Long hackathonId, final Long teamId) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + HackathonTeamVote vote = hackathonTeamVoteRepository.findByHackathonIdAndTeamIdAndMemberId(hackathonId, teamId, me.getId()).orElse(null); + if ( vote != null ) { + vote.delete(); + hackathonTeamVoteRepository.save(vote); + } + + } +} diff --git a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java index 037bd76d..978d0081 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java @@ -1,5 +1,6 @@ package sw_css.hackathon.domain; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -11,8 +12,9 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import org.hibernate.annotations.SQLRestriction; -import sw_css.member.domain.StudentMember; +import sw_css.member.domain.Member; @Entity @Getter @@ -33,6 +35,16 @@ public class HackathonTeamVote { private HackathonTeam team; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "student_id", nullable = false) - private StudentMember studentMember; + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false) + @Setter(AccessLevel.PUBLIC) + private Boolean isDeleted; + + public HackathonTeamVote(Hackathon hackathon, HackathonTeam team, Member member) { + this(null, hackathon, team, member, false); + } + + public void delete() { this.isDeleted = true; } } diff --git a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java index 9f7d415f..43f99f61 100644 --- a/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java @@ -1,8 +1,13 @@ package sw_css.hackathon.domain.repository; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import sw_css.hackathon.domain.HackathonTeamVote; public interface HackathonTeamVoteRepository extends JpaRepository { Long countByHackathonIdAndTeamId(Long hackathonId, Long teamId); + + boolean existsByHackathonIdAndTeamIdAndMemberId(Long hackathonId, Long teamId, Long memberId); + + Optional findByHackathonIdAndTeamIdAndMemberId(Long hackathonId, Long teamId, Long memberId); } diff --git a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java index 150f507e..efa69584 100644 --- a/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -11,6 +11,7 @@ public enum HackathonExceptionType implements BaseExceptionType { INVALID_TEAM_MEMBER(HttpStatus.BAD_REQUEST, "팀원이 중복으로 존재할 수 없습니다."), INVALID_TEAM_CREATOR(HttpStatus.BAD_REQUEST, "팀원이 아닌 사람이 팀을 만들 수 없습니다."), INVALID_TEAM_UPDATER(HttpStatus.BAD_REQUEST, "해당 팀의 수정 권한이 없습니다."), + CANNOT_DUPLICATE_VOTE(HttpStatus.BAD_REQUEST, "투표는 한 번만 할 수 있습니다."), NOT_EXIST_FILE(HttpStatus.BAD_REQUEST, "파일을 첨부해야 합니다."), CANNOT_OPEN_FILE(HttpStatus.BAD_REQUEST, "파일을 열 수 없습니다."), UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "파일 유형이 png, jpg, jpeg 가 아닙니다."); diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 8bd1f5f0..358c5af3 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -125,7 +125,7 @@ create table hackathon_team_vote id bigint auto_increment primary key, hackathon_id bigint not null, team_id bigint not null, - student_id bigint not null, + member_id bigint not null, is_deleted boolean not null, created_at datetime(6) not null default current_timestamp(6) ); diff --git a/backend/src/main/resources/test-data.sql b/backend/src/main/resources/test-data.sql index 6cb17682..8c3a6a61 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -50,21 +50,21 @@ insert into hackathon_team (hackathon_id, name, image_url, work, github_url, pri values(1, '팀명4', '1.png', '프로젝트명4', 'https://www.naver.com', 'NONE_PRIZE', 2, false); ## hackathon team vote -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 2, 202012341, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 2, 202012342, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 2, 202012341, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 2, 202012342, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 3, 202012341, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 3, 202012342, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 6, 202012341, false); -insert into hackathon_team_vote (hackathon_id, team_id, student_id, is_deleted) +insert into hackathon_team_vote (hackathon_id, team_id, member_id, is_deleted) values(1, 5, 202012342, false); ## hackathon team member diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index 7aadd79d..59c522dd 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -29,6 +29,7 @@ import sw_css.hackathon.application.HackathonQueryService; import sw_css.hackathon.application.HackathonTeamCommandService; import sw_css.hackathon.application.HackathonTeamQueryService; +import sw_css.hackathon.application.HackathonTeamVoteCommandService; import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; import sw_css.member.application.MemberQueryService; @@ -87,6 +88,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected HackathonTeamCommandService hackathonTeamCommandService; + @MockBean + protected HackathonTeamVoteCommandService hackathonTeamVoteCommandService; + @MockBean protected FileService fileService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java new file mode 100644 index 00000000..2e6e9bf3 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java @@ -0,0 +1,72 @@ +package sw_css.restdocs.docs; + +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.request.PathParametersSnippet; +import sw_css.hackathon.api.HackathonTeamVoteController; +import sw_css.member.domain.Member; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(HackathonTeamVoteController.class) +public class HackathonTeamVoteApiDocsTest extends RestDocsTest { + + @Test + @DisplayName("[성공] 회원은 팀에 투표할 수 있다.") + public void voteHackathonTeam() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해당 팀이 속한 해커톤 id"), + parameterWithName("teamId").description("팀의 id") + ); + + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); + final Long hackathonId = 1L; + final Long teamId = 1L; + final String token = "Bearer Access Token"; + + // when + doNothing().when(hackathonTeamVoteCommandService).voteHackathonTeam(me, hackathonId, teamId); + + // then + mockMvc.perform( + post("/hackathons/{hackathonId}/teams/{teamId}/vote", hackathonId, teamId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("hackathon-team-vote", pathParameterSnippet)); + } + + @Test + @DisplayName("[성공] 회원은 팀의 투표를 취소할 수 있다.") + public void cancelHackathonTeamVote() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해당 팀이 속한 해커톤 id"), + parameterWithName("teamId").description("팀의 id") + ); + + final Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); + final Long hackathonId = 1L; + final Long teamId = 1L; + final String token = "Bearer Access Token"; + + // when + doNothing().when(hackathonTeamVoteCommandService).voteHackathonTeam(me, hackathonId, teamId); + + // then + mockMvc.perform( + delete("/hackathons/{hackathonId}/teams/{teamId}/vote", hackathonId, teamId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("hackathon-team-vote-cancel", pathParameterSnippet)); + } +} From 7783179278c7759cdd768a5723a715a6a6869152 Mon Sep 17 00:00:00 2001 From: llddang Date: Thu, 2 Jan 2025 06:43:52 +0900 Subject: [PATCH 35/35] =?UTF-8?q?feat:=20=ED=95=B4=EC=BB=A4=ED=86=A4=20?= =?UTF-8?q?=ED=88=AC=ED=91=9C=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20test=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #165 --- backend/src/docs/asciidoc/index.adoc | 16 ++++++- .../api/HackathonTeamVoteController.java | 22 +++++++-- .../HackathonTeamVoteCommandService.java | 4 +- .../HackathonTeamVoteQueryService.java | 37 +++++++++++++++ .../response/HackathonTeamVoteResponse.java | 6 +++ .../java/sw_css/restdocs/RestDocsTest.java | 4 ++ .../docs/HackathonTeamVoteApiDocsTest.java | 45 +++++++++++++++++-- 7 files changed, 123 insertions(+), 11 deletions(-) create mode 100644 backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteQueryService.java create mode 100644 backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamVoteResponse.java diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index dfae359e..1024c972 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -604,7 +604,7 @@ include::{snippets}/hackathon-team-delete/http-response.adoc[] == 일반 - 해커톤 팀 투표 -=== `POST`: 헤커톤 팀 투표 +=== `GET`: 헤커톤 팀 투표 조회 .HTTP Request include::{snippets}/hackathon-team-vote/http-request.adoc[] @@ -614,6 +614,20 @@ include::{snippets}/hackathon-team-vote/path-parameters.adoc[] .HTTP Response include::{snippets}/hackathon-team-vote/http-response.adoc[] +.Response Body +include::{snippets}/hackathon-team-vote/response-fields.adoc[] + + +=== `POST`: 헤커톤 팀 투표 등록 +.HTTP Request +include::{snippets}/hackathon-team-vote-register/http-request.adoc[] + +.Path Parameters +include::{snippets}/hackathon-team-vote-register/path-parameters.adoc[] + +.HTTP Response +include::{snippets}/hackathon-team-vote-register/http-response.adoc[] + === `DELETE`: 헤커톤 팀 투표 취소 .HTTP Request diff --git a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java index e5e242cf..231be932 100644 --- a/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java @@ -4,12 +4,15 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; import sw_css.hackathon.application.HackathonTeamVoteCommandService; +import sw_css.hackathon.application.HackathonTeamVoteQueryService; +import sw_css.hackathon.application.dto.response.HackathonTeamVoteResponse; import sw_css.member.domain.Member; import sw_css.utils.annotation.MemberInterface; @@ -18,25 +21,36 @@ @RestController @RequiredArgsConstructor public class HackathonTeamVoteController { + private final HackathonTeamVoteCommandService hackathonTeamVoteCommandService; + private final HackathonTeamVoteQueryService hackathonTeamVoteQueryService; + + @GetMapping + public ResponseEntity findHackathonTeamVote( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + return ResponseEntity.ok(hackathonTeamVoteQueryService.findHackathonTeamVote(me, hackathonId, teamId)); + } @PostMapping - public ResponseEntity voteHackathonTeam( + public ResponseEntity registerHackathonTeamVote( @MemberInterface Member me, @PathVariable Long hackathonId, @PathVariable Long teamId ) { - hackathonTeamVoteCommandService.voteHackathonTeam(me, hackathonId, teamId); + hackathonTeamVoteCommandService.registerHackathonTeamVote(me, hackathonId, teamId); return ResponseEntity.noContent().build(); } @DeleteMapping - public ResponseEntity cancelHackathonTeamVote( + public ResponseEntity deleteHackathonTeamVote( @MemberInterface Member me, @PathVariable Long hackathonId, @PathVariable Long teamId ) { - hackathonTeamVoteCommandService.cancelHackathonTeamVote(me, hackathonId, teamId); + hackathonTeamVoteCommandService.deleteHackathonTeamVote(me, hackathonId, teamId); return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java index de349a9e..5a333869 100644 --- a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java @@ -23,7 +23,7 @@ public class HackathonTeamVoteCommandService { private final HackathonRepository hackathonRepository; private final HackathonTeamRepository hackathonTeamRepository; - public void voteHackathonTeam(final Member me, final Long hackathonId, final Long teamId) { + public void registerHackathonTeamVote(final Member me, final Long hackathonId, final Long teamId) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( @@ -38,7 +38,7 @@ public void voteHackathonTeam(final Member me, final Long hackathonId, final Lon } } - public void cancelHackathonTeamVote(final Member me, final Long hackathonId, final Long teamId) { + public void deleteHackathonTeamVote(final Member me, final Long hackathonId, final Long teamId) { final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); final HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( diff --git a/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteQueryService.java new file mode 100644 index 00000000..0503992c --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteQueryService.java @@ -0,0 +1,37 @@ +package sw_css.hackathon.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import sw_css.hackathon.application.dto.response.HackathonTeamVoteResponse; +import sw_css.hackathon.domain.repository.HackathonRepository; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.hackathon.domain.repository.HackathonTeamVoteRepository; +import sw_css.hackathon.exception.HackathonException; +import sw_css.hackathon.exception.HackathonExceptionType; +import sw_css.member.domain.Member; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HackathonTeamVoteQueryService { + + private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamVoteRepository hackathonTeamVoteRepository; + + public HackathonTeamVoteResponse findHackathonTeamVote(final Member me, final Long hackathonId, final Long teamId) { + validateHackathonIdAndTeamId(hackathonId, teamId); + + boolean voted = hackathonTeamVoteRepository.existsByHackathonIdAndTeamIdAndMemberId(hackathonId, teamId, me.getId()); + + return new HackathonTeamVoteResponse(voted); + } + + private void validateHackathonIdAndTeamId(final Long hackathonId, final Long teamId) { + hackathonRepository.findById(hackathonId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + } +} diff --git a/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamVoteResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamVoteResponse.java new file mode 100644 index 00000000..2e998582 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamVoteResponse.java @@ -0,0 +1,6 @@ +package sw_css.hackathon.application.dto.response; + +public record HackathonTeamVoteResponse( + boolean voted +) { +} diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index 59c522dd..b828e236 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -30,6 +30,7 @@ import sw_css.hackathon.application.HackathonTeamCommandService; import sw_css.hackathon.application.HackathonTeamQueryService; import sw_css.hackathon.application.HackathonTeamVoteCommandService; +import sw_css.hackathon.application.HackathonTeamVoteQueryService; import sw_css.helper.ApiTestHelper; import sw_css.major.application.MajorQueryService; import sw_css.member.application.MemberQueryService; @@ -88,6 +89,9 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected HackathonTeamCommandService hackathonTeamCommandService; + @MockBean + protected HackathonTeamVoteQueryService hackathonTeamVoteQueryService; + @MockBean protected HackathonTeamVoteCommandService hackathonTeamVoteCommandService; diff --git a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java index 2e6e9bf3..ab00da24 100644 --- a/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java @@ -1,28 +1,65 @@ package sw_css.restdocs.docs; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; import org.springframework.restdocs.request.PathParametersSnippet; import sw_css.hackathon.api.HackathonTeamVoteController; +import sw_css.hackathon.application.dto.response.HackathonTeamVoteResponse; import sw_css.member.domain.Member; import sw_css.restdocs.RestDocsTest; @WebMvcTest(HackathonTeamVoteController.class) public class HackathonTeamVoteApiDocsTest extends RestDocsTest { + @Test + @DisplayName("[성공] 회원은 팀 투표를 조회할 수 있다.") + public void findHackathonTeamVote() throws Exception { + // given + final PathParametersSnippet pathParameterSnippet = pathParameters( + parameterWithName("hackathonId").description("해당 팀이 속한 해커톤 id"), + parameterWithName("teamId").description("팀의 id") + ); + + final ResponseFieldsSnippet responseFieldSnippet = responseFields( + fieldWithPath("voted").type(JsonFieldType.BOOLEAN).description("해당 팀에 투표했는지 여부")); + + final Long hackathonId = 1L; + final Long teamId = 1L; + final HackathonTeamVoteResponse response = new HackathonTeamVoteResponse(true); + final String token = "Bearer Access Token"; + + // when + when(hackathonTeamVoteQueryService.findHackathonTeamVote(any(), any(), any())).thenReturn(response); + + // then + mockMvc.perform( + RestDocumentationRequestBuilders.get("/hackathons/{hackathonId}/teams/{teamId}/vote", hackathonId, teamId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isOk()) + .andDo(document("hackathon-team-vote", pathParameterSnippet, responseFieldSnippet)); + } + @Test @DisplayName("[성공] 회원은 팀에 투표할 수 있다.") - public void voteHackathonTeam() throws Exception { + public void registerHackathonTeamVote() throws Exception { // given final PathParametersSnippet pathParameterSnippet = pathParameters( parameterWithName("hackathonId").description("해당 팀이 속한 해커톤 id"), @@ -35,14 +72,14 @@ public void voteHackathonTeam() throws Exception { final String token = "Bearer Access Token"; // when - doNothing().when(hackathonTeamVoteCommandService).voteHackathonTeam(me, hackathonId, teamId); + doNothing().when(hackathonTeamVoteCommandService).registerHackathonTeamVote(me, hackathonId, teamId); // then mockMvc.perform( post("/hackathons/{hackathonId}/teams/{teamId}/vote", hackathonId, teamId) .header(HttpHeaders.AUTHORIZATION, token)) .andExpect(status().isNoContent()) - .andDo(document("hackathon-team-vote", pathParameterSnippet)); + .andDo(document("hackathon-team-vote-register", pathParameterSnippet)); } @Test @@ -60,7 +97,7 @@ public void cancelHackathonTeamVote() throws Exception { final String token = "Bearer Access Token"; // when - doNothing().when(hackathonTeamVoteCommandService).voteHackathonTeam(me, hackathonId, teamId); + doNothing().when(hackathonTeamVoteCommandService).deleteHackathonTeamVote(me, hackathonId, teamId); // then mockMvc.perform(