From bf50d3a60d1270048819bc573dc1540cfae6da77 Mon Sep 17 00:00:00 2001 From: HyoBin Yang <50162252+HyoBN@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:07:58 +0900 Subject: [PATCH 1/4] =?UTF-8?q?Feature/140=20=E2=9C=A8=20[FEAT]=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=B0=A8=EB=8B=A8=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#146)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: 유저 차단하기 API * :art: 사용하지 않는 API 삭제 * :sparkles: 유저 차단 해지, 차단 목록 조회 API --- src/main/java/zipdabang/server/base/Code.java | 8 ++ .../server/converter/MemberConverter.java | 25 ++++- .../BlockedMemberRepository.java | 10 ++ .../server/service/MemberService.java | 4 + .../serviceImpl/MemberServiceImpl.java | 47 ++++++++ .../web/controller/MemberRestController.java | 104 +++++++++++------- .../dto/responseDto/MemberResponseDto.java | 27 ++++- 7 files changed, 183 insertions(+), 42 deletions(-) diff --git a/src/main/java/zipdabang/server/base/Code.java b/src/main/java/zipdabang/server/base/Code.java index 6ca6d95..0839cb9 100644 --- a/src/main/java/zipdabang/server/base/Code.java +++ b/src/main/java/zipdabang/server/base/Code.java @@ -21,6 +21,8 @@ public enum Code { PHONE_NUMBER_EXIST(HttpStatus.OK, 2054, "이미 인증된 전화번호입니다."), AUTO_LOGIN_MAIN(HttpStatus.OK, 2055, "홈 화면으로 이동하세요"), AUTO_LOGIN_NOT_MAIN(HttpStatus.OK, 2056, "로그인 화면으로 이동하세요"), + BLOCKED_MEMBER_NOT_FOUND(HttpStatus.OK, 2057, "차단한 유저가 없습니다"), + //recipe response @@ -84,6 +86,12 @@ public enum Code { //BAD_REQUEST DEREGISTER_FAIL(HttpStatus.OK, 4061, "탈퇴할 수 없는 유저입니다. 탈퇴 불가 사유가 존재합니다."), + + ALREADY_BLOCKED_MEMBER(HttpStatus.OK, 4062, "이미 차단된 사용자입니다."), + BLOCK_SELF(HttpStatus.OK, 4063, "자신을 차단할 수 없습니다."), + + + // market error //recipe error diff --git a/src/main/java/zipdabang/server/converter/MemberConverter.java b/src/main/java/zipdabang/server/converter/MemberConverter.java index 0713ae1..851b2e8 100644 --- a/src/main/java/zipdabang/server/converter/MemberConverter.java +++ b/src/main/java/zipdabang/server/converter/MemberConverter.java @@ -31,7 +31,6 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; @Component @@ -314,4 +313,28 @@ public static Deregister toDeregister(String phoneNum, MemberRequestDto.Deregist .feedback(request.getFeedback()) .build(); } + + public static MemberResponseDto.MemberSimpleDto toMemberSimpleDto(Member member) { + return MemberResponseDto.MemberSimpleDto.builder() + .memberId(member.getMemberId()) + .profileUrl(member.getProfileUrl()) + .nickname(member.getNickname()) + .createdAt(TimeConverter.ConvertTime(member.getCreatedAt())) + .build(); + } + + public static MemberResponseDto.PagingMemberListDto toPagingMemberListDto(Page memberPage) { + List memberSimpleDtoList = memberPage.getContent().stream() + .map(MemberConverter::toMemberSimpleDto).collect(Collectors.toList()); + + return MemberResponseDto.PagingMemberListDto.builder() + .memberSimpleDtoList(memberSimpleDtoList) + .isFirst(memberPage.isFirst()) + .isLast(memberPage.isLast()) + .currentPageElements(memberPage.getNumberOfElements()) + .totalElements(memberPage.getTotalElements()) + .totalPage(memberPage.getTotalPages()) + .build(); + } + } diff --git a/src/main/java/zipdabang/server/repository/memberRepositories/BlockedMemberRepository.java b/src/main/java/zipdabang/server/repository/memberRepositories/BlockedMemberRepository.java index 13528af..f5bb807 100644 --- a/src/main/java/zipdabang/server/repository/memberRepositories/BlockedMemberRepository.java +++ b/src/main/java/zipdabang/server/repository/memberRepositories/BlockedMemberRepository.java @@ -1,6 +1,9 @@ package zipdabang.server.repository.memberRepositories; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import zipdabang.server.domain.member.BlockedMember; import zipdabang.server.domain.member.Member; @@ -9,6 +12,13 @@ public interface BlockedMemberRepository extends JpaRepository { List findByOwner(Member owner); + + @Query("select b.blocked from BlockedMember b") + Page findBlockedByOwner(Member owner, PageRequest pageRequest); Optional findByOwnerAndBlocked(Member owner, Member blocked); + boolean existsByOwnerAndBlocked(Member owner, Member blocked); + + void deleteByOwnerAndBlocked(Member owner, Member blocked); + } diff --git a/src/main/java/zipdabang/server/service/MemberService.java b/src/main/java/zipdabang/server/service/MemberService.java index 63607db..290786b 100644 --- a/src/main/java/zipdabang/server/service/MemberService.java +++ b/src/main/java/zipdabang/server/service/MemberService.java @@ -52,4 +52,8 @@ public interface MemberService { public void inactivateMember(Member member); public void saveDeregisterReasons(Long deregisterId, List deregisterTypeList); + + public void blockMember(Member owner, Long blocked); + public void unblockMember(Member owner, Long blockedId); + public Page findBlockedMember(Integer page, Member member); } diff --git a/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java b/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java index 2271700..770296b 100644 --- a/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java +++ b/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java @@ -14,6 +14,7 @@ import zipdabang.server.base.Code; import zipdabang.server.base.exception.handler.AuthNumberException; import zipdabang.server.base.exception.handler.MemberException; +import zipdabang.server.base.exception.handler.RecipeException; import zipdabang.server.converter.MemberConverter; import zipdabang.server.domain.Category; import zipdabang.server.domain.enums.DeregisterType; @@ -73,6 +74,7 @@ public class MemberServiceImpl implements MemberService { private final AmazonS3Manager s3Manager; private final DeregisterRepository deregisterRepository; private final DeregisterReasonRepository deregisterReasonRepository; + private final BlockedMemberRepository blockedMemberRepository; @Value("${paging.size}") private Integer pageSize; @@ -315,4 +317,49 @@ public void saveDeregisterReasons(Long deregisterId, List deregi // . // } } + + @Override + @Transactional + public void blockMember(Member owner, Long blockedId) { + if (owner.getMemberId() == blockedId) { + throw new MemberException(Code.BLOCK_SELF); + } + Member blocked = memberRepository.findById(blockedId).orElseThrow(() -> new MemberException(Code.MEMBER_NOT_FOUND)); + if (blockedMemberRepository.existsByOwnerAndBlocked(owner, blocked)) { + throw new MemberException(Code.ALREADY_BLOCKED_MEMBER); + } + blockedMemberRepository.save( + BlockedMember.builder() + .owner(owner) + .blocked(blocked) + .build()); + } + + @Override + @Transactional + public void unblockMember(Member owner, Long blockedId) { + Member blocked = memberRepository.findById(blockedId).orElseThrow(() -> new MemberException(Code.MEMBER_NOT_FOUND)); + blockedMemberRepository.deleteByOwnerAndBlocked(owner, blocked); + } + + @Override + @Transactional + public Page findBlockedMember(Integer page, Member member) { + + Page blockedMembers = blockedMemberRepository.findBlockedByOwner(member, PageRequest.of(page, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))); + if (blockedMembers.getContent().isEmpty()) { + throw new MemberException(Code.BLOCKED_MEMBER_NOT_FOUND); + } + if(blockedMembers.getTotalPages() <= page) + throw new MemberException(Code.OVER_PAGE_INDEX_ERROR); + + return blockedMembers; + + } } + + + + + + diff --git a/src/main/java/zipdabang/server/web/controller/MemberRestController.java b/src/main/java/zipdabang/server/web/controller/MemberRestController.java index 1143996..6887d5b 100644 --- a/src/main/java/zipdabang/server/web/controller/MemberRestController.java +++ b/src/main/java/zipdabang/server/web/controller/MemberRestController.java @@ -1,7 +1,6 @@ package zipdabang.server.web.controller; import com.fasterxml.jackson.core.JsonProcessingException; -import io.swagger.annotations.ApiParam; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameters; @@ -24,8 +23,9 @@ import zipdabang.server.auth.handler.annotation.AuthMember; import zipdabang.server.base.Code; import zipdabang.server.base.ResponseDto; +import zipdabang.server.base.exception.handler.MemberException; +import zipdabang.server.base.exception.handler.RecipeException; import zipdabang.server.converter.MemberConverter; -import zipdabang.server.converter.RootConverter; import zipdabang.server.domain.Category; import zipdabang.server.domain.member.Inquery; import zipdabang.server.domain.member.Member; @@ -43,7 +43,6 @@ import org.springframework.web.bind.annotation.*; import zipdabang.server.sms.dto.SmsResponseDto; import zipdabang.server.utils.dto.OAuthResult; -import zipdabang.server.web.dto.responseDto.RootResponseDto; import javax.validation.Valid; import java.io.IOException; @@ -90,16 +89,6 @@ public ResponseDto logout(@AuthMember Member return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "logout")); } - @PatchMapping("/members/quit") - public ResponseDto quit(@RequestBody MemberRequestDto.quitMember request) { - return null; - } - - @PatchMapping("/members/restore") - public ResponseDto restore(@RequestBody MemberRequestDto.restoreMember request) { - return null; - } - //소셜로그인 @Operation(summary = "🎪figma[온보딩1] 소셜로그인 API ✔️", description = "소셜로그인 API, 응답으로 로그인(메인으로 이동), 회원가입(정보 입력으로 이동) code로 구분하며 query String으로 카카오인지 구글인지 주면 됩니다.") @@ -163,27 +152,8 @@ public ResponseDto authPhoneNum(@RequestBody Me } - //프로필 수정 - @PatchMapping(value = "/members", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) - public ResponseDto updateProfile(@ModelAttribute MemberRequestDto.memberProfileDto request) { - return null; - } - - //프로필 조회 - @GetMapping("/members/{memberId}") - public ResponseDto showProfile(@PathVariable("memberId") Long memberId) { - return null; - } - - //내 프로필 조회 - @GetMapping("/members") - public ResponseDto showMyProfile(@AuthMember Member member) { - return null; - } - - // 내 선호 음료 조회 - @Operation(summary = "[figma 더보기 - 즐겨마시는 음료 종류 1] 유저 선호 카테고리 조회 API ✔️", description = "유저 선호 카테고리 조회 API입니다.") + @Operation(summary = "[figma 더보기 - 즐겨마시는 음료 종류 1] 유저 선호 카테고리 조회 API ✔️🔑", description = "유저 선호 카테고리 조회 API입니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -201,7 +171,7 @@ public ResponseDto memberPreferCatego // 회원정보 조회 및 수정 APIs - @Operation(summary = "[figma 더보기 - 회원 정보 1] 회원정보 조회 API ✔️", description = "회원정보 조회 API입니다.") + @Operation(summary = "[figma 더보기 - 회원 정보 1] 회원정보 조회 API ✔️🔑", description = "회원정보 조회 API입니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -215,7 +185,7 @@ public ResponseDto showMyInfo(@AuthMemb return ResponseDto.of(MemberConverter.toMemberInfoDto(member, memberPreferCategoryDto)); } - @Operation(summary = "[figma 더보기 - 회원 정보 1] 프로필사진 수정 API ✔️", description = "프로필사진 수정 API입니다.") + @Operation(summary = "[figma 더보기 - 회원 정보 1] 프로필사진 수정 API ✔️🔑", description = "프로필사진 수정 API입니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -228,7 +198,7 @@ public ResponseDto updateProfileImage(@AuthMe return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "updateProfileImage")); } - @Operation(summary = "[figma 더보기 - 회원 정보 수정 1] 기본정보 수정 API ✔️", description = "기본정보 수정 API입니다.") + @Operation(summary = "[figma 더보기 - 회원 정보 수정 1] 기본정보 수정 API ✔️🔑", description = "기본정보 수정 API입니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -242,7 +212,7 @@ public ResponseDto updateBasicInfo(@AuthMembe return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "updateBasicInfo")); } - @Operation(summary = "[figma 더보기 - 회원 정보 수정 2] 상세정보 수정 API ✔️", description = "상세정보 수정 API입니다.") + @Operation(summary = "[figma 더보기 - 회원 정보 수정 2] 상세정보 수정 API ✔️🔑", description = "상세정보 수정 API입니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -255,7 +225,7 @@ public ResponseDto updateDetailInfo(@AuthMemb return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "updateDetailInfo")); } - @Operation(summary = "[figma 더보기 - 회원 정보 수정 3] 닉네임 수정 API ✔️", description = "닉네임 수정 API입니다.") + @Operation(summary = "[figma 더보기 - 회원 정보 수정 3] 닉네임 수정 API ✔️🔑", description = "닉네임 수정 API입니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -270,7 +240,7 @@ public ResponseDto updateNickname(@AuthMember // 내 선호 음료 카테고리 수정 - @Operation(summary = "[figma 더보기 - 즐겨마시는 음료 종류 1] 유저 선호 카테고리 수정 API ✔️", description = "유저 선호 카테고리 수정 API입니다. 카테고리명(커피, 차 등)을 넣으시면 됩니다.") + @Operation(summary = "[figma 더보기 - 즐겨마시는 음료 종류 1] 유저 선호 카테고리 수정 API ✔️🔑", description = "유저 선호 카테고리 수정 API입니다. 카테고리명(커피, 차 등)을 넣으시면 됩니다.") @Parameters({ @Parameter(name = "member", hidden = true), }) @@ -366,7 +336,7 @@ public ResponseDto showInquery(@CheckTempMembe Page inqueryPage = memberService.findInquery(member, page); return ResponseDto.of(MemberConverter.toInqueryListDto(inqueryPage)); } - @Operation(summary = "[figma 더보기 - 회원 탈퇴] 회원 탈퇴 API ✔️", description = "회원 탈퇴 API입니다.
테스트를 위해 임시로 해당 유저의 상세주소를 \"TEST\" 로 설정하면(상세정보 수정 API - zipCode) 탈퇴 불가능한 경우로 처리되도록 해놨습니다.
deregisterTypes 종류
"+ + @Operation(summary = "[figma 더보기 - 회원 탈퇴] 회원 탈퇴 API ✔️🔑", description = "회원 탈퇴 API입니다.
테스트를 위해 임시로 해당 유저의 상세주소를 \"TEST\" 로 설정하면(상세정보 수정 API - zipCode) 탈퇴 불가능한 경우로 처리되도록 해놨습니다.
deregisterTypes 종류
"+ "- NOTHING_TO_BUY(\"사고싶은 물건이 없어요.\"),
" + "- DISINTERESTED(\"앱을 이용하지 않아요.\"),
" + "- UNCOMFORTABLE(\"앱 이용이 불편해요.\"),
" + @@ -386,4 +356,58 @@ public ResponseDto deregister(@CheckDeregiste return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "deregister")); } + + + @Operation(summary = "유저 차단 API ✔️🔑", description = "유저 차단 API 입니다.") + @Parameters({ + @Parameter(name = "member", hidden = true), + }) + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK 성공, 유저 차단 완료"), + @ApiResponse(responseCode = "4052", description = "해당 사용자가 존재하지 않습니다"), + @ApiResponse(responseCode = "4062", description = "이미 차단된 사용자입니다."), + @ApiResponse(responseCode = "4063", description = "자신을 차단할 수 없습니다."), + }) + @PostMapping("/members/block") + public ResponseDto block(@AuthMember Member member, Long blocked) { + memberService.blockMember(member, blocked); + return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "Block")); + } + + @Operation(summary = "유저 차단 해지 API ✔️🔑", description = "유저 차단 해지 API 입니다.") + @Parameters({ + @Parameter(name = "member", hidden = true), + }) + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK 성공, 유저 차단 해지 완료"), + @ApiResponse(responseCode = "4052", description = "해당 사용자가 존재하지 않습니다"), + }) + @DeleteMapping("/members/unblock") + public ResponseDto unblock(@AuthMember Member member, Long blocked) { + memberService.unblockMember(member, blocked); + return ResponseDto.of(MemberConverter.toMemberStatusDto(member.getMemberId(), "Unblock")); + } + + @Operation(summary = "차단 유저 목록 조회 API 🔑", description = "차단 유저 목록 조회 API 입니다.") + @Parameters({ + @Parameter(name = "member", hidden = true), + @Parameter(name = "page", description = "페이지 번호, 1부터 시작") + }) + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK 성공, 차단 유저 목록 조회 완료"), + @ApiResponse(responseCode = "4052", description = "해당 사용자가 존재하지 않습니다"), + @ApiResponse(responseCode = "4054", description = "BAD_REQUEST , 페이지 번호가 없거나 0 이하", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4055", description = "BAD_REQUEST , 페이지 번호가 초과함", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + }) + @GetMapping("/members/blockedList") + public ResponseDto blockerMemberList(@RequestParam(name = "page", required = false) Integer page, @AuthMember Member member) { + if (page == null) + page = 1; + else if (page < 1) + throw new MemberException(Code.UNDER_PAGE_INDEX_ERROR); + page -= 1; + + Page blockedMembers = memberService.findBlockedMember(page, member); + return ResponseDto.of(MemberConverter.toPagingMemberListDto(blockedMembers)); + } } diff --git a/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java b/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java index 5090f60..4cc6033 100644 --- a/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java +++ b/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java @@ -1,7 +1,6 @@ package zipdabang.server.web.dto.responseDto; import lombok.*; -import zipdabang.server.domain.Category; import zipdabang.server.domain.enums.GenderType; @@ -185,4 +184,30 @@ public static class InqueryListDto{ Boolean isFirst; Boolean isLast; } + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class MemberSimpleDto { + private Long memberId; + private String profileUrl; + private String nickname; + private String createdAt; + + } + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class PagingMemberListDto { + private List memberSimpleDtoList; + Long totalElements; + Integer currentPageElements; + Integer totalPage; + Boolean isFirst; + Boolean isLast; + } + } From 0180a569f37844db969e89d7fb3ded13bc77c775 Mon Sep 17 00:00:00 2001 From: Hanvp Date: Sat, 30 Sep 2023 22:06:42 +0900 Subject: [PATCH 2/4] =?UTF-8?q?:recycle:=20Feature/148=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A0=88=EC=8B=9C=ED=94=BC=20=EC=A2=8B=EC=95=84?= =?UTF-8?q?=EC=9A=94/=EC=8A=A4=ED=81=AC=EB=9E=A9=20=EB=B6=88=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/zipdabang/server/base/Code.java | 6 ++++-- .../server/service/serviceImpl/RecipeServiceImpl.java | 6 ++++++ .../server/web/controller/RecipeRestController.java | 2 ++ src/main/resources/application.yml | 8 ++++---- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/main/java/zipdabang/server/base/Code.java b/src/main/java/zipdabang/server/base/Code.java index 0839cb9..89e1073 100644 --- a/src/main/java/zipdabang/server/base/Code.java +++ b/src/main/java/zipdabang/server/base/Code.java @@ -115,11 +115,13 @@ public enum Code { //BAD_REQUEST NOT_COMMENT_OWNER(HttpStatus.OK, 4108, "본인이 작성한 댓글이 아닙니다. 변경할 수 없습니다"), //BAD_REQUEST - RECIPE_OWNER(HttpStatus.OK, 4109, "본인의 레시피입니다. 신고/차단할 수 없습니다"), + RECIPE_OWNER(HttpStatus.OK, 4109, "본인의 레시피입니다. 좋아요/스크랩/신고/차단할 수 없습니다"), //BAD_REQUEST - COMMENT_OWNER(HttpStatus.OK, 4110, "본인의 댓글입니다. 신고/차단할 수 없습니다"), + COMMENT_OWNER(HttpStatus.OK, 4110, "본인의 댓글입니다. 좋아요/스크랩/신고/차단할 수 없습니다"), //BAD_REQUEST NO_TEMP_RECIPE_EXIST(HttpStatus.OK, 4111, "해당 임시저장 Id가 존재하지 않습니다."), + + //INTERNAL_SERVER_ERROR INTERNAL_ERROR(HttpStatus.OK, 5000, "Internal server Error"), //INTERNAL_SERVER_ERROR diff --git a/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java b/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java index 15c7c40..36986fb 100644 --- a/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java +++ b/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java @@ -306,6 +306,9 @@ private List getBlockedMember(Member member) { public Recipe updateLikeOnRecipe(Long recipeId, Member member) { Recipe recipe = recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(Code.NO_RECIPE_EXIST)); + if(recipe.getMember() == member) + throw new RecipeException(Code.RECIPE_OWNER); + Optional likesExist = likesRepository.findByRecipeAndMember(recipe,member); if(likesExist.isEmpty()) { @@ -325,6 +328,9 @@ public Recipe updateLikeOnRecipe(Long recipeId, Member member) { public Recipe updateScrapOnRecipe(Long recipeId, Member member) { Recipe recipe = recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(Code.NO_RECIPE_EXIST)); + if(recipe.getMember() == member) + throw new RecipeException(Code.RECIPE_OWNER); + Optional scrapExist = scrapRepository.findByRecipeAndMember(recipe,member); if(scrapExist.isEmpty()) { diff --git a/src/main/java/zipdabang/server/web/controller/RecipeRestController.java b/src/main/java/zipdabang/server/web/controller/RecipeRestController.java index a23489e..f6e2473 100644 --- a/src/main/java/zipdabang/server/web/controller/RecipeRestController.java +++ b/src/main/java/zipdabang/server/web/controller/RecipeRestController.java @@ -337,6 +337,7 @@ public ResponseDto recipeWeekBest(@AuthMember M @ApiResponse(responseCode = "4008", description = "UNAUTHORIZED, 토큰 없음, 토큰 줘요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), @ApiResponse(responseCode = "4052", description = "BAD_REQUEST, 사용자가 없습니다. 이 api에서 이거 생기면 백앤드 개발자 호출", content = @Content(schema = @Schema(implementation = ResponseDto.class))), @ApiResponse(responseCode = "4101", description = "BAD_REQUEST, 해당 recipeId를 가진 recipe가 없어요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4109", description = "BAD_REQUEST, 본인의 레시피입니다. 좋아요/스크랩할 수 없습니다", content = @Content(schema = @Schema(implementation = ResponseDto.class))), @ApiResponse(responseCode = "5000", description = "SERVER ERROR, 백앤드 개발자에게 알려주세요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), }) @Parameters({ @@ -357,6 +358,7 @@ public ResponseDto recipeScrapOrCancel(@PathV @ApiResponse(responseCode = "4008", description = "UNAUTHORIZED, 토큰 없음, 토큰 줘요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), @ApiResponse(responseCode = "4052", description = "BAD_REQUEST, 사용자가 없습니다. 이 api에서 이거 생기면 백앤드 개발자 호출", content = @Content(schema = @Schema(implementation = ResponseDto.class))), @ApiResponse(responseCode = "4101", description = "BAD_REQUEST, 해당 recipeId를 가진 recipe가 없어요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4109", description = "BAD_REQUEST, 본인의 레시피입니다. 좋아요/스크랩할 수 없습니다", content = @Content(schema = @Schema(implementation = ResponseDto.class))), @ApiResponse(responseCode = "5000", description = "SERVER ERROR, 백앤드 개발자에게 알려주세요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), }) @Parameters({ diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e50765b..13b7c84 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,11 +12,11 @@ spring: enabled: always ## # local redis -# redis: -# host: localhost - redis: - host: zipdabang-redis.osattk.ng.0001.apn2.cache.amazonaws.com + host: localhost + +# redis: +# host: zipdabang-redis.osattk.ng.0001.apn2.cache.amazonaws.com batch: jdbc: initialize-schema: always From f256c0dd6e7d4b5580e4bfbec39f04152e155e94 Mon Sep 17 00:00:00 2001 From: Hanvp Date: Sat, 30 Sep 2023 23:24:32 +0900 Subject: [PATCH 3/4] =?UTF-8?q?:sparkles:=20Feature/148=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC=EB=B3=84=20top5=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/converter/RecipeConverter.java | 22 +++++++++++ .../server/service/RecipeService.java | 2 + .../serviceImpl/RecipeServiceImpl.java | 22 +++++++++++ .../annotation/ExistRecipeCategory.java | 18 +++++++++ .../ExistRecipeCategoryValidator.java | 35 ++++++++++++++++++ .../web/controller/RecipeRestController.java | 37 ++++++++++++++----- .../dto/responseDto/RecipeResponseDto.java | 25 +++++++++++++ src/main/resources/application.yml | 8 ++-- 8 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 src/main/java/zipdabang/server/validation/annotation/ExistRecipeCategory.java create mode 100644 src/main/java/zipdabang/server/validation/validator/ExistRecipeCategoryValidator.java diff --git a/src/main/java/zipdabang/server/converter/RecipeConverter.java b/src/main/java/zipdabang/server/converter/RecipeConverter.java index bd10027..f04d1d9 100644 --- a/src/main/java/zipdabang/server/converter/RecipeConverter.java +++ b/src/main/java/zipdabang/server/converter/RecipeConverter.java @@ -63,6 +63,28 @@ public class RecipeConverter { private static AmazonS3Manager staticAmazonS3Manager; private static TimeConverter staticTimeConverter; + public static RecipeResponseDto.PerCategoryPreview toPerCategoryPreview(Long categoryId, List recipeList, Member member) { + return RecipeResponseDto.PerCategoryPreview.builder() + .categoryId(categoryId) + .totalElements(recipeList.size()) + .recipeList(recipeList.stream() + .map(recipe -> toRecipePreviewDto(recipe,member)) + .collect(Collectors.toList())) + .build(); + } + + public static RecipeResponseDto.RecipePreviewDto toRecipePreviewDto(Recipe recipe, Member member) { + return RecipeResponseDto.RecipePreviewDto.builder() + .recipeId(recipe.getId()) + .recipeName(recipe.getName()) + .nickname(recipe.getMember().getNickname()) + .likes(recipe.getTotalLike()) + .scraps(recipe.getTotalScrap()) + .isLiked(staticLikesRepository.findByRecipeAndMember(recipe, member).isPresent()) + .isScrapped(staticScrapRepository.findByRecipeAndMember(recipe,member).isPresent()) + .build(); + } + @PostConstruct public void init() { diff --git a/src/main/java/zipdabang/server/service/RecipeService.java b/src/main/java/zipdabang/server/service/RecipeService.java index bf916bf..5d5fb9e 100644 --- a/src/main/java/zipdabang/server/service/RecipeService.java +++ b/src/main/java/zipdabang/server/service/RecipeService.java @@ -57,4 +57,6 @@ public interface RecipeService { TempRecipe tempCreate(RecipeRequestDto.TempRecipeDto request, MultipartFile thumbnail, List stepImages, Member member) throws IOException; TempRecipe tempUpdate(Long tempId, RecipeRequestDto.TempRecipeDto request, MultipartFile thumbnail, List stepImages, Member member) throws IOException; + + List getTop5RecipePerCategory(Long categoryId); } diff --git a/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java b/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java index 36986fb..8ea214a 100644 --- a/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java +++ b/src/main/java/zipdabang/server/service/serviceImpl/RecipeServiceImpl.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import static zipdabang.server.domain.recipe.QComment.comment; @@ -350,6 +351,27 @@ public List getAllRecipeCategories() { return recipeCategoryRepository.findAll(); } + @Override + public List getTop5RecipePerCategory(Long categoryId) { + QRecipe qRecipe = recipe; + QRecipeCategoryMapping qRecipeCategoryMapping = recipeCategoryMapping; + + AtomicLong index = new AtomicLong(1); + List recipeList = queryFactory + .selectFrom(recipe) + .join(recipe.categoryMappingList, recipeCategoryMapping).fetchJoin() + .where( + recipeCategoryMapping.category.id.eq(categoryId) + ) + .limit(5) + .orderBy(recipe.totalLike.desc(), recipe.createdAt.desc()) + .fetch(); + + log.info(recipeList.toString()); + + return recipeList; + } + @Override public Page recipeListByCategory(Long categoryId, Integer pageIndex, Member member, String order) { diff --git a/src/main/java/zipdabang/server/validation/annotation/ExistRecipeCategory.java b/src/main/java/zipdabang/server/validation/annotation/ExistRecipeCategory.java new file mode 100644 index 0000000..7a98b5b --- /dev/null +++ b/src/main/java/zipdabang/server/validation/annotation/ExistRecipeCategory.java @@ -0,0 +1,18 @@ +package zipdabang.server.validation.annotation; + + +import zipdabang.server.validation.validator.ExistRecipeCategoryValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = ExistRecipeCategoryValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistRecipeCategory { + String message() default "범위에 없는 categoryId를 전달했습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/zipdabang/server/validation/validator/ExistRecipeCategoryValidator.java b/src/main/java/zipdabang/server/validation/validator/ExistRecipeCategoryValidator.java new file mode 100644 index 0000000..96bf65e --- /dev/null +++ b/src/main/java/zipdabang/server/validation/validator/ExistRecipeCategoryValidator.java @@ -0,0 +1,35 @@ +package zipdabang.server.validation.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import zipdabang.server.base.Code; +import zipdabang.server.domain.recipe.RecipeCategory; +import zipdabang.server.repository.recipeRepositories.RecipeCategoryRepository; +import zipdabang.server.validation.annotation.ExistRecipeCategory; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class ExistRecipeCategoryValidator implements ConstraintValidator { + + private final RecipeCategoryRepository recipeCategoryRepository; + + @Override + public void initialize(ExistRecipeCategory constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long value, ConstraintValidatorContext context) { + Optional findRecipeCategory = recipeCategoryRepository.findById(value); + if(findRecipeCategory.isEmpty()){ + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(Code.NO_RECIPE_CATEGORY_EXIST.toString()).addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/zipdabang/server/web/controller/RecipeRestController.java b/src/main/java/zipdabang/server/web/controller/RecipeRestController.java index f6e2473..ede5bdf 100644 --- a/src/main/java/zipdabang/server/web/controller/RecipeRestController.java +++ b/src/main/java/zipdabang/server/web/controller/RecipeRestController.java @@ -14,7 +14,6 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.servlet.view.RedirectView; import zipdabang.server.auth.handler.annotation.AuthMember; import zipdabang.server.base.Code; import zipdabang.server.base.ResponseDto; @@ -24,6 +23,7 @@ import zipdabang.server.domain.recipe.*; import zipdabang.server.service.RecipeService; import zipdabang.server.validation.annotation.CheckTempMember; +import zipdabang.server.validation.annotation.ExistRecipeCategory; import zipdabang.server.web.dto.requestDto.RecipeRequestDto; import zipdabang.server.web.dto.responseDto.RecipeResponseDto; @@ -192,10 +192,7 @@ public ResponseDto searchRecipePre @Parameter(name = "keyword", description = "query string 검색할 단어") }) @GetMapping(value = "/members/recipes/search/{categoryId}") - public ResponseDto searchRecipe(@PathVariable Long categoryId, @RequestParam(name = "keyword") String keyword, @RequestParam(name = "pageIndex", required = false) Integer pageIndex, @AuthMember Member member) { - - if (recipeService.checkRecipeCategoryExist(categoryId) == false) - throw new RecipeException(Code.NO_RECIPE_CATEGORY_EXIST); + public ResponseDto searchRecipe(@ExistRecipeCategory @PathVariable Long categoryId, @RequestParam(name = "keyword") String keyword, @RequestParam(name = "pageIndex", required = false) Integer pageIndex, @AuthMember Member member) { if (pageIndex == null) pageIndex = 1; @@ -214,6 +211,31 @@ else if (pageIndex < 1) return ResponseDto.of(RecipeConverter.toPagingRecipeDtoList(recipes, member)); } + @Operation(summary = "🍹figma 레시피2, 레시피 카테고리 별 top5 화면 API 🔑 ✔", description = "레시피 카테고리별 top5 조회 화면 API입니다.") + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK, 목록이 있을 땐 이 응답임"), + @ApiResponse(responseCode = "2100", description = "OK, 목록이 없을 경우", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4003", description = "UNAUTHORIZED, 토큰 모양이 이상함, 토큰 제대로 주세요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4005", description = "UNAUTHORIZED, 엑세스 토큰 만료, 리프레시 토큰 사용", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4008", description = "UNAUTHORIZED, 토큰 없음, 토큰 줘요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4052", description = "BAD_REQUEST, 사용자가 없습니다. 이 api에서 이거 생기면 백앤드 개발자 호출", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4105", description = "BAD_REQUEST, 해당 id를 가진 레시피 카테고리가 없습니다. 잘못 보내줬어요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + + @ApiResponse(responseCode = "5000", description = "SERVER ERROR, 백앤드 개발자에게 알려주세요", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + }) + @Parameters({ + @Parameter(name = "member", hidden = true), + }) + @GetMapping(value = "/members/recipes/categories/{categoryId}/top5") + public ResponseDto RecipeCategoryTop5(@ExistRecipeCategory @PathVariable Long categoryId, @AuthMember Member member) { + + List recipeList = recipeService.getTop5RecipePerCategory(categoryId); + + log.info(recipeList.toString()); + + return ResponseDto.of(RecipeConverter.toPerCategoryPreview(categoryId, recipeList ,member)); + } + @Operation(summary = "🍹figma 레시피2, 카테고리 별 레시피 목록 조회 API 🔑 ✔", description = "카테고리 별 레시피 목록 조회 화면 API입니다. pageIndex로 페이징") @ApiResponses({ @ApiResponse(responseCode = "2000", description = "OK, 목록이 있을 땐 이 응답임"), @@ -235,10 +257,7 @@ else if (pageIndex < 1) @Parameter(name = "order", description = "query string 조회 방식. 인기순: likes, 조회순: views, 최신순: latest로 넘겨주세요, 기본값 latest") }) @GetMapping(value = "/members/recipes/categories/{categoryId}") - public ResponseDto recipeListByCategory(@PathVariable Long categoryId, @RequestParam(name = "order", required = false) String order, @RequestParam(name = "pageIndex", required = false) Integer pageIndex, @AuthMember Member member) { - - if (recipeService.checkRecipeCategoryExist(categoryId) == false) - throw new RecipeException(Code.NO_RECIPE_CATEGORY_EXIST); + public ResponseDto recipeListByCategory(@ExistRecipeCategory @PathVariable Long categoryId, @RequestParam(name = "order", required = false) String order, @RequestParam(name = "pageIndex", required = false) Integer pageIndex, @AuthMember Member member) { if (pageIndex == null) pageIndex = 1; diff --git a/src/main/java/zipdabang/server/web/dto/responseDto/RecipeResponseDto.java b/src/main/java/zipdabang/server/web/dto/responseDto/RecipeResponseDto.java index 19d3d76..0f63822 100644 --- a/src/main/java/zipdabang/server/web/dto/responseDto/RecipeResponseDto.java +++ b/src/main/java/zipdabang/server/web/dto/responseDto/RecipeResponseDto.java @@ -29,6 +29,31 @@ public static class TempRecipeStatusDto{ private String calledAt; } + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class PerCategoryPreview{ + private List recipeList; + private Long categoryId; + private Integer totalElements; + } + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class RecipePreviewDto { + private Long recipeId; + private String recipeName; + private String nickname; + private String thumbnailUrl; + private Long likes; + private Long scraps; + private Boolean isLiked; + private Boolean isScrapped; + } + @Builder @Getter @AllArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 13b7c84..e50765b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,11 +12,11 @@ spring: enabled: always ## # local redis - redis: - host: localhost - # redis: -# host: zipdabang-redis.osattk.ng.0001.apn2.cache.amazonaws.com +# host: localhost + + redis: + host: zipdabang-redis.osattk.ng.0001.apn2.cache.amazonaws.com batch: jdbc: initialize-schema: always From a5dc21e0132e8f3e84329ab4d71729bbff08fef8 Mon Sep 17 00:00:00 2001 From: YONGWOOK CHOI <60510921+CYY1007@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:09:39 +0900 Subject: [PATCH 4/4] =?UTF-8?q?issue/147=20=E2=9C=A8=20[FEAT]=20=ED=8C=94?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=ED=8C=94=EB=A1=9C=EC=9E=89=20API=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :sparkles: Feat : 팔로우하기 API 구현 * :sparkles: Feat : 팔로우중인 사용자 조회 API 구현 * :sparkles: Feat : 나를 팔로우 중인 사용자 조회 API 구현 * redis 설정 임시로 변경 --- src/main/java/zipdabang/server/base/Code.java | 4 ++ .../server/converter/MemberConverter.java | 61 +++++++++++++++++++ .../server/domain/member/Follow.java | 23 ++++++- .../server/domain/member/Member.java | 9 +++ .../memberRepositories/FollowRepository.java | 14 +++++ .../server/service/MemberService.java | 9 +++ .../serviceImpl/MemberServiceImpl.java | 41 +++++++++++++ .../validation/annotation/ExistMember.java | 17 ++++++ .../ExistMemberRequestBodyValidator.java | 37 +++++++++++ .../web/controller/MemberRestController.java | 49 ++++++++++++++- .../web/dto/requestDto/MemberRequestDto.java | 2 +- .../dto/responseDto/MemberResponseDto.java | 58 ++++++++++++++++++ src/main/resources/application.yml | 6 +- 13 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 src/main/java/zipdabang/server/repository/memberRepositories/FollowRepository.java create mode 100644 src/main/java/zipdabang/server/validation/annotation/ExistMember.java create mode 100644 src/main/java/zipdabang/server/validation/validator/ExistMemberRequestBodyValidator.java diff --git a/src/main/java/zipdabang/server/base/Code.java b/src/main/java/zipdabang/server/base/Code.java index 89e1073..f1a201a 100644 --- a/src/main/java/zipdabang/server/base/Code.java +++ b/src/main/java/zipdabang/server/base/Code.java @@ -90,7 +90,11 @@ public enum Code { ALREADY_BLOCKED_MEMBER(HttpStatus.OK, 4062, "이미 차단된 사용자입니다."), BLOCK_SELF(HttpStatus.OK, 4063, "자신을 차단할 수 없습니다."), + // BAD_REQUEST + TARGET_MEMBER_NOT_FOUND(HttpStatus.OK, 4064,"대상 사용자가 없습니다.."), + //FORBIDDEN + SELF_FOLLOW_FORBIDDEN(HttpStatus.OK, 4065, "스스로 팔로우는 안됩니다."), // market error diff --git a/src/main/java/zipdabang/server/converter/MemberConverter.java b/src/main/java/zipdabang/server/converter/MemberConverter.java index 851b2e8..e06484e 100644 --- a/src/main/java/zipdabang/server/converter/MemberConverter.java +++ b/src/main/java/zipdabang/server/converter/MemberConverter.java @@ -337,4 +337,65 @@ public static MemberResponseDto.PagingMemberListDto toPagingMemberListDto(Page followingMembers = owner.getFollowingList().stream() + .map(Follow::getTargetMember).collect(Collectors.toList()); + + return MemberResponseDto.FollowerInfoDto.builder() + .id(member.getMemberId()) + .caption(member.getCaption()) + .nickname(member.getNickname()) + .imageUrl(member.getProfileUrl()) + .isFollowing(followingMembers.contains(member)) + .build(); + } + + public static MemberResponseDto.FollowingListDto toFollowingListDto(Page followList){ + List followInfoDtoList = followList.stream() + .map(follow -> toFollowInfoDto(follow.getTargetMember())).collect(Collectors.toList()); + + return MemberResponseDto.FollowingListDto.builder() + .followingList(followInfoDtoList) + .isFirst(followList.isFirst()) + .isLast(followList.isLast()) + .totalPage(followList.getTotalPages()) + .totalElements(followList.getTotalElements()) + .currentPageElements(followList.getNumberOfElements()) + .build(); + } + + public static MemberResponseDto.FollowerListDto toFollowerListDto(Page followList, Member owner){ + List followerInfoDtoList = followList.stream() + .map(follow -> toFollowerInfoDto(follow.getFollowingMember(), owner)).collect(Collectors.toList()); + + return MemberResponseDto.FollowerListDto.builder() + .followerList(followerInfoDtoList) + .isFirst(followList.isFirst()) + .isLast(followList.isLast()) + .totalPage(followList.getTotalPages()) + .totalElements(followList.getTotalElements()) + .currentPageElements(followList.getNumberOfElements()) + .build(); + } } diff --git a/src/main/java/zipdabang/server/domain/member/Follow.java b/src/main/java/zipdabang/server/domain/member/Follow.java index 3d37723..55cbf1e 100644 --- a/src/main/java/zipdabang/server/domain/member/Follow.java +++ b/src/main/java/zipdabang/server/domain/member/Follow.java @@ -20,7 +20,28 @@ public class Follow extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + // follow 객체 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "target_id") - private Member member; + private Member targetMember; + + // follow 주체 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "following_id") + private Member followingMember; + + public void setTargetMember(Member targetMember){ + if (this.targetMember != null) + targetMember.getFollowerList().remove(this); + this.targetMember = targetMember; + targetMember.getFollowerList().add(this); + } + + public void setFollowingMember(Member followingMember){ + if (this.followingMember != null) + followingMember.getFollowerList().remove(this); + this.followingMember = followingMember; + followingMember.getFollowerList().add(this); + } + } diff --git a/src/main/java/zipdabang/server/domain/member/Member.java b/src/main/java/zipdabang/server/domain/member/Member.java index ec8a7e8..73e7f0a 100644 --- a/src/main/java/zipdabang/server/domain/member/Member.java +++ b/src/main/java/zipdabang/server/domain/member/Member.java @@ -43,6 +43,8 @@ public class Member extends BaseEntity { @Column(length = 18) private String phoneNum; + private String caption; + @Column(length = 6) private String birth; @@ -107,6 +109,13 @@ public class Member extends BaseEntity { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List inqueryList; + // 나를 따르는 = follow 테이블에서 targetMember가 나인, + @OneToMany(mappedBy = "targetMember", cascade = CascadeType.ALL) + private List followerList; + + // 내가 따르는 = follow 테이블에서 followingMember가 나인, + @OneToMany(mappedBy = "followingMember", cascade = CascadeType.ALL) + private List followingList; public void setProfileUrl(String profileUrl) { this.profileUrl = profileUrl; diff --git a/src/main/java/zipdabang/server/repository/memberRepositories/FollowRepository.java b/src/main/java/zipdabang/server/repository/memberRepositories/FollowRepository.java new file mode 100644 index 0000000..c51679e --- /dev/null +++ b/src/main/java/zipdabang/server/repository/memberRepositories/FollowRepository.java @@ -0,0 +1,14 @@ +package zipdabang.server.repository.memberRepositories; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import zipdabang.server.domain.member.Follow; +import zipdabang.server.domain.member.Member; + +public interface FollowRepository extends JpaRepository { + + Page findAllByTargetMember(Member member, PageRequest pageRequest); + Page findAllByFollowingMember(Member member, PageRequest pageRequest); +} diff --git a/src/main/java/zipdabang/server/service/MemberService.java b/src/main/java/zipdabang/server/service/MemberService.java index 290786b..eefaa7d 100644 --- a/src/main/java/zipdabang/server/service/MemberService.java +++ b/src/main/java/zipdabang/server/service/MemberService.java @@ -3,6 +3,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import zipdabang.server.domain.Category; +import zipdabang.server.domain.member.Follow; import zipdabang.server.domain.member.Inquery; import zipdabang.server.domain.enums.DeregisterType; import zipdabang.server.domain.member.Member; @@ -22,6 +23,8 @@ public interface MemberService { Optional checkExistNickname(String nickname); + Optional findMemberById(Long id); + public void existByPhoneNumber(String phoneNum); OAuthJoin.OAuthJoinDto joinInfoComplete(MemberRequestDto.MemberInfoDto request, String type); @@ -56,4 +59,10 @@ public interface MemberService { public void blockMember(Member owner, Long blocked); public void unblockMember(Member owner, Long blockedId); public Page findBlockedMember(Integer page, Member member); + + public Follow createFollow(Long targetId, Member member); + + Page findFollowing(Member member, Integer page); + + Page findFollower(Member member, Integer page); } diff --git a/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java b/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java index 770296b..6e17d99 100644 --- a/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java +++ b/src/main/java/zipdabang/server/service/serviceImpl/MemberServiceImpl.java @@ -76,6 +76,8 @@ public class MemberServiceImpl implements MemberService { private final DeregisterReasonRepository deregisterReasonRepository; private final BlockedMemberRepository blockedMemberRepository; + private final FollowRepository followRepository; + @Value("${paging.size}") private Integer pageSize; @@ -120,6 +122,11 @@ public Optional checkExistNickname(String nickname){ return memberRepository.findByNickname(nickname); } + @Override + public Optional findMemberById(Long id) { + return memberRepository.findById(id); + } + @Override public void existByPhoneNumber(String phoneNum) { if (memberRepository.existsByPhoneNum(phoneNum)) { @@ -356,6 +363,40 @@ public Page findBlockedMember(Integer page, Member member) { return blockedMembers; } + + @Override + @Transactional + public Follow createFollow(Long targetId, Member member) { + + if(targetId.equals(member.getMemberId())) + throw new MemberException(Code.SELF_FOLLOW_FORBIDDEN); + Follow follow = MemberConverter.toFollow(); + follow.setFollowingMember(member); + follow.setTargetMember(memberRepository.findById(targetId).get()); + return followRepository.save(follow); + } + + @Override + public Page findFollowing(Member member, Integer page) { + page -= 1; + Page followingMember = followRepository.findAllByFollowingMember(member, PageRequest.of(page, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))); + + if(followingMember.getTotalPages() <= page) + throw new MemberException(Code.OVER_PAGE_INDEX_ERROR); + + return followingMember; + } + + @Override + public Page findFollower(Member member, Integer page) { + page -= 1; + Page followerMember = followRepository.findAllByTargetMember(member, PageRequest.of(page, pageSize, Sort.by(Sort.Direction.DESC, "createdAt"))); + + if(followerMember.getTotalPages() <= page) + throw new MemberException(Code.OVER_PAGE_INDEX_ERROR); + + return followerMember; + } } diff --git a/src/main/java/zipdabang/server/validation/annotation/ExistMember.java b/src/main/java/zipdabang/server/validation/annotation/ExistMember.java new file mode 100644 index 0000000..7dd28db --- /dev/null +++ b/src/main/java/zipdabang/server/validation/annotation/ExistMember.java @@ -0,0 +1,17 @@ +package zipdabang.server.validation.annotation; + +import zipdabang.server.validation.validator.ExistMemberRequestBodyValidator; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Documented +@Constraint(validatedBy = ExistMemberRequestBodyValidator.class) +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExistMember { + String message() default "해당하는 유저가 존재하지 않습니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/zipdabang/server/validation/validator/ExistMemberRequestBodyValidator.java b/src/main/java/zipdabang/server/validation/validator/ExistMemberRequestBodyValidator.java new file mode 100644 index 0000000..94935da --- /dev/null +++ b/src/main/java/zipdabang/server/validation/validator/ExistMemberRequestBodyValidator.java @@ -0,0 +1,37 @@ +package zipdabang.server.validation.validator; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import zipdabang.server.base.Code; +import zipdabang.server.domain.member.Member; +import zipdabang.server.service.MemberService; +import zipdabang.server.validation.annotation.ExistMember; +import zipdabang.server.web.dto.requestDto.MemberRequestDto; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class ExistMemberRequestBodyValidator implements ConstraintValidator{ + + private final MemberService memberService; + + @Override + public void initialize(ExistMember constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(Long value, ConstraintValidatorContext context) { + + Optional memberById = memberService.findMemberById(value); + if(memberById.isEmpty()) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(Code.TARGET_MEMBER_NOT_FOUND.toString()).addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/zipdabang/server/web/controller/MemberRestController.java b/src/main/java/zipdabang/server/web/controller/MemberRestController.java index 6887d5b..34146fd 100644 --- a/src/main/java/zipdabang/server/web/controller/MemberRestController.java +++ b/src/main/java/zipdabang/server/web/controller/MemberRestController.java @@ -24,9 +24,9 @@ import zipdabang.server.base.Code; import zipdabang.server.base.ResponseDto; import zipdabang.server.base.exception.handler.MemberException; -import zipdabang.server.base.exception.handler.RecipeException; import zipdabang.server.converter.MemberConverter; import zipdabang.server.domain.Category; +import zipdabang.server.domain.member.Follow; import zipdabang.server.domain.member.Inquery; import zipdabang.server.domain.member.Member; import zipdabang.server.redis.domain.RefreshToken; @@ -37,6 +37,7 @@ import zipdabang.server.validation.annotation.CheckPage; import zipdabang.server.validation.annotation.CheckTempMember; import zipdabang.server.validation.annotation.CheckDeregister; +import zipdabang.server.validation.annotation.ExistMember; import zipdabang.server.web.dto.requestDto.MemberRequestDto; import zipdabang.server.web.dto.responseDto.MemberResponseDto; @@ -410,4 +411,50 @@ else if (page < 1) Page blockedMembers = memberService.findBlockedMember(page, member); return ResponseDto.of(MemberConverter.toPagingMemberListDto(blockedMembers)); } + + + @Operation(summary = "🎪팔로우하기 API", description = "팔로우하기 API 입니다.") + @PostMapping("/members/followings/{targetId}") + @Parameters({ + @Parameter(name = "member", hidden = true) + }) + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK 성공"), + @ApiResponse(responseCode = "4064", description = "BAD_REQEUST , 팔로우하려는 대상이 없음", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4065", description = "FORBIDDEN , 스스로는 팔로우가 안됩니다", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + }) + public ResponseDto followMember(@CheckTempMember @AuthMember Member member, @ExistMember @PathVariable(name = "targetId") Long targetId){ + Follow follow = memberService.createFollow(targetId, member); + return ResponseDto.of(MemberConverter.toFollowingResultDto(follow)); + } + + @Operation(summary = "🎪팔로우중인 사용자 조회 API", description = "팔로우중인 사용자 조회 API 입니다. 페이지 주세요") + @GetMapping("/members/followings") + @Parameters({ + @Parameter(name = "member", hidden = true) + }) + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK 성공"), + @ApiResponse(responseCode = "4054", description = "BAD_REQUEST , 페이지 번호가 없거나 0 이하", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4055", description = "BAD_REQUEST , 페이지 번호가 초과함", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + }) + public ResponseDto getFollowingMember(@CheckPage Integer page, @CheckTempMember @AuthMember Member member){ + Page following = memberService.findFollowing(member, page); + return ResponseDto.of(MemberConverter.toFollowingListDto(following)); + } + + @Operation(summary = "🎪나를 팔로잉 하는 사용자 조회 API", description = "나를 팔로잉 하는 사용자 조회 API 입니다. 페이지 주세요") + @GetMapping("/members/followers") + @Parameters({ + @Parameter(name = "member", hidden = true) + }) + @ApiResponses({ + @ApiResponse(responseCode = "2000", description = "OK 성공"), + @ApiResponse(responseCode = "4054", description = "BAD_REQUEST , 페이지 번호가 없거나 0 이하", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + @ApiResponse(responseCode = "4055", description = "BAD_REQUEST , 페이지 번호가 초과함", content = @Content(schema = @Schema(implementation = ResponseDto.class))), + }) + public ResponseDto getFollowerMember(@CheckPage Integer page, @CheckTempMember @AuthMember Member member){ + Page follower = memberService.findFollower(member, page); + return ResponseDto.of(MemberConverter.toFollowerListDto(follower, member)); + } } diff --git a/src/main/java/zipdabang/server/web/dto/requestDto/MemberRequestDto.java b/src/main/java/zipdabang/server/web/dto/requestDto/MemberRequestDto.java index 45f6dac..d43bbef 100644 --- a/src/main/java/zipdabang/server/web/dto/requestDto/MemberRequestDto.java +++ b/src/main/java/zipdabang/server/web/dto/requestDto/MemberRequestDto.java @@ -5,13 +5,13 @@ import javax.persistence.EnumType; import javax.persistence.Enumerated; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.util.List; import org.springframework.lang.Nullable; import org.springframework.web.multipart.MultipartFile; import zipdabang.server.domain.enums.DeregisterType; -import zipdabang.server.web.dto.responseDto.MemberResponseDto; public class MemberRequestDto { diff --git a/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java b/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java index 4cc6033..1048475 100644 --- a/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java +++ b/src/main/java/zipdabang/server/web/dto/responseDto/MemberResponseDto.java @@ -210,4 +210,62 @@ public static class PagingMemberListDto { Boolean isLast; } + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FollowingResultDto{ + Long targetId; + LocalDateTime followAt; + } + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FollowInfoDto{ + Long id; + String nickname; + String imageUrl; + String caption; + } + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FollowerInfoDto{ + Long id; + String nickname; + String imageUrl; + String caption; + Boolean isFollowing; + } + + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FollowingListDto{ + List followingList; + Long totalElements; + Integer currentPageElements; + Integer totalPage; + Boolean isFirst; + Boolean isLast; + } + + @Builder + @Getter + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @NoArgsConstructor(access = AccessLevel.PROTECTED) + public static class FollowerListDto{ + List followerList; + Long totalElements; + Integer currentPageElements; + Integer totalPage; + Boolean isFirst; + Boolean isLast; + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e50765b..fbf03ab 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -14,9 +14,11 @@ spring: ## # local redis # redis: # host: localhost - redis: - host: zipdabang-redis.osattk.ng.0001.apn2.cache.amazonaws.com + briefing-redis.bjyb5r.ng.0001.apne1.cache.amazonaws.com + +# redis: +# host: zipdabang-redis.osattk.ng.0001.apn2.cache.amazonaws.com batch: jdbc: initialize-schema: always