Skip to content

Commit

Permalink
[FEAT] 관심 있는 프로필 카드에 찔러보기 요청 생성 API, 찔러보기 요청 수, 상세 정보 조회 API 개발 (#124)
Browse files Browse the repository at this point in the history
* [FEAT] ProfileCard 도메인 관련 Custom Exception 클래스 구현

* [FEAT] ProfileCard 도메인 관련 Repository 구현

* [FEAT] 사용자가 보낸 찔러보기 요청 조회 응답 DTO 구현

* [FEAT] 사용자가 받은 찔러보기 요청 응답 DTO 구현

* [FEAT] 관심 있는 프로필 카드에 대한 찔러보기 요청 생성 DTO 구현

* [FEAT] 관심 있는 프로필 카드에 대한 찔러보기 요청 생성 DTO 구현

* [FEAT] 찔러보기 요청을 통해 사용 가능한 찔러보기 요청 수 차감 수정 로직 구현

* [FEAT] 찔러보기 요청 관련 에러코드 작성

* [FEAT] 불필요한 에러코드 제거

* [FEAT] 찔러보기 요청 업데이트 로직 구현, 테이블 컬럼명 변경

* [FEAT] 테이블 컬럼명 변경

* [FEAT] userId를 이용해 프로필 카드를 조회하는 메서드 구현

* [FEAT] 프로필 카드의 찔러보기 요청 수 업데이트 메서드 구현

* [FEAT] 찔러보기 요청 생성 로직, 사용자가 받은 찔러보기 요청 수 조회 로직, 사용자가 보낸 찔러보기 요청 수, 요청 상세 정보 조회 로직 구현

* [FEAT] queryDsl을 이용해 찔러보기 요청 수 조회, 요청 상세 정보 조회 기능 구현

* [FEAT] 찔러보기 요청 생성 API, 찔러보기 요청 수 조회 API, 찔러보기 요청 상세 조회 API 구현

* [FEAT] Swagger 작성, 관련 에러코드 작성

* [REFACT] Controller 엔드포인트 수정, 메서드명 수정

* [REFACT] ProfileCard Controller 별도 분리

* [FEAT] 읽기전용 @transactional 어노테이션 추가

* [FEAT] @Schema description 설명 추가
  • Loading branch information
JIN-076 authored Feb 21, 2024
1 parent 18a9f08 commit ece2405
Show file tree
Hide file tree
Showing 25 changed files with 602 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package io.oeid.mogakgo.common.swagger.template;

import io.oeid.mogakgo.common.base.CursorPaginationInfoReq;
import io.oeid.mogakgo.common.base.CursorPaginationResult;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerProfileCardLikeErrorExamples;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerUserErrorExamples;
import io.oeid.mogakgo.domain.profile.presentation.dto.req.UserProfileLikeCreateAPIReq;
import io.oeid.mogakgo.domain.profile.presentation.dto.res.UserProfileLikeAPIRes;
import io.oeid.mogakgo.domain.profile.presentation.dto.res.UserProfileLikeCreateAPIRes;
import io.oeid.mogakgo.domain.profile.presentation.dto.res.UserProfileLikeInfoAPIRes;
import io.oeid.mogakgo.exception.dto.ErrorResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;

@Tag(name = "Profile Card Like", description = "프로필 카드 찔러보기 관련 API")
public interface ProfileCardLikeSwagger {

@Operation(summary = "관심 있는 프로필 카드에 대해 찔러보기 요청 생성", description = "사용자가 관심 있는 프로필 카드에 찔러보기 요청을 보낼 때 사용하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "201", description = "찔러보기 요청 생성 성공",
content = @Content(schema = @Schema(implementation = UserProfileLikeCreateAPIRes.class))),
@ApiResponse(responseCode = "400", description = "요청한 데이터가 유효하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = {
@ExampleObject(name = "E040102", value = SwaggerProfileCardLikeErrorExamples.PROFILE_CARD_LIKE_ALREADY_EXIST),
@ExampleObject(name = "E040103", value = SwaggerProfileCardLikeErrorExamples.INVALID_PROFILE_CARD_LIKE_RECEIVER_INFO)
}
)),
@ApiResponse(responseCode = "403", description = "본인이 아닌 프로필 카드에 대한 찔러보기 요청 권한이 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E040101", value = SwaggerProfileCardLikeErrorExamples.PROFILE_CARD_LIKE_FORBIDDEN_OPERATION)
)),
@ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND)
)),
})
ResponseEntity<UserProfileLikeCreateAPIRes> create(
@Parameter(hidden = true) Long userId,
UserProfileLikeCreateAPIReq request
);

