diff --git a/src/main/java/com/clokey/server/domain/follow/dao/FollowRepository.java b/src/main/java/com/clokey/server/domain/follow/dao/FollowRepository.java index c2225b78..45dd605d 100644 --- a/src/main/java/com/clokey/server/domain/follow/dao/FollowRepository.java +++ b/src/main/java/com/clokey/server/domain/follow/dao/FollowRepository.java @@ -3,8 +3,12 @@ import com.clokey.server.domain.model.mapping.Follow; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface FollowRepository extends JpaRepository { boolean existsByFollowing_IdAndFollowed_Id(Long followingId, Long followedId); + + Optional findByFollowing_IdAndFollowed_Id(Long followingId, Long followedId); } diff --git a/src/main/java/com/clokey/server/domain/member/api/MemberRestController.java b/src/main/java/com/clokey/server/domain/member/api/MemberRestController.java index 0912b9e1..70d5f7cd 100644 --- a/src/main/java/com/clokey/server/domain/member/api/MemberRestController.java +++ b/src/main/java/com/clokey/server/domain/member/api/MemberRestController.java @@ -1,15 +1,16 @@ package com.clokey.server.domain.member.api; +import com.clokey.server.domain.member.application.FollowCommandService; import com.clokey.server.domain.member.application.GetUserQueryService; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; import com.clokey.server.domain.member.application.ProfileCommandService; import com.clokey.server.domain.member.exception.annotation.IdExist; import com.clokey.server.domain.member.exception.annotation.IdValid; +import com.clokey.server.domain.member.exception.annotation.NotFollowMyself; import com.clokey.server.global.common.response.BaseResponse; import com.clokey.server.global.error.code.status.SuccessStatus; import io.swagger.v3.oas.annotations.Operation; import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -21,14 +22,15 @@ public class MemberRestController { private final ProfileCommandService profileCommandService; private final GetUserQueryService getUserQueryService; + private final FollowCommandService followCommandService; @Operation(summary = "프로필 수정 API", description = "사용자의 프로필 정보를 수정하는 API입니다.") @PatchMapping("users/{user_id}/profile") - public BaseResponse updateProfile( + public BaseResponse updateProfile( @PathVariable("user_id") Long userId, - @RequestBody @Valid MemberResponseDTO.ProfileRQ request) { + @RequestBody @Valid MemberDTO.ProfileRQ request) { - MemberResponseDTO.ProfileRP response = profileCommandService.updateProfile(userId, request); + MemberDTO.ProfileRP response = profileCommandService.updateProfile(userId, request); return BaseResponse.onSuccess(SuccessStatus.MEMBER_ACTION_SUCCESS, response); } @@ -47,10 +49,34 @@ public BaseResponse checkID( public BaseResponse getUser( @IdValid @PathVariable("clokey_id") String clokeyId) { - MemberResponseDTO.GetUserRP response = getUserQueryService.getUser(clokeyId); + MemberDTO.GetUserRP response = getUserQueryService.getUser(clokeyId); return BaseResponse.onSuccess(SuccessStatus.MEMBER_SUCCESS, response); } + + @Operation(summary = "팔로우 조회 API", description = "내가 다른 사용자를 팔로우하고있는지 확인하는 API입니다.") + @PostMapping("users/follow/check") + public BaseResponse followCheck( + @RequestBody @Valid MemberDTO.FollowRQ request){ + + MemberDTO.FollowRP response= followCommandService.followCheck(request); + + return BaseResponse.onSuccess(SuccessStatus.MEMBER_SUCCESS, response); + } + + + + @Operation(summary = "팔로우 API", description = "다른 사용자를 팔로우/언팔로우하는 API입니다. 호출시마다 기존 상태와 반대로 변경됩니다.") + @PostMapping("users/follow") + public BaseResponse follow( + @NotFollowMyself @RequestBody @Valid MemberDTO.FollowRQ request) { + + followCommandService.follow(request); + + return BaseResponse.onSuccess(SuccessStatus.MEMBER_ACTION_SUCCESS, null); + } + + } diff --git a/src/main/java/com/clokey/server/domain/member/application/FollowCommandService.java b/src/main/java/com/clokey/server/domain/member/application/FollowCommandService.java new file mode 100644 index 00000000..a7b574f1 --- /dev/null +++ b/src/main/java/com/clokey/server/domain/member/application/FollowCommandService.java @@ -0,0 +1,10 @@ +package com.clokey.server.domain.member.application; + +import com.clokey.server.domain.member.dto.MemberDTO; + +public interface FollowCommandService { + void follow(MemberDTO.FollowRQ request); + + MemberDTO.FollowRP followCheck(MemberDTO.FollowRQ request); +} + diff --git a/src/main/java/com/clokey/server/domain/member/application/FollowCommandServiceImpl.java b/src/main/java/com/clokey/server/domain/member/application/FollowCommandServiceImpl.java new file mode 100644 index 00000000..f11113ad --- /dev/null +++ b/src/main/java/com/clokey/server/domain/member/application/FollowCommandServiceImpl.java @@ -0,0 +1,58 @@ +package com.clokey.server.domain.member.application; + +import com.clokey.server.domain.follow.dao.FollowRepository; +import com.clokey.server.domain.member.dto.MemberDTO; +import com.clokey.server.domain.model.Member; +import com.clokey.server.domain.model.mapping.Follow; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Service +@RequiredArgsConstructor +public class FollowCommandServiceImpl implements FollowCommandService { + + private final MemberRepositoryService memberRepositoryService; + private final FollowRepository followRepository; + + @Override + public MemberDTO.FollowRP followCheck(MemberDTO.FollowRQ request) { + Long myUserId = memberRepositoryService.findMemberByClokeyId(request.getMyClokeyId()).getId(); + Long yourUserId = memberRepositoryService.findMemberByClokeyId(request.getYourClokeyId()).getId(); + + boolean isFollow = followRepository.existsByFollowing_IdAndFollowed_Id(myUserId, yourUserId); + + return new MemberDTO.FollowRP(isFollow); + } + + @Override + @Transactional + public void follow(MemberDTO.FollowRQ request) { + // myClokeyId로 사용자 조회 + Long myUserId = memberRepositoryService.findMemberByClokeyId(request.getMyClokeyId()).getId(); + Long yourUserId = memberRepositoryService.findMemberByClokeyId(request.getYourClokeyId()).getId(); + + // 팔로우 관계가 존재하는지 확인 + boolean isFollow = followRepository.existsByFollowing_IdAndFollowed_Id(myUserId, yourUserId); + + if (isFollow) { + // 팔로우가 이미 존재하면 언팔로우 처리 + Follow follow = followRepository.findByFollowing_IdAndFollowed_Id(myUserId, yourUserId) + .orElseThrow(() -> new IllegalStateException("팔로우 관계가 존재하지 않습니다.")); + + // 팔로우 삭제 (언팔로우) + followRepository.delete(follow); + } else { + // 팔로우가 존재하지 않으면 팔로우 처리 + Follow follow = Follow.builder() + .following(memberRepositoryService.findMemberById(myUserId)) + .followed(memberRepositoryService.findMemberById(yourUserId)) + .build(); + + // 팔로우 저장 + followRepository.save(follow); + } + } + +} diff --git a/src/main/java/com/clokey/server/domain/member/application/GetUserQueryService.java b/src/main/java/com/clokey/server/domain/member/application/GetUserQueryService.java index 58051c2c..81addbc3 100644 --- a/src/main/java/com/clokey/server/domain/member/application/GetUserQueryService.java +++ b/src/main/java/com/clokey/server/domain/member/application/GetUserQueryService.java @@ -1,9 +1,9 @@ package com.clokey.server.domain.member.application; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; public interface GetUserQueryService { - MemberResponseDTO.GetUserRP getUser(String clokeyId); + MemberDTO.GetUserRP getUser(String clokeyId); } diff --git a/src/main/java/com/clokey/server/domain/member/application/GetUserQueryServiceImpl.java b/src/main/java/com/clokey/server/domain/member/application/GetUserQueryServiceImpl.java index 6fbac83e..d4834c28 100644 --- a/src/main/java/com/clokey/server/domain/member/application/GetUserQueryServiceImpl.java +++ b/src/main/java/com/clokey/server/domain/member/application/GetUserQueryServiceImpl.java @@ -1,7 +1,7 @@ package com.clokey.server.domain.member.application; import com.clokey.server.domain.member.converter.GetUserConverter; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; import com.clokey.server.domain.model.Member; import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; @@ -21,8 +21,8 @@ public class GetUserQueryServiceImpl implements GetUserQueryService { private EntityManager entityManager; @Override - @Transactional - public MemberResponseDTO.GetUserRP getUser(String clokeyId) { + @Transactional(readOnly = true) // 트랜잭션 읽기 전용으로 설정 + public MemberDTO.GetUserRP getUser(String clokeyId) { Member member = memberRepositoryService.findMemberByClokeyId(clokeyId); Long recordCount = countHistoryByMember(member); @@ -32,21 +32,24 @@ public MemberResponseDTO.GetUserRP getUser(String clokeyId) { return GetUserConverter.toGetUserResponseDTO(member, recordCount, followerCount, followingCount); } - private Long countHistoryByMember(Member member) { + @Transactional(readOnly = true) // 트랜잭션 읽기 전용으로 설정 + public Long countHistoryByMember(Member member) { String jpql = "SELECT COUNT(h) FROM History h WHERE h.member = :member"; TypedQuery query = entityManager.createQuery(jpql, Long.class); query.setParameter("member", member); return query.getSingleResult(); } - private Long countFollowersByMember(Member member) { + @Transactional(readOnly = true) // 트랜잭션 읽기 전용으로 설정 + public Long countFollowersByMember(Member member) { String jpql = "SELECT COUNT(f) FROM Follow f WHERE f.followed = :member"; TypedQuery query = entityManager.createQuery(jpql, Long.class); query.setParameter("member", member); return query.getSingleResult(); } - private Long countFollowingByMember(Member member) { + @Transactional(readOnly = true) // 트랜잭션 읽기 전용으로 설정 + public Long countFollowingByMember(Member member) { String jpql = "SELECT COUNT(f) FROM Follow f WHERE f.following = :member"; TypedQuery query = entityManager.createQuery(jpql, Long.class); query.setParameter("member", member); @@ -56,3 +59,4 @@ private Long countFollowingByMember(Member member) { + diff --git a/src/main/java/com/clokey/server/domain/member/application/MemberRepositoryServiceImpl.java b/src/main/java/com/clokey/server/domain/member/application/MemberRepositoryServiceImpl.java index 0401a630..b61006cb 100644 --- a/src/main/java/com/clokey/server/domain/member/application/MemberRepositoryServiceImpl.java +++ b/src/main/java/com/clokey/server/domain/member/application/MemberRepositoryServiceImpl.java @@ -10,6 +10,7 @@ import jakarta.persistence.TypedQuery; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @@ -23,28 +24,32 @@ public class MemberRepositoryServiceImpl implements MemberRepositoryService { private EntityManager entityManager; @Override + @Transactional(readOnly = true) // 읽기 전용 트랜잭션 public boolean memberExist(Long memberId) { return memberRepository.existsById(memberId); } @Override + @Transactional(readOnly = true) // 읽기 전용 트랜잭션 public Optional getMember(Long memberId) { return memberRepository.findById(memberId); } - @Override + @Transactional(readOnly = true) // 읽기 전용 트랜잭션 public Member findMemberById(Long memberId) { return memberRepository.findById(memberId) .orElseThrow(() -> new MemberException(ErrorStatus.NO_SUCH_MEMBER)); } @Override + @Transactional // 쓰기 트랜잭션 public Member saveMember(Member member) { return memberRepository.save(member); } @Override + @Transactional(readOnly = true) // 읽기 전용 트랜잭션 public boolean idExist(String clokeyId) { String jpql = "SELECT COUNT(m) > 0 FROM Member m WHERE m.clokeyId = :clokeyId"; TypedQuery query = entityManager.createQuery(jpql, Boolean.class); @@ -53,6 +58,7 @@ public boolean idExist(String clokeyId) { } @Override + @Transactional(readOnly = true) // 읽기 전용 트랜잭션 public Member findMemberByClokeyId(String clokeyId) { String jpql = "SELECT m FROM Member m WHERE m.clokeyId = :clokeyId"; TypedQuery query = entityManager.createQuery(jpql, Member.class); @@ -62,5 +68,4 @@ public Member findMemberByClokeyId(String clokeyId) { .findFirst() .orElseThrow(() -> new IllegalArgumentException("클로키 아이디에 해당하는 사용자가 없습니다.")); } - -} \ No newline at end of file +} diff --git a/src/main/java/com/clokey/server/domain/member/application/ProfileCommandService.java b/src/main/java/com/clokey/server/domain/member/application/ProfileCommandService.java index 57761c95..68db403a 100644 --- a/src/main/java/com/clokey/server/domain/member/application/ProfileCommandService.java +++ b/src/main/java/com/clokey/server/domain/member/application/ProfileCommandService.java @@ -1,9 +1,9 @@ package com.clokey.server.domain.member.application; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; public interface ProfileCommandService { - MemberResponseDTO.ProfileRP updateProfile(Long userId, MemberResponseDTO.ProfileRQ request); + MemberDTO.ProfileRP updateProfile(Long userId, MemberDTO.ProfileRQ request); } diff --git a/src/main/java/com/clokey/server/domain/member/application/ProfileCommandServiceImpl.java b/src/main/java/com/clokey/server/domain/member/application/ProfileCommandServiceImpl.java index 980723c6..da0c1155 100644 --- a/src/main/java/com/clokey/server/domain/member/application/ProfileCommandServiceImpl.java +++ b/src/main/java/com/clokey/server/domain/member/application/ProfileCommandServiceImpl.java @@ -1,7 +1,7 @@ package com.clokey.server.domain.member.application; import com.clokey.server.domain.member.converter.ProfileConverter; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; import com.clokey.server.domain.model.Member; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,7 +15,7 @@ public class ProfileCommandServiceImpl implements ProfileCommandService { @Override @Transactional - public MemberResponseDTO.ProfileRP updateProfile(Long userId, MemberResponseDTO.ProfileRQ request) { + public MemberDTO.ProfileRP updateProfile(Long userId, MemberDTO.ProfileRQ request) { // 사용자 확인 Member member = memberRepositoryService.findMemberById(userId); diff --git a/src/main/java/com/clokey/server/domain/member/converter/GetUserConverter.java b/src/main/java/com/clokey/server/domain/member/converter/GetUserConverter.java index 76bfaaeb..25a67d42 100644 --- a/src/main/java/com/clokey/server/domain/member/converter/GetUserConverter.java +++ b/src/main/java/com/clokey/server/domain/member/converter/GetUserConverter.java @@ -1,12 +1,12 @@ package com.clokey.server.domain.member.converter; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; import com.clokey.server.domain.model.Member; public class GetUserConverter { - public static MemberResponseDTO.GetUserRP toGetUserResponseDTO(Member member, Long recordCount, Long followerCount, Long followingCount) { - return MemberResponseDTO.GetUserRP.builder() + public static MemberDTO.GetUserRP toGetUserResponseDTO(Member member, Long recordCount, Long followerCount, Long followingCount) { + return MemberDTO.GetUserRP.builder() .clokeyId(member.getClokeyId()) .profileImageUrl(member.getProfileImageUrl()) .recordCount(recordCount) diff --git a/src/main/java/com/clokey/server/domain/member/converter/ProfileConverter.java b/src/main/java/com/clokey/server/domain/member/converter/ProfileConverter.java index 70dfc92f..6f0298fc 100644 --- a/src/main/java/com/clokey/server/domain/member/converter/ProfileConverter.java +++ b/src/main/java/com/clokey/server/domain/member/converter/ProfileConverter.java @@ -1,6 +1,6 @@ package com.clokey.server.domain.member.converter; -import com.clokey.server.domain.member.dto.MemberResponseDTO; +import com.clokey.server.domain.member.dto.MemberDTO; import com.clokey.server.domain.model.Member; import java.time.LocalDateTime; @@ -8,8 +8,8 @@ public class ProfileConverter { - public static MemberResponseDTO.ProfileRP toProfileRPDTO(Member member) { - return MemberResponseDTO.ProfileRP.builder() + public static MemberDTO.ProfileRP toProfileRPDTO(Member member) { + return MemberDTO.ProfileRP.builder() .id(member.getId()) .bio(member.getBio()) .email(member.getEmail()) diff --git a/src/main/java/com/clokey/server/domain/member/dto/MemberResponseDTO.java b/src/main/java/com/clokey/server/domain/member/dto/MemberDTO.java similarity index 63% rename from src/main/java/com/clokey/server/domain/member/dto/MemberResponseDTO.java rename to src/main/java/com/clokey/server/domain/member/dto/MemberDTO.java index 8c38e374..d94d835c 100644 --- a/src/main/java/com/clokey/server/domain/member/dto/MemberResponseDTO.java +++ b/src/main/java/com/clokey/server/domain/member/dto/MemberDTO.java @@ -1,6 +1,8 @@ package com.clokey.server.domain.member.dto; import com.clokey.server.domain.member.exception.annotation.EssentialFieldNotNull; +import com.clokey.server.domain.member.exception.annotation.IdValid; +import com.clokey.server.domain.member.exception.annotation.NotFollowMyself; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -8,7 +10,7 @@ import java.time.LocalDateTime; -public class MemberResponseDTO { +public class MemberDTO { @Builder @Getter @@ -33,14 +35,14 @@ public static class GetUserRP { public static class ProfileRQ { @EssentialFieldNotNull - private String nickname; + String nickname; @EssentialFieldNotNull - private String clokeyId; + String clokeyId; - private String profileImageUrl; + String profileImageUrl; - private String bio; + String bio; } @@ -58,4 +60,30 @@ public static class ProfileRP { String profileImageUrl; LocalDateTime updatedAt; } + + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class FollowRQ{ + + @IdValid + String myClokeyId; + @IdValid + String yourClokeyId; + + } + + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class FollowRP{ + + boolean isFollow; + + } } diff --git a/src/main/java/com/clokey/server/domain/member/exception/annotation/NotFollowMyself.java b/src/main/java/com/clokey/server/domain/member/exception/annotation/NotFollowMyself.java new file mode 100644 index 00000000..8973c554 --- /dev/null +++ b/src/main/java/com/clokey/server/domain/member/exception/annotation/NotFollowMyself.java @@ -0,0 +1,24 @@ +package com.clokey.server.domain.member.exception.annotation; + +import com.clokey.server.domain.member.exception.validator.NotFollowMyselfValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Documented +@Constraint(validatedBy = NotFollowMyselfValidator.class) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER , ElementType.TYPE}) // 메소드, 필드, 파라미터에서 사용 가능 +@Retention(RetentionPolicy.RUNTIME) +public @interface NotFollowMyself { + String message() default "myClokeyId와 yourClokeyId는 동일할 수 없습니다."; // 기본 오류 메시지 + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/clokey/server/domain/member/exception/validator/NotFollowMyselfValidator.java b/src/main/java/com/clokey/server/domain/member/exception/validator/NotFollowMyselfValidator.java new file mode 100644 index 00000000..17f05f04 --- /dev/null +++ b/src/main/java/com/clokey/server/domain/member/exception/validator/NotFollowMyselfValidator.java @@ -0,0 +1,33 @@ +package com.clokey.server.domain.member.exception.validator; + +import com.clokey.server.domain.member.dto.MemberDTO; +import com.clokey.server.domain.member.exception.annotation.NotFollowMyself; +import com.clokey.server.global.error.code.status.ErrorStatus; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class NotFollowMyselfValidator implements ConstraintValidator { + @Override + public void initialize(NotFollowMyself constraintAnnotation) { + ConstraintValidator.super.initialize(constraintAnnotation); + } + + @Override + public boolean isValid(MemberDTO.FollowRQ followRQ, ConstraintValidatorContext constraintValidatorContext) { + if (followRQ == null ) { + constraintValidatorContext.disableDefaultConstraintViolation(); + constraintValidatorContext.buildConstraintViolationWithTemplate(ErrorStatus.ESSENTIAL_INPUT_REQUIRED.toString()).addConstraintViolation(); + return false; + } + + String myClokeyId = followRQ.getMyClokeyId(); + String yourClokeyId = followRQ.getYourClokeyId(); + + if(myClokeyId.equals(yourClokeyId)){ + constraintValidatorContext.disableDefaultConstraintViolation(); + constraintValidatorContext.buildConstraintViolationWithTemplate(ErrorStatus.CANNOT_FOLLOW_MYSELF.toString()).addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/clokey/server/global/error/code/status/ErrorStatus.java b/src/main/java/com/clokey/server/global/error/code/status/ErrorStatus.java index 3981b78b..984e786e 100644 --- a/src/main/java/com/clokey/server/global/error/code/status/ErrorStatus.java +++ b/src/main/java/com/clokey/server/global/error/code/status/ErrorStatus.java @@ -25,6 +25,7 @@ public enum ErrorStatus implements BaseErrorCode { CLOKEY_ID_INVALID(HttpStatus.BAD_REQUEST,"MEMBER_4004","잘못된 클로키 아이디입니다."), DUPLICATE_CLOKEY_ID(HttpStatus.BAD_REQUEST,"MEMBER_4005","중복된 클로키 아이디입니다."), ESSENTIAL_INPUT_REQUIRED(HttpStatus.BAD_REQUEST,"MEMBER_4006","필수 입력 요소 값이 누락되었습니다."), + CANNOT_FOLLOW_MYSELF(HttpStatus.BAD_REQUEST,"MEMBER_4007", "팔로우 아이디와 팔로잉 아이디가 같을 수 없습니다."), //옷 에러 NO_SUCH_CLOTH(HttpStatus.NOT_FOUND,"CLOTH_4041","존재하지 않는 옷 ID입니다."),