Skip to content

Commit

Permalink
[화살] 화살 연관 로직 수정(#68) (#81)
Browse files Browse the repository at this point in the history
* feat: lombok 활용하도록 변경(#68)

* feat: gender boolean 값 string 변환 메서드 추가(#68)

* feat: 사용자 엔티티 수정(#68)

- 같은 사용자(자기 자신) 검증 로직, UserException 이용토록 수정
- 성별 boolean, string  두 타입으로 제공하는 메서드 추가
- 같은 성별 사용자인 지 검증하는 메서드 추가

* feat: 사용자 연관 예외 추가(#68)

- 같은 사용자(자기 자신)일 경우
- 같은 성별 사용자인 경우
- 휴면 상태 사용자인 경우

* feat: 사용자 엔티티 boolean 타입 성별 getter 사용 위치 수정(#68)

- getter 메서드명 변경에 따른 사용위치 수정

* feat: 휴면 상태 사용자에게 화살 보냄 예외 정의(#68)

* feat: 불필요한 화살 보내기 요청 DTO 삭제(#68)

* feat: 화살 보내기 비즈니스 로직 수정(#68)

- 보내는 사용자 휴면 상태 검증 추가
- 받는 사용자 휴면 상태 검증 추가
- 보내는 사용자와 받는 사용자 성별 동일 여부 검증 추가
- 화살 개수 1개만 보낼 수 있도록 수정

* feat: 화살 보내기 API 수정(#68)

한 번에 하나의 화살만 보낼 수 있기 때문에 별도의 요청 DTO(request body)
없이 작업을 수행하도록 API 수정

* feat: 보내는 사용자, 받는 사용자 필드명 수정(#68)

* feat: 화살 내역 엔티티 필드명 변경에 따른 사용 위치 수정(#68)

* refactor: 패키지 경로 이동(#68)

* feat: 최종 접속 일시 갱신 메서드 파라미터명 변경(#68)

* feat: 최종 접속 일시 갱신 로직 추가(#68)

출석 체크 요청시 무조건적인 최종 접속 일시 갱신을 위해 사용

* feat: 인증되지 않은 사용자 예외 추가(#68)

* feat: 최종 접속 일시 갱신 인터셉터 추가(#68)

- 출석 체크 성공/실패 여부와 상관 없이 사용자 최종 접속 일시는 무조건 갱신
- 이 인터셉터를 통해 출석 체크 요청 이후 무조건적으로 최종 접속 일시를 업데이트 처리

* feat: 최종 접속 일시 갱신 인터셉터 등록 로직 추가(#68)

- 출석 체크 요청시에만 최종 접속 일시 갱신을 수행하도록 인터셉터 등록

* feat: 출석 체크 비즈니스 로직 수정(#68)

- 최종 접속 일시 갱신을 별도의 인터셉터가 수행하도록 분리
- 휴면 상태 사용자는 출석 체크 불가하도록 검증
- 처음 가입한 사용자는 최종 접속 일시가 null이므로 접속 일시 검증을 수행하지 않음

* feat: 사용자 화살 개수 응답 DTO 사용 위치 수정(#68)

* feat: 메서드 파라미터명 수정(#68)

* feat: 출석 체크 로직, 출석 체크 날짜 파라미터 추가(#68)

테스트 가능한 형태로 외부에서 출석 체크 날짜를 주입받도록 수정

* feat: 출석 체크 API 로직, 출석 체크 날짜 생성 코드 추가(#68)

* feat: 화살 충전 로직, 충전일 파라미터 추가(#68)

- 기존 로직은 로직 실행일 기준으로 충전 최대 횟수 검증 수행
- 테스트하기 어려운 구조, 외부에서 충전일을 전달받도록 변경

* feat: 화살 충전 API, 충전일 생성 코드 추가(#68)

화살 충전 버즈니스 로직이 충전일을 외부에서 제공받는 형태로
변경됨에 따라 충전일 생성하는 코드 추가

* feat: 최종 접속 일시 업데이트 로직, 간단한 형태로 수정(#68)

* feat: 화살 내역 타입 추가(#68)

화살 내역을 경우에 따라 구분하기 위해 추가한 열거형, 다음 종류들이 존재
- 출석 체크
- 광고 시청을 통한 화살 획득
- 사용자간 송수신

* feat: 화살 내역 타입 필드 추가(#68)

* feat: 광고 시청 가능 여부 확인 로직, 화살 내역 타입 비교 조건 추가(#68)

* feat: 화살 연관 로직들 수정(#68)

- 화살 내역 생성시 화살 내역 타입을 함께 포함하여 내역마다 구분이 가능하도록 수정
- 불필요한 상수, 지역 변수로 구성
- 의미가 더 잘 드러나도록 변수명 적절히 수정

* feat: 사용하지 않는 최종 접속 일시 업데이트 인터셉터 삭제(#68)

* feat: 사용하지 않는 예외 삭제(#68)

* feat: 코드 정렬(#68)

* feat: 출석 체크 가능 여부 확인 로직 수정(#68)

- 기존 로직에서는 이미 출석 체크한 상황을 예외로 다룸
- 사용자 화살 증가와 최종 접속 일시 갱신을 한 트랜잭션으로 다루기 어려운 구조
- 이미 출석 체크한 상황을 예외로 다루지 않고, 출석 체크일과 최종 접속 일시 비교 결과 반환
- 출석 체크 로직에서 비교 결과에 따라 진행 토록 로직 변경

* feat: 사용하지 않는 최종 접속 일시 업데이트 로직 삭제(#68)

* feat: 출석 체크 로직 수정(#68)

- 출석 체크 내역 저장, 사용자 화살 증가와 최종 접속 일시 갱신을 한 트랜잭션으로 다루도록 변경
- 이를 위해 이미 출석 체크한 상황을 예외로 다루지 않음
- 출석 체크 진행 여부를 반환하여 출석 체크 진행 여부를 판단하도록 구성

* feat: 출석 체크 API 로직 수정(#68)

- 서비스 로직을 출석 체크 진행 여부를 반환하도록 변경
- 결과 값에 따라 적절한 응답을 클라이언트에 전달

* feat: 사용되지 않는 예외 삭제(#68)
  • Loading branch information
Minjae-An authored May 14, 2024
1 parent f4011d4 commit 4dfda2d
Show file tree
Hide file tree
Showing 16 changed files with 206 additions and 190 deletions.
43 changes: 23 additions & 20 deletions be/src/main/java/yeonba/be/arrow/controller/ArrowController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import yeonba.be.arrow.dto.UserArrowsResponse;
import yeonba.be.arrow.dto.request.ArrowSendRequest;
import yeonba.be.arrow.dto.response.UserArrowsResponse;
import yeonba.be.arrow.service.ArrowService;
import yeonba.be.util.CustomResponse;

Expand All @@ -38,47 +37,51 @@ public ResponseEntity<CustomResponse<UserArrowsResponse>> arrows(
}

@Operation(summary = "출석 체크", description = "출석 체크를 통해 사용자가 10개의 화살을 획득할 수 있습니다.")
@ApiResponse(responseCode = "202", description = "출석 체크 정상 처리")
@ApiResponse(responseCode = "200", description = "출석 체크 정상 처리")
@PostMapping("/daily-check")
public ResponseEntity<CustomResponse<Void>> dailyCheck(
@RequestAttribute("userId") long userId) {

arrowService.dailyCheck(userId);

return ResponseEntity
.accepted()
ResponseEntity<CustomResponse<Void>> response = ResponseEntity
.ok()
.body(new CustomResponse<>());

LocalDate dailyCheckDay = LocalDate.now();
if (!arrowService.dailyCheck(userId, dailyCheckDay)) {
response = ResponseEntity
.badRequest()
.body(new CustomResponse<>("이미 출석 체크한 사용자입니다."));
}

return response;
}

@Operation(summary = "화살 보내기", description = "다른 사용자에게 화살을 보낼 수 있습니다.")
@ApiResponse(responseCode = "202", description = "화살 전송 정상 처리")
@Operation(summary = "화살 보내기", description = "다른 사용자에게 화살을 1개 보낼 수 있습니다.")
@ApiResponse(responseCode = "200", description = "화살 전송 정상 처리")
@PostMapping("/users/{userId}/arrow")
public ResponseEntity<CustomResponse<Void>> sendArrow(
@RequestAttribute("userId") long senderId,
@Parameter(description = "화살 받는 사용자 ID", example = "1")
@PathVariable("userId") long receiverId,
@RequestBody ArrowSendRequest request) {
@PathVariable("userId") long receiverId) {

arrowService.sendArrow(
senderId,
receiverId,
request);
arrowService.sendArrow(senderId, receiverId);

return ResponseEntity
.accepted()
.ok()
.body(new CustomResponse<>());
}

@Operation(summary = "화살 충전", description = "광고 시청시 화살 5개를 충전할 수 있습니다.")
@ApiResponse(responseCode = "202", description = "화살 충전 정상 처리")
@ApiResponse(responseCode = "200", description = "화살 충전 정상 처리")
@PostMapping("/users/arrows")
public ResponseEntity<CustomResponse<Void>> chargeArrows(
@RequestAttribute("userId") long userId) {

arrowService.chargeArrows(userId);
LocalDate chargeDay = LocalDate.now();
arrowService.chargeArrows(userId, chargeDay);

return ResponseEntity
.accepted()
.ok()
.body(new CustomResponse<>());
}
}
18 changes: 0 additions & 18 deletions be/src/main/java/yeonba/be/arrow/dto/request/ArrowSendRequest.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package yeonba.be.arrow.dto;
package yeonba.be.arrow.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
Expand All @@ -8,9 +8,9 @@
@AllArgsConstructor
public class UserArrowsResponse {

@Schema(
type = "number",
description = "사용자 화살 개수",
example = "10")
private int arrows;
@Schema(
type = "number",
description = "사용자 화살 개수",
example = "10")
private int arrows;
}
10 changes: 10 additions & 0 deletions be/src/main/java/yeonba/be/arrow/entity/ArrowTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand All @@ -14,6 +16,7 @@
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import yeonba.be.arrow.enums.ArrowTransactionType;
import yeonba.be.user.entity.User;

@Table(name = "arrows_transactions")
Expand All @@ -38,23 +41,30 @@ public class ArrowTransaction {
@Column(nullable = false)
private int arrows;

@Enumerated(EnumType.STRING)
private ArrowTransactionType type;

@CreatedDate
@Column(nullable = false)
private LocalDateTime createdAt;

public ArrowTransaction(
ArrowTransactionType type,
User receiver,
int arrows) {

this.type = type;
this.receiver = receiver;
this.arrows = arrows;
}

public ArrowTransaction(
ArrowTransactionType type,
User sender,
User receiver,
int arrows) {

this.type = type;
this.sender = sender;
this.receiver = receiver;
this.arrows = arrows;
Expand Down
17 changes: 17 additions & 0 deletions be/src/main/java/yeonba/be/arrow/enums/ArrowTransactionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package yeonba.be.arrow.enums;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ArrowTransactionType {

DAILY_CHECK("출석 체크"),

REWARDS_FOR_WATCHING_ADVERTISEMENTS("광고 시청 통한 화살 획득"),

USER_TO_USER("사용자간 송수신");

private final String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
public interface ArrowTransactionRepository extends JpaRepository<ArrowTransaction, Long>,
ArrowTransactionRepositoryCustom {

boolean existsBySenderAndReceiver(User sentUser, User receivedUser);
boolean existsBySenderAndReceiver(User sender, User receiver);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package yeonba.be.arrow.repository;

import static yeonba.be.arrow.entity.QArrowTransaction.arrowTransaction;
import static yeonba.be.arrow.enums.ArrowTransactionType.REWARDS_FOR_WATCHING_ADVERTISEMENTS;
import static yeonba.be.user.entity.QUser.user;

import com.querydsl.jpa.impl.JPAQueryFactory;
Expand All @@ -20,8 +21,9 @@ public boolean canChargeByAdvertisement(long userId, LocalDateTime today) {
queryFactory
.select(arrowTransaction.count())
.from(arrowTransaction)
.innerJoin(arrowTransaction.sender, user)
.innerJoin(arrowTransaction.receiver, user)
.where(
arrowTransaction.type.eq(REWARDS_FOR_WATCHING_ADVERTISEMENTS),
arrowTransaction.sender.isNull(),
arrowTransaction.receiver.id.eq(userId),
arrowTransaction.createdAt.after(today)
Expand Down
106 changes: 59 additions & 47 deletions be/src/main/java/yeonba/be/arrow/service/ArrowService.java
Original file line number Diff line number Diff line change
@@ -1,52 +1,60 @@
package yeonba.be.arrow.service;

import static yeonba.be.arrow.enums.ArrowTransactionType.DAILY_CHECK;
import static yeonba.be.arrow.enums.ArrowTransactionType.REWARDS_FOR_WATCHING_ADVERTISEMENTS;
import static yeonba.be.arrow.enums.ArrowTransactionType.USER_TO_USER;

import java.time.LocalDate;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import yeonba.be.arrow.dto.UserArrowsResponse;
import yeonba.be.arrow.dto.request.ArrowSendRequest;
import yeonba.be.arrow.dto.response.UserArrowsResponse;
import yeonba.be.arrow.entity.ArrowTransaction;
import yeonba.be.arrow.repository.ArrowCommand;
import yeonba.be.arrow.repository.ArrowQuery;
import yeonba.be.exception.ArrowException;
import yeonba.be.exception.GeneralException;
import yeonba.be.exception.UserException;
import yeonba.be.user.entity.User;
import yeonba.be.user.repository.user.UserQuery;

@Service
@RequiredArgsConstructor
public class ArrowService {

private final int DAILY_CHECK_ARROW_COUNT = 10;
private final int ADVERTISEMENT_ARROW_COUNT = 5;
private final UserQuery userQuery;
private final ArrowCommand arrowCommand;
private final ArrowQuery arrowQuery;

/*
출석 체크는 다음 과정을 거쳐 이뤄진다.
1. 사용자 최종 접속 일시를 통해 이미 출석 체크하였는지 확인
2. 화살 송수신 내역 저장
3. 사용자 최종 접속 일시 갱신
4. 사용자 화살 개수 증가
*/
@Transactional
public void dailyCheck(long userId) {
public boolean dailyCheck(long userId, LocalDate dailyCheckDay) {

User dailyCheckUser = userQuery.findById(userId);

LocalDateTime dailyCheckedAt = LocalDateTime.now();
dailyCheckUser.validateDailyCheck(dailyCheckedAt.toLocalDate());
// 휴면 상태 사용자는 출석 체크 불가
if (dailyCheckUser.isInactive()) {
throw new GeneralException(UserException.INACTIVE_USER);
}

ArrowTransaction arrowTransaction = new ArrowTransaction(
dailyCheckUser,
DAILY_CHECK_ARROW_COUNT);
arrowCommand.save(arrowTransaction);
boolean canDailyCheck = dailyCheckUser.canDailyCheckAt(dailyCheckDay);

// 출석 체크 화살 내역 저장, 사용자 화살 증가
if (canDailyCheck) {
int dailyCheckArrows = 10;
ArrowTransaction arrowTransaction = new ArrowTransaction(
DAILY_CHECK,
dailyCheckUser,
dailyCheckArrows);
arrowCommand.save(arrowTransaction);

dailyCheckUser.plusArrow(dailyCheckArrows);
}

// 사용자 최종 접속 일시 갱신
dailyCheckUser.updateLastAccessedAt(LocalDateTime.now());

dailyCheckUser.updateLastAccessedAt(dailyCheckedAt);
dailyCheckUser.plusArrow(DAILY_CHECK_ARROW_COUNT);
return canDailyCheck;
}

@Transactional(readOnly = true)
Expand All @@ -57,54 +65,58 @@ public UserArrowsResponse getUserArrows(long userId) {
return new UserArrowsResponse(user.getArrow());
}

/*
화살 보내기 비즈니스 로직은 다음 과정을 거친다.
1. 자기 자신에게 화살을 보내는 상황 검증
2. 화살을 보낸 사용자에게 또 보내는 상황 검증
3. 화살 내역 저장
4. 보내는 사용자 화살 감소, 화살이 부족할 경우 예외 발생
5. 받는 사용자 화살 증가
*/
@Transactional
public void sendArrow(
long senderId,
long receiverId,
ArrowSendRequest request) {
public void sendArrow(long senderId, long receiverId) {

User sender = userQuery.findById(senderId);
User receiver = userQuery.findById(receiverId);

// 휴면 상태에선 화살을 보낼 수 없음
if (sender.isInactive()) {
throw new GeneralException(UserException.INACTIVE_USER);
}

// 휴면 상태인 사용자에게 화살을 보낼 수 없음
if (receiver.isInactive()) {
throw new GeneralException(ArrowException.CAN_NOT_SEND_ARROW_TO_INACTIVE_USER);
}

// 자기 자신에게 화살을 보낼 수 없음
sender.validateNotSameUser(receiver);

// 같은 성별 사용자에게 화살을 보낼 수 없음
sender.validateSameGender(receiver);

// 이미 화살을 보낸 사용자에게 화살을 보낼 수 없음
if (arrowQuery.isArrowTransactionExist(sender, receiver)) {
throw new GeneralException(ArrowException.ALREADY_SENT_ARROW_USER);
}

int arrows = request.getArrows();
ArrowTransaction arrowTransaction = new ArrowTransaction(
sender,
receiver,
arrows);
// 화살은 1개만 보낼 수 있음
int sendArrow = 1;
ArrowTransaction arrowTransaction =
new ArrowTransaction(USER_TO_USER, sender, receiver, sendArrow);
arrowCommand.save(arrowTransaction);

sender.minusArrow(arrows);
receiver.plusArrow(arrows);
sender.minusArrow(sendArrow);
receiver.plusArrow(sendArrow);
}

@Transactional
public void chargeArrows(long userId) {
public void chargeArrows(long userId, LocalDate chargeDay) {

User user = userQuery.findById(userId);
User arrowChargeUser = userQuery.findById(userId);

LocalDateTime today = LocalDate.now().atStartOfDay();
arrowQuery.validateAdvertisementArrowCount(userId, today);
LocalDateTime chargeDayStartTime = chargeDay.atStartOfDay();
arrowQuery.validateAdvertisementArrowCount(userId, chargeDayStartTime);

int chargeArrows = 5;
ArrowTransaction arrowTransaction = new ArrowTransaction(
user,
ADVERTISEMENT_ARROW_COUNT);
REWARDS_FOR_WATCHING_ADVERTISEMENTS,
arrowChargeUser,
chargeArrows);

arrowCommand.save(arrowTransaction);
user.plusArrow(ADVERTISEMENT_ARROW_COUNT);
arrowChargeUser.plusArrow(chargeArrows);
}

}
Loading

0 comments on commit 4dfda2d

Please sign in to comment.