@Operation(summary = "사용자가 받은 찔러보기 요청 수 조회", description = "사용자가 자신이 받은 찔러보기 요청 수를 조회할 때 사용하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "찔러보기 요청 수 조회 성공",
content = @Content(schema = @Schema(implementation = UserProfileLikeAPIRes.class))),
@ApiResponse(responseCode = "403", description = "본인이 아닌 프로필 카드에 대한 찔러보기 요청 권한이 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E040101", value = SwaggerProfileCardLikeErrorExamples.PROFILE_CARD_LIKE_FORBIDDEN_OPERATION)
)),
@ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND)
)),
})
ResponseEntity<UserProfileLikeAPIRes> getProfileLikeCountByReceiver(
@Parameter(hidden = true) Long userId,
@Parameter(description = "'찔러보기' 요청을 조회하는 사용자 ID", required = true) Long id
);

@Operation(summary = "사용자가 보낸 찔러보기 요청 수 조회", description = "사용자가 자신이 보낸 찔러보기 요청 수를 조회할 때 사용하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "찔러보기 요청 수 조회 성공",
content = @Content(schema = @Schema(implementation = UserProfileLikeAPIRes.class))),
@ApiResponse(responseCode = "403", description = "본인이 아닌 프로필 카드에 대한 찔러보기 요청 권한이 없음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E040101", value = SwaggerProfileCardLikeErrorExamples.PROFILE_CARD_LIKE_FORBIDDEN_OPERATION)
)),
@ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND)
)),
})
ResponseEntity<UserProfileLikeAPIRes> getProfileLikeCountBySender(
@Parameter(hidden = true) Long userId,
@Parameter(description = "'찔러보기' 요청을 조회하는 사용자 ID", required = true) Long id
);

