diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 7225f58f..1024c972 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -349,3 +349,292 @@ 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[] + +.Response Body +include::{snippets}/admin-hackathon-find-all/response-fields.adoc[] + +=== `GET`: 관리자의 특정 해커톤 조회 + +.HTTP Request +include::{snippets}/admin-hackathon-find/http-request.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[] + +=== `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[] + +=== `PATCH`: 관리자의 해커톤 수정 + +.HTTP Request +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[] + +=== `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[] + +=== `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[] + +=== `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[] + + +== 관리자 - 해커톤 팀 +=== `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[] + +=== `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[] + +== 일반 - 해커톤 + +=== `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[] + +=== `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[] + + +=== `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[] + + +== 일반 - 해커톤 팀 + +=== `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[] + + +=== `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[] + + +=== `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[] + + +=== `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[] + + +=== `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[] + + +== 일반 - 해커톤 팀 투표 + +=== `GET`: 헤커톤 팀 투표 조회 +.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[] + +.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 +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/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/admin/hackathon/api/AdminHackathonController.java b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java new file mode 100644 index 00000000..34508cac --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonController.java @@ -0,0 +1,126 @@ +package sw_css.admin.hackathon.api; + +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; +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; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +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; +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; + +@Validated +@RequestMapping("/admin/hackathons") +@RestController +@RequiredArgsConstructor +public class AdminHackathonController { + private final AdminHackathonCommandService adminHackathonCommandService; + private final AdminHackathonQueryService adminHackathonQueryService; + + @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( + adminHackathonQueryService.findAllHackathons(pageable, name, visibleStatus) + ); + } + + @GetMapping("/{hackathonId}") + public ResponseEntity findHackathonById( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId + ){ + return ResponseEntity.ok( + adminHackathonQueryService.findHackathonById(hackathonId)); + } + + @PostMapping + public ResponseEntity registerHackathon( + @AdminInterface FacultyMember facultyMember, + @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(); + } + + @PatchMapping("/{hackathonId}") + public ResponseEntity updateHackathon( + @AdminInterface FacultyMember facultyMember, + @RequestPart(value = "file", required = false) final MultipartFile file, + @RequestPart(value = "request") @Valid final AdminHackathonRequest request, + @PathVariable final Long hackathonId + ) { + adminHackathonCommandService.updateHackathon(hackathonId, file, request); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{hackathonId}") + public ResponseEntity deleteHackathon( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId + ){ + adminHackathonCommandService.deleteHackathon(hackathonId); + return ResponseEntity.noContent().build(); + } + + @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(adminHackathonQueryService.downloadHackathonVotesById(hackathonId)); + + } + + @PatchMapping("/{hackathonId}/active") + public ResponseEntity patchHackathon( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId, + @RequestBody @Valid AdminHackathonActiveRequest request + ){ + adminHackathonCommandService.activeHackathon(hackathonId, request.visibleStatus()); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/{hackathonId}/prize") + public ResponseEntity patchHackathonPrize( + @AdminInterface FacultyMember facultyMember, + @PathVariable final Long hackathonId, + @RequestBody @Valid AdminHackathonPrizeRequest request + ){ + adminHackathonCommandService.hackathonChangePrize(hackathonId, request.teams()); + return ResponseEntity.noContent().build(); + } +} 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..0ae2f93c --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/api/AdminHackathonTeamController.java @@ -0,0 +1,47 @@ +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.DeleteMapping; +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.hackathon.application.dto.request.HackathonTeamRequest; +import sw_css.member.domain.FacultyMember; +import sw_css.utils.annotation.AdminInterface; + +@Validated +@RequestMapping("admin/hackathons/{hackathonId}/teams/{teamId}") +@RestController +@RequiredArgsConstructor +@Transactional +public class AdminHackathonTeamController { + private final AdminHackathonTeamCommandService adminHackathonTeamCommandService; + + @PatchMapping() + public ResponseEntity updateHackathonTeam( + @AdminInterface FacultyMember facultyMember, + @PathVariable Long hackathonId, + @PathVariable Long teamId, + @RequestBody @Valid HackathonTeamRequest request + ) { + adminHackathonTeamCommandService.updateHackathonTeam(hackathonId, teamId, request); + return ResponseEntity.noContent().build(); + } + + @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/AdminHackathonCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java new file mode 100644 index 00000000..1ead72a3 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonCommandService.java @@ -0,0 +1,164 @@ +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.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.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; +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 +public class AdminHackathonCommandService { + + private final HackathonTeamRepository hackathonTeamRepository; + @Value("${data.file-path-prefix}") + private String filePathPrefix; + + private final HackathonRepository hackathonRepository; + + public Long registerHackathon(final MultipartFile file, final AdminHackathonRequest request) { + validateFileType(file); + 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); + + final Long newHackathonId = hackathonRepository.save(newHackathon).getId(); + uploadFile(file, newFilePath); + return newHackathonId; + } + + public void updateHackathon(final Long hackathonId, final MultipartFile file, final AdminHackathonRequest request) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + + if(file != null) { + validateFileType(file); + final String newFilePath = generateFilePath(file); + uploadFile(file, newFilePath); + hackathon.setImageUrl(newFilePath); + } + 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()); + hackathon.setPassword(request.password()); + hackathon.setApplyStartDate(request.applyStartDate()); + hackathon.setApplyEndDate(request.applyEndDate()); + hackathon.setHackathonStartDate(request.hackathonStartDate()); + hackathon.setHackathonEndDate(request.hackathonEndDate()); + hackathonRepository.save(hackathon); + } + + public void deleteHackathon(final Long hackathonId) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> 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 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 AdminHackathonException(AdminHackathonExceptionType.INVALID_ACTIVE_STATUS); + + hackathonRepository.save(hackathon); + } + + public void hackathonChangePrize(final Long hackathonId, List teams) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.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 AdminHackathonException(AdminHackathonExceptionType.INVALID_PRIZE_STATUS); + } + } + + private void validateDate(LocalDate startDate, LocalDate endDate, AdminHackathonExceptionType exceptionType) { + if (startDate.isAfter(endDate)) throw new AdminHackathonException(exceptionType); + } + + 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/admin/hackathon/application/AdminHackathonQueryService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java new file mode 100644 index 00000000..d9569e40 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonQueryService.java @@ -0,0 +1,159 @@ +package sw_css.admin.hackathon.application; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +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.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.AdminHackathonDetailResponse; +import sw_css.admin.hackathon.application.dto.response.AdminHackathonResponse; +import sw_css.admin.hackathon.domain.HackathonStatus; +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; +import sw_css.hackathon.domain.repository.HackathonTeamRepository; +import sw_css.hackathon.domain.repository.HackathonTeamVoteRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminHackathonQueryService { + private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamVoteRepository hackathonTeamVoteRepository; + + 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(HackathonStatus.ACTIVE.toString()), pageableWithSort); + return AdminHackathonResponse.from(hackathons); + } + if(name != null) { + Page hackathons = hackathonRepository.findByNameContaining(name, pageableWithSort); + return AdminHackathonResponse.from(hackathons); + } + if(visibleStatus != null) { + Page hackathons = hackathonRepository.findByVisibleStatus(visibleStatus.equals(HackathonStatus.ACTIVE.toString()), pageableWithSort); + return AdminHackathonResponse.from(hackathons); + } + Page hackathons = hackathonRepository.findAll(pageableWithSort); + return AdminHackathonResponse.from(hackathons); + } + + public AdminHackathonDetailResponse findHackathonById(final Long id) { + Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + + return AdminHackathonDetailResponse.of(hackathon); + } + + public byte[] downloadHackathonVotesById(final Long id) { + Hackathon hackathon = hackathonRepository.findById(id).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + + final List hackathonTeams = hackathonTeamRepository.findByHackathonIdSorted(hackathon.getId()); + + 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); + 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()); + } + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + workbook.write(bos); + workbook.close(); + return bos.toByteArray(); + } catch (IOException e) { + throw new AdminHackathonException(AdminHackathonExceptionType.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/AdminHackathonTeamCommandService.java b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java new file mode 100644 index 00000000..7a886c19 --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/AdminHackathonTeamCommandService.java @@ -0,0 +1,130 @@ +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.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.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, HackathonTeamRequest request) { + final Hackathon hackathon = hackathonRepository.findById(hackathonId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + hackathonTeam.setName(request.name()); + hackathonTeam.setWork(request.work()); + hackathonTeam.setGithubUrl(request.githubUrl()); + + hackathonTeamRepository.save(hackathonTeam); + + final HackathonTeamMember teamLeader = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderTrue(hackathonId, teamId); + final List teamMembers = hackathonTeamMemberRepository.findAllByHackathonIdAndTeamIdAndIsLeaderFalseOrderByStudentIdAsc(hackathonId, teamId); + + final HackathonTeamMemberRequest leader = request.leader(); + final List members = request.members().stream() + .sorted(Comparator.comparingLong(HackathonTeamRequest.HackathonTeamMemberRequest::id)) + .toList(); + + checkLeaderAndUpdate(teamLeader, leader, hackathon, hackathonTeam); + + for (HackathonTeamMember originMember : teamMembers) { + boolean found = false; + + Iterator iterator = members.iterator(); + while (iterator.hasNext()) { + HackathonTeamMemberRequest 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 (HackathonTeamMemberRequest member : members) { + validateTeamMember(member); + HackathonTeamMember newMember = new HackathonTeamMember(hackathon, hackathonTeam, member.id(), member.role()); + hackathonTeamMemberRepository.save(newMember); + } + } + + public void deleteHackathonTeam(Long hackathonId, Long teamId) { + hackathonRepository.findById(hackathonId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON)); + final HackathonTeam hackathonTeam = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow( + () -> new AdminHackathonException(AdminHackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + + hackathonTeam.delete(); + hackathonTeamRepository.save(hackathonTeam); + } + + private void checkMemberAndUpdate(HackathonTeamMember originMember, HackathonTeamMemberRequest member) { + validateTeamMember(member); + if ( !originMember.getRole().equals(member.role()) ) { + originMember.setRole(member.role()); + hackathonTeamMemberRepository.save(originMember); + } + } + + private void checkLeaderAndUpdate(HackathonTeamMember originLeader, HackathonTeamMemberRequest 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(HackathonTeamMemberRequest teamMember) { + validateStudentId(teamMember.id()); + validateRole(teamMember.role()); + } + + private void validateRole(String role){ + try { + HackathonRole.valueOf(role); + } catch (IllegalArgumentException e) { + throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_ROLE_STATUS); + } + } + + private void validateStudentId(Long studentId){ + if ( !studentId.toString().matches(StudentId.STUDENT_ID_REGEX) ) + throw new AdminHackathonException(AdminHackathonExceptionType.INVALID_STUDENT_ID); + } + +} 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/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/application/dto/request/AdminHackathonRequest.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonRequest.java new file mode 100644 index 00000000..455aaabf --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/request/AdminHackathonRequest.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 AdminHackathonRequest( + @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 +) { +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonDetailResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonDetailResponse.java new file mode 100644 index 00000000..743d84ec --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonDetailResponse.java @@ -0,0 +1,36 @@ +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 AdminHackathonDetailResponse( + Long id, + String name, + String description, + String imageUrl, + @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 AdminHackathonDetailResponse of(Hackathon hackathon) { + return new AdminHackathonDetailResponse( + hackathon.getId(), + hackathon.getName(), + hackathon.getDescription(), + hackathon.getImageUrl(), + hackathon.getApplyStartDate(), + hackathon.getApplyEndDate(), + hackathon.getHackathonStartDate(), + hackathon.getHackathonEndDate(), + hackathon.getPassword(), + hackathon.isVisibleStatus() + ); + } +} diff --git a/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonResponse.java b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonResponse.java new file mode 100644 index 00000000..bb75b4ce --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/application/dto/response/AdminHackathonResponse.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 AdminHackathonResponse( + 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 AdminHackathonResponse( + 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/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/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/AdminHackathonExceptionType.java b/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonExceptionType.java new file mode 100644 index 00000000..12e9e89f --- /dev/null +++ b/backend/src/main/java/sw_css/admin/hackathon/exception/AdminHackathonExceptionType.java @@ -0,0 +1,36 @@ +package sw_css.admin.hackathon.exception; + +import org.springframework.http.HttpStatus; +import sw_css.base.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, "파일을 첨부해야 합니다."), + CANNOT_OPEN_FILE(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, "대회 시작일이 대회 마지막날 보다 이후일 수 없습니다."), + UNSUPPORTED_FILE_TYPE(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "파일 유형이 png, jpg, jpeg 가 아닙니다."); + + private final HttpStatus httpStatus; + private final String errorMessage; + + AdminHackathonExceptionType(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/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/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/HackathonController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java new file mode 100644 index 00000000..de7ac2ca --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonController.java @@ -0,0 +1,54 @@ +package sw_css.hackathon.api; + +import java.util.List; +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.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.HackathonPrizeResponse; +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) + ); + } + + @GetMapping("{hackathonId}") + public ResponseEntity findHackathonById( + final @PathVariable Long hackathonId + ) { + return ResponseEntity.ok( + hackathonQueryService.findHackathon(hackathonId) + ); + } + + @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/api/HackathonTeamController.java b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java new file mode 100644 index 00000000..819313be --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamController.java @@ -0,0 +1,84 @@ +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; +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.RequestBody; +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") +@RestController +@RequiredArgsConstructor +public class HackathonTeamController { + + private final HackathonTeamQueryService hackathonTeamQueryService; + private final HackathonTeamCommandService hackathonTeamCommandService; + + @GetMapping + public ResponseEntity> findAllHackathonTeams( + Pageable pageable, + @PathVariable Long hackathonId + ) { + return ResponseEntity.ok(hackathonTeamQueryService.findAllHackathonTeam(pageable, hackathonId)); + } + + @GetMapping("{teamId}") + public ResponseEntity findHackathonTeamById( + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + return ResponseEntity.ok(hackathonTeamQueryService.findHackathonTeam(hackathonId, teamId)); + } + + @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(me, hackathonId, file, request); + + return ResponseEntity.created(URI.create("/hackathons/" + hackathonId + "/teams/" + teamId)).build(); + } + + @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(); + } + + @DeleteMapping("{teamId}") + public ResponseEntity deleteHackathonTeam( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + hackathonTeamCommandService.deleteHackathonTeam(me, hackathonId, teamId); + return ResponseEntity.noContent().build(); + } +} 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..231be932 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/api/HackathonTeamVoteController.java @@ -0,0 +1,56 @@ +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.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; + +@Validated +@RequestMapping("/hackathons/{hackathonId}/teams/{teamId}/vote") +@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 registerHackathonTeamVote( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + hackathonTeamVoteCommandService.registerHackathonTeamVote(me, hackathonId, teamId); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping + public ResponseEntity deleteHackathonTeamVote( + @MemberInterface Member me, + @PathVariable Long hackathonId, + @PathVariable Long teamId + ) { + hackathonTeamVoteCommandService.deleteHackathonTeamVote(me, hackathonId, teamId); + return ResponseEntity.noContent().build(); + } +} 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..3f5c5d19 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonQueryService.java @@ -0,0 +1,79 @@ +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; +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.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; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HackathonQueryService { + + private final HackathonRepository hackathonRepository; + private final HackathonTeamRepository hackathonTeamRepository; + private final HackathonTeamMemberRepository hackathonTeamMemberRepository; + + 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); + } + + public HackathonDetailResponse findHackathon(final Long id) { + Hackathon hackathon = hackathonRepository.findByIdAndVisibleStatusIsTrue(id).orElseThrow( + () -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON)); + + 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/HackathonTeamCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java new file mode 100644 index 00000000..6724298b --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamCommandService.java @@ -0,0 +1,239 @@ +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.Iterator; +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.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.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; + +@Service +@RequiredArgsConstructor +@Transactional +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 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, me); + + final HackathonTeam newTeam = hackathonTeamRepository.save(team); + uploadFile(file, newFilePath); + + validateAllHackathonTeamMember(me, 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(); + } + + public void updateHackathonTeam(final Member me, final Long hackathonId, final Long teamId, final HackathonTeamRequest request) { + 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)); + + 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); + } + } + + 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()) ) { + 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()); + validateHackathonTeamMember(leader); + + members.forEach(member -> { + 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) { + 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 HackathonException(HackathonExceptionType.INVALID_ROLE_STATUS); + } + } + + private void validateFileType(final MultipartFile file) { + 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/hackathon/application/HackathonTeamQueryService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java new file mode 100644 index 00000000..0467d18a --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamQueryService.java @@ -0,0 +1,87 @@ +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.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; +import sw_css.member.domain.repository.StudentMemberRepository; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class HackathonTeamQueryService { + + private final HackathonTeamRepository hackathonTeamRepository; + 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) { + validateHackathonId(hackathonId); + + Page teams = hackathonTeamRepository.findByHackathonIdWithPageable(hackathonId, pageable); + + List teamResponses = teams.getContent().stream().map( + this::convertHackathonTeamToHackathonTeamResponse).toList(); + + return new PageImpl<>(teamResponses, teams.getPageable(), teams.getTotalElements()); + } + + public HackathonTeamResponse findHackathonTeam(final Long hackathonId, final Long teamId) { + validateHackathonId(hackathonId); + + HackathonTeam team = hackathonTeamRepository.findByHackathonIdAndId(hackathonId, teamId).orElseThrow(() -> new HackathonException(HackathonExceptionType.NOT_FOUND_HACKATHON_TEAM)); + final Long vote = hackathonTeamVoteRepository.countByHackathonIdAndTeamId(hackathonId, teamId); + + 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){ + 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/HackathonTeamVoteCommandService.java b/backend/src/main/java/sw_css/hackathon/application/HackathonTeamVoteCommandService.java new file mode 100644 index 00000000..5a333869 --- /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 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( + () -> 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 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( + () -> 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/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/request/HackathonTeamRequest.java b/backend/src/main/java/sw_css/hackathon/application/dto/request/HackathonTeamRequest.java new file mode 100644 index 00000000..09939f54 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/request/HackathonTeamRequest.java @@ -0,0 +1,26 @@ +package sw_css.hackathon.application.dto.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record HackathonTeamRequest( + @NotBlank(message="팀명을 기재해주세요.") + String name, + @NotBlank(message="프로젝트명을 기재해주세요.") + String work, + @NotBlank(message="프로젝트의 깃헙 레포지토리의 url을 기재해주세요.") + String githubUrl, + @NotNull(message="팀장의 학번과 역할을 기재해주세요.") + HackathonTeamMemberRequest leader, + @NotNull(message="팀원의 정보를 넣어주세요.") + List members +) { + public record HackathonTeamMemberRequest( + @NotNull(message="팀원의 학번을 기재해주세요.") + Long id, + @NotBlank(message="팀원의 역할을 기재해주세요.") + String role + ){} +} + 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/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/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/application/dto/response/HackathonTeamResponse.java b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java new file mode 100644 index 00000000..f708f283 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/application/dto/response/HackathonTeamResponse.java @@ -0,0 +1,29 @@ +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, + String work, + String githubUrl, + String imageUrl, + Long vote, + 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/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/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..cb39ffbb --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/Hackathon.java @@ -0,0 +1,64 @@ +package sw_css.hackathon.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.LocalDate; +import lombok.AccessLevel; +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") +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/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/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/HackathonTeam.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java new file mode 100644 index 00000000..b70dc011 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeam.java @@ -0,0 +1,64 @@ +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 lombok.Setter; +import org.hibernate.annotations.SQLRestriction; +import sw_css.member.domain.Member; + + +@Entity +@Getter +@Setter +@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 work; + + @Column(nullable = false) + private String githubUrl; + + @Column(nullable = false) + private String imageUrl; + + @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, Member createdBy) { + this(null, hackathon, name, work, githubUrl, imageUrl, HackathonPrize.NONE_PRIZE.toString(), createdBy, false); + } + + public void delete() { + isDeleted = true; + } +} 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..0e298b1b --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamMember.java @@ -0,0 +1,59 @@ +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 lombok.Setter; +import org.hibernate.annotations.SQLRestriction; + +@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 = "hackathon_id", nullable = false) + private Hackathon hackathon; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "team_id", nullable = false) + private HackathonTeam team; + + @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); + } + + public void delete() { this.isDeleted = true; } +} 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..978d0081 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamVote.java @@ -0,0 +1,50 @@ +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 lombok.Setter; +import org.hibernate.annotations.SQLRestriction; +import sw_css.member.domain.Member; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@SQLRestriction("is_deleted = false") +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 = "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/HackathonTeamWithVote.java b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java new file mode 100644 index 00000000..b6ca4810 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/HackathonTeamWithVote.java @@ -0,0 +1,16 @@ +package sw_css.hackathon.domain; + +public record HackathonTeamWithVote( + Long id, + Long hackathonId, + String name, + String work, + String githubUrl, + String imageUrl, + 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/HackathonRepository.java b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java new file mode 100644 index 00000000..37cdbb16 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonRepository.java @@ -0,0 +1,19 @@ +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.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 findAllByVisibleStatusIsTrue(Pageable pageable); + Page findAllByNameContainingAndVisibleStatusIsTrue(String name, Pageable pageable); + + Optional findByIdAndVisibleStatusIsTrue(long id); +} 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..26bb3004 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamMemberRepository.java @@ -0,0 +1,15 @@ +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); + + Long countByHackathonIdAndTeamId(Long hackathonId, Long teamId); + + List 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 new file mode 100644 index 00000000..0ca49e7f --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamRepository.java @@ -0,0 +1,51 @@ +package sw_css.hackathon.domain.repository; + +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; +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 " + + "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") + List findByHackathonIdSorted(@Param("hackathonId") Long hackathonId); + + @Query("SELECT new sw_css.hackathon.domain.HackathonTeamWithVote(" + + "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 " + + "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); + + List findByHackathonIdAndPrizeEquals(Long hackathonId, String prize); +} 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..43f99f61 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/domain/repository/HackathonTeamVoteRepository.java @@ -0,0 +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/HackathonException.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonException.java new file mode 100644 index 00000000..00a7dab5 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonException.java @@ -0,0 +1,17 @@ +package sw_css.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/hackathon/exception/HackathonExceptionType.java b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java new file mode 100644 index 00000000..efa69584 --- /dev/null +++ b/backend/src/main/java/sw_css/hackathon/exception/HackathonExceptionType.java @@ -0,0 +1,37 @@ +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, "해당 해커톤이 존재하지 않습니다."), + 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_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 가 아닙니다."); + + + 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/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/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/main/resources/schema.sql b/backend/src/main/resources/schema.sql index 91f3297b..358c5af3 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema.sql @@ -6,6 +6,10 @@ 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_vote; +drop table if exists sw_css.hackathon_team_member; create table member ( @@ -85,3 +89,55 @@ 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, + prize varchar(255), + created_by bigint not null, + 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, + member_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, + hackathon_id bigint not null, + 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 63754c40..8c3a6a61 100644 --- a/backend/src/main/resources/test-data.sql +++ b/backend/src/main/resources/test-data.sql @@ -2,16 +2,77 @@ 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 기업 개발자'); +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 +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, 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, member_id, is_deleted) +values(1, 2, 202012341, false); +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, member_id, is_deleted) +values(1, 2, 202012341, false); +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, member_id, is_deleted) +values(1, 3, 202012341, false); +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, member_id, is_deleted) +values(1, 6, 202012341, false); +insert into hackathon_team_vote (hackathon_id, team_id, member_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) +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 INSERT INTO sw_css.milestone_history (id, milestone_id, student_id, description, file_url, status, reject_reason, count, diff --git a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java index 9b382b26..b828e236 100644 --- a/backend/src/test/java/sw_css/restdocs/RestDocsTest.java +++ b/backend/src/test/java/sw_css/restdocs/RestDocsTest.java @@ -15,6 +15,9 @@ 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.hackathon.application.AdminHackathonTeamCommandService; import sw_css.admin.member.application.MemberAdminQueryService; import sw_css.admin.milestone.application.MilestoneHistoryAdminCommandService; import sw_css.admin.milestone.application.MilestoneHistoryAdminQueryService; @@ -23,6 +26,11 @@ 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.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; @@ -63,6 +71,30 @@ public abstract class RestDocsTest extends ApiTestHelper { @MockBean protected MemberAdminQueryService memberAdminQueryService; + @MockBean + protected AdminHackathonQueryService adminHackathonQueryService; + + @MockBean + protected AdminHackathonCommandService adminHackathonCommandService; + + @MockBean + protected AdminHackathonTeamCommandService adminHackathonTeamCommandService; + + @MockBean + protected HackathonQueryService hackathonQueryService; + + @MockBean + protected HackathonTeamQueryService hackathonTeamQueryService; + + @MockBean + protected HackathonTeamCommandService hackathonTeamCommandService; + + @MockBean + protected HackathonTeamVoteQueryService hackathonTeamVoteQueryService; + + @MockBean + protected HackathonTeamVoteCommandService hackathonTeamVoteCommandService; + @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..5bfd7b62 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonApiDocsTest.java @@ -0,0 +1,172 @@ +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.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.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.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; + +@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)); + } + + @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)); + } + + @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)); + } +} 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..9bb6bd1a --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamApiDocsTest.java @@ -0,0 +1,291 @@ +package sw_css.restdocs.docs; + +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.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; +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.requestParts; +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.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; +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.PathParametersSnippet; +import org.springframework.restdocs.request.RequestPartsSnippet; +import sw_css.hackathon.api.HackathonTeamController; +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.member.domain.Member; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(HackathonTeamController.class) +public class HackathonTeamApiDocsTest extends RestDocsTest { + + @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("팀의 투표 득표수"), + 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, "학생 이름", "학생 전공", 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); + + 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(), 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); + + // 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)); + } + + @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)); + } + + @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 Member me = new Member(1L, "ddang@pusan.ac.kr", "ddang", "qwer1234!", "01012341234"); + final Long hackathonId = 1L; + final Long teamId = 1L; + + // when + when(hackathonTeamCommandService.registerHackathonTeam(me, 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)); + } + + @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)); + } + + @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)); + } + +} 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..ab00da24 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/HackathonTeamVoteApiDocsTest.java @@ -0,0 +1,109 @@ +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 registerHackathonTeamVote() 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).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-register", 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).deleteHackathonTeamVote(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)); + } +} 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"; 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..d46c69dd --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonApiDocsTest.java @@ -0,0 +1,327 @@ +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.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; +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.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.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.PathParametersSnippet; +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.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) +public class AdminHackathonApiDocsTest 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("조죄할 해커톤 명"), + 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", queryParametersSnippet, responseBodySnippet)); + } + + @Test + @DisplayName("[성공] 관리자가 해커톤 상세 조회할 수 있다.") + public void findHackathon() 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("해커톤 대회 마지막날"), + 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", pathParameterSnippet, 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)); + } + + @Test + @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))"), + 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", 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(delete("/admin/hackathons/{hackathonId}", hackathonId) + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()) + .andDo(document("admin-hackathon-delete", pathParameters)); + } + + @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}/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( + 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)); + } + + @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)); + } + + +} 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..d4138c79 --- /dev/null +++ b/backend/src/test/java/sw_css/restdocs/docs/admin/AdminHackathonTeamApiDocsTest.java @@ -0,0 +1,89 @@ +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.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; +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.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.hackathon.application.dto.request.HackathonTeamRequest; +import sw_css.hackathon.application.dto.request.HackathonTeamRequest.HackathonTeamMemberRequest; +import sw_css.restdocs.RestDocsTest; + +@WebMvcTest(AdminHackathonTeamController.class) +public class AdminHackathonTeamApiDocsTest extends RestDocsTest { + + @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 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 + 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)); + } + + @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)); + } +}