diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java index bdb5bc29..a4f0e82e 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/controller/CoordinateController.java @@ -17,6 +17,7 @@ import org.clokey.global.paging.SortDirection; import org.clokey.response.BaseResponse; import org.clokey.response.SliceResponse; +import org.springframework.beans.TypeMismatchException; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -155,13 +156,23 @@ public BaseResponse toggleCoordinateLike(@PathVariable Long coordinateId) return BaseResponse.onSuccess(GlobalBaseSuccessCode.NO_CONTENT, null); } - @GetMapping("/my-favorites") + @GetMapping("/favorites") @Operation( operationId = "Coordinate_getFavoriteCoordinates", - summary = "나의 최애 코디 조회", - description = "나의 최애 코디를 조회하는 API입니다.") - public BaseResponse> getFavoriteCoordinates() { - List response = coordinateService.getFavoriteCoordinates(); + summary = "최애 코디 조회", + description = "최애 코디를 조회하는 API입니다.") + public BaseResponse> getFavoriteCoordinates( + @RequestParam(required = false) String memberId) { + Long parsedMemberId = null; + if (memberId != null && !memberId.isBlank()) { + try { + parsedMemberId = Long.valueOf(memberId); + } catch (NumberFormatException e) { + throw new TypeMismatchException(memberId, Long.class, e); + } + } + List response = + coordinateService.getFavoriteCoordinates(parsedMemberId); return BaseResponse.onSuccess(GlobalBaseSuccessCode.OK, response); } } diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java index c616509a..53a3d576 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateService.java @@ -34,5 +34,5 @@ SliceResponse getDailyCoordinates( void toggleCoordinateLike(Long coordinateId); - List getFavoriteCoordinates(); + List getFavoriteCoordinates(Long memberId); } diff --git a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java index 914ee78a..3260c383 100644 --- a/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/coordinate/service/CoordinateServiceImpl.java @@ -24,11 +24,15 @@ import org.clokey.domain.image.event.ImageDeleteEvent; import org.clokey.domain.lookbook.exception.LookBookErrorCode; import org.clokey.domain.lookbook.repository.LookBookRepository; +import org.clokey.domain.member.exception.MemberErrorCode; +import org.clokey.domain.member.repository.BlockRepository; +import org.clokey.domain.member.repository.MemberRepository; import org.clokey.exception.BaseCustomException; import org.clokey.global.paging.SortDirection; import org.clokey.global.util.MemberUtil; import org.clokey.lookbook.entity.LookBook; import org.clokey.member.entity.Member; +import org.clokey.member.enums.Visibility; import org.clokey.response.SliceResponse; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Slice; @@ -46,6 +50,8 @@ public class CoordinateServiceImpl implements CoordinateService { private final CoordinateRepository coordinateRepository; private final ClothRepository clothRepository; private final LookBookRepository lookBookRepository; + private final MemberRepository memberRepository; + private final BlockRepository blockRepository; private final CoordinateClothRepository coordinateClothRepository; private final RedisTemplate redisTemplate; @@ -373,11 +379,13 @@ public void toggleCoordinateLike(Long coordinateId) { } @Override - public List getFavoriteCoordinates() { + public List getFavoriteCoordinates(Long memberId) { final Member currentMember = memberUtil.getCurrentMember(); + final Long targetMemberId = + resolveFavoriteCoordinateTargetMemberId(currentMember, memberId); List favoriteCoordinates = - coordinateRepository.findLikedCoordinatesByMemberId(currentMember.getId()); + coordinateRepository.findLikedCoordinatesByMemberId(targetMemberId); if (favoriteCoordinates.isEmpty()) { return List.of(); @@ -386,6 +394,23 @@ public List getFavoriteCoordinates() { return favoriteCoordinates.stream().map(FavoriteCoordinateResponse::from).toList(); } + private Long resolveFavoriteCoordinateTargetMemberId(Member currentMember, Long memberId) { + if (memberId == null || memberId.equals(currentMember.getId())) { + return currentMember.getId(); + } + + Member targetMember = + memberRepository + .findById(memberId) + .orElseThrow( + () -> new BaseCustomException(MemberErrorCode.MEMBER_NOT_FOUND)); + + validatePublicMember(targetMember); + validateNotBlocked(currentMember.getId(), targetMember.getId()); + + return targetMember.getId(); + } + private void validateAllClothesExist(List clothIds, Map clothMap) { boolean hasMissing = clothIds.stream().anyMatch(clothId -> !clothMap.containsKey(clothId)); @@ -469,6 +494,19 @@ private void validateCoordinateLikeLimit(Long memberId, Coordinate coordinate) { } } + private void validatePublicMember(Member member) { + if (member.getVisibility().equals(Visibility.PRIVATE)) { + throw new BaseCustomException(MemberErrorCode.PRIVATE_MEMBER_ACCESS_DENIED); + } + } + + private void validateNotBlocked(Long currentMemberId, Long targetMemberId) { + if (blockRepository.existsByBlockerIdAndBlockedIdOrBlockerIdAndBlockedId( + currentMemberId, targetMemberId, targetMemberId, currentMemberId)) { + throw new BaseCustomException(MemberErrorCode.BLOCKED_MEMBER_ACCESS_DENIED); + } + } + private LookBook getLookBookById(Long lookBookId) { return lookBookRepository .findById(lookBookId) diff --git a/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java b/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java index 57f58e69..1fa39c04 100644 --- a/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java +++ b/clokey-api/src/main/java/org/clokey/domain/member/service/MemberServiceImpl.java @@ -148,6 +148,10 @@ public MemberInfoResponse getMemberInfo(Long memberId) { Member currentMember = memberUtil.getCurrentMember(); Member targetMember = getMemberById(memberId); + if (!currentMember.getId().equals(targetMember.getId())) { + validateBlockedMutual(currentMember.getId(), targetMember.getId()); + } + return memberRepository.findMemberInfoById(currentMember.getId(), memberId); } @@ -250,6 +254,13 @@ private void validateBlocked(Member currentMember, Member targetMember) { } } + private void validateBlockedMutual(Long currentMemberId, Long targetMemberId) { + if (blockRepository.existsByBlockerIdAndBlockedIdOrBlockerIdAndBlockedId( + currentMemberId, targetMemberId, targetMemberId, currentMemberId)) { + throw new BaseCustomException(MemberErrorCode.BLOCKED_MEMBER_ACCESS_DENIED); + } + } + private void validateFollowMyself(Member followFrom, Member followTo) { if (followFrom.getId().equals(followTo.getId())) { throw new BaseCustomException(MemberErrorCode.CANNOT_FOLLOW_MYSELF); diff --git a/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java b/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java index d4ba629b..3ca27dfb 100644 --- a/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/coordinate/controller/CoordinateControllerTest.java @@ -1650,10 +1650,10 @@ class 최애_코디_조회_요청_시 { new FavoriteCoordinateResponse( 2L, "testImageUrl2", "testCoordinateName2")); - given(coordinateService.getFavoriteCoordinates()).willReturn(response); + given(coordinateService.getFavoriteCoordinates(null)).willReturn(response); // when & then - ResultActions perform = mockMvc.perform(get("/coordinate/my-favorites")); + ResultActions perform = mockMvc.perform(get("/coordinate/favorites")); perform.andExpect(status().isOk()) .andExpect(jsonPath("$.isSuccess").value(true)) diff --git a/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java b/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java index ffda8002..5176eabe 100644 --- a/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/coordinate/service/CoordinateServiceImplTest.java @@ -31,11 +31,14 @@ import org.clokey.domain.image.event.ImageDeleteEvent; import org.clokey.domain.lookbook.exception.LookBookErrorCode; import org.clokey.domain.lookbook.repository.LookBookRepository; +import org.clokey.domain.member.exception.MemberErrorCode; +import org.clokey.domain.member.repository.BlockRepository; import org.clokey.domain.member.repository.MemberRepository; import org.clokey.exception.BaseCustomException; import org.clokey.global.paging.SortDirection; import org.clokey.global.util.MemberUtil; import org.clokey.lookbook.entity.LookBook; +import org.clokey.member.entity.Block; import org.clokey.member.entity.Member; import org.clokey.member.entity.OauthInfo; import org.clokey.member.enums.OauthProvider; @@ -56,6 +59,7 @@ class CoordinateServiceImplTest extends IntegrationTest { @Autowired private CoordinateRepository coordinateRepository; @Autowired private MemberRepository memberRepository; + @Autowired private BlockRepository blockRepository; @Autowired private CoordinateClothRepository coordinateClothRepository; @Autowired private CategoryRepository categoryRepository; @Autowired private ClothRepository clothRepository; @@ -1944,7 +1948,8 @@ void setUp() { @Test void 유효한_요청이면_좋아요한_코디를_반환한다() { // when - List responses = coordinateService.getFavoriteCoordinates(); + List responses = + coordinateService.getFavoriteCoordinates(null); // then assertThat(responses) @@ -1960,11 +1965,51 @@ void setUp() { given(memberUtil.getCurrentMember()).willReturn(member); // when - List responses = coordinateService.getFavoriteCoordinates(); + List responses = + coordinateService.getFavoriteCoordinates(null); // then assertThat(responses).isEmpty(); } + + @Test + void memberId를_전달하면_해당_회원의_좋아요한_코디를_반환한다() { + // when + List responses = + coordinateService.getFavoriteCoordinates(1L); + + // then + assertThat(responses) + .extracting("coordinateId", "imageUrl", "coordinateName") + .containsExactly( + tuple(1L, "testUrl1", "testName1"), tuple(2L, "testUrl2", "testName2")); + } + + @Test + void 비공개_계정의_memberId로_조회하면_예외가_발생한다() { + // given + Member member = memberRepository.findById(2L).orElseThrow(); + member.changeVisibility(); + memberRepository.saveAndFlush(member); + + // when & then + assertThatThrownBy(() -> coordinateService.getFavoriteCoordinates(2L)) + .isInstanceOf(BaseCustomException.class) + .hasMessage(MemberErrorCode.PRIVATE_MEMBER_ACCESS_DENIED.getMessage()); + } + + @Test + void 차단_관계의_memberId로_조회하면_예외가_발생한다() { + // given + Member currentMember = memberRepository.findById(1L).orElseThrow(); + Member targetMember = memberRepository.findById(2L).orElseThrow(); + blockRepository.save(Block.createBlock(currentMember, targetMember)); + + // when & then + assertThatThrownBy(() -> coordinateService.getFavoriteCoordinates(2L)) + .isInstanceOf(BaseCustomException.class) + .hasMessage(MemberErrorCode.BLOCKED_MEMBER_ACCESS_DENIED.getMessage()); + } } @Nested diff --git a/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java b/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java index 1a751332..c6f22465 100644 --- a/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java +++ b/clokey-api/src/test/java/org/clokey/domain/member/service/MemberServiceTest.java @@ -787,6 +787,27 @@ void setUp() { .isInstanceOf(BaseCustomException.class) .hasMessage(MemberErrorCode.MEMBER_NOT_FOUND.getMessage()); } + + @Test + void 내가_차단한_회원의_정보를_조회하면_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> memberService.getMemberInfo(3L)) + .isInstanceOf(BaseCustomException.class) + .hasMessage(MemberErrorCode.BLOCKED_MEMBER_ACCESS_DENIED.getMessage()); + } + + @Test + void 나를_차단한_회원의_정보를_조회하면_예외가_발생한다() { + // given + Member currentMember = memberRepository.findById(1L).orElseThrow(); + Member targetMember = memberRepository.findById(2L).orElseThrow(); + blockRepository.save(Block.createBlock(targetMember, currentMember)); + + // when & then + assertThatThrownBy(() -> memberService.getMemberInfo(2L)) + .isInstanceOf(BaseCustomException.class) + .hasMessage(MemberErrorCode.BLOCKED_MEMBER_ACCESS_DENIED.getMessage()); + } } @Nested