From 889f53bce29034516db66c0937118a08ff711e07 Mon Sep 17 00:00:00 2001 From: zionhann Date: Wed, 25 Oct 2023 02:23:05 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=A7=A4=ED=95=91=EC=9D=B4=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=B2=A0=EC=9D=B4=EC=8A=A4=20=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=EC=A7=80=EC=99=80=20=ED=98=B8=ED=99=98?= =?UTF-8?q?=EB=90=98=EB=8F=84=EB=A1=9D=20=EC=84=A4=EC=A0=95=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../csee/histudy/util/ImagePathMapper.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/main/java/edu/handong/csee/histudy/util/ImagePathMapper.java diff --git a/src/main/java/edu/handong/csee/histudy/util/ImagePathMapper.java b/src/main/java/edu/handong/csee/histudy/util/ImagePathMapper.java new file mode 100644 index 0000000..985750e --- /dev/null +++ b/src/main/java/edu/handong/csee/histudy/util/ImagePathMapper.java @@ -0,0 +1,49 @@ +package edu.handong.csee.histudy.util; + +import edu.handong.csee.histudy.domain.Image; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class ImagePathMapper { + + @Value("${custom.jwt.issuer}") + private String origin; + + @Value("${custom.resource.path}") + private String imageBasePath; + + private final String firebaseStoragePrefix = "https://firebasestorage.googleapis.com"; + + public Map parseImageToMapWithFullPath(List images) { + return images.stream() + .collect(Collectors.toUnmodifiableMap(Image::getId, img -> getFullPath(img.getPath()))); + } + + public List extractFilename(List pathname) { + if (pathname == null) { + return null; + } + return pathname.stream().map(this::extractFilename).toList(); + } + + public String getFullPath(String pathname) { + if (pathname == null) { + return null; + } + return (pathname.startsWith(firebaseStoragePrefix)) + ? pathname + : origin + imageBasePath + pathname; + } + + private String extractFilename(String pathname) { + if (pathname.startsWith(firebaseStoragePrefix)) { + return pathname; + } + int lastIndex = pathname.lastIndexOf('/'); + return (lastIndex >= 0) ? pathname.substring(lastIndex + 1) : pathname; + } +} From ebf6d011f32c9a4e7e8d5f798ec6cd6270f32625 Mon Sep 17 00:00:00 2001 From: zionhann Date: Wed, 25 Oct 2023 02:25:24 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EB=A7=A4=ED=95=91=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=AC=EB=A7=A4=ED=8C=85=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../histudy/controller/AdminController.java | 211 ++++----- .../histudy/controller/PublicController.java | 23 +- .../histudy/controller/TeamController.java | 265 +++++------ .../csee/histudy/domain/GroupReport.java | 15 +- .../handong/csee/histudy/dto/ImageDto.java | 22 +- .../handong/csee/histudy/dto/ReportDto.java | 178 ++++---- .../handong/csee/histudy/dto/TeamRankDto.java | 71 ++- .../csee/histudy/service/ImageService.java | 204 ++++----- .../csee/histudy/service/ReportService.java | 222 ++++----- .../csee/histudy/service/TeamService.java | 429 +++++++++--------- .../group/ReportGroupCourseServiceTest.java | 19 +- 11 files changed, 796 insertions(+), 863 deletions(-) diff --git a/src/main/java/edu/handong/csee/histudy/controller/AdminController.java b/src/main/java/edu/handong/csee/histudy/controller/AdminController.java index 3f2cd11..bec3c51 100644 --- a/src/main/java/edu/handong/csee/histudy/controller/AdminController.java +++ b/src/main/java/edu/handong/csee/histudy/controller/AdminController.java @@ -13,145 +13,130 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @Tag(name = "관리자 API") @SecurityRequirement(name = "ADMIN") @RestController @RequiredArgsConstructor @RequestMapping("/api/admin") public class AdminController { - private final TeamService teamService; - private final UserService userService; + private final TeamService teamService; + private final UserService userService; - @Operation(summary = "그룹별 활동 조회") - @GetMapping(value = "/manageGroup") - public ResponseEntity> getTeams(@RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - String email = claims.getSubject(); - return ResponseEntity.ok(teamService.getTeams(email)); - } - throw new ForbiddenException(); + @Operation(summary = "그룹별 활동 조회") + @GetMapping(value = "/manageGroup") + public ResponseEntity> getTeams(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + String email = claims.getSubject(); + return ResponseEntity.ok(teamService.getTeams(email)); } + throw new ForbiddenException(); + } - @Deprecated - @Operation(summary = "그룹 삭제") - @DeleteMapping("/group") - public ResponseEntity deleteTeam( - @RequestBody TeamIdDto dto, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return ResponseEntity.ok(teamService.deleteTeam(dto, claims.getSubject())); - } - throw new ForbiddenException(); + @Deprecated + @Operation(summary = "그룹 삭제") + @DeleteMapping("/group") + public ResponseEntity deleteTeam( + @RequestBody TeamIdDto dto, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return ResponseEntity.ok(teamService.deleteTeam(dto, claims.getSubject())); } + throw new ForbiddenException(); + } + + @Operation(summary = "특정 그룹 보고서 조회") + @GetMapping("/groupReport/{id}") + public ResponseEntity getTeamReports( + @Parameter(description = "그룹 아이디", required = true) @PathVariable(name = "id") long id, + @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + TeamReportDto res = teamService.getTeamReports(id, claims.getSubject()); - @Operation(summary = "특정 그룹 보고서 조회") - @GetMapping("/groupReport/{id}") - public ResponseEntity getTeamReports( - @Parameter(description = "그룹 아이디", required = true) - @PathVariable(name = "id") long id, - @RequestAttribute Claims claims, - @Value("${custom.resource.path}") String imageBasePath, - @Value("${custom.jwt.issuer}") String baseUri) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - TeamReportDto res = teamService.getTeamReports(id, claims.getSubject()); - res.getReports() - .forEach(report -> - report.addPathToFilename(baseUri + imageBasePath)); - return ResponseEntity.ok(res); - } - throw new ForbiddenException(); + return ResponseEntity.ok(res); } + throw new ForbiddenException(); + } - /** - * 스터디 신청한 유저 목록 조회(신청O 그룹?) - * - *

그룹 배정 여부와 관계 없이 - * 스터디를 신청한 유저 목록을 표시한다

- * - * @param claims 토큰 페이로드 - * @return 스터디 신청한 유저 목록 - */ - @Operation(summary = "그룹 배정 여부와 관계 없이 스터디 신청한 유저 목록 조회") - @GetMapping("/allUsers") - public ResponseEntity> getAppliedUsers(@RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return ResponseEntity.ok(userService.getAppliedUsers()); - } - throw new ForbiddenException(); + /** + * 스터디 신청한 유저 목록 조회(신청O 그룹?) + * + *

그룹 배정 여부와 관계 없이 스터디를 신청한 유저 목록을 표시한다 + * + * @param claims 토큰 페이로드 + * @return 스터디 신청한 유저 목록 + */ + @Operation(summary = "그룹 배정 여부와 관계 없이 스터디 신청한 유저 목록 조회") + @GetMapping("/allUsers") + public ResponseEntity> getAppliedUsers(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return ResponseEntity.ok(userService.getAppliedUsers()); } + throw new ForbiddenException(); + } - @Operation(summary = "그룹 매칭") - @PostMapping("/team-match") - public ResponseEntity matchTeam(@RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return ResponseEntity.ok(teamService.matchTeam()); - } - throw new ForbiddenException(); + @Operation(summary = "그룹 매칭") + @PostMapping("/team-match") + public ResponseEntity matchTeam(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return ResponseEntity.ok(teamService.matchTeam()); } + throw new ForbiddenException(); + } - /** - * 그룹 미배정 학생 목록 조회(신청? 그룹X) - * - *

스터디 신청 여부와 관계 없이 - * 가입된 유저 중에서 그룹이 배정되지 않은 유저 목록을 표시한다

- * - * @param claims 토큰 페이로드 - * @return 그룹 미배정 학생 목록 - */ - @Operation(summary = "매칭되지 않은 유저 목록 조회") - @GetMapping("/unmatched-users") - public ResponseEntity> getUnmatchedUsers(@RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return ResponseEntity.ok(userService.getUnmatchedUsers()); - } - throw new ForbiddenException(); + /** + * 그룹 미배정 학생 목록 조회(신청? 그룹X) + * + *

스터디 신청 여부와 관계 없이 가입된 유저 중에서 그룹이 배정되지 않은 유저 목록을 표시한다 + * + * @param claims 토큰 페이로드 + * @return 그룹 미배정 학생 목록 + */ + @Operation(summary = "매칭되지 않은 유저 목록 조회") + @GetMapping("/unmatched-users") + public ResponseEntity> getUnmatchedUsers(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return ResponseEntity.ok(userService.getUnmatchedUsers()); } + throw new ForbiddenException(); + } - @Operation(summary = "특정 유저 지원폼 삭제") - @DeleteMapping("/form") - public UserDto.UserInfo deleteForm( - @RequestParam String sid, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return userService.deleteUserForm(sid); - } - throw new ForbiddenException(); + @Operation(summary = "특정 유저 지원폼 삭제") + @DeleteMapping("/form") + public UserDto.UserInfo deleteForm(@RequestParam String sid, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return userService.deleteUserForm(sid); } + throw new ForbiddenException(); + } - @Operation(summary = "유저 정보 수정") - @PostMapping("/edit-user") - public UserDto.UserInfo editUser( - @RequestBody UserDto.UserEdit form, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return userService.editUser(form); - } - throw new ForbiddenException(); + @Operation(summary = "유저 정보 수정") + @PostMapping("/edit-user") + public UserDto.UserInfo editUser( + @RequestBody UserDto.UserEdit form, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return userService.editUser(form); } + throw new ForbiddenException(); + } - /** - * 스터디 신청한 유저 목록 조회(신청O 그룹X) - * - *

스터디를 신청했으나 - * 그룹이 배정되지 않은 유저 목록을 조회한다 - * 이 목록은 그룹 매칭 대상자 목록과 같다

- * - * @param claims 토큰 페이로드 - * @return 스터디 신청했으나 그룹이 배정되지 않은 유저 목록 - */ - @Operation(summary = "스터디를 신청했으나 그룹이 배정되지 않은 유저 목록 조회") - @GetMapping("/users/unassigned") - public ResponseEntity> unassignedUser(@RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.ADMIN)) { - return ResponseEntity.ok(userService.getAppliedWithoutGroup()); - } - throw new ForbiddenException(); + /** + * 스터디 신청한 유저 목록 조회(신청O 그룹X) + * + *

스터디를 신청했으나 그룹이 배정되지 않은 유저 목록을 조회한다 이 목록은 그룹 매칭 대상자 목록과 같다 + * + * @param claims 토큰 페이로드 + * @return 스터디 신청했으나 그룹이 배정되지 않은 유저 목록 + */ + @Operation(summary = "스터디를 신청했으나 그룹이 배정되지 않은 유저 목록 조회") + @GetMapping("/users/unassigned") + public ResponseEntity> unassignedUser(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.ADMIN)) { + return ResponseEntity.ok(userService.getAppliedWithoutGroup()); } + throw new ForbiddenException(); + } } diff --git a/src/main/java/edu/handong/csee/histudy/controller/PublicController.java b/src/main/java/edu/handong/csee/histudy/controller/PublicController.java index 4d5fe05..a8989fa 100644 --- a/src/main/java/edu/handong/csee/histudy/controller/PublicController.java +++ b/src/main/java/edu/handong/csee/histudy/controller/PublicController.java @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -16,21 +15,11 @@ @RequestMapping("/api/public") public class PublicController { - private final TeamService teamService; + private final TeamService teamService; - @Value("${custom.resource.path}") - private String imageBasePath; - - @Value("${custom.jwt.issuer}") - private String baseUri; - - @Operation(summary = "그룹 목록 조회") - @GetMapping("/teams") - public TeamRankDto getTeams() { - TeamRankDto res = teamService.getAllTeams(); - res.getTeams().forEach(teamInfo -> - teamInfo.addPathToFilename(baseUri + imageBasePath)); - - return res; - } + @Operation(summary = "그룹 목록 조회") + @GetMapping("/teams") + public TeamRankDto getTeams() { + return teamService.getAllTeams(); + } } diff --git a/src/main/java/edu/handong/csee/histudy/controller/TeamController.java b/src/main/java/edu/handong/csee/histudy/controller/TeamController.java index 118b3dc..1e57eb4 100644 --- a/src/main/java/edu/handong/csee/histudy/controller/TeamController.java +++ b/src/main/java/edu/handong/csee/histudy/controller/TeamController.java @@ -23,16 +23,14 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.commons.collections.map.SingletonMap; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import java.util.List; -import java.util.Optional; - @Tag(name = "스터디 그룹 API") @SecurityRequirement(name = "MEMBER") @RestController @@ -40,154 +38,129 @@ @RequestMapping("/api/team") public class TeamController { - private final ReportService reportService; - private final CourseService courseService; - private final TeamService teamService; - private final ImageService imageService; - private final UserRepository userRepository; - - @Value("${custom.resource.path}") - String imageBasePath; - - @Value("${custom.jwt.issuer}") - String baseUri; - - @Operation(summary = "그룹 스터디 보고서 생성") - @PostMapping("/reports") - public ReportDto.ReportInfo createReport( - @RequestBody ReportForm form, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - ReportDto.ReportInfo res = reportService.createReport(form, claims.getSubject()); - res.getImages().forEach(image -> - image.addPathToFilename(baseUri + imageBasePath)); - - return res; - } - throw new ForbiddenException(); + private final ReportService reportService; + private final CourseService courseService; + private final TeamService teamService; + private final ImageService imageService; + private final UserRepository userRepository; + + @Operation(summary = "그룹 스터디 보고서 생성") + @PostMapping("/reports") + public ReportDto.ReportInfo createReport( + @RequestBody ReportForm form, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + return reportService.createReport(form, claims.getSubject()); } - - @Operation(summary = "그룹 보고서 목록 조회") - @GetMapping("/reports") - public ReportDto getMyGroupReports( - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - List reports = reportService.getReports(claims.getSubject()); - reports.forEach(report -> - report.getImages().forEach( - image -> image.addPathToFilename(baseUri + imageBasePath))); - return new ReportDto(reports); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + @Operation(summary = "그룹 보고서 목록 조회") + @GetMapping("/reports") + public ReportDto getMyGroupReports(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + List reports = reportService.getReports(claims.getSubject()); + return new ReportDto(reports); } - - @Operation(summary = "그룹 특정 보고서 조회") - @SecurityRequirements({ - @SecurityRequirement(name = "MEMBER"), - @SecurityRequirement(name = "ADMIN") - }) - @GetMapping("/reports/{reportId}") - public ResponseEntity getReport( - @PathVariable Long reportId, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER, Role.ADMIN)) { - Optional reportsOr = reportService.getReport(reportId); - reportsOr.ifPresent(report -> - report.getImages().forEach(image -> - image.addPathToFilename(baseUri + imageBasePath))); - - return reportsOr - .map(ResponseEntity::ok) - .orElseGet(() -> ResponseEntity.notFound().build()); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + @Operation(summary = "그룹 특정 보고서 조회") + @SecurityRequirements({ + @SecurityRequirement(name = "MEMBER"), + @SecurityRequirement(name = "ADMIN") + }) + @GetMapping("/reports/{reportId}") + public ResponseEntity getReport( + @PathVariable Long reportId, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER, Role.ADMIN)) { + Optional reportsOr = reportService.getReport(reportId); + + return reportsOr.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build()); } - - @Operation(summary = "그룹 특정 보고서 수정") - @Parameter(name = "reportId", in = ParameterIn.PATH, example = "1") - @PatchMapping("/reports/{reportId}") - public ResponseEntity updateReport( - @PathVariable Long reportId, - @RequestBody ReportForm form, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - return (reportService.updateReport(reportId, form)) - ? ResponseEntity.ok().build() - : ResponseEntity.notFound().build(); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + @Operation(summary = "그룹 특정 보고서 수정") + @Parameter(name = "reportId", in = ParameterIn.PATH, example = "1") + @PatchMapping("/reports/{reportId}") + public ResponseEntity updateReport( + @PathVariable Long reportId, @RequestBody ReportForm form, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + return (reportService.updateReport(reportId, form)) + ? ResponseEntity.ok().build() + : ResponseEntity.notFound().build(); } - - @Operation(summary = "그룹 특정 보고서 삭제") - @Parameter(name = "reportId", in = ParameterIn.PATH, example = "1") - @DeleteMapping("/reports/{reportId}") - public ResponseEntity deleteReport( - @PathVariable Long reportId, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - return (reportService.deleteReport(reportId)) - ? ResponseEntity.ok().build() - : ResponseEntity.notFound().build(); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + @Operation(summary = "그룹 특정 보고서 삭제") + @Parameter(name = "reportId", in = ParameterIn.PATH, example = "1") + @DeleteMapping("/reports/{reportId}") + public ResponseEntity deleteReport( + @PathVariable Long reportId, @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + return (reportService.deleteReport(reportId)) + ? ResponseEntity.ok().build() + : ResponseEntity.notFound().build(); } - - @Operation(summary = "그룹 선택 강의 목록 조회") - @GetMapping("/courses") - public ResponseEntity getTeamCourses( - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - return ResponseEntity.ok( - new CourseDto( - courseService.getTeamCourses(claims.getSubject()))); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + @Operation(summary = "그룹 선택 강의 목록 조회") + @GetMapping("/courses") + public ResponseEntity getTeamCourses(@RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + return ResponseEntity.ok(new CourseDto(courseService.getTeamCourses(claims.getSubject()))); } - - @Operation(summary = "그룹 팀원 목록 조회") - @GetMapping("/users") - public ResponseEntity> getTeamUsers( - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - return ResponseEntity.ok(teamService.getTeamUsers(claims.getSubject())); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + @Operation(summary = "그룹 팀원 목록 조회") + @GetMapping("/users") + public ResponseEntity> getTeamUsers( + @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + return ResponseEntity.ok(teamService.getTeamUsers(claims.getSubject())); } - - /** - * 이미지를 업로드하고, 저장한 이미지 경로를 반환하는 API - * - *

스터디 보고서를 생성하는 API를 호출하기 전에 호출되어야 한다. - * - * @param image 이미지 파일 - * @param claims 토큰 페이로드 - * @return 저장한 이미지 경로 - * @see #createReport(ReportForm, Claims) - */ - @Operation(summary = "스터디 보고서에 들어갈 인증 이미지 업로드") - @PostMapping(path = {"/reports/image", "/reports/{reportIdOr}/image"}, consumes = "multipart/form-data") - @ApiResponse( - responseCode = "200", - content = @Content( - mediaType = "application/json", - examples = @ExampleObject( - value = "{\"imagePath\": \"/path/to/image.png\"}" - ) - ) - ) - public ResponseEntity uploadImage( - @PathVariable(required = false) Optional reportIdOr, - @RequestParam MultipartFile image, - @RequestAttribute Claims claims) { - if (Role.isAuthorized(claims, Role.MEMBER)) { - StudyGroup studyGroup = userRepository.findUserByEmail(claims.getSubject()) - .orElseThrow(UserNotFoundException::new) - .getStudyGroup(); - - String filename = imageService.getImagePaths(image, studyGroup.getTag(), reportIdOr); - SingletonMap response = new SingletonMap("imagePath", baseUri + imageBasePath + filename); - return ResponseEntity.ok(response); - } - throw new ForbiddenException(); + throw new ForbiddenException(); + } + + /** + * 이미지를 업로드하고, 저장한 이미지 경로를 반환하는 API + * + *

스터디 보고서를 생성하는 API를 호출하기 전에 호출되어야 한다. + * + * @param image 이미지 파일 + * @param claims 토큰 페이로드 + * @return 저장한 이미지 경로 + * @see #createReport(ReportForm, Claims) + */ + @Operation(summary = "스터디 보고서에 들어갈 인증 이미지 업로드") + @PostMapping( + path = {"/reports/image", "/reports/{reportIdOr}/image"}, + consumes = "multipart/form-data") + @ApiResponse( + responseCode = "200", + content = + @Content( + mediaType = "application/json", + examples = @ExampleObject(value = "{\"imagePath\": \"/path/to/image.png\"}"))) + public ResponseEntity uploadImage( + @PathVariable(required = false) Optional reportIdOr, + @RequestParam MultipartFile image, + @RequestAttribute Claims claims) { + if (Role.isAuthorized(claims, Role.MEMBER)) { + StudyGroup studyGroup = + userRepository + .findUserByEmail(claims.getSubject()) + .orElseThrow(UserNotFoundException::new) + .getStudyGroup(); + + String filename = imageService.getImagePaths(image, studyGroup.getTag(), reportIdOr); + SingletonMap response = new SingletonMap("imagePath", filename); + + return ResponseEntity.ok(response); } + throw new ForbiddenException(); + } } diff --git a/src/main/java/edu/handong/csee/histudy/domain/GroupReport.java b/src/main/java/edu/handong/csee/histudy/domain/GroupReport.java index 3d909e7..d8bb902 100644 --- a/src/main/java/edu/handong/csee/histudy/domain/GroupReport.java +++ b/src/main/java/edu/handong/csee/histudy/domain/GroupReport.java @@ -95,24 +95,17 @@ private void insert(List images) { } this.images.clear(); this.images.addAll(images.stream() - .map(img -> { - String filename = extractFilenameFromPath(img); - return new Image(filename, this); - }).toList()); + .map(img -> new Image(img, this)) + .toList()); } - private String extractFilenameFromPath(String fullPath) { - int lastIndex = fullPath.lastIndexOf('/'); - return (lastIndex >= 0) ? fullPath.substring(lastIndex + 1) : fullPath; - } - - public boolean update(ReportForm form, List participants, List courses) { + public boolean update(ReportForm form, List images, List participants, List courses) { this.title = requireNonNullElse(form.getTitle(), this.title); this.content = requireNonNullElse(form.getContent(), this.content); this.totalMinutes = requireNonNullElse(form.getTotalMinutes(), this.totalMinutes); this.add(participants); - this.insert(form.getImages()); + this.insert(images); this.study(courses); studyGroup.updateTotalMinutes(); diff --git a/src/main/java/edu/handong/csee/histudy/dto/ImageDto.java b/src/main/java/edu/handong/csee/histudy/dto/ImageDto.java index 60d0154..dc526a7 100644 --- a/src/main/java/edu/handong/csee/histudy/dto/ImageDto.java +++ b/src/main/java/edu/handong/csee/histudy/dto/ImageDto.java @@ -9,21 +9,17 @@ @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor(access = AccessLevel.PROTECTED) public class ImageDto { - public ImageDto(Image entity) { - this.id = entity.getId(); - this.url = entity.getPath(); - } + public ImageDto(Image entity) { + this.id = entity.getId(); + this.url = entity.getPath(); + } - @Schema(description = "Image ID", example = "1", type = "number") - private long id; + @Schema(description = "Image ID", example = "1", type = "number") + private long id; - @Schema(description = "Image URL", example = "/path/to/image.png") - private String url; - - public void addPathToFilename(String imageBasePath) { - this.url = imageBasePath + this.url; - } + @Schema(description = "Image URL", example = "/path/to/image.png") + private String url; } diff --git a/src/main/java/edu/handong/csee/histudy/dto/ReportDto.java b/src/main/java/edu/handong/csee/histudy/dto/ReportDto.java index dc8b8f4..6eb2909 100644 --- a/src/main/java/edu/handong/csee/histudy/dto/ReportDto.java +++ b/src/main/java/edu/handong/csee/histudy/dto/ReportDto.java @@ -1,108 +1,108 @@ package edu.handong.csee.histudy.dto; import edu.handong.csee.histudy.domain.GroupReport; -import edu.handong.csee.histudy.domain.Image; import edu.handong.csee.histudy.domain.ReportUser; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.Map; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; - - @AllArgsConstructor @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) public class ReportDto { - @Schema(description = "List of reports", type = "array") - private List reports; - - @Getter - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class ReportInfo { - - public ReportInfo(GroupReport entity) { - this.id = entity.getId(); - this.title = entity.getTitle(); - this.content = entity.getContent(); - this.totalMinutes = entity.getTotalMinutes(); - this.participants = entity.getParticipants().stream() - .map(ReportUser::getUser) - .map(UserDto.UserBasic::new) - .toList(); - this.courses = entity.getCourses() - .stream() - .map(rc -> - rc.getGroupCourse().getCourse()) - .map(CourseDto.BasicCourseInfo::new) - .toList(); - this.images = entity.getImages().stream() - .map(ImageDto::new) - .toList(); - this.regDate = entity.getLastModifiedDate().toString(); - } - - @Schema(description = "Report ID", type = "number", example = "1") - private Long id; - - @Schema(description = "Report Title", example = "Week 15 Report") - private String title; - - @Schema(description = "Report Content", example = "This is a report for week 15") - private String content; - - @Schema(description = "Total minutes of the report", type = "number", example = "60") - private long totalMinutes; - - @Schema(description = "Participant SIDs of the report", type = "array") - private List participants; - - @Schema(description = "Course names of the report", type = "array", example = "[\"OOP\", \"OS\"]") - private List courses; - - @Schema(description = "Images of the report", type = "array") - private List images; - - @Schema(description = "Report Last Modified Date", example = "2021-06-01 00:00:00") - private String regDate; + @Schema(description = "List of reports", type = "array") + private List reports; + + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class ReportInfo { + + public ReportInfo(GroupReport entity, Map imageFullPaths) { + this.id = entity.getId(); + this.title = entity.getTitle(); + this.content = entity.getContent(); + this.totalMinutes = entity.getTotalMinutes(); + this.participants = + entity.getParticipants().stream() + .map(ReportUser::getUser) + .map(UserDto.UserBasic::new) + .toList(); + this.courses = + entity.getCourses().stream() + .map(rc -> rc.getGroupCourse().getCourse()) + .map(CourseDto.BasicCourseInfo::new) + .toList(); + this.images = + entity.getImages().stream() + .map(img -> new ImageDto(img.getId(), imageFullPaths.get(img.getId()))) + .toList(); + this.regDate = entity.getLastModifiedDate().toString(); } - @AllArgsConstructor(access = AccessLevel.PRIVATE) - @Getter - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class ReportBasic { - public ReportBasic(GroupReport groupReport) { - this.id = groupReport.getId(); - this.title = groupReport.getTitle(); - this.regDate = groupReport.getLastModifiedDate().toString(); - this.totalMinutes = groupReport.getTotalMinutes(); - this.thumbnail = groupReport.getImages() - .stream() - .findFirst() - .map(Image::getPath) - .orElse(null); - } - - @Schema(description = "Report ID", type = "number", example = "1") - private long id; - - @Schema(description = "Report Title", example = "Week 15 Report") - private String title; - - @Schema(description = "Report Last Modified Date", example = "2021-06-01 00:00:00") - private String regDate; - - @Schema(description = "Total minutes of the report", type = "number", example = "60") - private long totalMinutes; - - @Schema(description = "Thumbnail of the report", example = "https://histudy.s3.ap-northeast-2.amazonaws.com/2021-06-01-00-00-00-1") - private String thumbnail; - - public void addPathToFilename(String imageBasePath) { - this.thumbnail = imageBasePath + this.thumbnail; - } + @Schema(description = "Report ID", type = "number", example = "1") + private Long id; + + @Schema(description = "Report Title", example = "Week 15 Report") + private String title; + + @Schema(description = "Report Content", example = "This is a report for week 15") + private String content; + + @Schema(description = "Total minutes of the report", type = "number", example = "60") + private long totalMinutes; + + @Schema(description = "Participant SIDs of the report", type = "array") + private List participants; + + @Schema( + description = "Course names of the report", + type = "array", + example = "[\"OOP\", \"OS\"]") + private List courses; + + @Schema(description = "Images of the report", type = "array") + private List images; + + @Schema(description = "Report Last Modified Date", example = "2021-06-01 00:00:00") + private String regDate; + } + + @AllArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class ReportBasic { + public ReportBasic(GroupReport groupReport, Map imageMap) { + this.id = groupReport.getId(); + this.title = groupReport.getTitle(); + this.regDate = groupReport.getLastModifiedDate().toString(); + this.totalMinutes = groupReport.getTotalMinutes(); + this.thumbnail = + groupReport.getImages().stream() + .findFirst() + .map(image -> imageMap.get(image.getId())) + .orElse(null); } + + @Schema(description = "Report ID", type = "number", example = "1") + private long id; + + @Schema(description = "Report Title", example = "Week 15 Report") + private String title; + + @Schema(description = "Report Last Modified Date", example = "2021-06-01 00:00:00") + private String regDate; + + @Schema(description = "Total minutes of the report", type = "number", example = "60") + private long totalMinutes; + + @Schema( + description = "Thumbnail of the report", + example = "https://histudy.s3.ap-northeast-2.amazonaws.com/2021-06-01-00-00-00-1") + private String thumbnail; + } } diff --git a/src/main/java/edu/handong/csee/histudy/dto/TeamRankDto.java b/src/main/java/edu/handong/csee/histudy/dto/TeamRankDto.java index a59fe6c..a3bc718 100644 --- a/src/main/java/edu/handong/csee/histudy/dto/TeamRankDto.java +++ b/src/main/java/edu/handong/csee/histudy/dto/TeamRankDto.java @@ -1,64 +1,51 @@ package edu.handong.csee.histudy.dto; -import edu.handong.csee.histudy.domain.Image; import edu.handong.csee.histudy.domain.StudyGroup; import edu.handong.csee.histudy.domain.User; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; - @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) public class TeamRankDto { - @Schema(description = "List of teams", type = "array") - private List teams; - - public TeamRankDto(List teamInfos) { - this.teams = teamInfos; - } + @Schema(description = "List of teams", type = "array") + private List teams; - @Getter - @NoArgsConstructor(access = AccessLevel.PRIVATE) - public static class TeamInfo { - @Schema(description = "Team Tag", example = "1", type = "number") - private int id; + public TeamRankDto(List teamInfos) { + this.teams = teamInfos; + } - @Schema(description = "Team members", type = "array", example = "[\"John Doe\", \"Jane Doe\"]") - private List members; + @Getter + @NoArgsConstructor(access = AccessLevel.PRIVATE) + public static class TeamInfo { + @Schema(description = "Team Tag", example = "1", type = "number") + private int id; - @Schema(description = "Number of reports created", type = "number", example = "5") - private int reports; + @Schema(description = "Team members", type = "array", example = "[\"John Doe\", \"Jane Doe\"]") + private List members; - @Schema(description = "Total time studied", type = "number", example = "120") - private long totalMinutes; + @Schema(description = "Number of reports created", type = "number", example = "5") + private int reports; - @Schema(description = "Team thumbnail(from the latest report)", type = "string", example = "https://i.imgur.com/3QXm2oF.png") - private String thumbnail; + @Schema(description = "Total time studied", type = "number", example = "120") + private long totalMinutes; - public TeamInfo(StudyGroup studyGroup) { - this.id = studyGroup.getTag(); - this.members = studyGroup.getMembers() - .stream() - .map(User::getName) - .toList(); - this.reports = studyGroup.getReports().size(); - this.totalMinutes = studyGroup.getTotalMinutes(); - this.thumbnail = studyGroup.getReports() - .stream() - .reduce((first, second) -> second) - .flatMap((report -> report.getImages() - .stream() - .findFirst() - .map(Image::getPath))) - .orElse(null); - } + @Schema( + description = "Team thumbnail(from the latest report)", + type = "string", + example = "https://i.imgur.com/3QXm2oF.png") + private String thumbnail; - public void addPathToFilename(String imageBasePath) { - this.thumbnail = imageBasePath + this.thumbnail; - } + public TeamInfo(StudyGroup studyGroup, String imgPath) { + this.id = studyGroup.getTag(); + this.members = studyGroup.getMembers().stream().map(User::getName).toList(); + this.reports = studyGroup.getReports().size(); + this.totalMinutes = studyGroup.getTotalMinutes(); + this.thumbnail = imgPath; } + } } diff --git a/src/main/java/edu/handong/csee/histudy/service/ImageService.java b/src/main/java/edu/handong/csee/histudy/service/ImageService.java index b704f3d..c71d7ec 100644 --- a/src/main/java/edu/handong/csee/histudy/service/ImageService.java +++ b/src/main/java/edu/handong/csee/histudy/service/ImageService.java @@ -1,18 +1,13 @@ package edu.handong.csee.histudy.service; +import static org.springframework.util.ResourceUtils.isUrl; + import edu.handong.csee.histudy.domain.Image; import edu.handong.csee.histudy.exception.FileTransferException; import edu.handong.csee.histudy.exception.ReportNotFoundException; import edu.handong.csee.histudy.repository.GroupReportRepository; +import edu.handong.csee.histudy.util.ImagePathMapper; import edu.handong.csee.histudy.util.Utils; -import org.apache.commons.io.IOUtils; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; -import org.springframework.core.io.UrlResource; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -20,123 +15,114 @@ import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; - -import static org.springframework.util.ResourceUtils.isUrl; +import lombok.RequiredArgsConstructor; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Service @Transactional +@RequiredArgsConstructor public class ImageService { - @Value("${custom.resource.location}") - private String imageBaseLocation; - private final GroupReportRepository groupReportRepository; + @Value("${custom.resource.location}") + private String imageBaseLocation; - public ImageService(GroupReportRepository groupReportRepository) { - this.groupReportRepository = groupReportRepository; - } + private final GroupReportRepository groupReportRepository; - public String getImagePaths(MultipartFile imageAsFormData, Integer tag, Optional reportIdOr) { - if (reportIdOr.isPresent()) { - Long id = reportIdOr.get(); - Optional sameResource = getSameContent(imageAsFormData, id); - - if (sameResource.isPresent()) { - return sameResource.get(); - } - } - int year = Utils.getCurrentYear(); - int semester = Utils.getCurrentSemester(); - String formattedDateTime = Utils.getCurrentFormattedDateTime("yyyyMMdd_HHmmss"); - - String originalName = Objects.requireNonNullElse(imageAsFormData.getOriginalFilename(), ".jpg"); - String extension = originalName.substring(originalName.lastIndexOf(".")); - - // yyyy-{1|2}-group{%02d}-report_{yyyyMMdd}_{HHmmss}.{extension} - // e.g. 2023-2-group1-report_20230923_123456.jpg - String pathname = String.format("%d-%d-group%02d-report_%s%s", - year, - semester, - tag, - formattedDateTime, - extension); - return saveImage(imageAsFormData, pathname); - } + private final ImagePathMapper imagePathMapper; - private String saveImage( - MultipartFile image, - String pathname) { - try { - File file = new File(imageBaseLocation + pathname); - File dir = file.getParentFile(); - - if (!dir.exists()) { - dir.mkdirs(); - } - image.transferTo(file); - return pathname; - } catch (IOException e) { - throw new FileTransferException(); - } - } + public String getImagePaths( + MultipartFile imageAsFormData, Integer tag, Optional reportIdOr) { + if (reportIdOr.isPresent()) { + Long id = reportIdOr.get(); + Optional sameResource = getSameContent(imageAsFormData, id); - private Optional getSameContent(MultipartFile src, Long reportId) { - List targetPaths = groupReportRepository.findById(reportId) - .orElseThrow(ReportNotFoundException::new) - .getImages() - .stream() - .map(Image::getPath) - .toList(); - - return targetPaths.stream() - .filter(path -> { - try { - return (isUrl(path)) - ? contentMatches(src, new URL(path)) - : contentMatches(src, Path.of(imageBaseLocation + path)); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - }).findAny(); + if (sameResource.isPresent()) { + return imagePathMapper.getFullPath(sameResource.get()); + } } - - private boolean contentMatches(MultipartFile src, Path targetPath) { - try { - byte[] targetContent = Files.readAllBytes(targetPath); - return contentMatches(src.getBytes(), targetContent); - } catch (IOException e) { - throw new RuntimeException(e); - } + int year = Utils.getCurrentYear(); + int semester = Utils.getCurrentSemester(); + String formattedDateTime = Utils.getCurrentFormattedDateTime("yyyyMMdd_HHmmss"); + + String originalName = Objects.requireNonNullElse(imageAsFormData.getOriginalFilename(), ".jpg"); + String extension = originalName.substring(originalName.lastIndexOf(".")); + + // yyyy-{1|2}-group{%02d}-report_{yyyyMMdd}_{HHmmss}.{extension} + // e.g. 2023-2-group1-report_20230923_123456.jpg + String pathname = + String.format( + "%d-%d-group%02d-report_%s%s", year, semester, tag, formattedDateTime, extension); + String savedImagePath = saveImage(imageAsFormData, pathname); + + return imagePathMapper.getFullPath(savedImagePath); + } + + private String saveImage(MultipartFile image, String pathname) { + try { + File file = new File(imageBaseLocation + pathname); + File dir = file.getParentFile(); + + if (!dir.exists()) { + dir.mkdirs(); + } + image.transferTo(file); + return pathname; + } catch (IOException e) { + throw new FileTransferException(); } - - private boolean contentMatches(MultipartFile src, URL targetPath) { - try (InputStream in = targetPath.openStream()) { - byte[] targetContent = IOUtils.toByteArray(in); - return contentMatches(src.getBytes(), targetContent); - } catch (IOException e) { - throw new RuntimeException(e); - } + } + + private Optional getSameContent(MultipartFile src, Long reportId) { + List targetPaths = + groupReportRepository + .findById(reportId) + .orElseThrow(ReportNotFoundException::new) + .getImages() + .stream() + .map(Image::getPath) + .toList(); + + return targetPaths.stream() + .filter( + path -> { + try { + return (isUrl(path)) + ? contentMatches(src, new URL(path)) + : contentMatches(src, Path.of(imageBaseLocation + path)); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }) + .findAny(); + } + + private boolean contentMatches(MultipartFile src, Path targetPath) { + try { + byte[] targetContent = Files.readAllBytes(targetPath); + return contentMatches(src.getBytes(), targetContent); + } catch (IOException e) { + throw new RuntimeException(e); } - - private boolean contentMatches(byte[] sourceContent, byte[] targetContent) { - return Arrays.equals(sourceContent, targetContent); + } + + private boolean contentMatches(MultipartFile src, URL targetPath) { + try (InputStream in = targetPath.openStream()) { + byte[] targetContent = IOUtils.toByteArray(in); + return contentMatches(src.getBytes(), targetContent); + } catch (IOException e) { + throw new RuntimeException(e); } + } - public Resource fetchImage(String imageName) { - try { - Path path = Paths.get(imageBaseLocation + imageName); - Resource resource = new UrlResource(path.toUri()); - - if (resource.exists() && resource.isReadable()) { - return resource; - } - throw new RuntimeException(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } - } + private boolean contentMatches(byte[] sourceContent, byte[] targetContent) { + return Arrays.equals(sourceContent, targetContent); + } } diff --git a/src/main/java/edu/handong/csee/histudy/service/ReportService.java b/src/main/java/edu/handong/csee/histudy/service/ReportService.java index 85d505f..c88f95c 100644 --- a/src/main/java/edu/handong/csee/histudy/service/ReportService.java +++ b/src/main/java/edu/handong/csee/histudy/service/ReportService.java @@ -9,117 +9,131 @@ import edu.handong.csee.histudy.repository.GroupCourseRepository; import edu.handong.csee.histudy.repository.GroupReportRepository; import edu.handong.csee.histudy.repository.UserRepository; +import edu.handong.csee.histudy.util.ImagePathMapper; +import java.util.List; +import java.util.Map; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; - @Service @RequiredArgsConstructor @Transactional public class ReportService { - private final GroupReportRepository groupReportRepository; - private final UserRepository userRepository; - private final CourseRepository courseRepository; - private final GroupCourseRepository groupCourseRepository; - - public ReportDto.ReportInfo createReport(ReportForm form, String email) { - User user = userRepository.findUserByEmail(email) - .orElseThrow(UserNotFoundException::new); - - List participants = form.getParticipants() - .stream() - .map(userRepository::findById) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - - List courses = form.getCourses() - .stream() - .map(courseRepository::findById) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - - // filter groupCourses by form.getCourses() - List groupCourses = groupCourseRepository - .findAllByStudyGroup(user.getStudyGroup()); - groupCourses.removeIf(gc -> !courses.contains(gc.getCourse())); - - GroupReport report = GroupReport.builder() - .title(form.getTitle()) - .content(form.getContent()) - .totalMinutes(form.getTotalMinutes()) - .studyGroup(user.getStudyGroup()) - .participants(participants) - .images(form.getImages()) - .courses(groupCourses) - .build(); - - GroupReport saved = groupReportRepository.save(report); - return new ReportDto.ReportInfo(saved); - } - - public List getReports(String email) { - StudyGroup studyGroup = userRepository.findUserByEmail(email) - .orElseThrow() - .getStudyGroup(); - - return studyGroup.getReports() - .stream() - .map(ReportDto.ReportInfo::new) - .toList(); - } - - public List getAllReports() { - return groupReportRepository.findAll() - .stream() - .map(ReportDto.ReportInfo::new) - .toList(); - } - - public boolean updateReport(Long reportId, ReportForm form) { - List participants = form.getParticipants() - .stream() - .map(userRepository::findById) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - - List courses = form.getCourses() - .stream() - .map(courseRepository::findById) - .filter(Optional::isPresent) - .map(Optional::get) - .toList(); - - GroupReport targetReport = groupReportRepository.findById(reportId) - .orElseThrow(ReportNotFoundException::new); - - // filter groupCourses by form.getCourses() - List groupCourses = groupCourseRepository - .findAllByStudyGroup(targetReport.getStudyGroup()); - groupCourses.removeIf(gc -> !courses.contains(gc.getCourse())); - targetReport.update(form, participants, groupCourses); - - return true; - } - - public Optional getReport(Long reportId) { - return groupReportRepository.findById(reportId) - .map(ReportDto.ReportInfo::new); - } - - public boolean deleteReport(Long reportId) { - Optional reportOr = groupReportRepository.findById(reportId); - - if (reportOr.isEmpty()) { - return false; - } else { - groupReportRepository.delete(reportOr.get()); - return true; - } + private final GroupReportRepository groupReportRepository; + private final UserRepository userRepository; + private final CourseRepository courseRepository; + private final GroupCourseRepository groupCourseRepository; + + private final ImagePathMapper imagePathMapper; + + public ReportDto.ReportInfo createReport(ReportForm form, String email) { + User user = userRepository.findUserByEmail(email).orElseThrow(UserNotFoundException::new); + + List participants = + form.getParticipants().stream() + .map(userRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + List courses = + form.getCourses().stream() + .map(courseRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + // filter groupCourses by form.getCourses() + List groupCourses = + groupCourseRepository.findAllByStudyGroup(user.getStudyGroup()); + groupCourses.removeIf(gc -> !courses.contains(gc.getCourse())); + + // parse image path to filename + // /path/to/image.png -> image.png + List imageFilenames = imagePathMapper.extractFilename(form.getImages()); + + GroupReport report = + GroupReport.builder() + .title(form.getTitle()) + .content(form.getContent()) + .totalMinutes(form.getTotalMinutes()) + .studyGroup(user.getStudyGroup()) + .participants(participants) + .images(imageFilenames) + .courses(groupCourses) + .build(); + + GroupReport saved = groupReportRepository.save(report); + Map imgFullPaths = imagePathMapper.parseImageToMapWithFullPath(saved.getImages()); + + return new ReportDto.ReportInfo(saved, imgFullPaths); + } + + public List getReports(String email) { + StudyGroup studyGroup = userRepository.findUserByEmail(email).orElseThrow().getStudyGroup(); + + return studyGroup.getReports().stream() + .map( + report -> { + Map imgFullPaths = + imagePathMapper.parseImageToMapWithFullPath(report.getImages()); + return new ReportDto.ReportInfo(report, imgFullPaths); + }) + .toList(); + } + + public boolean updateReport(Long reportId, ReportForm form) { + List participants = + form.getParticipants().stream() + .map(userRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + List courses = + form.getCourses().stream() + .map(courseRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + + GroupReport targetReport = + groupReportRepository.findById(reportId).orElseThrow(ReportNotFoundException::new); + + // filter groupCourses by form.getCourses() + List groupCourses = + groupCourseRepository.findAllByStudyGroup(targetReport.getStudyGroup()); + groupCourses.removeIf(gc -> !courses.contains(gc.getCourse())); + + // parse image path to filename + // /path/to/image.png -> image.png + List imageFilenames = imagePathMapper.extractFilename(form.getImages()); + targetReport.update(form, imageFilenames, participants, groupCourses); + + return true; + } + + public Optional getReport(Long reportId) { + return groupReportRepository + .findById(reportId) + .map( + report -> { + Map imgFullPaths = + imagePathMapper.parseImageToMapWithFullPath(report.getImages()); + return new ReportDto.ReportInfo(report, imgFullPaths); + }); + } + + public boolean deleteReport(Long reportId) { + Optional reportOr = groupReportRepository.findById(reportId); + + if (reportOr.isEmpty()) { + return false; + } else { + groupReportRepository.delete(reportOr.get()); + return true; } + } } diff --git a/src/main/java/edu/handong/csee/histudy/service/TeamService.java b/src/main/java/edu/handong/csee/histudy/service/TeamService.java index dd7f7c9..a20b22d 100644 --- a/src/main/java/edu/handong/csee/histudy/service/TeamService.java +++ b/src/main/java/edu/handong/csee/histudy/service/TeamService.java @@ -5,232 +5,243 @@ import edu.handong.csee.histudy.exception.UserNotFoundException; import edu.handong.csee.histudy.repository.StudyGroupRepository; import edu.handong.csee.histudy.repository.UserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Sort; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import edu.handong.csee.histudy.util.ImagePathMapper; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @Transactional public class TeamService { - private final StudyGroupRepository studyGroupRepository; - private final UserRepository userRepository; - private final UserService userService; - - public List getTeams(String email) { - return studyGroupRepository.findAll( - Sort.by(Sort.DEFAULT_DIRECTION, "tag")) - .stream() - .map(TeamDto::new) - .toList(); - } - - public int deleteTeam(TeamIdDto dto, String email) { - if (studyGroupRepository.existsById(dto.getGroupId())) { - studyGroupRepository.deleteById(dto.getGroupId()); - return 1; - } - return 0; - } - - public TeamReportDto getTeamReports(long id, String email) { - StudyGroup studyGroup = studyGroupRepository.findById(id).orElseThrow(); - List users = studyGroup.getMembers().stream() - .map(u -> UserDto.UserBasic.builder() - .id(u.getId()) - .sid(u.getSid()) - .name(u.getName()) - .build()).toList(); - List reports = studyGroup.getReports() - .stream() - .map(ReportDto.ReportBasic::new).toList(); - return new TeamReportDto(studyGroup.getId(), - studyGroup.getTag(), - users, studyGroup.getTotalMinutes(), reports); - } - - public List getTeamUsers(String email) { - User user = userRepository.findUserByEmail(email) - .orElseThrow(UserNotFoundException::new); - - return user.getStudyGroup() - .getMembers() - .stream() - .map(UserDto.UserMeWithMasking::new) - .toList(); - } - - public TeamRankDto getAllTeams() { - List teams = studyGroupRepository - .findAll(Sort.by(Sort.Direction.DESC, "totalMinutes")) - .stream() - .map(TeamRankDto.TeamInfo::new) - .toList(); - return new TeamRankDto(teams); - } - - public TeamDto.MatchResults matchTeam() { - // Get users who are not in a team - List users = userRepository.findUnassignedApplicants(); - int latestGroupTag = studyGroupRepository.countMaxTag().orElse(0); - AtomicInteger tag = new AtomicInteger(latestGroupTag + 1); - - // First matching - List teamsWithFriends = matchFriendFirst(users, tag); - - // Remove users who have already been matched - users.removeAll(teamsWithFriends.stream() - .map(StudyGroup::getMembers) - .flatMap(Collection::stream) - .toList()); - - // Second matching - List teamsWithoutFriends = matchCourseFirst(users, tag); - - // Remove users who have already been matched - users.removeAll(teamsWithoutFriends.stream() - .map(StudyGroup::getMembers) - .flatMap(Collection::stream) - .toList()); - - // Third matching - List matchedCourseSecond = matchCourseSecond(users, tag); - - // Results - List matchedStudyGroups = new ArrayList<>(teamsWithFriends); - matchedStudyGroups.addAll(teamsWithoutFriends); - matchedStudyGroups.addAll(matchedCourseSecond); - - // Remove users who have already been matched - users.removeAll(matchedCourseSecond.stream() - .map(StudyGroup::getMembers) - .flatMap(Collection::stream) - .toList()); - - return new TeamDto.MatchResults(matchedStudyGroups, userService.getInfoFromUser(users)); - } - - public List matchFriendFirst(List users, AtomicInteger tag) { - // First matching - // Make teams with friends - return users.stream() - .map(User::getSentRequests) - .flatMap(Collection::stream) - .filter(Friendship::isAccepted) - .map(f -> f.makeTeam(tag)) - .distinct() - .toList(); + private final StudyGroupRepository studyGroupRepository; + private final UserRepository userRepository; + private final UserService userService; + private final ImagePathMapper imagePathMapper; + + public List getTeams(String email) { + return studyGroupRepository.findAll(Sort.by(Sort.DEFAULT_DIRECTION, "tag")).stream() + .map(TeamDto::new) + .toList(); + } + + public int deleteTeam(TeamIdDto dto, String email) { + if (studyGroupRepository.existsById(dto.getGroupId())) { + studyGroupRepository.deleteById(dto.getGroupId()); + return 1; } - - public List matchCourseFirst(List users, AtomicInteger tag) { - List results = new ArrayList<>(); - Set targetUsers = new HashSet<>(users); - - List userCourses = targetUsers.stream() - .flatMap(u -> - u.getCourseSelections().stream()) - .sorted(Comparator.comparingInt(UserCourse::getPriority)) - .toList(); - - List sortedKeys = userCourses.stream() - .collect(Collectors.groupingBy(UserCourse::getPriority)) - .keySet().stream() - .sorted() - .toList(); - - sortedKeys.forEach(priority -> { - Map> courseToUserMap = userCourses.stream() - .filter(uc -> - uc.getPriority().equals(priority) - && uc.getUser().isNotInAnyGroup()) - .collect(Collectors.groupingBy( - UserCourse::getCourse, - Collectors.mapping(UserCourse::getUser, Collectors.toList()))); - - courseToUserMap.forEach((course, _users) -> { + return 0; + } + + public TeamReportDto getTeamReports(long id, String email) { + StudyGroup studyGroup = studyGroupRepository.findById(id).orElseThrow(); + List users = + studyGroup.getMembers().stream().map(UserDto.UserBasic::new).toList(); + List reports = + studyGroup.getReports().stream() + .map( + report -> { + Map imgFullPaths = + imagePathMapper.parseImageToMapWithFullPath(report.getImages()); + return new ReportDto.ReportBasic(report, imgFullPaths); + }) + .toList(); + + return new TeamReportDto( + studyGroup.getId(), studyGroup.getTag(), users, studyGroup.getTotalMinutes(), reports); + } + + public List getTeamUsers(String email) { + User user = userRepository.findUserByEmail(email).orElseThrow(UserNotFoundException::new); + + return user.getStudyGroup().getMembers().stream().map(UserDto.UserMeWithMasking::new).toList(); + } + + public TeamRankDto getAllTeams() { + List teams = + studyGroupRepository.findAll(Sort.by(Sort.Direction.DESC, "totalMinutes")).stream() + .map( + group -> { + String path = + group.getReports().stream() + .reduce((first, second) -> second) + .flatMap( + (report -> + report.getImages().stream().findFirst().map(Image::getPath))) + .orElse(null); + String fullPath = imagePathMapper.getFullPath(path); + return new TeamRankDto.TeamInfo(group, fullPath); + }) + .toList(); + return new TeamRankDto(teams); + } + + public TeamDto.MatchResults matchTeam() { + // Get users who are not in a team + List users = userRepository.findUnassignedApplicants(); + int latestGroupTag = studyGroupRepository.countMaxTag().orElse(0); + AtomicInteger tag = new AtomicInteger(latestGroupTag + 1); + + // First matching + List teamsWithFriends = matchFriendFirst(users, tag); + + // Remove users who have already been matched + users.removeAll( + teamsWithFriends.stream().map(StudyGroup::getMembers).flatMap(Collection::stream).toList()); + + // Second matching + List teamsWithoutFriends = matchCourseFirst(users, tag); + + // Remove users who have already been matched + users.removeAll( + teamsWithoutFriends.stream() + .map(StudyGroup::getMembers) + .flatMap(Collection::stream) + .toList()); + + // Third matching + List matchedCourseSecond = matchCourseSecond(users, tag); + + // Results + List matchedStudyGroups = new ArrayList<>(teamsWithFriends); + matchedStudyGroups.addAll(teamsWithoutFriends); + matchedStudyGroups.addAll(matchedCourseSecond); + + // Remove users who have already been matched + users.removeAll( + matchedCourseSecond.stream() + .map(StudyGroup::getMembers) + .flatMap(Collection::stream) + .toList()); + + return new TeamDto.MatchResults(matchedStudyGroups, userService.getInfoFromUser(users)); + } + + public List matchFriendFirst(List users, AtomicInteger tag) { + // First matching + // Make teams with friends + return users.stream() + .map(User::getSentRequests) + .flatMap(Collection::stream) + .filter(Friendship::isAccepted) + .map(f -> f.makeTeam(tag)) + .distinct() + .toList(); + } + + public List matchCourseFirst(List users, AtomicInteger tag) { + List results = new ArrayList<>(); + Set targetUsers = new HashSet<>(users); + + List userCourses = + targetUsers.stream() + .flatMap(u -> u.getCourseSelections().stream()) + .sorted(Comparator.comparingInt(UserCourse::getPriority)) + .toList(); + + List sortedKeys = + userCourses.stream() + .collect(Collectors.groupingBy(UserCourse::getPriority)) + .keySet() + .stream() + .sorted() + .toList(); + + sortedKeys.forEach( + priority -> { + Map> courseToUserMap = + userCourses.stream() + .filter(uc -> uc.getPriority().equals(priority) && uc.getUser().isNotInAnyGroup()) + .collect( + Collectors.groupingBy( + UserCourse::getCourse, + Collectors.mapping(UserCourse::getUser, Collectors.toList()))); + + courseToUserMap.forEach( + (course, _users) -> { List matchedGroupList = createGroup(_users, tag); results.addAll(matchedGroupList); - }); + }); }); - return results; - } - - private List createGroup(List group, AtomicInteger tag) { - List matchedGroupList = new ArrayList<>(); - - while (group.size() >= 5) { - // If the group has more than 5 elements, split the group - // Split the group into 5 elements - // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] -> [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11] - List subGroup = new ArrayList<>(group.subList(0, 5)); - - // Create a team with only 5 elements - StudyGroup studyGroup = new StudyGroup(tag.getAndIncrement(), subGroup); - matchedGroupList.add(studyGroup); - - // Remove the elements that have already been added to the team - group.removeAll(subGroup); - } - if (group.size() >= 3) { - // If the remaining elements are 3 ~ 4 - // Create a team with 3 ~ 4 elements - StudyGroup studyGroup = new StudyGroup(tag.getAndIncrement(), group); - matchedGroupList.add(studyGroup); - } - return matchedGroupList; - } + return results; + } - private List matchCourseSecond(List users, AtomicInteger tag) { - List results = new ArrayList<>(); - Set targetUsers = new HashSet<>(users); + private List createGroup(List group, AtomicInteger tag) { + List matchedGroupList = new ArrayList<>(); - Map> courseToUserByPriority = preparePriorityQueueOfUsers(targetUsers); + while (group.size() >= 5) { + // If the group has more than 5 elements, split the group + // Split the group into 5 elements + // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] -> [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11] + List subGroup = new ArrayList<>(group.subList(0, 5)); - // Make teams with 3 ~ 5 elements - courseToUserByPriority.forEach((course, queue) -> { - List group = queue.stream() - .filter(User::isNotInAnyGroup) - .sorted(queue.comparator()) - .collect(Collectors.toList()); + // Create a team with only 5 elements + StudyGroup studyGroup = new StudyGroup(tag.getAndIncrement(), subGroup); + matchedGroupList.add(studyGroup); - List matchedGroupList = createGroup(group, tag); - results.addAll(matchedGroupList); - }); - return results; + // Remove the elements that have already been added to the team + group.removeAll(subGroup); } - - private Map> preparePriorityQueueOfUsers(Set targetUsers) { - // Group users by course - Map> courseToUserCourses = targetUsers.stream() - .flatMap(u -> u.getCourseSelections().stream()) - .collect(Collectors.groupingBy( - UserCourse::getCourse, - Collectors.mapping( - Function.identity(), - Collectors.toList()))); - - Map> courseToUsersByPriority = new HashMap<>(); - courseToUserCourses.forEach((_course, _userCourses) -> { - _userCourses.sort(Comparator.comparingInt(UserCourse::getPriority)); - - List sortedUsers = _userCourses.stream() - .map(UserCourse::getUser) - .toList(); - - PriorityQueue userPriorityQueue = new PriorityQueue<>( - sortedUsers.size(), - Comparator.comparingInt(sortedUsers::indexOf)); - - userPriorityQueue.addAll(sortedUsers); - courseToUsersByPriority.put(_course, userPriorityQueue); - } - ); - return courseToUsersByPriority; + if (group.size() >= 3) { + // If the remaining elements are 3 ~ 4 + // Create a team with 3 ~ 4 elements + StudyGroup studyGroup = new StudyGroup(tag.getAndIncrement(), group); + matchedGroupList.add(studyGroup); } + return matchedGroupList; + } + + private List matchCourseSecond(List users, AtomicInteger tag) { + List results = new ArrayList<>(); + Set targetUsers = new HashSet<>(users); + + Map> courseToUserByPriority = + preparePriorityQueueOfUsers(targetUsers); + + // Make teams with 3 ~ 5 elements + courseToUserByPriority.forEach( + (course, queue) -> { + List group = + queue.stream() + .filter(User::isNotInAnyGroup) + .sorted(queue.comparator()) + .collect(Collectors.toList()); + + List matchedGroupList = createGroup(group, tag); + results.addAll(matchedGroupList); + }); + return results; + } + + private Map> preparePriorityQueueOfUsers(Set targetUsers) { + // Group users by course + Map> courseToUserCourses = + targetUsers.stream() + .flatMap(u -> u.getCourseSelections().stream()) + .collect( + Collectors.groupingBy( + UserCourse::getCourse, + Collectors.mapping(Function.identity(), Collectors.toList()))); + + Map> courseToUsersByPriority = new HashMap<>(); + courseToUserCourses.forEach( + (_course, _userCourses) -> { + _userCourses.sort(Comparator.comparingInt(UserCourse::getPriority)); + + List sortedUsers = _userCourses.stream().map(UserCourse::getUser).toList(); + + PriorityQueue userPriorityQueue = + new PriorityQueue<>( + sortedUsers.size(), Comparator.comparingInt(sortedUsers::indexOf)); + + userPriorityQueue.addAll(sortedUsers); + courseToUsersByPriority.put(_course, userPriorityQueue); + }); + return courseToUsersByPriority; + } } diff --git a/src/test/java/edu/handong/csee/histudy/group/ReportGroupCourseServiceTest.java b/src/test/java/edu/handong/csee/histudy/group/ReportGroupCourseServiceTest.java index 4839276..bd4a8d9 100644 --- a/src/test/java/edu/handong/csee/histudy/group/ReportGroupCourseServiceTest.java +++ b/src/test/java/edu/handong/csee/histudy/group/ReportGroupCourseServiceTest.java @@ -1,5 +1,9 @@ package edu.handong.csee.histudy.group; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + import edu.handong.csee.histudy.controller.form.ReportForm; import edu.handong.csee.histudy.domain.*; import edu.handong.csee.histudy.dto.TeamDto; @@ -10,6 +14,9 @@ import edu.handong.csee.histudy.repository.*; import edu.handong.csee.histudy.service.ReportService; import edu.handong.csee.histudy.service.TeamService; +import java.io.IOException; +import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -18,14 +25,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.transaction.annotation.Transactional; -import java.io.IOException; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - @SpringBootTest @Transactional public class ReportGroupCourseServiceTest { @@ -244,8 +243,8 @@ void TeamServiceTest_193() { assertThat(res.getTeams().get(0).getTotalMinutes()).isEqualTo(210); assertThat(res.getTeams().get(1).getTotalMinutes()).isEqualTo(180); - assertThat(res.getTeams().get(0).getThumbnail()).isEqualTo("img3.jpg"); // team 2 - assertThat(res.getTeams().get(1).getThumbnail()).isEqualTo("img2.jpg"); // team 1 + assertThat(res.getTeams().get(0).getThumbnail()).contains("img3.jpg"); // team 2 + assertThat(res.getTeams().get(1).getThumbnail()).contains("img2.jpg"); // team 1 } }