diff --git a/build.gradle b/build.gradle index 67629786..c9a03190 100644 --- a/build.gradle +++ b/build.gradle @@ -105,6 +105,10 @@ dependencies { annotationProcessor "jakarta.persistence:jakarta.persistence-api" implementation "jakarta.annotation:jakarta.annotation-api" implementation "com.querydsl:querydsl-codegen:${queryDslVersion}" + + // tink + implementation 'com.google.crypto.tink:tink-android:1.4.0-rc1' + implementation 'com.google.crypto.tink:apps-rewardedads:1.10.0' } asciidoctor { diff --git a/src/docs/asciidoc/create-event-history.adoc b/src/docs/asciidoc/create-event-history.adoc index 00ccbcca..90bd2dcc 100644 --- a/src/docs/asciidoc/create-event-history.adoc +++ b/src/docs/asciidoc/create-event-history.adoc @@ -1,10 +1,9 @@ :reproducible: -== 공지 조회 +== 이벤트 참여 === 요청 include::{snippets}/api/v1/event/join/1/http-request.adoc[] -include::{snippets}/api/v1/event/join/2/http-request.adoc[] === 응답 @@ -12,7 +11,7 @@ include::{snippets}/api/v1/event/join/1/http-response.adoc[] === 주의 -- "tag": "LUNCH_EVENT" | "ADMOB" +- "tag": "LUNCH_EVENT" === NOTE diff --git a/src/docs/asciidoc/find-event.adoc b/src/docs/asciidoc/find-event.adoc index 4dbb06a8..4b486a4a 100644 --- a/src/docs/asciidoc/find-event.adoc +++ b/src/docs/asciidoc/find-event.adoc @@ -1,5 +1,5 @@ :reproducible: -== 공지 조회 +== 이벤트 조회 === 요청 @@ -17,7 +17,7 @@ include::{snippets}/api/v1/event/3/http-response.adoc[] - data: *Response*[] - *Response* -- tag : "LUNCH_EVENT" | "ADMOB" +- tag : "LUNCH_EVENT" * LUNCH_EVENT에 해당하는 *Response*가 없으면 Render 해주지 말아주세요 - startDate : "2024-01-01T00:00:00+09:00" - endDate : "2024-12-31T00:00:00+09:00" diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 24ffc13a..eb3d84b5 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -110,8 +110,10 @@ === Event API -* 🆕 link:find-event.html[공지 조회, 2024-02-06] +* 🆕 link:find-event.html[이벤트 조회, 2024-02-06] * 🆕 link:create-event-history.html[이벤트 참여, 2024-02-07] -* 🆕 link:reward-event.html[이벤트 보상, 2024-02-07] \ No newline at end of file +* 🆕 link:reward-event.html[이벤트 보상, 2024-02-07] + +* 🆕 link:reward-admob.html[광고보고 보상 얻기, 2024-02-11] \ No newline at end of file diff --git a/src/docs/asciidoc/reward-admob.adoc b/src/docs/asciidoc/reward-admob.adoc new file mode 100644 index 00000000..5ebed6ac --- /dev/null +++ b/src/docs/asciidoc/reward-admob.adoc @@ -0,0 +1,35 @@ +:reproducible: +== 이벤트 참여 + +=== 요청 + +include::{snippets}/api/v1/admob/reward/http-request.adoc[] + +=== request body + +- "rewardType": String -> "ADMOB_POINT" | "ADMOB_MULTIPLE_POINT" +* ADMOB_POINT : 광고 보고 10 포인트 +* ADMOB_MULTIPLE_POINT : 광고 보고 포인트 2배 이벤트 + +- "randomType" : String -> "FIXED" | "ADMOB_RANDOM" +* FIXED : 고정값 (현재 이것만 사용) +* ADMOB_RANDOM : 랜덤값 (추후 랜덤으로 바뀔 것 고려) +- "uuid" : String -> UUID4 형식만 적용 +- "rewardNumber" : Integer -> 포인트인 경우 10, 투표 포인트 2배 이벤트인 경우 현재 투표 후 받은 포인트 보내줘야함 + +=== 응답 + +include::{snippets}/api/v1/admob/reward/http-response.adoc[] + +=== NOTE + +- Header에 무작위한 UUID4 값을 넣어주세요 +* 예시) IdempotencyKey: 0397b5f3-ecdc-47d6-b5d7-2b1afcf00e87 +- 주의사항 +* 같은 멱등성키를 2번 요청하면, 400번 에러. +- ADMOB +* ADMOB 서버에 SSV(ServerSideVerification) Options의 customData에 입력한 것과 동일한 멱등성 키를 넘겨주세요. + +=== CHANGELOG + +- 2024.02.11 릴리즈 \ No newline at end of file diff --git a/src/main/java/com/yello/server/domain/authorization/filter/JwtExceptionFilter.java b/src/main/java/com/yello/server/domain/authorization/filter/JwtExceptionFilter.java index a7bc2921..c2083b41 100644 --- a/src/main/java/com/yello/server/domain/authorization/filter/JwtExceptionFilter.java +++ b/src/main/java/com/yello/server/domain/authorization/filter/JwtExceptionFilter.java @@ -48,6 +48,7 @@ protected void doFilterInternal( || requestPath.startsWith("/api/v1/admin/login") || requestPath.startsWith("/v2/apple/notifications") || requestPath.startsWith("/v2/google/notifications") + || requestPath.startsWith("/api/v1/admob/verify") || (requestPath.startsWith("/api/v1/auth") && !requestPath.startsWith("/api/v1/auth/token/issue"))) { filterChain.doFilter(request, response); diff --git a/src/main/java/com/yello/server/domain/authorization/filter/JwtFilter.java b/src/main/java/com/yello/server/domain/authorization/filter/JwtFilter.java index 7e7015de..76fa1f3a 100644 --- a/src/main/java/com/yello/server/domain/authorization/filter/JwtFilter.java +++ b/src/main/java/com/yello/server/domain/authorization/filter/JwtFilter.java @@ -41,7 +41,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse || requestPath.startsWith("/api/v1/admin/login") || requestPath.startsWith("/api/v1/auth") || requestPath.startsWith("/v2/apple/notifications") - || requestPath.startsWith("/v2/google/notifications")) { + || requestPath.startsWith("/v2/google/notifications") + || requestPath.startsWith("/api/v1/admob/verify")) { filterChain.doFilter(request, response); return; } diff --git a/src/main/java/com/yello/server/domain/authorization/service/AuthService.java b/src/main/java/com/yello/server/domain/authorization/service/AuthService.java index cffd8941..40140383 100644 --- a/src/main/java/com/yello/server/domain/authorization/service/AuthService.java +++ b/src/main/java/com/yello/server/domain/authorization/service/AuthService.java @@ -117,8 +117,8 @@ public void recommendUser(String recommendYelloId, String userYelloId) { UserDataType.RECOMMENDED); recommendedUser.addRecommendCount(1L); - recommendedUser.addPoint(RECOMMEND_POINT); - user.addPoint(RECOMMEND_POINT); + recommendedUser.addPointBySubscribe(RECOMMEND_POINT); + user.addPointBySubscribe(RECOMMEND_POINT); if (recommended.isEmpty()) { recommendedUser.addTicketCount(1); diff --git a/src/main/java/com/yello/server/domain/event/controller/EventController.java b/src/main/java/com/yello/server/domain/event/controller/EventController.java index 4fe61519..9e86a223 100644 --- a/src/main/java/com/yello/server/domain/event/controller/EventController.java +++ b/src/main/java/com/yello/server/domain/event/controller/EventController.java @@ -1,13 +1,16 @@ package com.yello.server.domain.event.controller; -import static com.yello.server.global.common.ErrorCode.IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION; -import static com.yello.server.global.common.ErrorCode.IDEMPOTENCY_KEY_INVALID_FORM_BAD_REQUEST_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.ADMOB_URI_BAD_REQUEST_EXCEPTION; import static com.yello.server.global.common.SuccessCode.EVENT_JOIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.EVENT_NOTICE_SUCCESS; import static com.yello.server.global.common.SuccessCode.EVENT_REWARD_SUCCESS; +import static com.yello.server.global.common.SuccessCode.REWARD_ADMOB_SUCCESS; +import static com.yello.server.global.common.SuccessCode.VERIFY_ADMOB_SSV_SUCCESS; import static com.yello.server.global.common.util.ConstantUtil.IdempotencyKeyHeader; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ser.Serializers.Base; +import com.yello.server.domain.event.dto.request.AdmobRewardRequest; import com.yello.server.domain.event.dto.request.EventJoinRequest; import com.yello.server.domain.event.dto.response.EventResponse; import com.yello.server.domain.event.dto.response.EventRewardResponse; @@ -16,12 +19,16 @@ import com.yello.server.domain.user.entity.User; import com.yello.server.global.common.annotation.AccessTokenUser; import com.yello.server.global.common.dto.BaseResponse; +import com.yello.server.global.common.factory.UuidFactory; import jakarta.servlet.http.HttpServletRequest; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.val; -import org.springframework.util.StringUtils; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -36,7 +43,8 @@ public class EventController { private final EventService eventService; @GetMapping("/v1/event") - public BaseResponse> getEvents(@AccessTokenUser User user) throws JsonProcessingException { + public BaseResponse> getEvents(@AccessTokenUser User user) + throws JsonProcessingException { val data = eventService.getEvents(user.getId()); return BaseResponse.success(EVENT_NOTICE_SUCCESS, data); } @@ -44,18 +52,8 @@ public BaseResponse> getEvents(@AccessTokenUser User user) t @PostMapping("/v1/event") public BaseResponse joinEvent(@AccessTokenUser User user, HttpServletRequest requestServlet, @RequestBody EventJoinRequest request) { - final String idempotencyKey = requestServlet.getHeader(IdempotencyKeyHeader); - if (!StringUtils.hasText(idempotencyKey)) { - throw new EventBadRequestException(IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION); - } - - UUID uuidIdempotencyKey; - try { - uuidIdempotencyKey = UUID.fromString(idempotencyKey); - } catch (IllegalArgumentException e) { - throw new EventBadRequestException(IDEMPOTENCY_KEY_INVALID_FORM_BAD_REQUEST_EXCEPTION); - } - + UUID uuidIdempotencyKey = + UuidFactory.checkUuid(requestServlet.getHeader(IdempotencyKeyHeader)); eventService.joinEvent(user.getId(), uuidIdempotencyKey, request); return BaseResponse.success(EVENT_JOIN_SUCCESS); } @@ -63,19 +61,30 @@ public BaseResponse joinEvent(@AccessTokenUser User user, HttpServletRequest req @PostMapping("/v1/event/reward") public BaseResponse rewardEvent(@AccessTokenUser User user, HttpServletRequest requestServlet) throws JsonProcessingException { - final String idempotencyKey = requestServlet.getHeader(IdempotencyKeyHeader); - if (!StringUtils.hasText(idempotencyKey)) { - throw new EventBadRequestException(IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION); - } + UUID uuidIdempotencyKey = + UuidFactory.checkUuid(requestServlet.getHeader(IdempotencyKeyHeader)); + val data = eventService.rewardEvent(user.getId(), uuidIdempotencyKey); + return BaseResponse.success(EVENT_REWARD_SUCCESS, data); + } - UUID uuidIdempotencyKey; + @GetMapping("/v1/admob/verify") + public ResponseEntity verifyAdmob(HttpServletRequest request) { + URI uri; try { - uuidIdempotencyKey = UUID.fromString(idempotencyKey); - } catch (IllegalArgumentException e) { - throw new EventBadRequestException(IDEMPOTENCY_KEY_INVALID_FORM_BAD_REQUEST_EXCEPTION); + uri = + new URI(request.getScheme(), null, request.getServerName(), request.getServerPort(), + request.getRequestURI(), request.getQueryString(), null); + } catch (URISyntaxException e) { + throw new EventBadRequestException(ADMOB_URI_BAD_REQUEST_EXCEPTION); } + eventService.verifyAdmobReward(uri, request); - val data = eventService.rewardEvent(user.getId(), uuidIdempotencyKey); - return BaseResponse.success(EVENT_REWARD_SUCCESS, data); + return new ResponseEntity<>(HttpStatus.OK); + } + + @PostMapping("/v1/admob/reward") + public BaseResponse rewardAdmob(@AccessTokenUser User user, @RequestBody AdmobRewardRequest request) { + val data = eventService.rewardAdmob(user.getId(), request); + return BaseResponse.success(REWARD_ADMOB_SUCCESS, data); } } diff --git a/src/main/java/com/yello/server/domain/event/dto/request/AdmobRewardRequest.java b/src/main/java/com/yello/server/domain/event/dto/request/AdmobRewardRequest.java new file mode 100644 index 00000000..b7e917ef --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/dto/request/AdmobRewardRequest.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.event.dto.request; + +import lombok.Builder; + +@Builder +public record AdmobRewardRequest( + String rewardType, + String randomType, + String uuid, + Integer rewardNumber +) { + +} diff --git a/src/main/java/com/yello/server/domain/event/dto/request/AdmobSsvRequest.java b/src/main/java/com/yello/server/domain/event/dto/request/AdmobSsvRequest.java new file mode 100644 index 00000000..3db850f8 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/dto/request/AdmobSsvRequest.java @@ -0,0 +1,45 @@ +package com.yello.server.domain.event.dto.request; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import lombok.Builder; + +@Builder +public record AdmobSsvRequest( + String customData, + String signature, + Long keyId, + String transactionId, + String rewardItem, + Integer rewardAmount + +) { + public static AdmobSsvRequest of(Map parameters ) { + Function getParameter = (key) -> + Optional.ofNullable(parameters.get(key)) + .flatMap(arr -> Arrays.stream(arr).findFirst()) + .orElse(""); + + long keyId = Optional.ofNullable(parameters.get("key_id")) + .flatMap(arr -> Arrays.stream(arr).findFirst()) + .map(Long::parseLong) + .orElse(0L); + + int rewardAmount = Optional.ofNullable(parameters.get("reward_amount")) + .flatMap(arr -> Arrays.stream(arr).findFirst()) + .map(Integer::parseInt) + .orElse(0); + + return AdmobSsvRequest.builder() + .customData(getParameter.apply("custom_data")) + .signature(getParameter.apply("signature")) + .keyId(keyId) + .transactionId(getParameter.apply("transaction_id")) + .rewardItem(getParameter.apply("reward_item")) + .rewardAmount(rewardAmount) + .build(); + } + +} diff --git a/src/main/java/com/yello/server/domain/event/entity/EventHistory.java b/src/main/java/com/yello/server/domain/event/entity/EventHistory.java index a48554b3..92cbecce 100644 --- a/src/main/java/com/yello/server/domain/event/entity/EventHistory.java +++ b/src/main/java/com/yello/server/domain/event/entity/EventHistory.java @@ -37,4 +37,15 @@ public class EventHistory extends AuditingTimeEntity { @Column private UUID idempotencyKey; + + public static EventHistory of(User user, UUID uuidIdempotencyKey) { + return EventHistory.builder() + .user(user) + .idempotencyKey(uuidIdempotencyKey) + .build(); + } + + public void update(User user) { + this.user = user; + } } diff --git a/src/main/java/com/yello/server/domain/event/entity/EventInstance.java b/src/main/java/com/yello/server/domain/event/entity/EventInstance.java index 95f80bd4..29f83cb3 100644 --- a/src/main/java/com/yello/server/domain/event/entity/EventInstance.java +++ b/src/main/java/com/yello/server/domain/event/entity/EventInstance.java @@ -1,5 +1,7 @@ package com.yello.server.domain.event.entity; +import static com.yello.server.global.common.util.ConstantUtil.GlobalZoneId; + import com.yello.server.global.common.entity.ZonedDateTimeConverter; import jakarta.persistence.Column; import jakarta.persistence.Convert; @@ -10,6 +12,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.time.OffsetTime; import java.time.ZonedDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; @@ -48,6 +51,15 @@ public class EventInstance { @Builder.Default private Long remainEventCount = 0L; + public static EventInstance of(EventTime eventTime, EventHistory eventHistory) { + ZonedDateTime now = ZonedDateTime.now(GlobalZoneId); + return EventInstance.builder() + .eventHistory(eventHistory) + .eventTime(eventTime) + .instanceDate(now) + .build(); + } + public void subRemainEventCount(Long amount) { this.remainEventCount -= amount; } diff --git a/src/main/java/com/yello/server/domain/event/entity/EventInstanceReward.java b/src/main/java/com/yello/server/domain/event/entity/EventInstanceReward.java index 4d8cf899..d707371a 100644 --- a/src/main/java/com/yello/server/domain/event/entity/EventInstanceReward.java +++ b/src/main/java/com/yello/server/domain/event/entity/EventInstanceReward.java @@ -45,5 +45,14 @@ public class EventInstanceReward extends AuditingTimeEntity { @Column private String rewardImage; + public static EventInstanceReward of(EventInstance eventInstance, EventReward eventReward) { + return EventInstanceReward.builder() + .eventInstance(eventInstance) + .rewardTag(eventReward.getTag()) + .rewardValue(eventReward.getMinRewardValue()) + .rewardTitle(String.format(eventReward.getRewardTitle(), eventReward.getMinRewardValue())) + .rewardImage(eventReward.getRewardImage()) + .build(); + } } diff --git a/src/main/java/com/yello/server/domain/event/entity/EventReward.java b/src/main/java/com/yello/server/domain/event/entity/EventReward.java index b6066617..ecd963c9 100644 --- a/src/main/java/com/yello/server/domain/event/entity/EventReward.java +++ b/src/main/java/com/yello/server/domain/event/entity/EventReward.java @@ -54,4 +54,8 @@ public class EventReward { @Column private String rewardImage; + + public void updateMinRewardValue(Long rewardValue) { + this.minRewardValue = rewardValue; + } } diff --git a/src/main/java/com/yello/server/domain/event/entity/RandomType.java b/src/main/java/com/yello/server/domain/event/entity/RandomType.java new file mode 100644 index 00000000..d3f9214f --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/RandomType.java @@ -0,0 +1,32 @@ +package com.yello.server.domain.event.entity; + +import static com.yello.server.global.common.ErrorCode.ENUM_BAD_REQUEST_EXCEPTION; + +import com.yello.server.global.exception.EnumIllegalArgumentException; +import java.util.Arrays; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum RandomType { + FIXED("FIXED"), + RANDOM("RANDOM"), + ADMOB_RANDOM("ADMOB_RANDOM"); + + private final String initial; + + public static RandomType fromCode(String dbData) { + return Arrays.stream(RandomType.values()) + .filter(v -> v.getInitial().equals(dbData)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static RandomType fromName(String name) { + return Arrays.stream(RandomType.values()) + .filter(v -> v.name().equals(name)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } +} diff --git a/src/main/java/com/yello/server/domain/event/entity/RandomTypeConverter.java b/src/main/java/com/yello/server/domain/event/entity/RandomTypeConverter.java new file mode 100644 index 00000000..83e6272d --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/RandomTypeConverter.java @@ -0,0 +1,27 @@ +package com.yello.server.domain.event.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.extern.log4j.Log4j2; + +@Converter +@Log4j2 +public class RandomTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(RandomType type) { + if (type == null) { + return null; + } + return type.name(); + } + + @Override + public RandomType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return RandomType.fromName(dbData); + } +} diff --git a/src/main/java/com/yello/server/domain/event/entity/RewardType.java b/src/main/java/com/yello/server/domain/event/entity/RewardType.java new file mode 100644 index 00000000..8a995b48 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/RewardType.java @@ -0,0 +1,33 @@ +package com.yello.server.domain.event.entity; + +import static com.yello.server.global.common.ErrorCode.ENUM_BAD_REQUEST_EXCEPTION; + +import com.yello.server.global.exception.EnumIllegalArgumentException; +import java.util.Arrays; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum RewardType { + POINT("POINT"), + TICKET("TICKET"), + ADMOB_POINT("ADMOB_POINT"), + ADMOB_MULTIPLE_POINT("ADMOB_MULTIPLE_POINT"); + + private final String initial; + + public static RewardType fromCode(String dbData) { + return Arrays.stream(RewardType.values()) + .filter(v -> v.getInitial().equals(dbData)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static RewardType fromName(String name) { + return Arrays.stream(RewardType.values()) + .filter(v -> v.name().equals(name)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } +} diff --git a/src/main/java/com/yello/server/domain/event/entity/RewardTypeConverter.java b/src/main/java/com/yello/server/domain/event/entity/RewardTypeConverter.java new file mode 100644 index 00000000..22eb7ef8 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/RewardTypeConverter.java @@ -0,0 +1,27 @@ +package com.yello.server.domain.event.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.extern.log4j.Log4j2; + +@Converter +@Log4j2 +public class RewardTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(RewardType type) { + if (type == null) { + return null; + } + return type.name(); + } + + @Override + public RewardType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return RewardType.fromName(dbData); + } +} diff --git a/src/main/java/com/yello/server/domain/event/exception/EventForbiddenException.java b/src/main/java/com/yello/server/domain/event/exception/EventForbiddenException.java new file mode 100644 index 00000000..304e1ceb --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/exception/EventForbiddenException.java @@ -0,0 +1,11 @@ +package com.yello.server.domain.event.exception; + +import com.yello.server.global.common.ErrorCode; +import com.yello.server.global.exception.CustomException; + +public class EventForbiddenException extends CustomException { + + public EventForbiddenException(ErrorCode error) { + super(error, "[EventForbiddenException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventRepository.java index f7d3b768..18177b61 100644 --- a/src/main/java/com/yello/server/domain/event/repository/EventRepository.java +++ b/src/main/java/com/yello/server/domain/event/repository/EventRepository.java @@ -41,4 +41,8 @@ public interface EventRepository { Optional findHistoryByIdempotencyKey(UUID idempotencyKey); Optional findInstanceByEventHistory(EventHistory eventHistory); + + EventReward findRewardByTag(String rewardTag); + + EventRewardMapping findRewardMappingByEventRewardId(Long eventRewardId); } diff --git a/src/main/java/com/yello/server/domain/event/repository/EventRepositoryImpl.java b/src/main/java/com/yello/server/domain/event/repository/EventRepositoryImpl.java index 8de36ba5..af3c2d6e 100644 --- a/src/main/java/com/yello/server/domain/event/repository/EventRepositoryImpl.java +++ b/src/main/java/com/yello/server/domain/event/repository/EventRepositoryImpl.java @@ -1,8 +1,12 @@ package com.yello.server.domain.event.repository; +import static com.yello.server.domain.event.entity.QEventReward.eventReward; +import static com.yello.server.domain.event.entity.QEventRewardMapping.eventRewardMapping; import static com.yello.server.global.common.ErrorCode.EVENT_NOT_FOUND_EXCEPTION; import static com.yello.server.global.common.ErrorCode.EVENT_RANDOM_NOT_FOUND_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.NOT_FOUND_EVENT_REWARD_EXCEPTION; +import com.querydsl.jpa.impl.JPAQueryFactory; import com.yello.server.domain.event.entity.Event; import com.yello.server.domain.event.entity.EventHistory; import com.yello.server.domain.event.entity.EventInstance; @@ -32,6 +36,7 @@ public class EventRepositoryImpl implements EventRepository { private final EventRewardJpaRepository eventRewardJpaRepository; private final EventRewardMappingJpaRepository eventRewardMappingJpaRepository; private final EventTimeJpaRepository eventTimeJpaRepository; + private final JPAQueryFactory jpaQueryFactory; @Override public EventHistory save(EventHistory newEventHistory) { @@ -100,4 +105,18 @@ public Optional findHistoryByIdempotencyKey(UUID idempotencyKey) { public Optional findInstanceByEventHistory(EventHistory eventHistory) { return eventInstanceJpaRepository.findTopByEventHistory(eventHistory); } + + @Override + public EventReward findRewardByTag(String rewardTag) { + return Optional.ofNullable(jpaQueryFactory.selectFrom(eventReward) + .where(eventReward.tag.eq(rewardTag)) + .fetchOne()).orElseThrow(() -> new EventNotFoundException(NOT_FOUND_EVENT_REWARD_EXCEPTION)); + } + + @Override + public EventRewardMapping findRewardMappingByEventRewardId(Long eventRewardId) { + return Optional.ofNullable(jpaQueryFactory.selectFrom(eventRewardMapping) + .where(eventRewardMapping.eventReward.id.eq(eventRewardId)) + .fetchFirst()).orElseThrow(() -> new EventNotFoundException(NOT_FOUND_EVENT_REWARD_EXCEPTION)); + } } diff --git a/src/main/java/com/yello/server/domain/event/service/EventService.java b/src/main/java/com/yello/server/domain/event/service/EventService.java index 4d1abbf2..a9da86f5 100644 --- a/src/main/java/com/yello/server/domain/event/service/EventService.java +++ b/src/main/java/com/yello/server/domain/event/service/EventService.java @@ -1,5 +1,6 @@ package com.yello.server.domain.event.service; +import static com.yello.server.global.common.ErrorCode.DUPLICATE_ADMOB_REWARD_EXCEPTION; import static com.yello.server.global.common.ErrorCode.EVENT_COUNT_BAD_REQUEST_EXCEPTION; import static com.yello.server.global.common.ErrorCode.EVENT_DATE_BAD_REQUEST_EXCEPTION; import static com.yello.server.global.common.ErrorCode.EVENT_TIME_BAD_REQUEST_EXCEPTION; @@ -10,6 +11,9 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.crypto.tink.apps.rewardedads.RewardedAdsVerifier; +import com.yello.server.domain.event.dto.request.AdmobRewardRequest; +import com.yello.server.domain.event.dto.request.AdmobSsvRequest; import com.yello.server.domain.event.dto.request.EventJoinRequest; import com.yello.server.domain.event.dto.response.EventResponse; import com.yello.server.domain.event.dto.response.EventRewardResponse; @@ -22,13 +26,19 @@ import com.yello.server.domain.event.entity.EventRewardMapping; import com.yello.server.domain.event.entity.EventTime; import com.yello.server.domain.event.entity.EventType; +import com.yello.server.domain.event.entity.RandomType; +import com.yello.server.domain.event.entity.RewardType; import com.yello.server.domain.event.exception.EventBadRequestException; +import com.yello.server.domain.event.exception.EventForbiddenException; import com.yello.server.domain.event.exception.EventNotFoundException; import com.yello.server.domain.event.repository.EventRepository; import com.yello.server.domain.user.entity.User; import com.yello.server.domain.user.repository.UserRepository; +import com.yello.server.global.common.factory.UuidFactory; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; +import java.net.URI; import java.time.Duration; import java.time.OffsetTime; import java.time.ZonedDateTime; @@ -39,6 +49,7 @@ import java.util.UUID; import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -67,11 +78,13 @@ public List getEvents(Long userId) throws JsonProcessingException for (Event event : eventList) { // 현재 시각에 유효한 이벤트 시간대 - final List eventTimeList = eventRepository.findAllByEventId(event.getId()).stream() - .filter( - eventTime -> nowTime.isAfter(eventTime.getStartTime()) && nowTime.isBefore(eventTime.getEndTime()) - ) - .toList(); + final List eventTimeList = + eventRepository.findAllByEventId(event.getId()).stream() + .filter( + eventTime -> nowTime.isAfter(eventTime.getStartTime()) && nowTime.isBefore( + eventTime.getEndTime()) + ) + .toList(); List eventInstanceList = new ArrayList<>(); if (!eventTimeList.isEmpty()) { @@ -87,7 +100,7 @@ public List getEvents(Long userId) throws JsonProcessingException && eventInstance.getInstanceDate().isBefore(event.getEndDate()) && nowTime.isAfter(eventInstance.getEventTime().getStartTime()) && nowTime.isBefore(eventInstance.getEventTime().getEndTime()) - && eventInstance.getRemainEventCount() == 0 + && eventInstance.getRemainEventCount()==0 ) .toList() ); @@ -97,7 +110,8 @@ public List getEvents(Long userId) throws JsonProcessingException // 이벤트 시간대가 있고, 보상 카운트가 0인 참여이력의 숫자가 RewardCount 보다 적을 때 // k번 이벤트 참여는 보상 카운트로 구현한다. boolean isEventAvailable = - !eventTimeList.isEmpty() && (eventTimeList.get(0).getRewardCount() > eventInstanceList.size()); + !eventTimeList.isEmpty() && (eventTimeList.get(0).getRewardCount() + > eventInstanceList.size()); final EventTime eventTime = isEventAvailable ? eventTimeList.get(0) : null; final List eventRewardMappingList = @@ -113,7 +127,8 @@ public List getEvents(Long userId) throws JsonProcessingException public void joinEvent(Long userId, UUID uuidIdempotencyKey, EventJoinRequest request) { // exception final User user = userRepository.getById(userId); - final Optional eventHistory = eventRepository.findHistoryByIdempotencyKey(uuidIdempotencyKey); + final Optional eventHistory = + eventRepository.findHistoryByIdempotencyKey(uuidIdempotencyKey); if (eventHistory.isPresent()) { throw new EventBadRequestException(IDEMPOTENCY_KEY_CONFLICT_EXCEPTION); } @@ -129,9 +144,11 @@ public void joinEvent(Long userId, UUID uuidIdempotencyKey, EventJoinRequest req } // 이벤트 시간대에 유효해야함. - final List eventTimeList = eventRepository.findAllByEventId(event.getId()).stream() - .filter(eventTime -> nowTime.isAfter(eventTime.getStartTime()) && nowTime.isBefore(eventTime.getEndTime())) - .toList(); + final List eventTimeList = + eventRepository.findAllByEventId(event.getId()).stream() + .filter(eventTime -> nowTime.isAfter(eventTime.getStartTime()) && nowTime.isBefore( + eventTime.getEndTime())) + .toList(); if (eventTimeList.isEmpty()) { throw new EventBadRequestException(EVENT_TIME_BAD_REQUEST_EXCEPTION); } @@ -151,15 +168,18 @@ public void joinEvent(Long userId, UUID uuidIdempotencyKey, EventJoinRequest req } @Transactional - public EventRewardResponse rewardEvent(Long userId, UUID uuidIdempotencyKey) throws JsonProcessingException { + public EventRewardResponse rewardEvent(Long userId, UUID uuidIdempotencyKey) + throws JsonProcessingException { // exception final User user = userRepository.getById(userId); - final Optional eventHistory = eventRepository.findHistoryByIdempotencyKey(uuidIdempotencyKey); + final Optional eventHistory = + eventRepository.findHistoryByIdempotencyKey(uuidIdempotencyKey); if (eventHistory.isEmpty()) { throw new EventNotFoundException(IDEMPOTENCY_KEY_NOT_FOUND_EXCEPTION); } // 멱등키에 해당하는 하나의 EventHistory는 반드시 하나의 EventInstance만 가진다. - final Optional eventInstance = eventRepository.findInstanceByEventHistory(eventHistory.get()); + final Optional eventInstance = + eventRepository.findInstanceByEventHistory(eventHistory.get()); ZonedDateTime now = ZonedDateTime.now(GlobalZoneId); OffsetTime nowTime = now.toOffsetDateTime().toOffsetTime(); @@ -179,21 +199,24 @@ public EventRewardResponse rewardEvent(Long userId, UUID uuidIdempotencyKey) thr throw new EventBadRequestException(EVENT_COUNT_BAD_REQUEST_EXCEPTION); } - final List eventRewardMappingList = eventRepository.findAllByEventTimeId( - eventInstance.get().getEventTime().getId()); + final List eventRewardMappingList = + eventRepository.findAllByEventTimeId( + eventInstance.get().getEventTime().getId()); // logic // EventRewardProbability 필드에 따라 보상을 선택한다. final EventRewardMapping randomRewardMapping = selectRandomly(eventRewardMappingList); final long randomValue = selectRandomValue(randomRewardMapping); - final EventInstanceReward eventInstanceReward = eventRepository.save(EventInstanceReward.builder() - .eventInstance(eventInstance.get()) - .rewardTag(randomRewardMapping.getEventReward().getTag()) - .rewardValue(randomValue) - .rewardTitle(String.format(randomRewardMapping.getEventReward().getRewardTitle(), randomValue)) - .rewardImage(randomRewardMapping.getEventReward().getRewardImage()) - .build()); + final EventInstanceReward eventInstanceReward = + eventRepository.save(EventInstanceReward.builder() + .eventInstance(eventInstance.get()) + .rewardTag(randomRewardMapping.getEventReward().getTag()) + .rewardValue(randomValue) + .rewardTitle(String.format(randomRewardMapping.getEventReward().getRewardTitle(), + randomValue)) + .rewardImage(randomRewardMapping.getEventReward().getRewardImage()) + .build()); /** * TODO 그냥 문자열로 저장하고 있는데, 어떻게 해야 enum의 문제를 극복하면서 switch와 함꼐 사용할 수 있을지 고민해야함. @@ -201,13 +224,94 @@ public EventRewardResponse rewardEvent(Long userId, UUID uuidIdempotencyKey) thr if (randomRewardMapping.getEventReward().getTag().equals("TICKET")) { user.addTicketCount((int) randomValue); } else if (randomRewardMapping.getEventReward().getTag().equals("POINT")) { - user.addPoint((int) randomValue); + user.addPointBySubscribe((int) randomValue); } eventInstance.get().subRemainEventCount(1L); return EventRewardResponse.of(eventInstanceReward); } - private @NotNull EventRewardMapping selectRandomly(@NotEmpty List eventRewardMappingList) { + @SneakyThrows + @Transactional + public void verifyAdmobReward(URI uri, HttpServletRequest request) { + // 1. admob 검증하기 + RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder() + .fetchVerifyingPublicKeysWith( + RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD) + .build(); + verifier.verify(uri.toString()); + System.out.println(" verify 성공 !!!!!!"); + + // 2. google이 넘겨준 query 정보 가져오기 + AdmobSsvRequest admobRequest = AdmobSsvRequest.of(request.getParameterMap()); + + // 3. 보상정보 멱등키랑 저장하기 + UUID uuidIdempotencyKey = UuidFactory.checkUuid(admobRequest.customData()); + + final Optional eventHistory = + eventRepository.findHistoryByIdempotencyKey(uuidIdempotencyKey); + if (eventHistory.isPresent()) { + throw new EventBadRequestException(IDEMPOTENCY_KEY_CONFLICT_EXCEPTION); + } + + eventRepository.save(EventHistory.of(null, uuidIdempotencyKey)); + } + + @Transactional + public EventRewardResponse rewardAdmob(Long userId, AdmobRewardRequest request) { + UUID uuid = UuidFactory.checkUuid(request.uuid()); + final User user = userRepository.getById(userId); + + // 멱등키를 통해 해당 history 찾기 + final Optional eventHistory = + eventRepository.findHistoryByIdempotencyKey(uuid); + if (eventHistory.isEmpty()) { + throw new EventBadRequestException(IDEMPOTENCY_KEY_NOT_FOUND_EXCEPTION); + } + + // history 있으면 userId로 세팅 + if (eventHistory.get().getUser()!=null) { + throw new EventForbiddenException(DUPLICATE_ADMOB_REWARD_EXCEPTION); + } + eventHistory.get().update(user); + + // event_random tag 확인 후 reward + EventReward eventReward = null; + switch (RandomType.fromCode(request.randomType())) { + case FIXED, ADMOB_RANDOM -> { + eventReward = handleRewardByType(request, user); + } + } + EventRewardMapping rewardMapping = + eventRepository.findRewardMappingByEventRewardId(eventReward.getId()); + + // event_instance에 해당 데이터 저장 + EventInstance eventInstance = eventRepository.save( + EventInstance.of(rewardMapping.getEventTime(), eventHistory.get())); + EventInstanceReward rewardInstance = + eventRepository.save(EventInstanceReward.of(eventInstance, eventReward)); + + return EventRewardResponse.of(rewardInstance); + } + + private EventReward handleRewardByType(AdmobRewardRequest request, User user) { + switch (RewardType.fromCode(request.rewardType())) { + case ADMOB_POINT -> { + EventReward rewardByTag = eventRepository.findRewardByTag(request.rewardType()); + user.addPoint(Math.toIntExact((rewardByTag.getMinRewardValue()))); + return rewardByTag; + } + case ADMOB_MULTIPLE_POINT -> { + EventReward rewardByTag = eventRepository.findRewardByTag(request.rewardType()); + user.addPoint(request.rewardNumber()); + rewardByTag.updateMinRewardValue(Long.valueOf(request.rewardNumber() * 2)); + return rewardByTag; + } + } + return null; + } + + private @NotNull EventRewardMapping selectRandomly( + @NotEmpty List eventRewardMappingList) { // 전체 확률 합계를 계산합니다. int totalProbability = eventRewardMappingList.stream() .mapToInt(EventRewardMapping::getEventRewardProbability) @@ -228,20 +332,23 @@ public EventRewardResponse rewardEvent(Long userId, UUID uuidIdempotencyKey) thr return eventRewardMappingList.get(0); } - private long selectRandomValue(@NotNull EventRewardMapping eventRewardMapping) throws JsonProcessingException { + private long selectRandomValue(@NotNull EventRewardMapping eventRewardMapping) + throws JsonProcessingException { EventReward eventReward = eventRewardMapping.getEventReward(); EventRandom eventRandom = eventRewardMapping.getEventRandom(); - List probabilityPoints = objectMapper.readValue(eventRandom.getProbabilityPointList(), - new TypeReference>() { - } - ); + List probabilityPoints = + objectMapper.readValue(eventRandom.getProbabilityPointList(), + new TypeReference>() { + } + ); return calculateRewardValue(probabilityPoints, eventReward.getMinRewardValue(), eventReward.getMaxRewardValue()); } - private long calculateRewardValue(List points, long minRewardValue, long maxRewardValue) { + private long calculateRewardValue(List points, long minRewardValue, + long maxRewardValue) { // 무작위 x 값을 생성합니다. double x = new Random().nextDouble(); @@ -249,14 +356,16 @@ private long calculateRewardValue(List points, long minRewardV for (int i = 0; i < points.size() - 1; i++) { if (x >= points.get(i).getX() && x < points.get(i + 1).getX()) { // x 값이 속하는 구간을 찾았으므로 해당 구간에서 무작위 보상값을 생성합니다. - return randomRewardValue(points.get(i).getY(), points.get(i + 1).getY(), minRewardValue, + return randomRewardValue(points.get(i).getY(), points.get(i + 1).getY(), + minRewardValue, maxRewardValue); } } // x 값이 마지막 구간에 속하는 경우 if (x >= points.get(points.size() - 1).getX()) { - return randomRewardValue(points.get(points.size() - 1).getY(), 1.0, minRewardValue, maxRewardValue); + return randomRewardValue(points.get(points.size() - 1).getY(), 1.0, minRewardValue, + maxRewardValue); } // 이 부분은 도달할 수 없지만 혹시 모를 오류를 대비해 예외를 던집니다. diff --git a/src/main/java/com/yello/server/domain/purchase/service/PurchaseManagerImpl.java b/src/main/java/com/yello/server/domain/purchase/service/PurchaseManagerImpl.java index d14f609b..bdfa4cc4 100644 --- a/src/main/java/com/yello/server/domain/purchase/service/PurchaseManagerImpl.java +++ b/src/main/java/com/yello/server/domain/purchase/service/PurchaseManagerImpl.java @@ -22,12 +22,10 @@ import com.yello.server.domain.user.entity.Subscribe; import com.yello.server.domain.user.entity.User; import com.yello.server.domain.user.repository.UserRepository; -import com.yello.server.global.common.factory.DecodeTokenFactory; +import com.yello.server.global.common.factory.DecodeFactory; import com.yello.server.global.common.factory.TokenFactory; -import com.yello.server.global.common.util.ConstantUtil; import com.yello.server.infrastructure.slack.dto.response.SlackAppleNotificationResponse; import java.util.Map; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @@ -77,7 +75,7 @@ public void handleAppleTransactionError(ResponseEntity @Override public AppleNotificationPayloadVO decodeApplePayload(String signedPayload) { - Map jsonPayload = DecodeTokenFactory.decodeToken(signedPayload); + Map jsonPayload = DecodeFactory.decodeToken(signedPayload); ObjectMapper objectMapper = new ObjectMapper(); String notificationType = jsonPayload.get("notificationType").toString(); @@ -98,7 +96,7 @@ public AppleNotificationPayloadVO decodeApplePayload(String signedPayload) { @Override public Purchase decodeAppleNotificationData(String signedTransactionInfo) { - Map decodeToken = DecodeTokenFactory.decodeToken(signedTransactionInfo); + Map decodeToken = DecodeFactory.decodeToken(signedTransactionInfo); String decodeTransactionId = decodeToken.get("transactionId").toString(); Purchase purchase = purchaseRepository.findByTransactionId(decodeTransactionId) @@ -223,7 +221,7 @@ public ApplePurchaseVO getPurchaseData(AppleNotificationPayloadVO payloadVO) { public AppleJwsTransactionResponse decodeAppleDataPayload(String signedTransactionInfo) { - Map decodeToken = DecodeTokenFactory.decodeToken(signedTransactionInfo); + Map decodeToken = DecodeFactory.decodeToken(signedTransactionInfo); String decodeOriginalTransactionId = decodeToken.get("originalTransactionId").toString(); String decodeTransactionId = decodeToken.get("transactionId").toString(); diff --git a/src/main/java/com/yello/server/domain/user/entity/User.java b/src/main/java/com/yello/server/domain/user/entity/User.java index c9afcfdb..76b52ddc 100644 --- a/src/main/java/com/yello/server/domain/user/entity/User.java +++ b/src/main/java/com/yello/server/domain/user/entity/User.java @@ -169,7 +169,7 @@ public void renew() { } - public void addPoint(Integer point) { + public void addPointBySubscribe(Integer point) { if (this.getSubscribe() == Subscribe.NORMAL) { this.point += point; return; @@ -180,6 +180,9 @@ public void addPoint(Integer point) { public void subPoint(Integer point) { this.point -= point; } + public void addPoint(Integer point) { + this.point += point; + } public void addRecommendCount(Long recommendCount) { this.recommendCount += recommendCount; diff --git a/src/main/java/com/yello/server/domain/vote/service/VoteService.java b/src/main/java/com/yello/server/domain/vote/service/VoteService.java index f2fc3823..4b358827 100644 --- a/src/main/java/com/yello/server/domain/vote/service/VoteService.java +++ b/src/main/java/com/yello/server/domain/vote/service/VoteService.java @@ -7,13 +7,11 @@ import static com.yello.server.global.common.ErrorCode.REVEAL_FULL_NAME_VOTE_EXCEPTION; import static com.yello.server.global.common.ErrorCode.WRONG_VOTE_TYPE_FORBIDDEN; import static com.yello.server.global.common.factory.TimeFactory.minusTime; -import static com.yello.server.global.common.util.ConstantUtil.ALL_VOTE_TYPE; import static com.yello.server.global.common.util.ConstantUtil.CHECK_FULL_NAME; import static com.yello.server.global.common.util.ConstantUtil.COOL_DOWN_TIME; import static com.yello.server.global.common.util.ConstantUtil.MINUS_TICKET_COUNT; import static com.yello.server.global.common.util.ConstantUtil.NO_FRIEND_COUNT; import static com.yello.server.global.common.util.ConstantUtil.RANDOM_COUNT; -import static com.yello.server.global.common.util.ConstantUtil.USER_VOTE_TYPE; import com.yello.server.domain.cooldown.entity.Cooldown; import com.yello.server.domain.cooldown.repository.CooldownRepository; @@ -47,7 +45,6 @@ import com.yello.server.domain.vote.exception.VoteForbiddenException; import com.yello.server.domain.vote.exception.VoteNotFoundException; import com.yello.server.domain.vote.repository.VoteRepository; -import com.yello.server.global.common.ErrorCode; import com.yello.server.infrastructure.rabbitmq.service.ProducerService; import java.time.LocalDateTime; import java.util.List; @@ -213,7 +210,7 @@ public VoteCreateVO createVote(Long userId, CreateVoteRequest request) { cooldown.updateDate(LocalDateTime.now()); producerService.produceVoteAvailableNotification(cooldown); - sender.addPoint(request.totalPoint()); + sender.addPointBySubscribe(request.totalPoint()); return VoteCreateVO.of(sender.getPoint(), votes); } diff --git a/src/main/java/com/yello/server/global/common/ErrorCode.java b/src/main/java/com/yello/server/global/common/ErrorCode.java index ca5f4adf..5544893a 100644 --- a/src/main/java/com/yello/server/global/common/ErrorCode.java +++ b/src/main/java/com/yello/server/global/common/ErrorCode.java @@ -40,11 +40,12 @@ public enum ErrorCode { USER_DATA_INVALID_ARGUMENT_EXCEPTION(BAD_REQUEST, "입력한 유저 데이터의 값이 올바르지 않습니다."), ENUM_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "존재하지 않는 열거형 타입입니다."), PROBABILITY_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "확률의 합이 100이 아닙니다."), - IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "멱등성 키를 헤더에 명시되어 있지 않습니다."), + IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "멱등성 키가 명시되어 있지 않습니다."), IDEMPOTENCY_KEY_INVALID_FORM_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "멱등성 키가 유효한 uuid4 형식이 아닙니다."), EVENT_DATE_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "해당 이벤트는 현재 유효한 날짜가 아닙니다."), EVENT_TIME_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "해당 이벤트는 현재 유효한 시간이 아닙니다."), EVENT_COUNT_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "해당 이벤트는 보상 횟수가 전부 소진되었습니다."), + ADMOB_URI_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "URI의 값이 올바르지 않습니다."), /** * 401 UNAUTHORIZED @@ -73,6 +74,7 @@ public enum ErrorCode { GOOGLE_SUBSCRIPTIONS_FORBIDDEN_EXCEPTION(FORBIDDEN, "이미 YELLO: PLUS를 구독한 상태입니다."), GOOGLE_SUBSCRIPTION_TRANSACTION_EXPIRED_EXCEPTION(FORBIDDEN, "이미 만료된 결제 내역의 영수증입니다."), WRONG_VOTE_TYPE_FORBIDDEN(FORBIDDEN, "잘못된 투표 유형입니다."), + DUPLICATE_ADMOB_REWARD_EXCEPTION(FORBIDDEN, "이미 광고 보상에 대한 처리가 완료되었습니다."), /** * 404 NOT FOUND @@ -107,6 +109,7 @@ public enum ErrorCode { EVENT_TIME_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 EventTime가 존재하지 않습니다."), IDEMPOTENCY_KEY_NOT_FOUND_EXCEPTION(NOT_FOUND, "멱등키의 이벤트가 존재하지 않습니다. 이벤트 참여를 먼저 해주세요"), EVENT_RANDOM_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 EventRandom가 존재하지 않습니다."), + NOT_FOUND_EVENT_REWARD_EXCEPTION(NOT_FOUND, "해당 EventReward가 존재하지 않습니다."), /** * 409 CONFLICT diff --git a/src/main/java/com/yello/server/global/common/SuccessCode.java b/src/main/java/com/yello/server/global/common/SuccessCode.java index 587db04e..b589d2c0 100644 --- a/src/main/java/com/yello/server/global/common/SuccessCode.java +++ b/src/main/java/com/yello/server/global/common/SuccessCode.java @@ -62,6 +62,7 @@ public enum SuccessCode { EVENT_REWARD_CREATE_ADMIN_SUCCESS(OK, "어드민 권한으로 이벤트 보상 생성에 성공하였습니다."), EVENT_JOIN_SUCCESS(OK, "이벤트 참여에 성공하였습니다."), EVENT_REWARD_SUCCESS(OK, "이벤트 보상에 성공하였습니다."), + VERIFY_ADMOB_SSV_SUCCESS(OK, "Admob ssv 검증에 성공하였습니다."), /** * 201 CREATED @@ -76,7 +77,8 @@ public enum SuccessCode { PURCHASE_SUBSCRIPTION_VERIFY_SUCCESS(CREATED, "애플 결제 검증 및 반영에 성공하였습니다."), GOOGLE_PURCHASE_SUBSCRIPTION_VERIFY_SUCCESS(CREATED, "구글 구독 결제 검증 및 반영에 성공하였습니다."), GOOGLE_PURCHASE_INAPP_VERIFY_SUCCESS(CREATED, "구글 인앱 결제 검증 및 반영에 성공하였습니다."), - LOGIN_USER_ADMIN_SUCCESS(CREATED, "어드민 로그인에 성공하였습니다."); + LOGIN_USER_ADMIN_SUCCESS(CREATED, "어드민 로그인에 성공하였습니다."), + REWARD_ADMOB_SUCCESS(CREATED, "Admob 광고 보고 보상받기에 성공했습니다."); private final HttpStatus httpStatus; diff --git a/src/main/java/com/yello/server/global/common/factory/DecodeTokenFactory.java b/src/main/java/com/yello/server/global/common/factory/DecodeFactory.java similarity index 93% rename from src/main/java/com/yello/server/global/common/factory/DecodeTokenFactory.java rename to src/main/java/com/yello/server/global/common/factory/DecodeFactory.java index 09064049..85d0747a 100644 --- a/src/main/java/com/yello/server/global/common/factory/DecodeTokenFactory.java +++ b/src/main/java/com/yello/server/global/common/factory/DecodeFactory.java @@ -3,7 +3,7 @@ import java.util.Map; import org.springframework.boot.json.BasicJsonParser; -public class DecodeTokenFactory { +public class DecodeFactory { public static Map decodeToken(String jwtToken) { final String payloadJWT = jwtToken.split("\\.")[1]; diff --git a/src/main/java/com/yello/server/global/common/factory/TokenFactoryImpl.java b/src/main/java/com/yello/server/global/common/factory/TokenFactoryImpl.java index cc0aaf83..05d0db88 100644 --- a/src/main/java/com/yello/server/global/common/factory/TokenFactoryImpl.java +++ b/src/main/java/com/yello/server/global/common/factory/TokenFactoryImpl.java @@ -57,7 +57,7 @@ public String generateAppleToken() { public void decodeTransactionToken(String signedTransactionInfo, String transactionId) { - Map decodeToken = DecodeTokenFactory.decodeToken(signedTransactionInfo); + Map decodeToken = DecodeFactory.decodeToken(signedTransactionInfo); String decodeTransactionId = decodeToken.get("transactionId").toString(); if (!transactionId.equals(decodeTransactionId)) { diff --git a/src/main/java/com/yello/server/global/common/factory/UuidFactory.java b/src/main/java/com/yello/server/global/common/factory/UuidFactory.java new file mode 100644 index 00000000..56034b09 --- /dev/null +++ b/src/main/java/com/yello/server/global/common/factory/UuidFactory.java @@ -0,0 +1,25 @@ +package com.yello.server.global.common.factory; + +import static com.yello.server.global.common.ErrorCode.IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.IDEMPOTENCY_KEY_INVALID_FORM_BAD_REQUEST_EXCEPTION; + +import com.yello.server.domain.event.exception.EventBadRequestException; +import java.util.UUID; +import org.springframework.util.StringUtils; + +public class UuidFactory { + + public static UUID checkUuid(String uuid) { + UUID uuidIdempotencyKey; + if (!StringUtils.hasText(uuid)) { + throw new EventBadRequestException(IDEMPOTENCY_KEY_BAD_REQUEST_EXCEPTION); + } + try { + uuidIdempotencyKey = UUID.fromString(uuid); + } catch (IllegalArgumentException e) { + throw new EventBadRequestException(IDEMPOTENCY_KEY_INVALID_FORM_BAD_REQUEST_EXCEPTION); + } + return uuidIdempotencyKey; + } + +} diff --git a/src/main/java/com/yello/server/global/common/util/ConstantUtil.java b/src/main/java/com/yello/server/global/common/util/ConstantUtil.java index f67a0d7c..d16758c3 100644 --- a/src/main/java/com/yello/server/global/common/util/ConstantUtil.java +++ b/src/main/java/com/yello/server/global/common/util/ConstantUtil.java @@ -76,7 +76,6 @@ public class ConstantUtil { public static final String USER_VOTE_TYPE = "send"; public static final String ALL_VOTE_TYPE = "all"; - private ConstantUtil() { throw new IllegalStateException(); } diff --git a/src/main/java/com/yello/server/global/exception/ControllerExceptionAdvice.java b/src/main/java/com/yello/server/global/exception/ControllerExceptionAdvice.java index e6933092..3b123387 100644 --- a/src/main/java/com/yello/server/global/exception/ControllerExceptionAdvice.java +++ b/src/main/java/com/yello/server/global/exception/ControllerExceptionAdvice.java @@ -21,6 +21,7 @@ import com.yello.server.domain.authorization.exception.NotValidTokenForbiddenException; import com.yello.server.domain.authorization.exception.OAuthException; import com.yello.server.domain.event.exception.EventBadRequestException; +import com.yello.server.domain.event.exception.EventForbiddenException; import com.yello.server.domain.event.exception.EventNotFoundException; import com.yello.server.domain.friend.exception.FriendException; import com.yello.server.domain.friend.exception.FriendNotFoundException; @@ -173,7 +174,8 @@ public ResponseEntity UnauthorizedException(CustomException except VoteForbiddenException.class, NotSignedInException.class, NotExpiredTokenForbiddenException.class, - NotValidTokenForbiddenException.class + NotValidTokenForbiddenException.class, + EventForbiddenException.class }) public ResponseEntity ForbiddenException(CustomException exception) { return ResponseEntity.status(FORBIDDEN) diff --git a/src/main/resources/static/docs/add-friend.html b/src/main/resources/static/docs/add-friend.html index 64bb0f9c..70ee9041 100644 --- a/src/main/resources/static/docs/add-friend.html +++ b/src/main/resources/static/docs/add-friend.html @@ -4,23 +4,25 @@ - + 친구 추가하기 - - - - + + + \ No newline at end of file diff --git a/src/main/resources/static/docs/login.html b/src/main/resources/static/docs/login.html index 79046842..fd84b612 100644 --- a/src/main/resources/static/docs/login.html +++ b/src/main/resources/static/docs/login.html @@ -4,23 +4,25 @@ - + 소셜 로그인 + + + +
+
+

이벤트 참여

+
+
+

요청

+
+
+
POST /api/v1/admob/reward HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+IdempotencyKey: 87552f7c-9b62-4b12-b567-1bd062b09288
+Content-Length: 134
+
+{
+  "rewardType" : "ADMOB_POINT",
+  "randomType" : "FIXED",
+  "uuid" : "87552f7c-9b62-4b12-b567-1bd062b09288",
+  "rewardNumber" : 10
+}
+
+
+
+
+

request body

+
+
    +
  • +

    "rewardType": String → "ADMOB_POINT" | "ADMOB_MULTIPLE_POINT"

    +
    +
      +
    • +

      ADMOB_POINT : 광고 보고 10 포인트

      +
    • +
    • +

      ADMOB_MULTIPLE_POINT : 광고 보고 포인트 2배 이벤트

      +
    • +
    +
    +
  • +
  • +

    "randomType" : String → "FIXED" | "ADMOB_RANDOM"

    +
    +
      +
    • +

      FIXED : 고정값 (현재 이것만 사용)

      +
    • +
    • +

      ADMOB_RANDOM : 랜덤값 (추후 랜덤으로 바뀔 것 고려)

      +
    • +
    +
    +
  • +
  • +

    "uuid" : String → UUID4 형식만 적용

    +
  • +
  • +

    "rewardNumber" : Integer → 포인트인 경우 10, 투표 포인트 2배 이벤트인 경우 현재 투표 후 받은 포인트 보내줘야함

    +
  • +
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 201,
+  "message" : "Admob 광고 보고 보상받기에 성공했습니다.",
+  "data" : {
+    "rewardTag" : "ADMOB_POINT",
+    "rewardValue" : 10,
+    "rewardTitle" : "10 포인트를 얻었어요!",
+    "rewardImage" : "https://storage.googleapis.com/yelloworld/image/ticket-reward.svg"
+  }
+}
+
+
+
+
+

NOTE

+
+
    +
  • +

    Header에 무작위한 UUID4 값을 넣어주세요

    +
    +
      +
    • +

      예시) IdempotencyKey: 0397b5f3-ecdc-47d6-b5d7-2b1afcf00e87

      +
    • +
    +
    +
  • +
  • +

    주의사항

    +
    +
      +
    • +

      같은 멱등성키를 2번 요청하면, 400번 에러.

      +
    • +
    +
    +
  • +
  • +

    ADMOB

    +
    +
      +
    • +

      ADMOB 서버에 SSV(ServerSideVerification) Options의 customData에 입력한 것과 동일한 멱등성 키를 넘겨주세요.

      +
    • +
    +
    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.11 릴리즈

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/search-department.html b/src/main/resources/static/docs/search-department.html index 2534ed88..43085cda 100644 --- a/src/main/resources/static/docs/search-department.html +++ b/src/main/resources/static/docs/search-department.html @@ -4,23 +4,25 @@ - + 대학교 학과 검색하기