@Operation(summary = "사용자가 보낸 찔러보기 요청 상세 리스트 조회", description = "사용자가 자신이 보낸 찔러보기 요청 상세 리스트 정보를 조회할 때 사용하는 API")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "찔러보기 요청 상세 리스트 조회 성공",
content = @Content(schema = @Schema(implementation = UserProfileLikeInfoAPIRes.class))),
@ApiResponse(responseCode = "403", description = "본인의 프로필 카드에 대한 찔러보기 요청만 조회할 수 있음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E040101", value = SwaggerProfileCardLikeErrorExamples.PROFILE_CARD_LIKE_FORBIDDEN_OPERATION)
)),
@ApiResponse(responseCode = "404", description = "요청한 데이터가 존재하지 않음",
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E020301", value = SwaggerUserErrorExamples.USER_NOT_FOUND)
)),
})
@Parameters({
@Parameter(name = "cursorId", description = "기준이 되는 커서 ID", example = "1"),
@Parameter(name = "pageSize", description = "요청할 데이터 크기", example = "5", required = true),
@Parameter(name = "sortOrder", description = "정렬 방향", example = "ASC"),
})
ResponseEntity<CursorPaginationResult<UserProfileLikeInfoAPIRes>> getProfileLikeInfoBySender(
@Parameter(hidden = true) Long userId,
@Parameter(description = "'찔러보기' 요청을 조회하는 사용자 ID", required = true) Long id,
@Parameter(hidden = true) CursorPaginationInfoReq pageable
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.oeid.mogakgo.common.swagger.template;

import io.oeid.mogakgo.common.annotation.UserId;
import io.oeid.mogakgo.common.base.CursorPaginationInfoReq;
import io.oeid.mogakgo.common.base.CursorPaginationResult;
import io.oeid.mogakgo.core.properties.swagger.error.SwaggerGeoErrorExamples;
Expand All @@ -22,9 +21,7 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;

@Tag(name = "Project Card", description = "프로젝트 카드 관련 API")
@SuppressWarnings("unused")
Expand Down Expand Up @@ -163,7 +160,7 @@ ResponseEntity<CursorPaginationResult<ProjectDetailAPIRes>> getRandomOrderedProj
content = @Content(
mediaType = "application/json",
schema = @Schema(implementation = ErrorResponse.class),
examples = @ExampleObject(name = "E050201", value = SwaggerProjectErrorExamples.PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION)))
examples = @ExampleObject(name = "E030101", value = SwaggerProjectErrorExamples.PROJECT_JOIN_REQUEST_FORBIDDEN_OPERATION)))
})
@Parameters({
@Parameter(name = "cursorId", description = "기준이 되는 커서 ID", example = "1"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import io.oeid.mogakgo.domain.auth.oauth.GithubOAuth2UserService;
import io.oeid.mogakgo.domain.auth.oauth.OAuth2AuthenticationSuccessHandler;
import java.util.Arrays;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.oeid.mogakgo.core.properties.swagger.error;

public class SwaggerProfileCardLikeErrorExamples {

public static final String PROFILE_CARD_LIKE_ALREADY_EXIST = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E040102\",\"message\":\"이미 찔러보기 요청을 전송한 프로필 카드에 찔러보기 요청을 전송할 수 없습니다.\"}";
public static final String INVALID_PROFILE_CARD_LIKE_RECEIVER_INFO = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":400,\"code\":\"E040103\",\"message\":\"찔러보기 요청의 사용자가 존재하지 않습니다.\"}";
public static final String PROFILE_CARD_LIKE_FORBIDDEN_OPERATION = "{\"timestamp\":\"2024-02-17T10:07:31.404Z\",\"statusCode\":403,\"code\":\"E040103\",\"message\":\"본인의 프로필 카드 외에 대해 찔러보기 요청 권한이 없습니다.\"}";

private SwaggerProfileCardLikeErrorExamples() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package io.oeid.mogakgo.domain.profile.application;

import static io.oeid.mogakgo.exception.code.ErrorCode400.PROFILE_CARD_LIKE_ALREADY_EXIST;
import static io.oeid.mogakgo.exception.code.ErrorCode403.PROFILE_CARD_LIKE_FORBIDDEN_OPERATION;

import io.oeid.mogakgo.common.base.CursorPaginationInfoReq;
import io.oeid.mogakgo.common.base.CursorPaginationResult;
import io.oeid.mogakgo.domain.profile.domain.entity.ProfileCardLike;
import io.oeid.mogakgo.domain.profile.exception.ProfileCardLikeException;
import io.oeid.mogakgo.domain.profile.infrastructure.ProfileCardLikeJpaRepository;
import io.oeid.mogakgo.domain.profile.presentation.dto.req.UserProfileLikeCreateAPIReq;
import io.oeid.mogakgo.domain.profile.presentation.dto.res.UserProfileLikeInfoAPIRes;
import io.oeid.mogakgo.domain.user.application.UserCommonService;
import io.oeid.mogakgo.domain.user.application.UserProfileService;
import io.oeid.mogakgo.domain.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class ProfileCardLikeService {

private final ProfileCardLikeJpaRepository profileCardLikeRepository;
private final ProfileCardService profileCardService;
private final UserCommonService userCommonService;
private final UserProfileService userProfileService;

@Transactional
public Long create(Long userId, UserProfileLikeCreateAPIReq request) {
User tokenUser = validateToken(userId);
validateSendor(tokenUser, request.getSenderId());
validateReceiver(userId);
validateLikeAlreadyExist(request.getSenderId(), request.getReceiverId());

User receiver = userCommonService.getUserById(request.getReceiverId());
ProfileCardLike profileCardLike = request.toEntity(tokenUser, receiver);
profileCardLikeRepository.save(profileCardLike);

profileCardService.increaseTotalLikeAmount(receiver.getId());
userProfileService.decreaseAvailableLikeCount(userId);

return profileCardLike.getId();
}

// 나의 찔러보기 요청 수 조회
public Long getReceivedLikeCountForProfileCard(Long userId, Long id) {
User tokenUser = validateToken(userId);
validateSendor(tokenUser, userId);

return profileCardLikeRepository.getLikeCount(id);
}

// 내가 보낸 찔러보기 요청 수 조회
public Long getSentLikeCountForProfileCard(Long userId, Long id) {
User tokenUser = validateToken(userId);
validateSendor(tokenUser, userId);

return profileCardLikeRepository.getLikeCountByCondition(id, null);
}

public CursorPaginationResult<UserProfileLikeInfoAPIRes> getLikeInfoSenderProfile(
Long userId, Long id, CursorPaginationInfoReq pageable) {
User tokenUser = validateToken(userId);
validateSendor(tokenUser, userId);

return profileCardLikeRepository.getLikeInfoBySender(id, pageable);
}

private User validateToken(Long userId) {
return userCommonService.getUserById(userId);
}

private void validateSendor(User tokenUser, Long userId) {
if (!tokenUser.getId().equals(userId)) {
throw new ProfileCardLikeException(PROFILE_CARD_LIKE_FORBIDDEN_OPERATION);
}
}

private void validateReceiver(Long userId) {
userCommonService.getUserById(userId);
}

private void validateLikeAlreadyExist(Long userId, Long creatorId) {
if (profileCardLikeRepository.findBySenderAndReceiver(userId, creatorId).isPresent()) {
throw new ProfileCardLikeException(PROFILE_CARD_LIKE_ALREADY_EXIST);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package io.oeid.mogakgo.domain.profile.application;

import static io.oeid.mogakgo.exception.code.ErrorCode400.INVALID_SERVICE_REGION;
import static io.oeid.mogakgo.exception.code.ErrorCode404.USER_NOT_FOUND;

import io.oeid.mogakgo.common.base.CursorPaginationInfoReq;
import io.oeid.mogakgo.common.base.CursorPaginationResult;
import io.oeid.mogakgo.domain.geo.domain.enums.Region;
import io.oeid.mogakgo.domain.geo.exception.GeoException;
import io.oeid.mogakgo.domain.profile.domain.entity.ProfileCard;
import io.oeid.mogakgo.domain.profile.infrastructure.ProfileCardJpaRepository;
import io.oeid.mogakgo.domain.user.application.UserCommonService;
import io.oeid.mogakgo.domain.user.domain.User;
import io.oeid.mogakgo.domain.user.exception.UserException;
import io.oeid.mogakgo.domain.user.presentation.dto.res.UserPublicApiResponse;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -38,6 +41,16 @@ public CursorPaginationResult<UserPublicApiResponse> getRandomOrderedProfileCard
return profiles;
}

public void increaseTotalLikeAmount(Long userId) {
var profileCard = getProfileCard(userId);
profileCard.addLike();
}

private ProfileCard getProfileCard(Long userId) {
return profileCardRepository.findByUserId(userId)
.orElseThrow(() -> new UserException(USER_NOT_FOUND));
}

private User validateToken(Long userId) {
return userCommonService.getUserById(userId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public class ProfileCard {
@Column(name = "total_like_amount", nullable = false)
private Long totalLikeAmount;

public void addLike() {
this.totalLikeAmount += 1;
}

@Builder
private ProfileCard(User user) {
this.user = user;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ProfileCardLike {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "profile_card_like_id")
@Column(name = "id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.oeid.mogakgo.domain.profile.exception;

import io.oeid.mogakgo.exception.code.ErrorCode;
import io.oeid.mogakgo.exception.exception_class.CustomException;

public class ProfileCardLikeException extends CustomException {

public ProfileCardLikeException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package io.oeid.mogakgo.domain.profile.infrastructure;

import io.oeid.mogakgo.domain.profile.domain.entity.ProfileCard;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProfileCardJpaRepository extends JpaRepository<ProfileCard, Long>,
ProfileCardRepositoryCustom {

Optional<ProfileCard> findByUserId(Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.oeid.mogakgo.domain.profile.infrastructure;

import io.oeid.mogakgo.domain.profile.domain.entity.ProfileCardLike;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProfileCardLikeJpaRepository extends JpaRepository<ProfileCardLike, Long>,
ProfileCardLikeRepositoryCustom {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.oeid.mogakgo.domain.profile.infrastructure;

import io.oeid.mogakgo.common.base.CursorPaginationInfoReq;
import io.oeid.mogakgo.common.base.CursorPaginationResult;
import io.oeid.mogakgo.domain.profile.domain.entity.ProfileCardLike;
import io.oeid.mogakgo.domain.profile.presentation.dto.res.UserProfileLikeInfoAPIRes;
import java.util.Optional;

public interface ProfileCardLikeRepositoryCustom {

Long getLikeCountByCondition(Long senderId, Long receiverId);
Long getLikeCount(Long userId);
CursorPaginationResult<UserProfileLikeInfoAPIRes> getLikeInfoBySender(
Long senderId, CursorPaginationInfoReq pageable);
Optional<ProfileCardLike> findBySenderAndReceiver(Long senderId, Long receiverId);
}
Loading

0 comments on commit ece2405

Please sign in to comment.