diff --git a/src/main/java/org/cotato/csquiz/api/member/controller/MemberController.java b/src/main/java/org/cotato/csquiz/api/member/controller/MemberController.java index 8b1b9873..3e9e846f 100644 --- a/src/main/java/org/cotato/csquiz/api/member/controller/MemberController.java +++ b/src/main/java/org/cotato/csquiz/api/member/controller/MemberController.java @@ -8,11 +8,15 @@ import org.cotato.csquiz.api.member.dto.MemberMyPageInfoResponse; import org.cotato.csquiz.api.member.dto.UpdatePasswordRequest; import org.cotato.csquiz.api.member.dto.UpdatePhoneNumberRequest; +import org.cotato.csquiz.api.member.dto.UpdateProfileImageRequest; import org.cotato.csquiz.common.config.jwt.JwtTokenProvider; +import org.cotato.csquiz.common.error.exception.ImageException; import org.cotato.csquiz.domain.auth.service.MemberService; import org.springframework.context.annotation.Description; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; @@ -41,17 +45,36 @@ public ResponseEntity findMemberInfo( public ResponseEntity updatePassword(@RequestHeader("Authorization") String authorizationHeader, @RequestBody @Valid UpdatePasswordRequest request) { String accessToken = jwtTokenProvider.getBearer(authorizationHeader); - memberService.updatePassword(accessToken, request.password()); + Long memberId = jwtTokenProvider.getMemberId(accessToken); + memberService.updatePassword(memberId, request.password()); return ResponseEntity.noContent().build(); } - @Operation(summary = "멤버 전화번호 수정", description = "멤버 전화번호 수정하기") + @Operation(summary = "멤버 전화번호 수정 API") @PatchMapping("/phone-number") - public ResponseEntity updatePhoneNumber( - @RequestHeader("Authorization") String authorizationHeader, + public ResponseEntity updatePhoneNumber(@RequestHeader("Authorization") String authorizationHeader, @RequestBody @Valid UpdatePhoneNumberRequest request) { String accessToken = jwtTokenProvider.getBearer(authorizationHeader); - memberService.updatePhoneNumber(accessToken,request.phoneNumber()); + memberService.updatePhoneNumber(accessToken, request.phoneNumber()); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "멤버 프로필 사진 수정 API") + @PatchMapping(value = "/profile-image", consumes = "multipart/form-data") + public ResponseEntity updateProfileImage( + @RequestHeader("Authorization") String authorizationHeader, + @ModelAttribute @Valid UpdateProfileImageRequest request) throws ImageException { + String accessToken = jwtTokenProvider.getBearer(authorizationHeader); + memberService.updateMemberProfileImage(accessToken, request.image()); + return ResponseEntity.noContent().build(); + } + + @Operation(summary = "멤버 프로필 사진 삭제 API") + @DeleteMapping("/profile-image") + public ResponseEntity deleteProfileImage( + @RequestHeader("Authorization") String authorizationHeader) { + String accessToken = jwtTokenProvider.getBearer(authorizationHeader); + memberService.deleteMemberProfileImage(accessToken); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/cotato/csquiz/api/member/dto/MemberMyPageInfoResponse.java b/src/main/java/org/cotato/csquiz/api/member/dto/MemberMyPageInfoResponse.java index 84ebcba7..56edc6a2 100644 --- a/src/main/java/org/cotato/csquiz/api/member/dto/MemberMyPageInfoResponse.java +++ b/src/main/java/org/cotato/csquiz/api/member/dto/MemberMyPageInfoResponse.java @@ -9,7 +9,8 @@ public record MemberMyPageInfoResponse( String name, Integer generationNumber, MemberPosition memberPosition, - String phoneNumber + String phoneNumber, + String profileImage ) { public static MemberMyPageInfoResponse of(Member member, String originPhoneNumber) { return new MemberMyPageInfoResponse( @@ -18,7 +19,8 @@ public static MemberMyPageInfoResponse of(Member member, String originPhoneNumbe member.getName(), member.getPassedGenerationNumber(), member.getPosition(), - originPhoneNumber + originPhoneNumber, + member.getProfileImage() != null ? member.getProfileImage().getUrl() : null ); } } diff --git a/src/main/java/org/cotato/csquiz/api/member/dto/UpdateProfileImageRequest.java b/src/main/java/org/cotato/csquiz/api/member/dto/UpdateProfileImageRequest.java new file mode 100644 index 00000000..173b5313 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/member/dto/UpdateProfileImageRequest.java @@ -0,0 +1,10 @@ +package org.cotato.csquiz.api.member.dto; + +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record UpdateProfileImageRequest( + @NotNull + MultipartFile image +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/mypage/controller/MyPageController.java b/src/main/java/org/cotato/csquiz/api/mypage/controller/MyPageController.java index c38edb5d..abfa28c8 100644 --- a/src/main/java/org/cotato/csquiz/api/mypage/controller/MyPageController.java +++ b/src/main/java/org/cotato/csquiz/api/mypage/controller/MyPageController.java @@ -2,7 +2,6 @@ import org.cotato.csquiz.api.mypage.dto.HallOfFameResponse; import org.cotato.csquiz.common.config.jwt.JwtTokenProvider; -import org.cotato.csquiz.api.mypage.dto.MyPageMemberInfoResponse; import org.cotato.csquiz.domain.education.service.MyPageService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,12 +28,4 @@ public ResponseEntity findHallOfFame(@RequestParam("generati return ResponseEntity.ok(myPageService.findHallOfFame(generationId, jwtTokenProvider.getMemberId(accessToken))); } - - @GetMapping("/info") - public ResponseEntity findUserInfo( - @RequestHeader("Authorization") String authorizationHeader) { - String accessToken = jwtTokenProvider.getBearer(authorizationHeader); - - return ResponseEntity.ok(myPageService.findMemberInfo(jwtTokenProvider.getMemberId(accessToken))); - } } diff --git a/src/main/java/org/cotato/csquiz/api/policy/controller/PolicyController.java b/src/main/java/org/cotato/csquiz/api/policy/controller/PolicyController.java new file mode 100644 index 00000000..d7d8701f --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/policy/controller/PolicyController.java @@ -0,0 +1,38 @@ +package org.cotato.csquiz.api.policy.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.cotato.csquiz.api.policy.dto.CheckMemberPoliciesRequest; +import org.cotato.csquiz.api.policy.dto.FindMemberPolicyResponse; +import org.cotato.csquiz.domain.auth.service.PolicyService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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.RestController; + +@Tag(name = "회원 정책 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/v2/api/policies") +public class PolicyController { + + private final PolicyService policyService; + + @Operation(summary = "체크하지 않은 정책 조회 API") + @GetMapping("/essential") + public ResponseEntity getUnCheckedPolicies(@RequestParam(value = "member-id") Long memberId) { + return ResponseEntity.ok().body(policyService.findUnCheckedPolicies(memberId)); + } + + @Operation(summary = "특정 정책에 대해 동의 여부 체크 API") + @PostMapping("/check") + public ResponseEntity checkPolicies(@RequestBody @Valid CheckMemberPoliciesRequest request){ + policyService.checkPolicies(request); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/policy/dto/CheckMemberPoliciesRequest.java b/src/main/java/org/cotato/csquiz/api/policy/dto/CheckMemberPoliciesRequest.java new file mode 100644 index 00000000..b2cd964f --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/policy/dto/CheckMemberPoliciesRequest.java @@ -0,0 +1,15 @@ +package org.cotato.csquiz.api.policy.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record CheckMemberPoliciesRequest( + @NotNull(message = "체크할 회원의 id는 필수 입니다.") + Long memberId, + @NotEmpty(message = "체크할 정책을 입력해주세요") + @Valid + List policies +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/policy/dto/CheckPolicyRequest.java b/src/main/java/org/cotato/csquiz/api/policy/dto/CheckPolicyRequest.java new file mode 100644 index 00000000..56bd2744 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/policy/dto/CheckPolicyRequest.java @@ -0,0 +1,14 @@ +package org.cotato.csquiz.api.policy.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record CheckPolicyRequest( + @Schema(description = "체크할 정책 PK") + @NotNull(message = "체크할 정책의 id를 입력해주세요.") + Long policyId, + @Schema(description = "정책 동의 여부") + @NotNull(message = "정책 동의 여부를 입력해주세요.") + Boolean isChecked +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/policy/dto/FindMemberPolicyResponse.java b/src/main/java/org/cotato/csquiz/api/policy/dto/FindMemberPolicyResponse.java new file mode 100644 index 00000000..29e4c591 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/policy/dto/FindMemberPolicyResponse.java @@ -0,0 +1,17 @@ +package org.cotato.csquiz.api.policy.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record FindMemberPolicyResponse( + Long memberId, + @Schema(description = "회원이 체크하지 않은 필수 정책 목록") + List essentialPolicies, + @Schema(description = "회원이 체크하지 않은 선택 정책 목록") + List optionalPolicies +) { + public static FindMemberPolicyResponse of(Long memberId, List essentialPolicies, + List optionalPolicies) { + return new FindMemberPolicyResponse(memberId, essentialPolicies, optionalPolicies); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/policy/dto/PolicyInfoResponse.java b/src/main/java/org/cotato/csquiz/api/policy/dto/PolicyInfoResponse.java new file mode 100644 index 00000000..e51b40c4 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/policy/dto/PolicyInfoResponse.java @@ -0,0 +1,25 @@ +package org.cotato.csquiz.api.policy.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.cotato.csquiz.domain.auth.entity.Policy; +import org.cotato.csquiz.domain.auth.enums.PolicyType; + +public record PolicyInfoResponse( + @Schema(description = "정책 PK") + Long policyId, + @Schema(description = "필수 동의 항목 여부") + PolicyType type, + @Schema(description = "정책 타이틀") + String title, + @Schema(description = "정책 내용 게시글") + String content +) { + public static PolicyInfoResponse from(Policy policy){ + return new PolicyInfoResponse( + policy.getId(), + policy.getPolicyType(), + policy.getTitle(), + policy.getContent() + ); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java b/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java index 7b7bfdac..fb713b54 100644 --- a/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java +++ b/src/main/java/org/cotato/csquiz/api/session/controller/SessionController.java @@ -1,21 +1,25 @@ package org.cotato.csquiz.api.session.controller; +import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cotato.csquiz.api.session.dto.AddSessionImageRequest; +import org.cotato.csquiz.api.session.dto.AddSessionImageResponse; import org.cotato.csquiz.api.session.dto.AddSessionRequest; import org.cotato.csquiz.api.session.dto.AddSessionResponse; import org.cotato.csquiz.api.session.dto.CsEducationOnSessionNumberResponse; +import org.cotato.csquiz.api.session.dto.DeleteSessionImageRequest; import org.cotato.csquiz.api.session.dto.SessionListResponse; -import org.cotato.csquiz.api.session.dto.UpdateSessionDescriptionRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionNumberRequest; -import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoRequest; +import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionRequest; import org.cotato.csquiz.domain.generation.service.SessionService; import org.cotato.csquiz.common.error.exception.ImageException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; @@ -33,20 +37,21 @@ public class SessionController { private final SessionService sessionService; + @Operation(summary = "Session 리스트 정보 얻기", description = "Get Session Infos") @GetMapping("") public ResponseEntity> findSessionsByGenerationId(@RequestParam Long generationId) { return ResponseEntity.status(HttpStatus.OK).body(sessionService.findSessionsByGenerationId(generationId)); } + @Operation(summary = "Session 추가하기", description = "세션 추가하기") @PostMapping(value = "/add", consumes = "multipart/form-data") public ResponseEntity addSession(@ModelAttribute @Valid AddSessionRequest request) throws ImageException { return ResponseEntity.status(HttpStatus.CREATED).body(sessionService.addSession(request)); } - @PatchMapping(value = "/update", consumes = "multipart/form-data") - public ResponseEntity updateSession(@ModelAttribute @Valid UpdateSessionRequest request) - throws ImageException { + @PatchMapping(value = "/update") + public ResponseEntity updateSession(@RequestBody @Valid UpdateSessionRequest request) { sessionService.updateSession(request); return ResponseEntity.noContent().build(); } @@ -57,16 +62,24 @@ public ResponseEntity updateSessionNumber(@RequestBody @Valid UpdateSessio return ResponseEntity.noContent().build(); } - @PatchMapping("/description") - public ResponseEntity updateSessionDescription(@RequestBody @Valid UpdateSessionDescriptionRequest request) { - sessionService.updateSessionDescription(request); + @Operation(summary = "Session 수정 - 사진 순서", description = "세션 사진 순서 바꾸기") + @PatchMapping("/image/order") + public ResponseEntity updateSessionImageOrder(@RequestBody UpdateSessionImageOrderRequest request) { + sessionService.updateSessionImageOrder(request); return ResponseEntity.noContent().build(); } - @PatchMapping(value = "/update/photo", consumes = "multipart/form-data") - public ResponseEntity updateSessionPhoto(@ModelAttribute @Valid UpdateSessionPhotoRequest request) + @Operation(summary = "Session 수정 - 사진 추가하기", description = "세션 수정 시 사진 추가하기, photoId 반환") + @PostMapping(value = "/image", consumes = "multipart/form-data") + public ResponseEntity additionalSessionImage(@ModelAttribute @Valid AddSessionImageRequest request) throws ImageException { - sessionService.updateSessionPhoto(request); + return ResponseEntity.status(HttpStatus.CREATED).body(sessionService.additionalSessionImage(request)); + } + + @Operation(summary = "Session 수정 - 사진 삭제하기", description = "사진 삭제하기") + @DeleteMapping(value = "/image") + public ResponseEntity deleteSessionImage(@RequestBody DeleteSessionImageRequest request) { + sessionService.deleteSessionImage(request); return ResponseEntity.noContent().build(); } diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java new file mode 100644 index 00000000..1d21b8d1 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageRequest.java @@ -0,0 +1,13 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; +import org.springframework.web.multipart.MultipartFile; + +public record AddSessionImageRequest( + + @NotNull + Long sessionId, + @NotNull + MultipartFile image +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageResponse.java new file mode 100644 index 00000000..4cdeca6f --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionImageResponse.java @@ -0,0 +1,15 @@ +package org.cotato.csquiz.api.session.dto; + +import org.cotato.csquiz.domain.generation.entity.SessionImage; + +public record AddSessionImageResponse( + Long imageId, + String imageUrl, + Integer order +) { + public static AddSessionImageResponse from(SessionImage sessionImage) { + return new AddSessionImageResponse(sessionImage.getId(), + sessionImage.getS3Info().getUrl(), + sessionImage.getOrder()); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java index a8f10f0d..efcaff72 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/AddSessionRequest.java @@ -1,5 +1,6 @@ package org.cotato.csquiz.api.session.dto; +import java.util.List; import org.cotato.csquiz.domain.generation.enums.CSEducation; import org.cotato.csquiz.domain.generation.enums.DevTalk; import org.cotato.csquiz.domain.generation.enums.ItIssue; @@ -10,7 +11,7 @@ public record AddSessionRequest( @NotNull Long generationId, - MultipartFile sessionImage, + List images, @NotNull String title, @NotNull diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/DeleteSessionImageRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/DeleteSessionImageRequest.java new file mode 100644 index 00000000..2023972a --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/DeleteSessionImageRequest.java @@ -0,0 +1,9 @@ +package org.cotato.csquiz.api.session.dto; + +import jakarta.validation.constraints.NotNull; + +public record DeleteSessionImageRequest( + @NotNull + Long imageId +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListImageInfoResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListImageInfoResponse.java new file mode 100644 index 00000000..278d8ad8 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListImageInfoResponse.java @@ -0,0 +1,16 @@ +package org.cotato.csquiz.api.session.dto; + +import java.util.List; +import org.cotato.csquiz.domain.generation.entity.SessionImage; + +public record SessionListImageInfoResponse( + Long imageId, + String imageUrl, + Integer order +) { + public static SessionListImageInfoResponse from(SessionImage sessionImage) { + return new SessionListImageInfoResponse(sessionImage.getId(), + sessionImage.getS3Info().getUrl(), + sessionImage.getOrder()); + } +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java index 987ecb8c..0ec746e5 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/SessionListResponse.java @@ -1,23 +1,27 @@ package org.cotato.csquiz.api.session.dto; +import java.util.List; import org.cotato.csquiz.domain.generation.embedded.SessionContents; import org.cotato.csquiz.domain.generation.entity.Session; +import org.cotato.csquiz.domain.generation.entity.SessionImage; public record SessionListResponse( Long sessionId, Integer sessionNumber, String title, - String photoUrl, + List imageInfos, String description, Long generationId, SessionContents sessionContents ) { - public static SessionListResponse from(Session session) { + public static SessionListResponse of(Session session, List sessionImages) { return new SessionListResponse( session.getId(), session.getNumber(), session.getTitle(), - (session.getPhotoS3Info() != null) ? session.getPhotoS3Info().getUrl() : null, + sessionImages.stream() + .map(SessionListImageInfoResponse::from) + .toList(), session.getDescription(), session.getGeneration().getId(), session.getSessionContents() diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionDescriptionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageOrderInfoRequest.java similarity index 57% rename from src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionDescriptionRequest.java rename to src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageOrderInfoRequest.java index 99fe7022..428ac5e3 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionDescriptionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageOrderInfoRequest.java @@ -2,10 +2,10 @@ import jakarta.validation.constraints.NotNull; -public record UpdateSessionDescriptionRequest( +public record UpdateSessionImageOrderInfoRequest( @NotNull - Long sessionId, + Long imageId, @NotNull - String description + Integer order ) { } diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageOrderRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageOrderRequest.java new file mode 100644 index 00000000..db553bfa --- /dev/null +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageOrderRequest.java @@ -0,0 +1,9 @@ +package org.cotato.csquiz.api.session.dto; + +import java.util.List; + +public record UpdateSessionImageOrderRequest( + Long sessionId, + List orderInfos +) { +} diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageRequest.java similarity index 75% rename from src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java rename to src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageRequest.java index e11f7d15..31f5d729 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionPhotoRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionImageRequest.java @@ -5,9 +5,9 @@ import org.springframework.web.multipart.MultipartFile; @Valid -public record UpdateSessionPhotoRequest( +public record UpdateSessionImageRequest( @NotNull Long sessionId, - MultipartFile sessionImage + MultipartFile image ) { } diff --git a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java index 7044b43b..995566ec 100644 --- a/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java +++ b/src/main/java/org/cotato/csquiz/api/session/dto/UpdateSessionRequest.java @@ -5,14 +5,10 @@ import org.cotato.csquiz.domain.generation.enums.ItIssue; import org.cotato.csquiz.domain.generation.enums.Networking; import jakarta.validation.constraints.NotNull; -import org.springframework.web.multipart.MultipartFile; public record UpdateSessionRequest( @NotNull Long sessionId, - MultipartFile sessionImage, - @NotNull - Boolean isPhotoUpdated, String title, String description, @NotNull @@ -21,7 +17,6 @@ public record UpdateSessionRequest( Networking networking, @NotNull CSEducation csEducation, - @NotNull DevTalk devTalk ) { diff --git a/src/main/java/org/cotato/csquiz/common/S3/S3Uploader.java b/src/main/java/org/cotato/csquiz/common/S3/S3Uploader.java index b98c9acf..7d3b8816 100644 --- a/src/main/java/org/cotato/csquiz/common/S3/S3Uploader.java +++ b/src/main/java/org/cotato/csquiz/common/S3/S3Uploader.java @@ -1,11 +1,12 @@ package org.cotato.csquiz.common.S3; +import static org.cotato.csquiz.common.util.FileUtil.checkAllowedImageFileExtension; +import static org.cotato.csquiz.common.util.FileUtil.extractFileExtension; + import com.amazonaws.SdkClientException; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.CannedAccessControlList; import com.amazonaws.services.s3.model.PutObjectRequest; -import java.util.List; -import java.util.Objects; import org.cotato.csquiz.common.entity.S3Info; import org.cotato.csquiz.common.error.ErrorCode; import org.cotato.csquiz.common.error.exception.ImageException; @@ -67,7 +68,7 @@ private void removeNewFile(File targetFile) { if (targetFile.delete()) { log.info("삭제 완료"); } else { - log.info("삭제 에러"); + log.error("삭제 에러"); } } @@ -78,7 +79,10 @@ private String putS3(File uploadFile, String fileName) { } private Optional convert(MultipartFile file) throws ImageException { - File convertFile = new File(System.getProperty("user.dir") + "/" + UUID.randomUUID()); + String fileExtension = extractFileExtension(file); + checkAllowedImageFileExtension(fileExtension); + + File convertFile = new File(System.getProperty("user.dir") + "/" + UUID.randomUUID() + "." + fileExtension); log.info("converted file name: {}", convertFile.getName()); try { diff --git a/src/main/java/org/cotato/csquiz/common/config/CorsConfig.java b/src/main/java/org/cotato/csquiz/common/config/CorsConfig.java index 4e286455..ea4e3554 100644 --- a/src/main/java/org/cotato/csquiz/common/config/CorsConfig.java +++ b/src/main/java/org/cotato/csquiz/common/config/CorsConfig.java @@ -16,6 +16,8 @@ public CorsFilter corsFilter() { config.setAllowCredentials(true); config.addAllowedOrigin("https://www.cotato.kr"); + config.addAllowedOrigin("https://qa.beta.cotato.kr"); + config.addAllowedOrigin("http://qa.beta.cotato.kr"); config.addAllowedOrigin("http://localhost:3000"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); diff --git a/src/main/java/org/cotato/csquiz/common/config/SwaggerConfig.java b/src/main/java/org/cotato/csquiz/common/config/SwaggerConfig.java index 61daeb26..255a090f 100644 --- a/src/main/java/org/cotato/csquiz/common/config/SwaggerConfig.java +++ b/src/main/java/org/cotato/csquiz/common/config/SwaggerConfig.java @@ -23,6 +23,7 @@ public OpenAPI customOpenAPI() { return new OpenAPI() .addServersItem(new Server().url("http://localhost:8080").description("Local Server")) .addServersItem(new Server().url("http://43.201.196.189:8082").description("Staging Server")) + .addServersItem(new Server().url("https://qa.beta.cotato.kr").description("QA Server Domain")) .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) .components(new Components().addSecuritySchemes("bearerAuth", new SecurityScheme() diff --git a/src/main/java/org/cotato/csquiz/common/config/filter/JwtAuthenticationFilter.java b/src/main/java/org/cotato/csquiz/common/config/filter/JwtAuthenticationFilter.java index 2a1fedba..aa661b57 100644 --- a/src/main/java/org/cotato/csquiz/common/config/filter/JwtAuthenticationFilter.java +++ b/src/main/java/org/cotato/csquiz/common/config/filter/JwtAuthenticationFilter.java @@ -2,11 +2,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; import java.time.ZoneId; import java.time.ZonedDateTime; import lombok.RequiredArgsConstructor; @@ -16,8 +14,8 @@ import org.cotato.csquiz.common.config.jwt.RefreshToken; import org.cotato.csquiz.common.config.jwt.RefreshTokenRepository; import org.cotato.csquiz.common.config.jwt.Token; -import org.cotato.csquiz.domain.auth.entity.Member; import org.cotato.csquiz.common.error.exception.FilterAuthenticationException; +import org.cotato.csquiz.domain.auth.entity.Member; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -49,7 +47,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, - Authentication authResult) throws IOException, ServletException { + Authentication authResult) { PrincipalDetails principal = (PrincipalDetails) authResult.getPrincipal(); String grantedAuthority = authResult.getAuthorities().stream() diff --git a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java index 293fccc1..35b41c99 100644 --- a/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java +++ b/src/main/java/org/cotato/csquiz/common/error/ErrorCode.java @@ -25,10 +25,16 @@ public enum ErrorCode { INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "A-002", "유효하지 않은 패스워드입니다."), INVALID_PHONE_NUMBER(HttpStatus.BAD_REQUEST, "A-003", "유효하지 않은 전화번호 입니다."), CODE_NOT_MATCH(HttpStatus.BAD_REQUEST, "A-101", "요청하신 코드가 일치하지 않습니다."), - EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "A-201", "해당 이메일이 존재하지 않습니다."), // 이게 소켓에 왜 필요한지, find 그 부분에서 발생해야할 예외일듯 + CODE_EXPIRED(HttpStatus.BAD_REQUEST, "A-102", "코드 유효 시간이 만료되었습니다."), + EMAIL_NOT_FOUND(HttpStatus.NOT_FOUND, "A-201", "해당 이메일이 존재하지 않습니다."), + REQUEST_AGAIN(HttpStatus.NOT_FOUND, "A-202", "해당 이메일에 대한 코드가 존재하지 않습니다. 다시 요청 해주세요"), EMAIL_DUPLICATED(HttpStatus.CONFLICT, "A-301", "존재하는 이메일 입니다."), PHONE_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "A-302", "존재하는 전화번호입니다."), + // 정책 관련 + SHOULD_AGREE_POLICY(HttpStatus.BAD_REQUEST, "P-001", "필수 정책에는 반드시 동의해야합니다."), + ALREADY_POLICY_CHECK(HttpStatus.CONFLICT, "P-301", "이미 동의한 정책입니다."), + //회원 관련 ROLE_IS_NOT_MATCH(HttpStatus.BAD_REQUEST, "M-101", "해당 ROLE은 변경할 수 없습니다."), ROLE_IS_NOT_OLD_MEMBER(HttpStatus.BAD_REQUEST, "M-103", "해당 회원의 ROLE은 OLD_MEMBER가 아닙니다."), @@ -45,6 +51,13 @@ public enum ErrorCode { EDUCATION_STATUS_NOT_BEFORE(HttpStatus.BAD_REQUEST, "E-402", "이미 시작한 적이 있는 교육입니다."), MEMBER_CANT_ACCESS(HttpStatus.BAD_REQUEST, "E-403", "해당 멤버의 ROLE로 접근할 수 없습니다"), + //세션 사진 + SESSION_IMAGE_COUNT_MISMATCH(HttpStatus.BAD_REQUEST, "P-101", "저장된 사진 수와 요청 사진 수가 다릅니다."), + SESSION_ORDER_INVALID(HttpStatus.BAD_REQUEST, "P-102", "입력한 순서는 유효하지 않습니다."), + + FILE_EXTENSION_FAULT(HttpStatus.BAD_REQUEST, "F-001", "해당 파일은 등록 할 수 없는 확장자명입니다."), + FILE_IS_EMPTY(HttpStatus.BAD_REQUEST, "F-002", "파일이 비어있습니다"), + INVALID_ANSWER(HttpStatus.BAD_REQUEST, "Q-101", "객관식 문제는 숫자 형식의 값만 정답으로 추가할 수 있습니다."), CONTENT_IS_NOT_ANSWER(HttpStatus.BAD_REQUEST, "Q-201", "추가되지 않은 정답을 추가할 수 없습니다."), QUIZ_NUMBER_DUPLICATED(HttpStatus.CONFLICT, "Q-301", "퀴즈 번호는 중복될 수 없습니다."), @@ -52,9 +65,9 @@ public enum ErrorCode { CONTENT_IS_ALREADY_ANSWER(HttpStatus.BAD_REQUEST, "Q-303", "이미 정답인 답을 추가했습니다"), QUIZ_ACCESS_DENIED(HttpStatus.BAD_REQUEST, "Q-401", "해당 퀴즈는 아직 접근할 수 없습니다."), QUIZ_TYPE_NOT_MATCH(HttpStatus.BAD_REQUEST, "Q-402", "주관식 정답만 추가 가능합니다."), - + KING_MEMBER_EXIST(HttpStatus.CONFLICT, "K-301", "이미 킹킹 멤버가 존재합니다"), - + SUBJECT_INVALID(HttpStatus.BAD_REQUEST, "E-000", "교육 주제는 NULL이거나 비어있을 수 없습니다."), PROCESSING(HttpStatus.CONFLICT, "D-999", "해당 키의 요청은 아직 처리 중 입니다."), @@ -69,7 +82,7 @@ public enum ErrorCode { IMAGE_DELETE_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, "S-003", "s3 이미지 삭제처리를 실패했습니다"), INTERNAL_SQL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-004", "SQL 관련 에러 발생"), ENUM_NOT_RESOLVED(HttpStatus.BAD_REQUEST, "S-005", "입력한 Enum이 존재하지 않습니다."), - SCORER_LOCK_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-006", "득점자 락 획득 과정에서 에러 발생"); + SCORER_LOCK_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "S-006", "득점자 락 획득 과정에서 에러 발생"), ; private final HttpStatus httpStatus; diff --git a/src/main/java/org/cotato/csquiz/common/error/handler/GlobalExceptionHandler.java b/src/main/java/org/cotato/csquiz/common/error/handler/GlobalExceptionHandler.java index 1f57ccf9..c9a0d35d 100644 --- a/src/main/java/org/cotato/csquiz/common/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/org/cotato/csquiz/common/error/handler/GlobalExceptionHandler.java @@ -39,8 +39,9 @@ public ResponseEntity handleAppCustomException(AppException e, Ht public ResponseEntity handleImageException(ImageException e, HttpServletRequest request) { log.error("이미지 처리 실패 예외 발생: {}", e.getErrorCode().getMessage()); log.error("에러가 발생한 지점 {}, {}", request.getMethod(), request.getRequestURI()); - ErrorResponse errorResponse = ErrorResponse.of(ErrorCode.IMAGE_PROCESSING_FAIL, request); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); + ErrorResponse errorResponse = ErrorResponse.of(e.getErrorCode(), request); + return ResponseEntity.status(e.getErrorCode().getHttpStatus()) + .body(errorResponse); } @Override diff --git a/src/main/java/org/cotato/csquiz/common/util/FileUtil.java b/src/main/java/org/cotato/csquiz/common/util/FileUtil.java new file mode 100644 index 00000000..34c06e22 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/common/util/FileUtil.java @@ -0,0 +1,26 @@ +package org.cotato.csquiz.common.util; + +import java.util.Arrays; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.ImageException; +import org.springframework.web.multipart.MultipartFile; + +public class FileUtil { + + private static final String[] ALLOWED_IMAGE_FILE_EXTENSIONS = {"png", "jpg", "jpeg", "heif"}; + + public static String extractFileExtension(MultipartFile file) throws ImageException { + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.contains(".")) { + throw new ImageException(ErrorCode.FILE_EXTENSION_FAULT); + } + + return originalFilename.substring(originalFilename.lastIndexOf(".") + 1); + } + + public static void checkAllowedImageFileExtension(String fileExtension) throws ImageException { + if (!Arrays.asList(ALLOWED_IMAGE_FILE_EXTENSIONS).contains(fileExtension)) { + throw new ImageException(ErrorCode.FILE_EXTENSION_FAULT); + } + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/cache/EmailRedisRepository.java b/src/main/java/org/cotato/csquiz/domain/auth/cache/EmailRedisRepository.java new file mode 100644 index 00000000..c5c3ae73 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/cache/EmailRedisRepository.java @@ -0,0 +1,30 @@ +package org.cotato.csquiz.domain.auth.cache; + +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.cotato.csquiz.domain.auth.enums.EmailType; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EmailRedisRepository { + + private static final int EXPIRATION_TIME = 15; + private final RedisTemplate redisTemplate; + + public Boolean saveEmail(EmailType type, final String email){ + String key = type.getKeyPrefix() + email; + return redisTemplate.opsForValue().setIfAbsent( + key, + type.getValue(), + EXPIRATION_TIME, + TimeUnit.MINUTES + ); + } + + public Boolean isEmailPresent(EmailType type, final String email) { + String key = type.getKeyPrefix() + email; + return redisTemplate.hasKey(key); + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/cache/VerificationCodeRedisRepository.java b/src/main/java/org/cotato/csquiz/domain/auth/cache/VerificationCodeRedisRepository.java index a0ab6077..a83cddd7 100644 --- a/src/main/java/org/cotato/csquiz/domain/auth/cache/VerificationCodeRedisRepository.java +++ b/src/main/java/org/cotato/csquiz/domain/auth/cache/VerificationCodeRedisRepository.java @@ -2,6 +2,7 @@ import java.util.concurrent.TimeUnit; import lombok.RequiredArgsConstructor; +import org.cotato.csquiz.domain.auth.enums.EmailType; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; @@ -13,13 +14,13 @@ public class VerificationCodeRedisRepository { private static final String KEY_PREFIX = "$email$"; private final RedisTemplate redisTemplate; - public String getByEmail(String email) { - String queryKey = KEY_PREFIX + email; + public String getByEmail(EmailType type, String email) { + String queryKey = type.getKeyPrefix() + KEY_PREFIX + email; return redisTemplate.opsForValue().get(queryKey); } - public void saveCodeWithEmail(String email, String verificationCode) { - String saveKey = KEY_PREFIX + email; + public void saveCodeWithEmail(EmailType type, String email, String verificationCode) { + String saveKey = type.getKeyPrefix() + KEY_PREFIX + email; redisTemplate.opsForValue().set( saveKey, verificationCode, diff --git a/src/main/java/org/cotato/csquiz/domain/auth/entity/Member.java b/src/main/java/org/cotato/csquiz/domain/auth/entity/Member.java index 4dc0c4c5..0af96356 100644 --- a/src/main/java/org/cotato/csquiz/domain/auth/entity/Member.java +++ b/src/main/java/org/cotato/csquiz/domain/auth/entity/Member.java @@ -12,6 +12,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.S3Info; import org.cotato.csquiz.domain.auth.enums.MemberPosition; import org.cotato.csquiz.domain.auth.enums.MemberRole; import org.cotato.csquiz.common.entity.BaseTimeEntity; @@ -55,6 +56,9 @@ public class Member extends BaseTimeEntity { @Column(name = "passed_generation_number") private Integer passedGenerationNumber; + @Column(name = "member_profile_image") + private S3Info profileImage; + @Builder public Member(String email, String password, String name, String phoneNumber) { this.email = email; @@ -82,4 +86,8 @@ public void updatePhoneNumber(String phoneNumber) { public void updatePosition(MemberPosition position) { this.position = position; } + + public void updateProfileImage(S3Info s3Info) { + this.profileImage = s3Info; + } } diff --git a/src/main/java/org/cotato/csquiz/domain/auth/entity/MemberPolicy.java b/src/main/java/org/cotato/csquiz/domain/auth/entity/MemberPolicy.java new file mode 100644 index 00000000..08af68b5 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/entity/MemberPolicy.java @@ -0,0 +1,51 @@ +package org.cotato.csquiz.domain.auth.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.BaseTimeEntity; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberPolicy extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "member_policy_id") + private Long id; + + @Column(name = "is_checked", nullable = false) + private Boolean isChecked; + + @Column(name = "check_time", nullable = false) + private LocalDateTime checkTime; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "policy_id") + private Policy policy; + + private MemberPolicy(Boolean isChecked, Member member, Policy policy) { + this.isChecked = isChecked; + this.member = member; + this.policy = policy; + this.checkTime = LocalDateTime.now(); + } + + public static MemberPolicy of(Boolean isChecked, Member member, Policy policy){ + return new MemberPolicy(isChecked, member, policy); + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/entity/Policy.java b/src/main/java/org/cotato/csquiz/domain/auth/entity/Policy.java new file mode 100644 index 00000000..b264f287 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/entity/Policy.java @@ -0,0 +1,39 @@ +package org.cotato.csquiz.domain.auth.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.BaseTimeEntity; +import org.cotato.csquiz.domain.auth.enums.PolicyType; +import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.DynamicInsert; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +public class Policy extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "policy_id") + private Long id; + + @Column(name = "policy_type", nullable = false) + @Enumerated(EnumType.STRING) + @ColumnDefault(value = "'ESSENTIAL'") + private PolicyType policyType; + + @Column(name = "policy_title", nullable = false) + private String title; + + @Column(name = "policy_content", nullable = false, columnDefinition = "TEXT") + private String content; +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/enums/EmailType.java b/src/main/java/org/cotato/csquiz/domain/auth/enums/EmailType.java new file mode 100644 index 00000000..e89af16e --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/enums/EmailType.java @@ -0,0 +1,16 @@ +package org.cotato.csquiz.domain.auth.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum EmailType { + SIGNUP("$sign-up", "exist", "코드 일치 성공"), + UPDATE_PASSWORD("$update-pwd", "exist", "코드 요청 후 검증 요청을 보내지 않음") + ; + + private final String keyPrefix; + private final String value; + private final String description; +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/enums/PolicyType.java b/src/main/java/org/cotato/csquiz/domain/auth/enums/PolicyType.java new file mode 100644 index 00000000..ab33efa3 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/enums/PolicyType.java @@ -0,0 +1,15 @@ +package org.cotato.csquiz.domain.auth.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum PolicyType { + + ESSENTIAL("필수 선택 정책"), + OPTIONAL("선택적인 정책"); + + + private final String description; +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/repository/MemberPolicyRepository.java b/src/main/java/org/cotato/csquiz/domain/auth/repository/MemberPolicyRepository.java new file mode 100644 index 00000000..35eb815f --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/repository/MemberPolicyRepository.java @@ -0,0 +1,9 @@ +package org.cotato.csquiz.domain.auth.repository; + +import java.util.List; +import org.cotato.csquiz.domain.auth.entity.MemberPolicy; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberPolicyRepository extends JpaRepository { + List findAllByMemberId(Long memberId); +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/repository/PolicyRepository.java b/src/main/java/org/cotato/csquiz/domain/auth/repository/PolicyRepository.java new file mode 100644 index 00000000..71923836 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/repository/PolicyRepository.java @@ -0,0 +1,12 @@ +package org.cotato.csquiz.domain.auth.repository; + +import java.util.List; +import org.cotato.csquiz.domain.auth.entity.Policy; +import org.cotato.csquiz.domain.auth.enums.PolicyType; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PolicyRepository extends JpaRepository { + List findAllByPolicyType(PolicyType policyType); + + List findAllByIdIn(List ids); +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/service/AuthService.java b/src/main/java/org/cotato/csquiz/domain/auth/service/AuthService.java index c12ec405..7805b691 100644 --- a/src/main/java/org/cotato/csquiz/domain/auth/service/AuthService.java +++ b/src/main/java/org/cotato/csquiz/domain/auth/service/AuthService.java @@ -16,10 +16,11 @@ import org.cotato.csquiz.common.config.jwt.RefreshToken; import org.cotato.csquiz.common.config.jwt.RefreshTokenRepository; import org.cotato.csquiz.common.config.jwt.Token; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; +import org.cotato.csquiz.domain.auth.enums.EmailType; import org.cotato.csquiz.domain.auth.constant.EmailConstants; import org.cotato.csquiz.domain.auth.entity.Member; -import org.cotato.csquiz.common.error.exception.AppException; -import org.cotato.csquiz.common.error.ErrorCode; import org.cotato.csquiz.domain.auth.repository.MemberRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -119,20 +120,20 @@ public void logout(LogoutRequest request, String refreshToken, HttpServletRespon public void sendSignUpEmail(SendEmailRequest request) { validateService.emailNotExist(request.email()); - emailVerificationService.sendVerificationCodeToEmail(request.email(), EmailConstants.SIGNUP_SUBJECT); + emailVerificationService.sendVerificationCodeToEmail(EmailType.SIGNUP, request.email(), EmailConstants.SIGNUP_SUBJECT); } public void verifySingUpCode(String email, String code) { - emailVerificationService.verifyCode(email, code); + emailVerificationService.verifyCode(EmailType.SIGNUP, email, code); } public void sendFindPasswordEmail(SendEmailRequest request) { validateService.emailExist(request.email()); - emailVerificationService.sendVerificationCodeToEmail(request.email(), EmailConstants.PASSWORD_SUBJECT); + emailVerificationService.sendVerificationCodeToEmail(EmailType.UPDATE_PASSWORD, request.email(), EmailConstants.PASSWORD_SUBJECT); } public FindPasswordResponse verifyPasswordCode(String email, String code) { - emailVerificationService.verifyCode(email, code); + emailVerificationService.verifyCode(EmailType.UPDATE_PASSWORD, email, code); Member findMember = memberRepository.findByEmail(email) .orElseThrow(() -> new AppException(ErrorCode.EMAIL_NOT_FOUND)); String role = findMember.getRole().getKey(); diff --git a/src/main/java/org/cotato/csquiz/domain/auth/service/EmailVerificationService.java b/src/main/java/org/cotato/csquiz/domain/auth/service/EmailVerificationService.java index e8c62f11..8cb1195b 100644 --- a/src/main/java/org/cotato/csquiz/domain/auth/service/EmailVerificationService.java +++ b/src/main/java/org/cotato/csquiz/domain/auth/service/EmailVerificationService.java @@ -15,6 +15,8 @@ import lombok.extern.slf4j.Slf4j; import org.cotato.csquiz.common.error.exception.AppException; import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.domain.auth.cache.EmailRedisRepository; +import org.cotato.csquiz.domain.auth.enums.EmailType; import org.cotato.csquiz.domain.auth.utils.EmailFormValidator; import org.cotato.csquiz.domain.auth.cache.VerificationCodeRedisRepository; import org.springframework.mail.javamail.JavaMailSender; @@ -33,13 +35,17 @@ public class EmailVerificationService { private final JavaMailSender mailSender; private final VerificationCodeRedisRepository verificationCodeRedisRepository; private final EmailFormValidator emailFormValidator; + private final EmailRedisRepository emailRedisRepository; - @Transactional - public void sendVerificationCodeToEmail(String recipient, String subject) { + public void sendVerificationCodeToEmail(EmailType type, String recipient, String subject) { emailFormValidator.validateEmailForm(recipient); + String verificationCode = getVerificationCode(); log.info("인증 번호 생성 완료"); - verificationCodeRedisRepository.saveCodeWithEmail(recipient, verificationCode); + + emailRedisRepository.saveEmail(type, recipient); + verificationCodeRedisRepository.saveCodeWithEmail(type, recipient, verificationCode); + sendEmailWithVerificationCode(recipient, verificationCode, subject); } @@ -82,11 +88,19 @@ private InternetAddress getInternetAddress() { } } - @Transactional - public void verifyCode(String email, String code) { - String savedVerificationCode = verificationCodeRedisRepository.getByEmail(email); - validateEmailCodeMatching(savedVerificationCode, code); - log.info("[이메일 인증 완료]: 성공한 이메일 == {}", email); + public void verifyCode(EmailType type, String email, String code) { + String savedVerificationCode = verificationCodeRedisRepository.getByEmail(type, email); + if (savedVerificationCode != null) { + validateEmailCodeMatching(savedVerificationCode, code); + log.info("[이메일 인증 완료]: 성공한 이메일 == {}", email); + return; + } + + if (emailRedisRepository.isEmailPresent(type, email)) { + throw new AppException(ErrorCode.CODE_EXPIRED); + } else { + throw new AppException(ErrorCode.REQUEST_AGAIN); + } } private void validateEmailCodeMatching(String savedVerificationCode, String code) { diff --git a/src/main/java/org/cotato/csquiz/domain/auth/service/MemberService.java b/src/main/java/org/cotato/csquiz/domain/auth/service/MemberService.java index ea466e33..ca8f3492 100644 --- a/src/main/java/org/cotato/csquiz/domain/auth/service/MemberService.java +++ b/src/main/java/org/cotato/csquiz/domain/auth/service/MemberService.java @@ -6,15 +6,19 @@ import org.cotato.csquiz.api.admin.dto.MemberInfoResponse; import org.cotato.csquiz.api.member.dto.MemberInfo; import org.cotato.csquiz.api.member.dto.MemberMyPageInfoResponse; +import org.cotato.csquiz.common.S3.S3Uploader; import org.cotato.csquiz.api.member.dto.UpdatePhoneNumberRequest; import org.cotato.csquiz.common.config.jwt.JwtTokenProvider; -import org.cotato.csquiz.domain.auth.entity.Member; -import org.cotato.csquiz.common.error.exception.AppException; +import org.cotato.csquiz.common.entity.S3Info; import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; +import org.cotato.csquiz.common.error.exception.ImageException; +import org.cotato.csquiz.domain.auth.entity.Member; import org.cotato.csquiz.domain.auth.repository.MemberRepository; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; @Slf4j @Service @@ -22,11 +26,14 @@ @RequiredArgsConstructor public class MemberService { + private static final String PROFILE_BUCKET_DIRECTORY = "profile"; + private final MemberRepository memberRepository; private final BCryptPasswordEncoder bCryptPasswordEncoder; private final JwtTokenProvider jwtTokenProvider; private final EncryptService encryptService; private final ValidateService validateService; + private final S3Uploader s3Uploader; public MemberInfoResponse findMemberInfo(Long id) { Member findMember = memberRepository.findById(id) @@ -45,10 +52,10 @@ public String findBackFourNumber(Member member) { } @Transactional - public void updatePassword(String accessToken, String password) { - Long memberId = jwtTokenProvider.getMemberId(accessToken); + public void updatePassword(final Long memberId, final String password) { Member findMember = memberRepository.findById(memberId) .orElseThrow(() -> new EntityNotFoundException("해당 회원을 찾을 수 없습니다.")); + validateService.checkPasswordPattern(password); validateIsSameBefore(findMember.getPassword(), password); @@ -71,6 +78,37 @@ public void updatePhoneNumber(String accessToken, String phoneNumber) { findMember.updatePhoneNumber(encryptedPhoneNumber); } + @Transactional + public void updateMemberProfileImage(String accessToken, MultipartFile image) throws ImageException { + if (image.isEmpty()) { + throw new AppException(ErrorCode.FILE_IS_EMPTY); + } + + Long memberId = jwtTokenProvider.getMemberId(accessToken); + Member findMember = memberRepository.findById(memberId) + .orElseThrow(() -> new EntityNotFoundException("해당 회원을 찾을 수 없습니다.")); + + if (findMember.getProfileImage() != null) { + s3Uploader.deleteFile(findMember.getProfileImage()); + } + + S3Info s3Info = s3Uploader.uploadFiles(image, PROFILE_BUCKET_DIRECTORY); + findMember.updateProfileImage(s3Info); + } + + @Transactional + public void deleteMemberProfileImage(String accessToken) { + Long memberId = jwtTokenProvider.getMemberId(accessToken); + Member findMember = memberRepository.findById(memberId) + .orElseThrow(() -> new EntityNotFoundException("해당 회원을 찾을 수 없습니다.")); + + if (findMember.getProfileImage() != null) { + s3Uploader.deleteFile(findMember.getProfileImage()); + } + + findMember.updateProfileImage(null); + } + public MemberMyPageInfoResponse findMyPageInfo(Long memberId) { Member findMember = memberRepository.findById(memberId) .orElseThrow(() -> new EntityNotFoundException("해당 회원을 찾을 수 없습니다.")); diff --git a/src/main/java/org/cotato/csquiz/domain/auth/service/PolicyService.java b/src/main/java/org/cotato/csquiz/domain/auth/service/PolicyService.java new file mode 100644 index 00000000..972711c1 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/auth/service/PolicyService.java @@ -0,0 +1,94 @@ +package org.cotato.csquiz.domain.auth.service; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.cotato.csquiz.api.policy.dto.CheckMemberPoliciesRequest; +import org.cotato.csquiz.api.policy.dto.CheckPolicyRequest; +import org.cotato.csquiz.api.policy.dto.FindMemberPolicyResponse; +import org.cotato.csquiz.api.policy.dto.PolicyInfoResponse; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; +import org.cotato.csquiz.domain.auth.entity.Member; +import org.cotato.csquiz.domain.auth.entity.MemberPolicy; +import org.cotato.csquiz.domain.auth.entity.Policy; +import org.cotato.csquiz.domain.auth.enums.PolicyType; +import org.cotato.csquiz.domain.auth.repository.MemberPolicyRepository; +import org.cotato.csquiz.domain.auth.repository.PolicyRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class PolicyService { + + private final MemberService memberService; + private final PolicyRepository policyRepository; + private final MemberPolicyRepository memberPolicyRepository; + + public FindMemberPolicyResponse findUnCheckedPolicies(final Long memberId) { + // 회원이 체크한 정책 + List checkedPolicies = memberPolicyRepository.findAllByMemberId(memberId).stream() + .filter(MemberPolicy::getIsChecked) + .map(MemberPolicy::getId) + .toList(); + + List uncheckedEssentialPolicies = policyRepository.findAllByPolicyType(PolicyType.ESSENTIAL) + .stream() + .filter(policy -> !checkedPolicies.contains(policy.getId())) + .map(PolicyInfoResponse::from) + .toList(); + + List uncheckedOptionalPolicies = policyRepository.findAllByPolicyType(PolicyType.OPTIONAL).stream() + .filter(policy -> !checkedPolicies.contains(policy.getId())) + .map(PolicyInfoResponse::from) + .toList(); + + return FindMemberPolicyResponse.of(memberId, uncheckedEssentialPolicies, uncheckedOptionalPolicies); + } + + @Transactional + public void checkPolicies(CheckMemberPoliciesRequest request) { + Member findMember = memberService.findById(request.memberId()); + + List policyIds = request.policies().stream() + .map(CheckPolicyRequest::policyId) + .toList(); + + if (isAlreadyChecked(findMember, policyIds)) { + throw new AppException(ErrorCode.ALREADY_POLICY_CHECK); + } + + Map policyMap = policyRepository.findAllByIdIn(policyIds).stream() + .collect(Collectors.toMap(Policy::getId, Function.identity())); + + List memberPolicies = request.policies().stream() + .map(policyRequest -> MemberPolicy.of(policyRequest.isChecked(), findMember, + policyMap.get(policyRequest.policyId()))) + .toList(); + + if (hasDisagreementInEssential(memberPolicies)) { + throw new AppException(ErrorCode.SHOULD_AGREE_POLICY); + } + + memberPolicyRepository.saveAll(memberPolicies); + } + + private boolean isAlreadyChecked(Member findMember, List policyIds) { + return memberPolicyRepository.findAllByMemberId(findMember.getId()).stream() + .map(MemberPolicy::getPolicy) + .map(Policy::getId) + .anyMatch(policyIds::contains); + } + + private boolean hasDisagreementInEssential(List checkedPolicies) { + return checkedPolicies.stream() + .filter(checkedPolicy -> checkedPolicy.getIsChecked().equals(false)) + .map(MemberPolicy::getPolicy) + .map(Policy::getPolicyType) + .anyMatch(PolicyType.ESSENTIAL::equals); + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/auth/service/ValidateService.java b/src/main/java/org/cotato/csquiz/domain/auth/service/ValidateService.java index 2bfc4e28..6c31bbcb 100644 --- a/src/main/java/org/cotato/csquiz/domain/auth/service/ValidateService.java +++ b/src/main/java/org/cotato/csquiz/domain/auth/service/ValidateService.java @@ -34,13 +34,13 @@ public void checkDuplicatePhoneNumber(String phone) { } public void emailNotExist(String email) { - if (memberRepository.findByEmail(email).isPresent()) { + if (memberRepository.existsByEmail(email)) { throw new AppException(ErrorCode.EMAIL_DUPLICATED); } } public void emailExist(String email) { - if (memberRepository.findByEmail(email).isEmpty()) { + if (!memberRepository.existsByEmail(email)) { throw new AppException(ErrorCode.EMAIL_NOT_FOUND); } } diff --git a/src/main/java/org/cotato/csquiz/domain/education/service/MyPageService.java b/src/main/java/org/cotato/csquiz/domain/education/service/MyPageService.java index 18b800fe..09598dc6 100644 --- a/src/main/java/org/cotato/csquiz/domain/education/service/MyPageService.java +++ b/src/main/java/org/cotato/csquiz/domain/education/service/MyPageService.java @@ -11,7 +11,6 @@ import org.cotato.csquiz.api.mypage.dto.HallOfFameInfo; import org.cotato.csquiz.api.mypage.dto.HallOfFameResponse; import org.cotato.csquiz.api.mypage.dto.MyHallOfFameInfo; -import org.cotato.csquiz.api.mypage.dto.MyPageMemberInfoResponse; import org.cotato.csquiz.domain.education.entity.Education; import org.cotato.csquiz.domain.education.entity.Quiz; import org.cotato.csquiz.domain.education.entity.Record; @@ -25,7 +24,6 @@ import org.cotato.csquiz.domain.auth.service.MemberService; import org.cotato.csquiz.domain.generation.repository.GenerationRepository; import org.cotato.csquiz.domain.auth.repository.MemberRepository; -import org.cotato.csquiz.domain.auth.service.EncryptService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,7 +41,6 @@ public class MyPageService { private final EducationRepository educationRepository; private final RecordRepository recordRepository; private final ScorerRepository scorerRepository; - private final EncryptService encryptService; public HallOfFameResponse findHallOfFame(Long generationId, Long memberId) { Generation findGeneration = generationRepository.findById(generationId) @@ -137,13 +134,6 @@ private List> sortTopMemberEntry(Map countByMe .toList(); } - public MyPageMemberInfoResponse findMemberInfo(Long memberId) { - Member member = findMemberById(memberId); - String originPhoneNumber = encryptService.decryptPhoneNumber(member.getPhoneNumber()); - - return MyPageMemberInfoResponse.of(member, originPhoneNumber); - } - private Member findMemberById(Long id) { return memberRepository.findById(id) .orElseThrow(() -> new EntityNotFoundException("해당 id를 가진 멤버를 찾을 수 없습니다.")); diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java index 847563d1..916a63ef 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/Session.java @@ -5,19 +5,20 @@ import jakarta.persistence.AttributeOverride; import jakarta.persistence.AttributeOverrides; import jakarta.persistence.Column; -import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.cotato.csquiz.common.entity.BaseTimeEntity; -import org.cotato.csquiz.common.entity.S3Info; import org.cotato.csquiz.domain.generation.embedded.SessionContents; import org.hibernate.annotations.DynamicInsert; @@ -38,9 +39,6 @@ public class Session extends BaseTimeEntity { @Column(name = "session_title", length = 100) private String title; - @Embedded - private S3Info photoS3Info; - @Column(name = "session_description") private String description; @@ -61,9 +59,8 @@ public class Session extends BaseTimeEntity { private SessionContents sessionContents; @Builder - public Session(Integer number, S3Info s3Info, String title, String description, Generation generation, SessionContents sessionContents) { + public Session(Integer number, String title, String description, Generation generation, SessionContents sessionContents) { this.number = number; - this.photoS3Info = s3Info; this.title = title; this.description = description; this.generation = generation; @@ -78,10 +75,6 @@ public void updateDescription(String description) { this.description = description; } - public void changePhotoUrl(S3Info photoUrl) { - this.photoS3Info = photoUrl; - } - public void updateSessionContents(SessionContents sessionContents) { this.sessionContents = sessionContents; } diff --git a/src/main/java/org/cotato/csquiz/domain/generation/entity/SessionImage.java b/src/main/java/org/cotato/csquiz/domain/generation/entity/SessionImage.java new file mode 100644 index 00000000..8c17d838 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/generation/entity/SessionImage.java @@ -0,0 +1,63 @@ +package org.cotato.csquiz.domain.generation.entity; + +import static jakarta.persistence.FetchType.LAZY; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +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.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.cotato.csquiz.common.entity.BaseTimeEntity; +import org.cotato.csquiz.common.entity.S3Info; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SessionImage extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "session_image_id") + private Long id; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "folderName", column = @Column(nullable = false)), + @AttributeOverride(name = "fileName", column = @Column(nullable = false)), + @AttributeOverride(name = "url", column = @Column(nullable = false)) + }) + private S3Info s3Info; + + @Column(name = "session_image_order", nullable = false) + private Integer order; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "session_id") + private Session session; + + @Builder + public SessionImage(Session session, Integer order, S3Info s3Info) { + this.session = session; + this.order = order; + this.s3Info = s3Info; + } + + public void updateOrder(Integer order) { + this.order = order; + } + + public void decreaseOrder() { + if (order > 0) { + order--; + } + } +} diff --git a/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java new file mode 100644 index 00000000..c6e87a17 --- /dev/null +++ b/src/main/java/org/cotato/csquiz/domain/generation/repository/SessionImageRepository.java @@ -0,0 +1,15 @@ +package org.cotato.csquiz.domain.generation.repository; + +import java.util.List; +import java.util.Optional; +import org.cotato.csquiz.domain.generation.entity.Session; +import org.cotato.csquiz.domain.generation.entity.SessionImage; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SessionImageRepository extends JpaRepository { + List findAllBySession(Session session); + + List findAllBySessionIn(List sessions); + + Optional findFirstBySessionOrderByOrderDesc(Session session); +} diff --git a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java index f6525bfe..8fc3d2e9 100644 --- a/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java +++ b/src/main/java/org/cotato/csquiz/domain/generation/service/SessionService.java @@ -1,27 +1,41 @@ package org.cotato.csquiz.domain.generation.service; import jakarta.persistence.EntityNotFoundException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.cotato.csquiz.api.session.dto.AddSessionImageResponse; +import org.cotato.csquiz.api.session.dto.DeleteSessionImageRequest; +import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderInfoRequest; +import org.cotato.csquiz.api.session.dto.UpdateSessionImageOrderRequest; +import org.cotato.csquiz.api.session.dto.AddSessionImageRequest; import org.cotato.csquiz.api.session.dto.AddSessionRequest; import org.cotato.csquiz.api.session.dto.AddSessionResponse; import org.cotato.csquiz.api.session.dto.CsEducationOnSessionNumberResponse; import org.cotato.csquiz.api.session.dto.SessionListResponse; -import org.cotato.csquiz.api.session.dto.UpdateSessionDescriptionRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionNumberRequest; -import org.cotato.csquiz.api.session.dto.UpdateSessionPhotoRequest; import org.cotato.csquiz.api.session.dto.UpdateSessionRequest; import org.cotato.csquiz.common.entity.S3Info; +import org.cotato.csquiz.common.error.ErrorCode; +import org.cotato.csquiz.common.error.exception.AppException; import org.cotato.csquiz.domain.education.entity.Education; import org.cotato.csquiz.domain.education.service.EducationService; import org.cotato.csquiz.domain.generation.embedded.SessionContents; +import org.cotato.csquiz.domain.generation.entity.SessionImage; import org.cotato.csquiz.domain.generation.enums.CSEducation; import org.cotato.csquiz.domain.generation.entity.Generation; import org.cotato.csquiz.domain.generation.entity.Session; import org.cotato.csquiz.common.error.exception.ImageException; import org.cotato.csquiz.common.S3.S3Uploader; import org.cotato.csquiz.domain.generation.repository.GenerationRepository; +import org.cotato.csquiz.domain.generation.repository.SessionImageRepository; import org.cotato.csquiz.domain.generation.repository.SessionRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,23 +50,20 @@ public class SessionService { private static final String SESSION_BUCKET_DIRECTORY = "session"; private final SessionRepository sessionRepository; private final GenerationRepository generationRepository; + private final SessionImageRepository sessionImageRepository; private final EducationService educationService; private final S3Uploader s3Uploader; @Transactional public AddSessionResponse addSession(AddSessionRequest request) throws ImageException { - S3Info s3Info = null; - if (isImageExist(request.sessionImage())) { - s3Info = s3Uploader.uploadFiles(request.sessionImage(), SESSION_BUCKET_DIRECTORY); - } Generation findGeneration = generationRepository.findById(request.generationId()) .orElseThrow(() -> new EntityNotFoundException("해당 기수를 찾을 수 없습니다.")); int sessionNumber = calculateLastSessionNumber(findGeneration); log.info("해당 기수에 추가된 마지막 세션 : {}", sessionNumber); + Session session = Session.builder() .number(sessionNumber + 1) - .s3Info(s3Info) .description(request.description()) .generation(findGeneration) .title(request.title()) @@ -66,6 +77,27 @@ public AddSessionResponse addSession(AddSessionRequest request) throws ImageExce Session savedSession = sessionRepository.save(session); log.info("세션 생성 완료"); + if (request.images() != null && !request.images().isEmpty()) { + AtomicInteger index = new AtomicInteger(0); + + List sessionImages = new ArrayList<>(); + + for (MultipartFile imageFile : request.images()) { + S3Info s3Info = s3Uploader.uploadFiles(imageFile, SESSION_BUCKET_DIRECTORY); + + SessionImage sessionImage = SessionImage.builder() + .session(savedSession) + .s3Info(s3Info) + .order(index.getAndIncrement()) + .build(); + + sessionImages.add(sessionImage); + } + + sessionImageRepository.saveAll(sessionImages); + log.info("세션 이미지 생성 완료"); + } + return AddSessionResponse.from(savedSession); } @@ -82,13 +114,7 @@ public void updateSessionNumber(UpdateSessionNumberRequest request) { } @Transactional - public void updateSessionDescription(UpdateSessionDescriptionRequest request) { - Session session = findSessionById(request.sessionId()); - session.updateDescription(request.description()); - } - - @Transactional - public void updateSession(UpdateSessionRequest request) throws ImageException { + public void updateSession(UpdateSessionRequest request) { Session session = findSessionById(request.sessionId()); session.updateDescription(request.description()); @@ -99,35 +125,88 @@ public void updateSession(UpdateSessionRequest request) throws ImageException { .itIssue(request.itIssue()) .networking(request.networking()) .build()); - if (request.isPhotoUpdated()) { - updatePhoto(session, request.sessionImage()); - } sessionRepository.save(session); } @Transactional - public void updateSessionPhoto(UpdateSessionPhotoRequest request) throws ImageException { + public AddSessionImageResponse additionalSessionImage(AddSessionImageRequest request) throws ImageException { Session session = findSessionById(request.sessionId()); - updatePhoto(session, request.sessionImage()); + + S3Info imageInfo = s3Uploader.uploadFiles(request.image(), SESSION_BUCKET_DIRECTORY); + + Integer imageOrder = sessionImageRepository.findFirstBySessionOrderByOrderDesc(session) + .map(sessionImage -> sessionImage.getOrder() + 1).orElse(0); + + SessionImage sessionImage = SessionImage.builder() + .session(session) + .s3Info(imageInfo) + .order(imageOrder) + .build(); + + return AddSessionImageResponse.from(sessionImageRepository.save(sessionImage)); } - private void updatePhoto(Session session, MultipartFile sessionImage) throws ImageException { - if (isImageExist(sessionImage)) { - S3Info s3Info = s3Uploader.uploadFiles(sessionImage, SESSION_BUCKET_DIRECTORY); - deleteOldImage(session); - session.changePhotoUrl(s3Info); + @Transactional + public void deleteSessionImage(DeleteSessionImageRequest request) { + SessionImage deleteImage = sessionImageRepository.findById(request.imageId()) + .orElseThrow(() -> new EntityNotFoundException("해당 사진을 찾을 수 없습니다.")); + s3Uploader.deleteFile(deleteImage.getS3Info()); + sessionImageRepository.delete(deleteImage); + + List reorderImages = sessionImageRepository.findAllBySession(deleteImage.getSession()).stream() + .filter(image -> image.getOrder() > deleteImage.getOrder()) + .toList(); + + for (SessionImage sessionImage : reorderImages) { + sessionImage.decreaseOrder(); + } + } + + @Transactional + public void updateSessionImageOrder(UpdateSessionImageOrderRequest request) { + Session sessionById = findSessionById(request.sessionId()); + List orderList = request.orderInfos(); + + List savedImages = sessionImageRepository.findAllBySession(sessionById); + + if (savedImages.size() != orderList.size()) { + throw new AppException(ErrorCode.SESSION_IMAGE_COUNT_MISMATCH); + } + + if (!isValidOrderRange(orderList)) { + throw new AppException(ErrorCode.SESSION_ORDER_INVALID); } - if (!isImageExist(sessionImage)) { - deleteOldImage(session); - session.changePhotoUrl(null); + + if (!isOrderUnique(orderList)) { + throw new AppException(ErrorCode.SESSION_ORDER_INVALID); } + + Map orderMap = orderList.stream() + .collect(Collectors.toMap(UpdateSessionImageOrderInfoRequest::imageId, Function.identity())); + + for (SessionImage savedImage : savedImages) { + if (orderMap.get(savedImage.getId()) == null) { + throw new EntityNotFoundException("해당 사진을 찾을 수 없습니다."); + } + savedImage.updateOrder(orderMap.get(savedImage.getId()).order()); + } + } + + private boolean isValidOrderRange(List orderList) { + return orderList.stream().noneMatch(orderInfo -> + orderInfo.order() < 0 || orderInfo.order() >= orderList.size()); } - private void deleteOldImage(Session session) { - if (session.getPhotoS3Info() != null) { - s3Uploader.deleteFile(session.getPhotoS3Info()); + private boolean isOrderUnique(List orderList) { + Set uniqueOrders = new HashSet<>(); + for (UpdateSessionImageOrderInfoRequest orderInfo : orderList) { + if (!uniqueOrders.add(orderInfo.order())) { + return false; + } } + + return true; } public List findSessionsByGenerationId(Long generationId) { @@ -136,8 +215,11 @@ public List findSessionsByGenerationId(Long generationId) { List sessions = sessionRepository.findAllByGeneration(generation); + Map> imagesGroupBySession = sessionImageRepository.findAllBySessionIn(sessions).stream() + .collect(Collectors.groupingBy(SessionImage::getSession)); + return sessions.stream() - .map(SessionListResponse::from) + .map(session -> SessionListResponse.of(session,imagesGroupBySession.getOrDefault(session, List.of()))) .toList(); } @@ -160,8 +242,4 @@ public List findAllNotLinkedCsOnSessionsByGe .map(CsEducationOnSessionNumberResponse::from) .toList(); } - - private boolean isImageExist(MultipartFile sessionImage) { - return sessionImage != null && !sessionImage.isEmpty(); - } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index aca58c92..953cb6f8 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -17,6 +17,7 @@ spring: properties: hibernate: format_sql: true + show-sql: true springdoc: swagger-ui: