diff --git a/.gitignore b/.gitignore index bf013630..8ac8c49c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,9 +41,15 @@ out/ application.yml application-dev.yml application-apple.yml +application-local.yml firebase*.json *client_secret*.json ### monitoring ### monitoring/prometheus/volume monitoring/grafana + +src/main/resources/application-local.yml + +### QueryDSL ### +src/main/generated/ diff --git a/README.md b/README.md index 4092da09..e19c300b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## Tech Stack -- [Spring Boot 2.7](https://spring.io/blog/2022/09/22/spring-boot-2-7-4-available-now) +- [Spring Boot 3.2.2](https://spring.io/blog/2024/01/19/spring-boot-3-2-2-available-now) with [Java 17](https://docs.oracle.com/en/java/javase/17/docs/api/index.html) - [MySQL](https://dev.mysql.com/doc/) & [JPA](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/) - [JUnit5](https://junit.org/junit5/docs/current/user-guide/) for testing diff --git a/build.gradle b/build.gradle index ebad094e..c9a03190 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,13 @@ +buildscript { + ext { + queryDslVersion = "5.0.0" + } +} + plugins { id 'java' - id 'org.springframework.boot' version '2.7.4' - id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id 'org.springframework.boot' version '3.2.2' + id 'io.spring.dependency-management' version '1.1.4' id "org.asciidoctor.jvm.convert" version "3.3.2" id 'jacoco' } @@ -32,14 +38,16 @@ repositories { } dependencies { + // Properties Migrator + runtimeOnly "org.springframework.boot:spring-boot-properties-migrator" + implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' // Repositories implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.amqp:spring-rabbit:2.3.12' + implementation 'org.springframework.amqp:spring-rabbit:3.1.1' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j:8.0.31' @@ -79,6 +87,7 @@ dependencies { asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.mockito:mockito-inline:5.2.0' // jwt decode implementation 'org.bouncycastle:bcprov-jdk15on:1.69' @@ -88,6 +97,18 @@ dependencies { // AOP implementation 'org.springframework.boot:spring-boot-starter-aop' + + // querydsl + implementation "com.querydsl:querydsl-core:${queryDslVersion}" + annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}:jakarta" + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta" + 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 { @@ -112,6 +133,7 @@ configurations { compileOnly { extendsFrom annotationProcessor } + querydsl.extendsFrom compileClasspath } jar.enabled = false @@ -128,6 +150,7 @@ jacocoTestReport { } // finalizedBy 'jacocoTestCoverageVerification' } + //jacocoTestCoverageVerification { // violationRules { // rule { diff --git a/src/docs/asciidoc/add-friend.adoc b/src/docs/asciidoc/add-friend.adoc index aaa42bf8..94d6578a 100644 --- a/src/docs/asciidoc/add-friend.adoc +++ b/src/docs/asciidoc/add-friend.adoc @@ -1,3 +1,4 @@ +:reproducible: == 친구 추가하기 === 요청 diff --git a/src/docs/asciidoc/apple.adoc b/src/docs/asciidoc/apple.adoc index 086e50a4..6d2ee981 100644 --- a/src/docs/asciidoc/apple.adoc +++ b/src/docs/asciidoc/apple.adoc @@ -1,3 +1,4 @@ +:reproducible: == Apple 구독 구매 검증하기 === 요청 diff --git a/src/docs/asciidoc/check-is-possible-admob.adoc b/src/docs/asciidoc/check-is-possible-admob.adoc new file mode 100644 index 00000000..6620b0cc --- /dev/null +++ b/src/docs/asciidoc/check-is-possible-admob.adoc @@ -0,0 +1,28 @@ +:reproducible: +== 상점에서 보상형 광고 가능한지 여부 + +=== 요청 + +include::{snippets}/api/v1/admob/possible/http-request.adoc[] + +=== 요청 파라미터 + +include::{snippets}/api/v1/admob/possible/path-parameters.adoc[] + +---- +tag -> ADMOB_POINT +---- +* 보상형 광고 다른곳에서 사용할 수도 있으므로 tag로 어떤 곳에서 사용하고 있는곳인지 tag로 명시 +* 현재는 상점에 있는 보상형 광고 (ADMOB_POINT를 tag에 요청) + +=== 응답 + +include::{snippets}/api/v1/admob/possible/http-response.adoc[] + +=== NOTE + + +=== CHANGELOG + +- 2024.02.17 API 릴리즈 +- 2024.02.16 명세서 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/check-keyword.adoc b/src/docs/asciidoc/check-keyword.adoc index 9a9e6107..848b0d43 100644 --- a/src/docs/asciidoc/check-keyword.adoc +++ b/src/docs/asciidoc/check-keyword.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 키워드 확인하기 === 요청 diff --git a/src/docs/asciidoc/check-user-by-id.adoc b/src/docs/asciidoc/check-user-by-id.adoc index 941d2a53..3206b86c 100644 --- a/src/docs/asciidoc/check-user-by-id.adoc +++ b/src/docs/asciidoc/check-user-by-id.adoc @@ -1,3 +1,4 @@ +:reproducible: == 유저 정보 조회하기 === 요청 diff --git a/src/docs/asciidoc/check-user-v2.adoc b/src/docs/asciidoc/check-user-v2.adoc new file mode 100644 index 00000000..c0efe9ab --- /dev/null +++ b/src/docs/asciidoc/check-user-v2.adoc @@ -0,0 +1,46 @@ +:reproducible: +== 내 정보 조회하기 V2 + +=== 요청 + +include::{snippets}/api/v2/user/http-request.adoc[] + +=== 응답 + +include::{snippets}/api/v2/user/http-response.adoc[] + +*필드 타입* + +- "userId": Long +- "name": String +- "yelloId": String +- "gender": "M" | "F" +- "email": String +- "profileImageUrl": String +- "social": "KAKAO" | "APPLE" +- "uuid": String(10) +- "deviceToken": String +- "groupId": Long +- "group": String +- "groupType": "UNIVERSITY" | "HIGH_SCHOOL" | "MIDDLE_SCHOOL" | "SOPT" +- "groupName": String +- "subGroupName": String +- "groupAdmissionYear": Integer +- "recommendCount": Long +- "ticketCount": Integer +- "point": Integer +- "subscribe": "normal" | "active" | "canceled" +- "yelloCount": Integer +- "friendCount": Integer + +=== Note + +- 내 정보 조회하기 V1가 제공했던 단편적인 정보를 보완하기 위해 설계된 API입니다. +- `Authroization` 헤더로 제공된 JWT Token에 담긴 유저의 정보가 응답으로 주어집니다. +- 유저 정보가 필요하면 해당 API를 사용하세요! + +=== CHANGELOG + +- 2024.01.07 첫 릴리즈 +- 2024.01.09 필드 타입 추가 +- 2024.01.30 groupId 필드 추가 \ No newline at end of file diff --git a/src/docs/asciidoc/check-user.adoc b/src/docs/asciidoc/check-user.adoc index f51ef64f..417ea727 100644 --- a/src/docs/asciidoc/check-user.adoc +++ b/src/docs/asciidoc/check-user.adoc @@ -1,4 +1,5 @@ -== 내 정보 조회하기 +:reproducible: +== 내 정보 조회하기 V1 === 요청 diff --git a/src/docs/asciidoc/check-vote-available.adoc b/src/docs/asciidoc/check-vote-available.adoc index 0d67b25f..f267cce1 100644 --- a/src/docs/asciidoc/check-vote-available.adoc +++ b/src/docs/asciidoc/check-vote-available.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 가능 여부 조회 === 요청 diff --git a/src/docs/asciidoc/create-event-history.adoc b/src/docs/asciidoc/create-event-history.adoc new file mode 100644 index 00000000..90bd2dcc --- /dev/null +++ b/src/docs/asciidoc/create-event-history.adoc @@ -0,0 +1,30 @@ +:reproducible: +== 이벤트 참여 + +=== 요청 + +include::{snippets}/api/v1/event/join/1/http-request.adoc[] + +=== 응답 + +include::{snippets}/api/v1/event/join/1/http-response.adoc[] + +=== 주의 + +- "tag": "LUNCH_EVENT" + +=== NOTE + +- Header에 무작위한 UUID4 값을 넣어주세요 +* 예시) IdempotencyKey: 0397b5f3-ecdc-47d6-b5d7-2b1afcf00e87 +- 주의사항 +* tag 요청값에 해당하는 이벤트의 날짜와 시간이 모두 유효해야함. +(뭔가 에러나면 서버요청 ㄱㄱ) +* 같은 멱등성키를 2번 요청하면, 400번 에러. +- ADMOB +* 광고를 시청하기 전, 해당 API를 호출. +* ADMOB 서버에 ServerSideVerificationOptions의 customData에 동일한 멱등성 키를 넘겨주세요. + +=== CHANGELOG + +- 2024.02.07 릴리즈 \ No newline at end of file diff --git a/src/docs/asciidoc/create-vote.adoc b/src/docs/asciidoc/create-vote.adoc index fb2b9761..e3443790 100644 --- a/src/docs/asciidoc/create-vote.adoc +++ b/src/docs/asciidoc/create-vote.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 생성하기 === 요청 diff --git a/src/docs/asciidoc/delete-friend.adoc b/src/docs/asciidoc/delete-friend.adoc index dd560fa5..67494799 100644 --- a/src/docs/asciidoc/delete-friend.adoc +++ b/src/docs/asciidoc/delete-friend.adoc @@ -1,3 +1,4 @@ +:reproducible: == 친구 삭제하기 === 요청 diff --git a/src/docs/asciidoc/delete-user-v2.adoc b/src/docs/asciidoc/delete-user-v2.adoc new file mode 100644 index 00000000..1938a368 --- /dev/null +++ b/src/docs/asciidoc/delete-user-v2.adoc @@ -0,0 +1,29 @@ +:reproducible: +== 탈퇴 & 사유 저장 v2 + +=== 요청 + +include::{snippets}/api/v2/user/deleteUser/http-request.adoc[] + +=== 응답 + +include::{snippets}/api/v2/user/deleteUser/http-response.adoc[] + +*필드 타입* + +- "value": String +* value는 탈퇴 사유를 보내주시면 됩니다. + + +*필드 타입* + +=== NOTE + +- AccessToken에 해당하는 User의 탈퇴 처리 및 탈퇴 사유를 저장하는 API입니다. +// - User의 다양한 정보를 저장하는 API로 범용적인 확장할 예정입니다. +// * 차후에 tag에 들어갈 수 있는 ENUM의 종류를 다양화 할 예정입니다. + +=== CHANGELOG + +- 2024.01.27 API 릴리즈 +- 2024.01.09 명세 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/delete-user.adoc b/src/docs/asciidoc/delete-user.adoc index 910ab4bc..f241e07e 100644 --- a/src/docs/asciidoc/delete-user.adoc +++ b/src/docs/asciidoc/delete-user.adoc @@ -1,3 +1,4 @@ +:reproducible: == 유저 탈퇴하기 === 요청 diff --git a/src/docs/asciidoc/device-token.adoc b/src/docs/asciidoc/device-token.adoc index 6185c3da..d9dd29f2 100644 --- a/src/docs/asciidoc/device-token.adoc +++ b/src/docs/asciidoc/device-token.adoc @@ -1,3 +1,4 @@ +:reproducible: == 디바이스 토큰 수정하기 === 요청 diff --git a/src/docs/asciidoc/edit-user.adoc b/src/docs/asciidoc/edit-user.adoc new file mode 100644 index 00000000..1abc26c0 --- /dev/null +++ b/src/docs/asciidoc/edit-user.adoc @@ -0,0 +1,39 @@ +:reproducible: +== 유저 프로필 수정 + +=== 요청 + +include::{snippets}/api/v1/user/update/http-request.adoc[] + +*필드 타입* + +- "name": String +- "yelloId": String +- "gender": "M" | "F" +- "email": String +- "profileImageUrl": String +- "groupId": Long +* 대학교 검색 또는 고등학교 검색 API를 이용하여 유저가 선택한 groupId를 입력해주세요. +* 해당 groupId에 해당하는 group이 고등학교면, 해당 유저는 고등학생 / 대학교면 대학생이 됩니다. +- "groupAdmissionYear": Integer +* 대학생이면 학번 / 고등학생이면 '반(class)'를 넣어주세요. + +=== 응답 + +include::{snippets}/api/v1/user/update/http-response.adoc[] + +*필드 타입* + +=== NOTE + +- 포인트 / 구독정보 / 로그인 정보와 같이 user-pure하지 않은 정보는 수정할 수 없도록 설계하였습니다. +* 해당 정보 수정API는 도메인 별로 만들 예정입니다. +- 비즈니스 로직인 **'1년에 1회 수정 가능하다'**라는 조건과 상관없이 여러번 호출하여 유저 정보 수정이 가능합니다. +* 해당 비즈니스 로직을 만족하기 위해서 link:user-data-get.html[프로필 수정 가능 여부 조회]를 같이 사용해주세요. +** UserGroup(`groupId`, `groupAdmissionYear`)에 대한 정보가 유저의 기존 정보와 달라졌을때**만** `프로필 수정 가능 여부` 가 갱신됩니다. + +=== CHANGELOG + +- 2024.02.02 groupId, groupAdmissionYear 변경에 따른 제약조건 추가 +- 2024.01.31 릴리즈 +- 2024.01.09 명세 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/find-event.adoc b/src/docs/asciidoc/find-event.adoc new file mode 100644 index 00000000..f8969cc0 --- /dev/null +++ b/src/docs/asciidoc/find-event.adoc @@ -0,0 +1,37 @@ +:reproducible: +== 이벤트 조회 + +=== 요청 + +include::{snippets}/api/v1/event/1/http-request.adoc[] + +=== 응답 + +include::{snippets}/api/v1/event/1/http-response.adoc[] + +include::{snippets}/api/v1/event/2/http-response.adoc[] + +include::{snippets}/api/v1/event/3/http-response.adoc[] + +=== 주의 + +- data: *Response*[] +- *Response* +- tag : "LUNCH_EVENT" +* LUNCH_EVENT에 해당하는 *Response*가 없으면 Render 해주지 말아주세요 +- startDate : "2024-01-01T00:00:00+09:00" +- endDate : "2024-12-31T00:00:00+09:00" +- title : "점심 시간 깜짝 선물!" +- subTitle : "평일 12-14시 최대 1회까지 참여 가능" +- animationList : string[] +* URL이 들어감. +- eventReward: *EventReward* | null +* 해당 필드가 **null**일 시, 이벤트 보여주지 않도록 해주세요. + +=== NOTE + +- *!* LUNCH_EVENT에 해당하는 Response가 없거나, LUNCH_EVENT Response의 eventReward가 null이면, 메인화면 접속시, 이벤트 화면을 띄워주지마세요. + +=== CHANGELOG + +- 2024.02.06 릴리즈 \ No newline at end of file diff --git a/src/docs/asciidoc/find-friend-votes-v2.adoc b/src/docs/asciidoc/find-friend-votes-v2.adoc new file mode 100644 index 00000000..c5694126 --- /dev/null +++ b/src/docs/asciidoc/find-friend-votes-v2.adoc @@ -0,0 +1,74 @@ +:reproducible: +== 친구 투표 전체 조회 v2 + +=== 요청 + +include::{snippets}/api/v2/vote/friend/http-request.adoc[] + +=== 요청 파라미터 + +include::{snippets}/api/v2/vote/friend/query-parameters.adoc[] + +=== 응답 + +include::{snippets}/api/v2/vote/friend/http-response.adoc[] + + +|=== +|`+type+`| 조회할 쪽지 종류 (null -> 모든쪽지, send-> 보낸쪽지) +|=== + + +*필드 타입* + +- "totalCount": Integer +- "friendVotes": *FriendVote*[] +- "isUserSenderVote" : Boolean (내가 보냈는지 여부) +- *FriendVote* +* "id": Long +* "senderId" : Long +* "senderName" : String +* "senderYelloId" : String +* "senderGender": "MALE" | "FEMALE" +* "senderProfileImage" : String +* "receiverId" : Long +* "receiverName": String +* "receiverYelloId" : String +* "receiverGender": "MALE" | "FEMALE" +* "receiverProfileImage": String +* "vote": *Vote* +* "isHintUsed": Boolean +* "createdAt": "{0}초 전" | "{0}분 전" | "{0}시간 전" | "{0}일 전" + +- *Vote* +* "nameHead": String +* "nameFoot": String +* "keywordHead": String +* "keyword": String +* "keywordFoot": String + + +=== Excpetion + +- 잘못된 type을 queryString에 보내는 경우 + +[http,json] +---- +{ + "status": 403, + "message": "[VoteForbiddenException] 잘못된 투표 유형입니다." +} +---- + +=== NOTE + +- 모든 종류의 쪽지를 조회할 때 `/api/v1/vote/friend?page=0` 으로 요청해주세요 +* type을 명시하지 마세요 +- 내가 보낸 쪽지를 조회할 때 `/api/v1/vote/friend?page=0&type=send` 으로 요청해주세요 +- `senderGender` 필드가 다른 API와 일관되지 못한점 미안해요 ㅠ + +=== CHANGELOG + +- 2924.01.30 API 릴리즈 +- 2024.01.26 필드 명세 업데이트 +- 2024.01.09 `type` 명세 업데이트 \ No newline at end of file diff --git a/src/docs/asciidoc/find-friend-votes.adoc b/src/docs/asciidoc/find-friend-votes.adoc index c6bbe0d4..f964a24b 100644 --- a/src/docs/asciidoc/find-friend-votes.adoc +++ b/src/docs/asciidoc/find-friend-votes.adoc @@ -1,4 +1,5 @@ -== 친구 투표 전체 조회 +:reproducible: +== 친구 투표 전체 조회 (최신버전 -> v2 확인) === 요청 @@ -6,8 +7,35 @@ include::{snippets}/api/v1/vote/findAllFriendVotes/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/vote/findAllFriendVotes/request-parameters.adoc[] +include::{snippets}/api/v1/vote/findAllFriendVotes/query-parameters.adoc[] === 응답 include::{snippets}/api/v1/vote/findAllFriendVotes/http-response.adoc[] + +*필드 타입* + +- "totalCount": Integer +- "friendVotes": *FriendVote*[] +- *FriendVote* +* "id": Long +* "receiverName": String +* "senderGender": "MALE" | "FEMALE" +* "receiverProfileImage": String +* "vote": *Vote* +* "isHintUsed": Boolean +* "createdAt": "{0}초 전" | "{0}분 전" | "{0}시간 전" | "{0}일 전" +- *Vote* +* "nameHead": String +* "nameFoot": String +* "keywordHead": String +* "keyword": String +* "keywordFoot": String + +=== NOTE + +- 최신버전 v2를 확인해주세요 + +=== CHANGELOG + +- 2024.01.09 `type` 최신버전 확인 바람 \ No newline at end of file diff --git a/src/docs/asciidoc/find-friends.adoc b/src/docs/asciidoc/find-friends.adoc index 6e1584ff..6e442a68 100644 --- a/src/docs/asciidoc/find-friends.adoc +++ b/src/docs/asciidoc/find-friends.adoc @@ -1,4 +1,4 @@ - +:reproducible: == 내 친구 전체 조회하기 === 요청 @@ -7,7 +7,7 @@ include::{snippets}/api/v1/friend/findAllFriend/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/friend/findAllFriend/request-parameters.adoc[] +include::{snippets}/api/v1/friend/findAllFriend/query-parameters.adoc[] === 응답 diff --git a/src/docs/asciidoc/find-group-friends.adoc b/src/docs/asciidoc/find-group-friends.adoc index d252c9a1..d7d17e51 100644 --- a/src/docs/asciidoc/find-group-friends.adoc +++ b/src/docs/asciidoc/find-group-friends.adoc @@ -1,3 +1,4 @@ +:reproducible: == 그룹 추천 친구 조회하기 === 요청 @@ -6,7 +7,7 @@ include::{snippets}/api/v1/friend/findAllRecommendSchoolFriends/http-request.ado === 요청 파라미터 -include::{snippets}/api/v1/friend/findAllRecommendSchoolFriends/request-parameters.adoc[] +include::{snippets}/api/v1/friend/findAllRecommendSchoolFriends/query-parameters.adoc[] === 응답 diff --git a/src/docs/asciidoc/find-kakao-friends.adoc b/src/docs/asciidoc/find-kakao-friends.adoc index 16b4ced0..d253f5d3 100644 --- a/src/docs/asciidoc/find-kakao-friends.adoc +++ b/src/docs/asciidoc/find-kakao-friends.adoc @@ -1,3 +1,4 @@ +:reproducible: == 카카오 추천 친구 조회하기 === 요청 @@ -6,7 +7,7 @@ include::{snippets}/api/v1/friend/findAllRecommendKakaoFriends/http-request.adoc === 요청 파라미터 -include::{snippets}/api/v1/friend/findAllRecommendKakaoFriends/request-parameters.adoc[] +include::{snippets}/api/v1/friend/findAllRecommendKakaoFriends/query-parameters.adoc[] === 응답 diff --git a/src/docs/asciidoc/find-notice.adoc b/src/docs/asciidoc/find-notice.adoc new file mode 100644 index 00000000..55ea9d28 --- /dev/null +++ b/src/docs/asciidoc/find-notice.adoc @@ -0,0 +1,59 @@ +:reproducible: +== 공지 조회 + +=== 요청 + +include::{snippets}/api/v1/notice/http-request.adoc[] + +=== 요청 파라미터 + +include::{snippets}/api/v1/notice/path-parameters.adoc[] + +=== 응답 + +include::{snippets}/api/v1/notice/http-response.adoc[] + +*path variable* + +- tag(ENUM 값) -> "NOTICE" | "BANNER | "PROFILE-BANNER" + +* NOTICE : 진입시 공지 +* BANNER : 내 쪽지 배너 +* PROFILE-BANNER : 프로필 배너 + +=== 주의 + +[http, json] + +*유효한 날짜의 공지가 존재하지 않는 경우* + +-> isAvailable은 false로 오고 날짜 제외한 나머지 값은 빈값으로 전달 + +---- +{ + "status": 200, + "message": "공지 조회에 성공하였습니다.", + "data": { + "imageUrl": "", + "redirectUrl": "", + "startDate": "2024-01-27", + "endDate": "2024-01-27", + "isAvailable": false, + "type": null, + "title": "" +} + +---- + +=== NOTE + +- 공지 정보를 조회하는 API입니다. +- isAvailable true일 때, 유효한 1개의 공지를 반환합니다. +* 요구사항에 따라 여러개의 공지를 반환할 수 있도록 염두하고 있습니다. +* 반환되는 공지를 무조건 View에 띄워주시면 되겠습니다. + +=== CHANGELOG + +- 2024.01.29 API tag 추가 및 수정 +- 2024.01.26 API 릴리즈 +- 2024.01.09 명세 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/find-onboarding-friends.adoc b/src/docs/asciidoc/find-onboarding-friends.adoc index f332948a..92a41136 100644 --- a/src/docs/asciidoc/find-onboarding-friends.adoc +++ b/src/docs/asciidoc/find-onboarding-friends.adoc @@ -1,3 +1,4 @@ +:reproducible: == 가입한 친구 목록 불러오기 === 요청 @@ -6,7 +7,7 @@ include::{snippets}/api/v1/auth/findOnBoardingFriends/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/auth/findOnBoardingFriends/request-parameters.adoc[] +include::{snippets}/api/v1/auth/findOnBoardingFriends/query-parameters.adoc[] === 응답 diff --git a/src/docs/asciidoc/find-question.adoc b/src/docs/asciidoc/find-question.adoc index 587fefec..5f331528 100644 --- a/src/docs/asciidoc/find-question.adoc +++ b/src/docs/asciidoc/find-question.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 질문 조회하기 === 요청 diff --git a/src/docs/asciidoc/find-vote.adoc b/src/docs/asciidoc/find-vote.adoc index 8b546e30..3286b75c 100644 --- a/src/docs/asciidoc/find-vote.adoc +++ b/src/docs/asciidoc/find-vote.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 상세 조회 === 요청 diff --git a/src/docs/asciidoc/find-votes.adoc b/src/docs/asciidoc/find-votes.adoc index 93df21d4..be1cfc2e 100644 --- a/src/docs/asciidoc/find-votes.adoc +++ b/src/docs/asciidoc/find-votes.adoc @@ -1,3 +1,4 @@ +:reproducible: == 내 투표 전체 조회 === 요청 @@ -6,7 +7,7 @@ include::{snippets}/api/v1/vote/findAllMyVotes/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/vote/findAllMyVotes/request-parameters.adoc[] +include::{snippets}/api/v1/vote/findAllMyVotes/query-parameters.adoc[] === 응답 diff --git a/src/docs/asciidoc/get-unread-vote.adoc b/src/docs/asciidoc/get-unread-vote.adoc index f8ddebda..b5bc3578 100644 --- a/src/docs/asciidoc/get-unread-vote.adoc +++ b/src/docs/asciidoc/get-unread-vote.adoc @@ -1,3 +1,4 @@ +:reproducible: == 읽지 않은 쪽지 개수 조회 === 요청 diff --git a/src/docs/asciidoc/google.adoc b/src/docs/asciidoc/google.adoc index 8b2450e5..6d3863bc 100644 --- a/src/docs/asciidoc/google.adoc +++ b/src/docs/asciidoc/google.adoc @@ -1,3 +1,4 @@ +:reproducible: == Google 구독 구매 검증하기 === 요청 diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index f5c91a32..e3d8a51a 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -5,6 +5,8 @@ :toc: left :toclevels: 3 :sectlinks: +:nofooter: +:nofootnotes: [[API-List]] == APIs @@ -17,15 +19,29 @@ * link:find-onboarding-friends.html[가입한 친구 목록 불러오기] -* link:search-userGroup.html[대학교 검색하기] +* link:search-school.html[대학교 검색하기] * link:search-department.html[대학교 학과 검색하기] +* link:search-high-name.html[고등학교 이름 검색하기] + +* link:search-high-class.html[고등학교 이름으로 학반 검색하기] + * link:reissue-token.html[토큰 재발급] === User API -* link:check-user.html[내 정보 조회하기] +* link:check-user.html[내 정보 조회하기 V1] + +* 🆕 link:check-user-v2.html[내 정보 조회하기 V2, 2024-01-30] + +* 🆕 link:edit-user.html[유저 정보 수정, 2024-01-31] + +* 🆕 link:user-data-get.html[프로필 수정 가능 여부 조회, 2024-01-31] + +* 🆕 link:user-data-post.html[유저 기타 정보 저장, 2024-01-31] + +* 🆕 link:purchase-info.html[유저 구독 정보, 2024-01-25] * link:check-user-by-id.html[특정 유저 정보 조회하기] @@ -33,13 +49,17 @@ * link:delete-user.html[유저 탈퇴] +* 🆕 link:delete-user-v2.html[유저 탈퇴 V2, 2024-01-30] + === Vote API * link:find-votes.html[내 투표 전체 조회하기] -* link:find-friend-votes.html[친구 투표 전체 조회하기] +* link:find-friend-votes.html[친구 투표 전체 조회하기, 2024-01-09] + +* 🆕 link:find-friend-votes-v2.html[친구 투표 전체 조회하기 v2, 2024-01-30] -* link:find-friend-votes.html[읽지 않은 쪽지 개수 조회하기] +* link:get-unread-vote.html[읽지 않은 쪽지 개수 조회하기] * link:find-vote.html[투표 상세 조회하기] @@ -81,4 +101,21 @@ === Pay API +- @Deprecated at 2024.03 * link:pay.html[결제 전환율 체크] + +=== Notice API + +* 🆕 link:find-notice.html[공지 조회, 2024-01-29] + +=== Event API + +* 🆕 link:find-event.html[이벤트 조회, 2024-02-06] + +* 🆕 link:create-event-history.html[이벤트 참여, 2024-02-07] + +* 🆕 link:reward-event.html[이벤트 보상, 2024-02-07] + +* 🆕 link:reward-admob.html[광고보고 보상 얻기, 2024-02-19] + +* 🆕 link:check-is-possible-admob.html[광고보고 보상 얻기 가능 여부 조회, 2024-02-17] \ No newline at end of file diff --git a/src/docs/asciidoc/login.adoc b/src/docs/asciidoc/login.adoc index 76d52f76..9d0280e3 100644 --- a/src/docs/asciidoc/login.adoc +++ b/src/docs/asciidoc/login.adoc @@ -1,3 +1,4 @@ +:reproducible: == 소셜 로그인 === 요청 diff --git a/src/docs/asciidoc/overview.adoc b/src/docs/asciidoc/overview.adoc index 09d6069a..9b8a292c 100644 --- a/src/docs/asciidoc/overview.adoc +++ b/src/docs/asciidoc/overview.adoc @@ -1,3 +1,4 @@ +:reproducible: [[overview]] == Overview diff --git a/src/docs/asciidoc/pay.adoc b/src/docs/asciidoc/pay.adoc index e20bec39..14a67d19 100644 --- a/src/docs/asciidoc/pay.adoc +++ b/src/docs/asciidoc/pay.adoc @@ -1,3 +1,4 @@ +:reproducible: == 친구 추가하기 === 요청 diff --git a/src/docs/asciidoc/purchase-check.adoc b/src/docs/asciidoc/purchase-check.adoc index 43c648dd..ddf0905f 100644 --- a/src/docs/asciidoc/purchase-check.adoc +++ b/src/docs/asciidoc/purchase-check.adoc @@ -1,3 +1,4 @@ +:reproducible: == 구독 상태 및 열람권 개수 조회하기 === 요청 diff --git a/src/docs/asciidoc/purchase-info.adoc b/src/docs/asciidoc/purchase-info.adoc new file mode 100644 index 00000000..ea4cd9a3 --- /dev/null +++ b/src/docs/asciidoc/purchase-info.adoc @@ -0,0 +1,25 @@ +:reproducible: +== 유저 구독 정보 + +=== 요청 + +include::{snippets}/api/v1/user/subscribe/http-request.adoc[] + +=== 응답 + +include::{snippets}/api/v1/user/subscribe/http-response.adoc[] + +*필드 타입* + +- "id": Long +- "subscribe": "normal" | "active" | "canceled" +- "expireDate": String(10) +* YYYY-MM-DD (ISO-8601) + +=== Note + +=== CHANGELOG + +- 2024.01.25 expiredDate 필드명 및 subscribe 소문자 오류 수정 +- 2024.01.23 API 릴리즈 +- 2024.01.09 명세 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/reissue-token.adoc b/src/docs/asciidoc/reissue-token.adoc index 663fb5b8..6c9edfb3 100644 --- a/src/docs/asciidoc/reissue-token.adoc +++ b/src/docs/asciidoc/reissue-token.adoc @@ -1,8 +1,6 @@ - +:reproducible: == 토큰 재발급 -operation::api/v1/auth/reIssueToken[snippets='http-request,http-response'] - === 요청 include::{snippets}/api/v1/auth/reIssueToken/http-request.adoc[] diff --git a/src/docs/asciidoc/reveal-full-name.adoc b/src/docs/asciidoc/reveal-full-name.adoc index c98234c9..938ae5ea 100644 --- a/src/docs/asciidoc/reveal-full-name.adoc +++ b/src/docs/asciidoc/reveal-full-name.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 이름 전체 조회 === 요청 diff --git a/src/docs/asciidoc/reveal-name.adoc b/src/docs/asciidoc/reveal-name.adoc index d40c61fa..6343e67a 100644 --- a/src/docs/asciidoc/reveal-name.adoc +++ b/src/docs/asciidoc/reveal-name.adoc @@ -1,3 +1,4 @@ +:reproducible: == 투표 이름 부분 조회 === 요청 diff --git a/src/docs/asciidoc/reward-admob.adoc b/src/docs/asciidoc/reward-admob.adoc new file mode 100644 index 00000000..32a5bc2e --- /dev/null +++ b/src/docs/asciidoc/reward-admob.adoc @@ -0,0 +1,34 @@ +: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 + +- 주의사항 +* 같은 멱등성키를 2번 요청하면, 400번 에러. +- ADMOB +* ADMOB 서버에 SSV(ServerSideVerification) Options의 customData에 입력한 것과 동일한 멱등성 키를 넘겨주세요. + +=== CHANGELOG + +- 2024.02.19 API ENUM, 명세서 수정 +- 2024.02.11 릴리즈 \ No newline at end of file diff --git a/src/docs/asciidoc/reward-event.adoc b/src/docs/asciidoc/reward-event.adoc new file mode 100644 index 00000000..52310784 --- /dev/null +++ b/src/docs/asciidoc/reward-event.adoc @@ -0,0 +1,34 @@ +:reproducible: +== 공지 조회 + +=== 요청 + +include::{snippets}/api/v1/event/reward/1/http-request.adoc[] + +=== 응답 + +include::{snippets}/api/v1/event/reward/1/http-response.adoc[] +include::{snippets}/api/v1/event/reward/2/http-response.adoc[] + +=== 주의 + +- "rewardTag": "TICKET" | "POINT" +- "rewardValue": Long +- "rewardTitle": String +- "rewardImage": String + +=== NOTE + +- Header에 이벤트 참여에 입력했던 멱등키를 넣어주세요. +* 예시) IdempotencyKey: 0397b5f3-ecdc-47d6-b5d7-2b1afcf00e87 +- ADMOB +* 광고를 시청한 후, 해당 API를 호출. +* ADMOB 서버에 ServerSideVerificationOptions의 customData에 동일한 멱등성 키를 넘겨주세요. +* 이벤트 참여에 넣어준 멱등키와 동일하여 검증이되면, 정상 보상, 그렇지 않으면 이상으로 판단하여 400번대 에러 +- 보상 +* 지금 랜덤 보상이 구현안되있는데, 차후 서버에서 보상 처리하겠음. +클라측에서는 View과 종속적인 값만 처리하면 됨. + +=== CHANGELOG + +- 2024.02.07 릴리즈 \ No newline at end of file diff --git a/src/docs/asciidoc/search-department.adoc b/src/docs/asciidoc/search-department.adoc index 467e457a..5b5864af 100644 --- a/src/docs/asciidoc/search-department.adoc +++ b/src/docs/asciidoc/search-department.adoc @@ -1,13 +1,14 @@ +:reproducible: == 대학교 학과 검색하기 === 요청 -include::{snippets}/api/v1/auth/findDepartmentsByKeyword/http-request.adoc[] +include::{snippets}/api/v1/auth/findAllUnivDepartmentName/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/auth/findDepartmentsByKeyword/request-parameters.adoc[] +include::{snippets}/api/v1/auth/findAllUnivDepartmentName/query-parameters.adoc[] === 응답 -include::{snippets}/api/v1/auth/findDepartmentsByKeyword/http-response.adoc[] +include::{snippets}/api/v1/auth/findAllUnivDepartmentName/http-response.adoc[] diff --git a/src/docs/asciidoc/search-friend.adoc b/src/docs/asciidoc/search-friend.adoc index f2cef4f3..e23dce32 100644 --- a/src/docs/asciidoc/search-friend.adoc +++ b/src/docs/asciidoc/search-friend.adoc @@ -1,5 +1,4 @@ - - +:reproducible: == 친구 검색하기 === 요청 @@ -8,7 +7,7 @@ include::{snippets}/api/v1/friend/searchFriend/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/friend/searchFriend/request-parameters.adoc[] +include::{snippets}/api/v1/friend/searchFriend/query-parameters.adoc[] === 응답 diff --git a/src/docs/asciidoc/search-high-class.adoc b/src/docs/asciidoc/search-high-class.adoc new file mode 100644 index 00000000..56bcf491 --- /dev/null +++ b/src/docs/asciidoc/search-high-class.adoc @@ -0,0 +1,14 @@ +:reproducible: +== 고등학교 이름으로 학반 검색하기 + +=== 요청 + +include::{snippets}/api/v1/auth/findGroupIdByName/http-request.adoc[] + +=== 요청 파라미터 + +include::{snippets}/api/v1/auth/findGroupIdByName/query-parameters.adoc[] + +=== 응답 + +include::{snippets}/api/v1/auth/findGroupIdByName/http-response.adoc[] diff --git a/src/docs/asciidoc/search-high-name.adoc b/src/docs/asciidoc/search-high-name.adoc new file mode 100644 index 00000000..56d3356f --- /dev/null +++ b/src/docs/asciidoc/search-high-name.adoc @@ -0,0 +1,14 @@ +:reproducible: +== 고등학교 이름 검색하기 + +=== 요청 + +include::{snippets}/api/v1/auth/findAllHighSchoolName/http-request.adoc[] + +=== 요청 파라미터 + +include::{snippets}/api/v1/auth/findAllHighSchoolName/query-parameters.adoc[] + +=== 응답 + +include::{snippets}/api/v1/auth/findAllHighSchoolName/http-response.adoc[] diff --git a/src/docs/asciidoc/search-school.adoc b/src/docs/asciidoc/search-school.adoc index e3713fa6..ae12d1b8 100644 --- a/src/docs/asciidoc/search-school.adoc +++ b/src/docs/asciidoc/search-school.adoc @@ -1,13 +1,14 @@ +:reproducible: == 대학교 검색하기 === 요청 -include::{snippets}/api/v1/auth/findSchoolsByKeyword/http-request.adoc[] +include::{snippets}/api/v1/auth/findAllUnivName/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/auth/findSchoolsByKeyword/request-parameters.adoc[] +include::{snippets}/api/v1/auth/findAllUnivName/query-parameters.adoc[] === 응답 -include::{snippets}/api/v1/auth/findSchoolsByKeyword/http-response.adoc[] +include::{snippets}/api/v1/auth/findAllUnivName/http-response.adoc[] diff --git a/src/docs/asciidoc/shuffle-friends.adoc b/src/docs/asciidoc/shuffle-friends.adoc index 77b77bac..7ddb20d4 100644 --- a/src/docs/asciidoc/shuffle-friends.adoc +++ b/src/docs/asciidoc/shuffle-friends.adoc @@ -1,3 +1,4 @@ +:reproducible: == 셔플한 친구 조회하기 === 요청 diff --git a/src/docs/asciidoc/signup.adoc b/src/docs/asciidoc/signup.adoc index bd5481cb..2ad4ceac 100644 --- a/src/docs/asciidoc/signup.adoc +++ b/src/docs/asciidoc/signup.adoc @@ -1,3 +1,4 @@ +:reproducible: == 회원가입 === 요청 diff --git a/src/docs/asciidoc/sub-check.adoc b/src/docs/asciidoc/sub-check.adoc index 00a19bdf..26c82f77 100644 --- a/src/docs/asciidoc/sub-check.adoc +++ b/src/docs/asciidoc/sub-check.adoc @@ -1,3 +1,4 @@ +:reproducible: == 구독 연장 유도 필요 여부 확인하기 === 요청 diff --git a/src/docs/asciidoc/user-data-get.adoc b/src/docs/asciidoc/user-data-get.adoc new file mode 100644 index 00000000..d9f600cc --- /dev/null +++ b/src/docs/asciidoc/user-data-get.adoc @@ -0,0 +1,35 @@ +:reproducible: +== 프로필 수정 가능 여부 조회 + +=== 요청 + +include::{snippets}/api/v1/user/data/read/1/http-request.adoc[] + +*필드 타입* + +- "TAG": "account-updated-at" | "recommended" | "withdraw-reason" + +=== 응답 + +include::{snippets}/api/v1/user/data/read/1/http-response.adoc[] +include::{snippets}/api/v1/user/data/read/2/http-response.adoc[] + +*필드 타입* + +- "tag": "ACCOUNT_UPDATED_AT" | "RECOMMENDED" | "WITHDRAW_REASON" +- "value": String +* ACCOUNT_UPDATED_AT일 때, `{boolean}|{updated_at}|{created_at}` 를 반환합니다. +** boolean 및 updated_at 값을 parse하여 사용해주세요. +** 예시) **"false|2024-01-31|2024-01-31"** +** "updated_at": "YYYY-mm-dd" | null + +=== NOTE + +- User의 다양한 정보를 조회하는 범용적인 API입니다. +- `account-updated-at` 을 통해 AccessToken에 해당하는 User의 프로필 수정 가능 여부 조회를 조회하세요. + +=== CHANGELOG + +- 2024.02.10 응답 케이스 추가 +- 2024.01.30 릴리즈 +- 2024.01.09 명세 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/user-data-post.adoc b/src/docs/asciidoc/user-data-post.adoc new file mode 100644 index 00000000..7c1cba39 --- /dev/null +++ b/src/docs/asciidoc/user-data-post.adoc @@ -0,0 +1,30 @@ +:reproducible: +== 유저 기타 정보 저장 (명세) + +=== 요청 + +include::{snippets}/api/v1/user/data/update/http-request.adoc[] + +*필드 타입* + +- "TAG": "withdraw-reason" | "account-update-at" | "recommended" +- "value": String +* `withdraw-reason` 일 때, 255byte 이내의 string +* `account-update-at` 또는 `recommended` 일 때, ISO-8601 + ZoneInfo +** 예시) `2011-12-03T10:15:30+01:00` + +=== 응답 + +include::{snippets}/api/v1/user/data/update/http-response.adoc[] + +=== NOTE + +- `account-update-at` 및 `recommended` 기능은 link:edit-user.html[유저 정보 수정] 및 link:signup.html[회원가입]에 통합되어있습니다 +* 정말 필요에 의해 값을 수정해야하는 경우만 사용해주세요. +- User의 다양한 정보를 저장하는 API로 범용적인 확장할 예정입니다. + +=== CHANGELOG + +- 2024.01.31 릴리즈 +- 2024.01.30 탈퇴 v2 분리로 인한 명세 업데이트 +- 2024.01.09 명세 작성 \ No newline at end of file diff --git a/src/docs/asciidoc/validate-yelloid.adoc b/src/docs/asciidoc/validate-yelloid.adoc index a917061b..0de66e04 100644 --- a/src/docs/asciidoc/validate-yelloid.adoc +++ b/src/docs/asciidoc/validate-yelloid.adoc @@ -1,3 +1,4 @@ +:reproducible: == 옐로 아이디 중복 확인 === 요청 @@ -6,7 +7,7 @@ include::{snippets}/api/v1/auth/getYelloIdValidation/http-request.adoc[] === 요청 파라미터 -include::{snippets}/api/v1/auth/getYelloIdValidation/request-parameters.adoc[] +include::{snippets}/api/v1/auth/getYelloIdValidation/query-parameters.adoc[] === 응답 diff --git a/src/main/java/com/yello/server/domain/admin/controller/AdminController.java b/src/main/java/com/yello/server/domain/admin/controller/AdminController.java index 0fa19c9b..eb8de5e2 100644 --- a/src/main/java/com/yello/server/domain/admin/controller/AdminController.java +++ b/src/main/java/com/yello/server/domain/admin/controller/AdminController.java @@ -1,10 +1,17 @@ package com.yello.server.domain.admin.controller; +import static com.yello.server.global.common.SuccessCode.CONFIGURATION_READ_ADMIN_SUCCESS; +import static com.yello.server.global.common.SuccessCode.CONFIGURATION_UPDATE_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.CREATE_VOTE_SUCCESS; import static com.yello.server.global.common.SuccessCode.DELETE_COOLDOWN_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.DELETE_QUESTION_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.DELETE_USER_ADMIN_SUCCESS; +import static com.yello.server.global.common.SuccessCode.EVENT_CREATE_ADMIN_SUCCESS; +import static com.yello.server.global.common.SuccessCode.EVENT_REWARD_CREATE_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.LOGIN_USER_ADMIN_SUCCESS; +import static com.yello.server.global.common.SuccessCode.NOTICE_CREATE_ADMIN_SUCCESS; +import static com.yello.server.global.common.SuccessCode.NOTICE_READ_ADMIN_SUCCESS; +import static com.yello.server.global.common.SuccessCode.NOTICE_UPDATE_DETAIL_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.READ_COOLDOWN_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.READ_QUESTION_ADMIN_SUCCESS; import static com.yello.server.global.common.SuccessCode.READ_QUESTION_DETAIL_ADMIN_SUCCESS; @@ -15,23 +22,31 @@ import static com.yello.server.global.common.factory.PaginationFactory.createPageableByNameSortDescLimitTen; import static com.yello.server.global.common.factory.PaginationFactory.createPageableLimitTen; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.yello.server.domain.admin.dto.request.AdminEventCreateRequest; +import com.yello.server.domain.admin.dto.request.AdminEventRewardCreateRequest; import com.yello.server.domain.admin.dto.request.AdminLoginRequest; +import com.yello.server.domain.admin.dto.request.AdminNoticeCreateRequest; import com.yello.server.domain.admin.dto.request.AdminQuestionVoteRequest; import com.yello.server.domain.admin.dto.request.AdminUserDetailRequest; +import com.yello.server.domain.admin.dto.response.AdminConfigurationResponse; +import com.yello.server.domain.admin.dto.response.AdminConfigurationUpdateRequest; import com.yello.server.domain.admin.dto.response.AdminCooldownResponse; import com.yello.server.domain.admin.dto.response.AdminLoginResponse; import com.yello.server.domain.admin.dto.response.AdminQuestionDetailResponse; import com.yello.server.domain.admin.dto.response.AdminQuestionResponse; import com.yello.server.domain.admin.dto.response.AdminUserDetailResponse; import com.yello.server.domain.admin.dto.response.AdminUserResponse; +import com.yello.server.domain.admin.entity.AdminConfigurationType; import com.yello.server.domain.admin.service.AdminService; +import com.yello.server.domain.notice.entity.Notice; 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.dto.EmptyObject; import com.yello.server.infrastructure.firebase.dto.request.NotificationCustomMessage; import com.yello.server.infrastructure.firebase.service.NotificationService; -import javax.annotation.Nullable; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.val; import org.springframework.web.bind.annotation.DeleteMapping; @@ -59,10 +74,10 @@ public BaseResponse postAdminLogin(@RequestBody AdminLoginRe @GetMapping("/user") public BaseResponse getUserAdmin(@AccessTokenUser User user, - @RequestParam Integer page, - @Nullable @RequestParam String field, - @Nullable @RequestParam String value) { - val data = (field==null && value==null) + @RequestParam(value = "page") Integer page, + @RequestParam(value = "field", required = false) String field, + @RequestParam(value = "value", required = false) String value) { + val data = (field == null && value == null) ? adminService.findUser(user.getId(), createPageableByNameSortDescLimitTen(page)) : adminService.findUserContaining(user.getId(), createPageableByNameSortDescLimitTen(page), @@ -72,29 +87,29 @@ public BaseResponse getUserAdmin(@AccessTokenUser User user, @GetMapping("/user/{id}") public BaseResponse getUserDetailAdmin(@AccessTokenUser User user, - @PathVariable Long id) { + @PathVariable(value = "id") Long id) { val data = adminService.findUserDetail(user.getId(), id); return BaseResponse.success(READ_USER_DETAIL_ADMIN_SUCCESS, data); } @PostMapping("/user/{id}") public BaseResponse postUserDetailAdmin(@AccessTokenUser User user, - @PathVariable Long id, @RequestBody AdminUserDetailRequest request) { + @PathVariable(value = "id") Long id, @RequestBody AdminUserDetailRequest request) { val data = adminService.updateUserDetail(user.getId(), id, request); return BaseResponse.success(UPDATE_USER_DETAIL_ADMIN_SUCCESS); } @DeleteMapping("/user") - public BaseResponse deleteUser(@AccessTokenUser User user, @RequestParam Long userId) { + public BaseResponse deleteUser(@AccessTokenUser User user, @RequestParam(value = "userId") Long userId) { adminService.deleteUser(user.getId(), userId); return BaseResponse.success(DELETE_USER_ADMIN_SUCCESS); } @GetMapping("/cooldown") public BaseResponse getCooldownAdmin(@AccessTokenUser User user, - @RequestParam Integer page, - @Nullable @RequestParam String yelloId) { - val data = yelloId==null + @RequestParam(value = "page") Integer page, + @RequestParam(value = "yelloId", required = false) String yelloId) { + val data = yelloId == null ? adminService.findCooldown(user.getId(), createPageableLimitTen(page)) : adminService.findCooldownContaining(user.getId(), createPageableLimitTen(page), yelloId); @@ -102,14 +117,15 @@ public BaseResponse getCooldownAdmin(@AccessTokenUser Use } @DeleteMapping("/cooldown") - public BaseResponse deleteCooldown(@AccessTokenUser User user, @RequestParam Long cooldownId) { + public BaseResponse deleteCooldown(@AccessTokenUser User user, + @RequestParam(value = "cooldownId") Long cooldownId) { adminService.deleteCooldown(user.getId(), cooldownId); return BaseResponse.success(DELETE_COOLDOWN_ADMIN_SUCCESS); } @GetMapping("/question") public BaseResponse getQuestionAdmin(@AccessTokenUser User user, - @RequestParam Integer page) { + @RequestParam(value = "page") Integer page) { val data = adminService.findQuestion(user.getId(), createPageable(page, 20)); return BaseResponse.success(READ_QUESTION_ADMIN_SUCCESS, data); } @@ -117,14 +133,14 @@ public BaseResponse getQuestionAdmin(@AccessTokenUser Use @GetMapping("/question/{id}") public BaseResponse getQuestionDetailAdmin( @AccessTokenUser User user, - @PathVariable Long id) { + @PathVariable(value = "id") Long id) { val data = adminService.findQuestionDetail(user.getId(), id); return BaseResponse.success(READ_QUESTION_DETAIL_ADMIN_SUCCESS, data); } @PostMapping("/question/{id}") public BaseResponse postQuestionSendAdmin(@AccessTokenUser User user, - @PathVariable Long id, @RequestBody AdminQuestionVoteRequest request) { + @PathVariable(value = "id") Long id, @RequestBody AdminQuestionVoteRequest request) { val data = adminService.createVote(user.getId(), id, request); data.forEach(notificationService::sendYelloNotification); @@ -132,7 +148,8 @@ public BaseResponse postQuestionSendAdmin(@AccessTokenUser User use } @DeleteMapping("/question") - public BaseResponse deleteQuestion(@AccessTokenUser User user, @RequestParam Long questionId) { + public BaseResponse deleteQuestion(@AccessTokenUser User user, + @RequestParam(value = "questionId") Long questionId) { adminService.deleteQuestion(user.getId(), questionId); return BaseResponse.success(DELETE_QUESTION_ADMIN_SUCCESS); } @@ -144,4 +161,57 @@ public BaseResponse postCustomNotificationSendAdmin(@AccessTokenUse return BaseResponse.success(CREATE_VOTE_SUCCESS, data); } + + @GetMapping("/configuration") + public BaseResponse getConfigurations(@RequestParam(value = "tag") String tag, + @AccessTokenUser User user) { + final AdminConfigurationType configurationType = AdminConfigurationType.fromCode(tag); + val data = adminService.getConfigurations(user.getId(), configurationType); + return BaseResponse.success(CONFIGURATION_READ_ADMIN_SUCCESS, null); + } + + @PostMapping("/configuration") + public BaseResponse postConfigurations( + @RequestBody AdminConfigurationUpdateRequest request, + @AccessTokenUser User user) { + final AdminConfigurationType configurationType = AdminConfigurationType.fromCode(request.tag()); + + val data = adminService.updateConfigurations(user.getId(), configurationType, request.value()); + + return BaseResponse.success(CONFIGURATION_UPDATE_ADMIN_SUCCESS, data); + } + + @GetMapping("/notice") + public BaseResponse> getNotices(@AccessTokenUser User user) { + val data = adminService.getNotices(user.getId()); + return BaseResponse.success(NOTICE_READ_ADMIN_SUCCESS, data); + } + + @PostMapping("/notice") + public BaseResponse createNotice(@AccessTokenUser User user, + @RequestBody AdminNoticeCreateRequest request) { + val data = adminService.createNotice(user.getId(), request); + return BaseResponse.success(NOTICE_CREATE_ADMIN_SUCCESS, data); + } + + @PostMapping("/notice/{id}") + public BaseResponse updateNotice(@AccessTokenUser User user, @PathVariable(value = "id") Long noticeId, + @RequestBody AdminNoticeCreateRequest request) { + val data = adminService.updateNotice(user.getId(), noticeId, request); + return BaseResponse.success(NOTICE_UPDATE_DETAIL_ADMIN_SUCCESS, data); + } + + @PostMapping("/event") + public BaseResponse createEvent(@AccessTokenUser User user, + @RequestBody AdminEventCreateRequest request) throws JsonProcessingException { + val data = adminService.createEvent(user.getId(), request); + return BaseResponse.success(EVENT_CREATE_ADMIN_SUCCESS, data); + } + + @PostMapping("/event/reward") + public BaseResponse createEventReward(@AccessTokenUser User user, + @RequestBody AdminEventRewardCreateRequest request) { + val data = adminService.createEventReward(user.getId(), request); + return BaseResponse.success(EVENT_REWARD_CREATE_ADMIN_SUCCESS, data); + } } diff --git a/src/main/java/com/yello/server/domain/admin/dto/request/AdminEventCreateRequest.java b/src/main/java/com/yello/server/domain/admin/dto/request/AdminEventCreateRequest.java new file mode 100644 index 00000000..98702e00 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/dto/request/AdminEventCreateRequest.java @@ -0,0 +1,33 @@ +package com.yello.server.domain.admin.dto.request; + +import com.yello.server.domain.event.entity.EventType; +import java.time.OffsetTime; +import java.util.List; + +public record AdminEventCreateRequest( + EventType tag, + String startDate, + String endDate, + String title, + String subTitle, + List animationList, + List eventReward +) { + + public record EventRewardVO( + OffsetTime startTime, + OffsetTime endTime, + Long rewardCount, + List eventRewardItem + ) { + + } + + public record EventRewardItemVO( + String tag, + Integer eventRewardProbability, + String randomTag + ) { + + } +} diff --git a/src/main/java/com/yello/server/domain/admin/dto/request/AdminEventRewardCreateRequest.java b/src/main/java/com/yello/server/domain/admin/dto/request/AdminEventRewardCreateRequest.java new file mode 100644 index 00000000..46334966 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/dto/request/AdminEventRewardCreateRequest.java @@ -0,0 +1,11 @@ +package com.yello.server.domain.admin.dto.request; + +public record AdminEventRewardCreateRequest( + String tag, + Long maxRewardValue, + Long minRewardValue, + String title, + String image +) { + +} diff --git a/src/main/java/com/yello/server/domain/admin/dto/request/AdminNoticeCreateRequest.java b/src/main/java/com/yello/server/domain/admin/dto/request/AdminNoticeCreateRequest.java new file mode 100644 index 00000000..7ba27af0 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/dto/request/AdminNoticeCreateRequest.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.admin.dto.request; + +public record AdminNoticeCreateRequest( + String imageUrl, + String redirectUrl, + String startDate, + String endDate, + Boolean isAvailable, + String tag, + String title +) { + +} diff --git a/src/main/java/com/yello/server/domain/admin/dto/request/AdminUserDetailRequest.java b/src/main/java/com/yello/server/domain/admin/dto/request/AdminUserDetailRequest.java index a985c470..ed743ecd 100644 --- a/src/main/java/com/yello/server/domain/admin/dto/request/AdminUserDetailRequest.java +++ b/src/main/java/com/yello/server/domain/admin/dto/request/AdminUserDetailRequest.java @@ -4,7 +4,7 @@ import com.yello.server.domain.user.entity.Gender; import com.yello.server.domain.user.entity.Social; import com.yello.server.domain.user.entity.Subscribe; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; public record AdminUserDetailRequest( @JsonIgnore Long id, diff --git a/src/main/java/com/yello/server/domain/admin/dto/response/AdminConfigurationResponse.java b/src/main/java/com/yello/server/domain/admin/dto/response/AdminConfigurationResponse.java new file mode 100644 index 00000000..e83015ae --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/dto/response/AdminConfigurationResponse.java @@ -0,0 +1,11 @@ +package com.yello.server.domain.admin.dto.response; + +import lombok.Builder; + +@Builder +public record AdminConfigurationResponse( + String tag, + String value +) { + +} diff --git a/src/main/java/com/yello/server/domain/admin/dto/response/AdminConfigurationUpdateRequest.java b/src/main/java/com/yello/server/domain/admin/dto/response/AdminConfigurationUpdateRequest.java new file mode 100644 index 00000000..164e8fee --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/dto/response/AdminConfigurationUpdateRequest.java @@ -0,0 +1,11 @@ +package com.yello.server.domain.admin.dto.response; + +import lombok.Builder; + +@Builder +public record AdminConfigurationUpdateRequest( + String tag, + String value +) { + +} diff --git a/src/main/java/com/yello/server/domain/admin/entity/AdminConfiguration.java b/src/main/java/com/yello/server/domain/admin/entity/AdminConfiguration.java new file mode 100644 index 00000000..a88c093b --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/entity/AdminConfiguration.java @@ -0,0 +1,32 @@ +package com.yello.server.domain.admin.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AdminConfiguration { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + @Convert(converter = AdminConfigurationTypeConverter.class) + private AdminConfigurationType tag; + + @Column + private String value; +} diff --git a/src/main/java/com/yello/server/domain/admin/entity/AdminConfigurationType.java b/src/main/java/com/yello/server/domain/admin/entity/AdminConfigurationType.java new file mode 100644 index 00000000..20643014 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/entity/AdminConfigurationType.java @@ -0,0 +1,33 @@ +package com.yello.server.domain.admin.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 AdminConfigurationType { + ACCESS_TOKEN_TIME("ACCESS_TOKEN_TIME"), + REFRESH_TOKEN_TIME("REFRESH_TOKEN_TIME"), + ADMIN_SITE_PASSWORD("ADMIN_SITE_PASSWORD"), + STATISTICS_USER_GROUP_START_DATE("STATISTICS_USER_GROUP_START_DATE"); + + private final String initial; + + public static AdminConfigurationType fromCode(String dbData) { + return Arrays.stream(AdminConfigurationType.values()) + .filter(v -> v.getInitial().equals(dbData)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static AdminConfigurationType fromName(String name) { + return Arrays.stream(AdminConfigurationType.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/admin/entity/AdminConfigurationTypeConverter.java b/src/main/java/com/yello/server/domain/admin/entity/AdminConfigurationTypeConverter.java new file mode 100644 index 00000000..5ff6c3a5 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/entity/AdminConfigurationTypeConverter.java @@ -0,0 +1,27 @@ +package com.yello.server.domain.admin.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.extern.log4j.Log4j2; + +@Converter +@Log4j2 +public class AdminConfigurationTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(AdminConfigurationType type) { + if (type == null) { + return null; + } + return type.name(); + } + + @Override + public AdminConfigurationType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return AdminConfigurationType.fromName(dbData); + } +} diff --git a/src/main/java/com/yello/server/domain/admin/entity/UserAdmin.java b/src/main/java/com/yello/server/domain/admin/entity/UserAdmin.java index 5119d364..c1f9b3e6 100644 --- a/src/main/java/com/yello/server/domain/admin/entity/UserAdmin.java +++ b/src/main/java/com/yello/server/domain/admin/entity/UserAdmin.java @@ -2,17 +2,20 @@ import com.yello.server.domain.user.entity.User; import com.yello.server.global.common.dto.AuditingTimeEntity; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Getter @Entity @@ -24,7 +27,8 @@ public class UserAdmin extends AuditingTimeEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @OneToOne(fetch = FetchType.LAZY) + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumn(name = "userId") + @OnDelete(action = OnDeleteAction.CASCADE) private User user; } diff --git a/src/main/java/com/yello/server/domain/admin/exception/AdminConfigurationNotFoundException.java b/src/main/java/com/yello/server/domain/admin/exception/AdminConfigurationNotFoundException.java new file mode 100644 index 00000000..84b6a6fb --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/exception/AdminConfigurationNotFoundException.java @@ -0,0 +1,11 @@ +package com.yello.server.domain.admin.exception; + +import com.yello.server.global.common.ErrorCode; +import com.yello.server.global.exception.CustomException; + +public class AdminConfigurationNotFoundException extends CustomException { + + public AdminConfigurationNotFoundException(ErrorCode error) { + super(error, "[AdminConfigurationNotFoundException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationJpaRepository.java b/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationJpaRepository.java new file mode 100644 index 00000000..fe803b83 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationJpaRepository.java @@ -0,0 +1,8 @@ +package com.yello.server.domain.admin.repository; + +import com.yello.server.domain.admin.entity.AdminConfiguration; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminConfigurationJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationRepository.java b/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationRepository.java new file mode 100644 index 00000000..fe2ff3af --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationRepository.java @@ -0,0 +1,14 @@ +package com.yello.server.domain.admin.repository; + +import com.yello.server.domain.admin.entity.AdminConfiguration; +import com.yello.server.domain.admin.entity.AdminConfigurationType; +import java.util.List; + +public interface AdminConfigurationRepository { + + List findConfigurations(AdminConfigurationType tag); + + void setConfigurations(AdminConfigurationType tag, String value); + + void deleteConfigurations(AdminConfigurationType tag); +} diff --git a/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationRepositoryImpl.java b/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationRepositoryImpl.java new file mode 100644 index 00000000..14ee434b --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/repository/AdminConfigurationRepositoryImpl.java @@ -0,0 +1,62 @@ +package com.yello.server.domain.admin.repository; + +import static com.yello.server.domain.admin.entity.QAdminConfiguration.adminConfiguration; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.yello.server.domain.admin.entity.AdminConfiguration; +import com.yello.server.domain.admin.entity.AdminConfigurationType; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminConfigurationRepositoryImpl implements AdminConfigurationRepository { + + private final AdminConfigurationJpaRepository adminConfigurationJpaRepository; + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findConfigurations(AdminConfigurationType tag) { + return jpaQueryFactory + .selectFrom(adminConfiguration) + .where(adminConfiguration.tag.eq(tag)) + .fetch(); + } + + @Transactional + @Override + public void setConfigurations(AdminConfigurationType tag, String value) { + final List configurations = jpaQueryFactory + .selectFrom(adminConfiguration) + .where(adminConfiguration.tag.eq(tag)) + .fetch(); + + if (configurations.isEmpty()) { + adminConfigurationJpaRepository.save( + AdminConfiguration.builder() + .tag(tag) + .value(value) + .build() + ); + return; + } + + jpaQueryFactory + .update(adminConfiguration) + .set(adminConfiguration.value, value) + .where(adminConfiguration.tag.eq(tag)) + .execute(); + } + + @Transactional + @Override + public void deleteConfigurations(AdminConfigurationType tag) { + jpaQueryFactory + .delete(adminConfiguration) + .where(adminConfiguration.tag.eq(tag)) + .execute(); + } +} diff --git a/src/main/java/com/yello/server/domain/admin/repository/AdminRepository.java b/src/main/java/com/yello/server/domain/admin/repository/AdminRepository.java new file mode 100644 index 00000000..f82e1fce --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/repository/AdminRepository.java @@ -0,0 +1,26 @@ +package com.yello.server.domain.admin.repository; + +import com.yello.server.domain.event.entity.Event; +import com.yello.server.domain.event.entity.EventRandom; +import com.yello.server.domain.event.entity.EventReward; +import com.yello.server.domain.event.entity.EventRewardMapping; +import com.yello.server.domain.event.entity.EventTime; + +public interface AdminRepository { + + Event save(Event newEvent); + + EventTime save(EventTime newEventTime); + + EventReward save(EventReward newEventReward); + + EventRewardMapping save(EventRewardMapping newEventRewardMapping); + + Event getById(Long eventId); + + EventTime getEventTimeById(Long eventTimeId); + + EventReward getByTag(String tag); + + EventRandom getByRandomTag(String randomTag); +} diff --git a/src/main/java/com/yello/server/domain/admin/repository/AdminRepositoryImpl.java b/src/main/java/com/yello/server/domain/admin/repository/AdminRepositoryImpl.java new file mode 100644 index 00000000..f2d425c9 --- /dev/null +++ b/src/main/java/com/yello/server/domain/admin/repository/AdminRepositoryImpl.java @@ -0,0 +1,87 @@ +package com.yello.server.domain.admin.repository; + +import static com.yello.server.domain.event.entity.QEvent.event; +import static com.yello.server.domain.event.entity.QEventTime.eventTime; +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.EVENT_REWARD_NOT_FOUND_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.EVENT_TIME_NOT_FOUND_EXCEPTION; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.yello.server.domain.admin.exception.UserAdminNotFoundException; +import com.yello.server.domain.event.entity.Event; +import com.yello.server.domain.event.entity.EventRandom; +import com.yello.server.domain.event.entity.EventReward; +import com.yello.server.domain.event.entity.EventRewardMapping; +import com.yello.server.domain.event.entity.EventTime; +import com.yello.server.domain.event.repository.EventJpaRepository; +import com.yello.server.domain.event.repository.EventRandomJpaRepository; +import com.yello.server.domain.event.repository.EventRewardJpaRepository; +import com.yello.server.domain.event.repository.EventRewardMappingJpaRepository; +import com.yello.server.domain.event.repository.EventTimeJpaRepository; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class AdminRepositoryImpl implements AdminRepository { + + private final EventJpaRepository eventJpaRepository; + private final EventRandomJpaRepository eventRandomJpaRepository; + private final EventRewardJpaRepository eventRewardJpaRepository; + private final EventRewardMappingJpaRepository eventRewardMappingJpaRepository; + private final EventTimeJpaRepository eventTimeJpaRepository; + private final JPAQueryFactory jpaQueryFactory; + + @Override + public Event save(Event newEvent) { + return eventJpaRepository.save(newEvent); + } + + @Override + public EventTime save(EventTime newEventTime) { + return eventTimeJpaRepository.save(newEventTime); + } + + @Override + public EventReward save(EventReward newEventReward) { + return eventRewardJpaRepository.save(newEventReward); + } + + @Override + public EventRewardMapping save(EventRewardMapping newEventRewardMapping) { + return eventRewardMappingJpaRepository.save(newEventRewardMapping); + } + + @Override + public Event getById(Long eventId) { + return Optional.ofNullable(jpaQueryFactory.selectFrom(event) + .where(event.id.eq(eventId)) + .fetchFirst()) + .orElseThrow(() -> new UserAdminNotFoundException(EVENT_NOT_FOUND_EXCEPTION)); + } + + @Override + public EventTime getEventTimeById(Long eventTimeId) { + return Optional.ofNullable(jpaQueryFactory.selectFrom(eventTime) + .where(eventTime.id.eq(eventTimeId)) + .fetchFirst()) + .orElseThrow(() -> new UserAdminNotFoundException(EVENT_TIME_NOT_FOUND_EXCEPTION)); + } + + /** + * findByEnum을 QueryDSL을 이용하여 조회시 오류가 발생하여 JPA로 구현한다. + */ + @Override + public EventReward getByTag(String tag) { + return eventRewardJpaRepository.findByTag(tag) + .orElseThrow(() -> new UserAdminNotFoundException(EVENT_REWARD_NOT_FOUND_EXCEPTION)); + } + + @Override + public EventRandom getByRandomTag(String randomTag) { + return eventRandomJpaRepository.findTopByRandomTag(randomTag) + .orElseThrow(() -> new UserAdminNotFoundException(EVENT_RANDOM_NOT_FOUND_EXCEPTION)); + } +} diff --git a/src/main/java/com/yello/server/domain/admin/service/AdminService.java b/src/main/java/com/yello/server/domain/admin/service/AdminService.java index 5f07c431..0b35f04f 100644 --- a/src/main/java/com/yello/server/domain/admin/service/AdminService.java +++ b/src/main/java/com/yello/server/domain/admin/service/AdminService.java @@ -1,14 +1,25 @@ package com.yello.server.domain.admin.service; +import static com.yello.server.domain.admin.entity.AdminConfigurationType.ADMIN_SITE_PASSWORD; +import static com.yello.server.global.common.ErrorCode.ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION; import static com.yello.server.global.common.ErrorCode.DEVICE_TOKEN_CONFLICT_USER_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.PROBABILITY_BAD_REQUEST_EXCEPTION; import static com.yello.server.global.common.ErrorCode.USER_ADMIN_BAD_REQUEST_EXCEPTION; import static com.yello.server.global.common.ErrorCode.USER_ADMIN_NOT_FOUND_EXCEPTION; import static com.yello.server.global.common.ErrorCode.UUID_CONFLICT_USER_EXCEPTION; import static com.yello.server.global.common.ErrorCode.YELLOID_CONFLICT_USER_EXCEPTION; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yello.server.domain.admin.dto.request.AdminEventCreateRequest; +import com.yello.server.domain.admin.dto.request.AdminEventCreateRequest.EventRewardItemVO; +import com.yello.server.domain.admin.dto.request.AdminEventCreateRequest.EventRewardVO; +import com.yello.server.domain.admin.dto.request.AdminEventRewardCreateRequest; import com.yello.server.domain.admin.dto.request.AdminLoginRequest; +import com.yello.server.domain.admin.dto.request.AdminNoticeCreateRequest; import com.yello.server.domain.admin.dto.request.AdminQuestionVoteRequest; import com.yello.server.domain.admin.dto.request.AdminUserDetailRequest; +import com.yello.server.domain.admin.dto.response.AdminConfigurationResponse; import com.yello.server.domain.admin.dto.response.AdminCooldownContentVO; import com.yello.server.domain.admin.dto.response.AdminCooldownResponse; import com.yello.server.domain.admin.dto.response.AdminLoginResponse; @@ -18,12 +29,25 @@ import com.yello.server.domain.admin.dto.response.AdminUserContentVO; import com.yello.server.domain.admin.dto.response.AdminUserDetailResponse; import com.yello.server.domain.admin.dto.response.AdminUserResponse; +import com.yello.server.domain.admin.entity.AdminConfiguration; +import com.yello.server.domain.admin.entity.AdminConfigurationType; +import com.yello.server.domain.admin.exception.AdminConfigurationNotFoundException; import com.yello.server.domain.admin.exception.UserAdminBadRequestException; import com.yello.server.domain.admin.exception.UserAdminNotFoundException; +import com.yello.server.domain.admin.repository.AdminConfigurationRepository; +import com.yello.server.domain.admin.repository.AdminRepository; import com.yello.server.domain.admin.repository.UserAdminRepository; -import com.yello.server.domain.authorization.service.TokenProvider; +import com.yello.server.domain.authorization.service.AuthManager; import com.yello.server.domain.cooldown.entity.Cooldown; import com.yello.server.domain.cooldown.repository.CooldownRepository; +import com.yello.server.domain.event.entity.Event; +import com.yello.server.domain.event.entity.EventRandom; +import com.yello.server.domain.event.entity.EventReward; +import com.yello.server.domain.event.entity.EventRewardMapping; +import com.yello.server.domain.event.entity.EventTime; +import com.yello.server.domain.notice.entity.Notice; +import com.yello.server.domain.notice.entity.NoticeType; +import com.yello.server.domain.notice.repository.NoticeRepository; import com.yello.server.domain.question.entity.Question; import com.yello.server.domain.question.repository.QuestionRepository; import com.yello.server.domain.user.entity.Gender; @@ -34,12 +58,13 @@ import com.yello.server.domain.vote.entity.Vote; import com.yello.server.domain.vote.repository.VoteRepository; import com.yello.server.global.common.dto.EmptyObject; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -49,24 +74,33 @@ @Transactional(readOnly = true) public class AdminService { - private final UserRepository userRepository; - private final UserManager userManager; - private final TokenProvider tokenProvider; + private final AdminConfigurationRepository adminConfigurationRepository; + private final AdminRepository adminRepository; + private final AuthManager authManager; private final CooldownRepository cooldownRepository; + private final NoticeRepository noticeRepository; + private final ObjectMapper objectMapper; private final QuestionRepository questionRepository; - private final VoteRepository voteRepository; private final UserAdminRepository userAdminRepository; + private final UserManager userManager; + private final UserRepository userRepository; + private final VoteRepository voteRepository; - @Value("${admin.password}") - String adminPassword; - + @Transactional public AdminLoginResponse login(AdminLoginRequest request) { + final List list = adminConfigurationRepository.findConfigurations( + ADMIN_SITE_PASSWORD); + if (list.isEmpty()) { + throw new AdminConfigurationNotFoundException(ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION); + } + String accessToken; + String adminPassWord = list.get(0).getValue(); - if (request.password().equals(adminPassword)) { + if (request.password().equals(adminPassWord)) { final User user = userManager.getOfficialUser(Gender.FEMALE); - accessToken = tokenProvider.createAccessToken(user.getId(), user.getUuid()); + accessToken = authManager.issueToken(user).accessToken(); } else { throw new UserAdminNotFoundException(USER_ADMIN_NOT_FOUND_EXCEPTION); } @@ -280,4 +314,165 @@ public void deleteQuestion(Long adminId, Long questionId) { // logic questionRepository.delete(question); } + + public AdminConfigurationResponse getConfigurations(Long adminId, AdminConfigurationType tag) { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + + final List configurations = adminConfigurationRepository.findConfigurations(tag); + + if (configurations.isEmpty()) { + throw new AdminConfigurationNotFoundException(ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION); + } + + // logic + return AdminConfigurationResponse.builder() + .tag(String.valueOf(tag)) + .value(configurations.get(0).getValue()) + .build(); + } + + @Transactional + public EmptyObject updateConfigurations(Long adminId, AdminConfigurationType tag, String value) { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + + adminConfigurationRepository.setConfigurations(tag, value); + + return EmptyObject.builder().build(); + } + + public List getNotices(Long adminId) { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + + final List noticeList = noticeRepository.findAll(); + + return noticeList; + } + + @Transactional + public EmptyObject createNotice(Long adminId, AdminNoticeCreateRequest request) { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + + ZonedDateTime startDate = ZonedDateTime.parse(request.startDate(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + ZonedDateTime endDate = ZonedDateTime.parse(request.endDate(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + final NoticeType tag = NoticeType.fromCode(request.tag()); + + final Notice notice = Notice.builder() + .imageUrl(request.imageUrl()) + .redirectUrl(request.redirectUrl()) + .startDate(startDate) + .endDate(endDate) + .isAvailable(true) + .tag(tag) + .title(request.title()) + .build(); + + noticeRepository.save(notice); + + return EmptyObject.builder().build(); + } + + @Transactional + public EmptyObject updateNotice(Long adminId, Long noticeId, AdminNoticeCreateRequest request) { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + final Notice notice = noticeRepository.getById(noticeId); + + ZonedDateTime startDate = ZonedDateTime.parse(request.startDate(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + ZonedDateTime endDate = ZonedDateTime.parse(request.endDate(), DateTimeFormatter.ISO_OFFSET_DATE_TIME); + final NoticeType tag = NoticeType.fromCode(request.tag()); + + noticeRepository.update(Notice.builder() + .id(notice.getId()) + .imageUrl(request.imageUrl()) + .redirectUrl(request.redirectUrl()) + .startDate(startDate) + .endDate(endDate) + .isAvailable(request.isAvailable()) + .tag(tag) + .title(request.title()) + .build()); + + return EmptyObject.builder().build(); + } + + @Transactional + public EmptyObject createEvent(Long adminId, AdminEventCreateRequest request) throws JsonProcessingException { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + + for (EventRewardVO vo : request.eventReward()) { + int sumOfProbability = 0; + + for (EventRewardItemVO itemVO : vo.eventRewardItem()) { + sumOfProbability += itemVO.eventRewardProbability(); + } + + if (sumOfProbability != 100) { + throw new UserAdminBadRequestException(PROBABILITY_BAD_REQUEST_EXCEPTION); + } + } + + // logic + String animationString = objectMapper.writeValueAsString(request.animationList()); + + final Event newEvent = adminRepository.save(Event.builder() + .tag(request.tag()) + .startDate(ZonedDateTime.parse(request.startDate(), DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + .endDate(ZonedDateTime.parse(request.endDate(), DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + .title(request.title()) + .subTitle(request.subTitle()) + .animation(animationString) + .build()); + + request.eventReward().forEach((eventRewardVO) -> { + final EventTime newEventTime = adminRepository.save(EventTime.builder() + .event(newEvent) + .startTime(eventRewardVO.startTime()) + .endTime(eventRewardVO.endTime()) + .rewardCount(eventRewardVO.rewardCount()) + .build()); + + eventRewardVO.eventRewardItem().forEach(eventRewardItemVO -> { + final EventReward eventReward = adminRepository.getByTag(eventRewardItemVO.tag()); + final EventRandom eventRandom = adminRepository.getByRandomTag(eventRewardItemVO.randomTag()); + + adminRepository.save(EventRewardMapping.builder() + .eventTime(newEventTime) + .eventReward(eventReward) + .eventRandom(eventRandom) + .eventRewardProbability(eventRewardItemVO.eventRewardProbability()) + .build()); + }); + }); + + return EmptyObject.builder().build(); + } + + @Transactional + public EmptyObject createEventReward(Long adminId, AdminEventRewardCreateRequest request) { + // exception + final User admin = userRepository.getById(adminId); + userAdminRepository.getByUser(admin); + + // login + adminRepository.save(EventReward.builder() + .tag(request.tag()) + .maxRewardValue(request.maxRewardValue()) + .minRewardValue(request.minRewardValue()) + .title(request.title()) + .image(request.image()) + .build()); + + return EmptyObject.builder().build(); + } } diff --git a/src/main/java/com/yello/server/domain/authorization/configuration/SecurityConfiguration.java b/src/main/java/com/yello/server/domain/authorization/configuration/SecurityConfiguration.java index ad06a64a..058e9b07 100644 --- a/src/main/java/com/yello/server/domain/authorization/configuration/SecurityConfiguration.java +++ b/src/main/java/com/yello/server/domain/authorization/configuration/SecurityConfiguration.java @@ -8,44 +8,63 @@ import com.yello.server.domain.authorization.service.TokenProvider; import com.yello.server.domain.user.repository.UserRepository; import com.yello.server.global.exception.ExceptionHandlerFilter; +import java.util.Arrays; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @Configuration @EnableWebSecurity @RequiredArgsConstructor -@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) +@EnableMethodSecurity(securedEnabled = true) public class SecurityConfiguration { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; - private final UserRepository userRepository; private final TokenProvider tokenProvider; + private final UserRepository userRepository; + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("*")); + configuration.setAllowedHeaders(Arrays.asList("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity - .httpBasic().disable() - .csrf().disable() - .cors() - .and() - .authorizeRequests() - .antMatchers("/api/v1/auth/oauth", "/api/v1/auth/signup").permitAll() - .antMatchers("/api/*").authenticated() - .and() - .sessionManagement() - .sessionCreationPolicy(STATELESS) - .and() - .exceptionHandling() - .authenticationEntryPoint(customAuthenticationEntryPoint) - .and() - .addFilterBefore(new JwtFilter(userRepository), - UsernamePasswordAuthenticationFilter.class) + .httpBasic(httpSecurityHttpBasicConfigurer -> { + httpSecurityHttpBasicConfigurer.disable(); + }) + .csrf(httpSecurityCsrfConfigurer -> { + httpSecurityCsrfConfigurer.disable(); + }) + .cors(httpSecurityCorsConfigurer -> { + httpSecurityCorsConfigurer.configurationSource(corsConfigurationSource()); + }) + .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> { + authorizationManagerRequestMatcherRegistry + .anyRequest().permitAll(); + }) + .sessionManagement(httpSecuritySessionManagementConfigurer -> { + httpSecuritySessionManagementConfigurer.sessionCreationPolicy(STATELESS); + }) + .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> { + httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(customAuthenticationEntryPoint); + }) + .addFilterBefore(new JwtFilter(userRepository), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtExceptionFilter(tokenProvider), JwtFilter.class) .addFilterBefore(new ExceptionHandlerFilter(), JwtExceptionFilter.class) .build(); diff --git a/src/main/java/com/yello/server/domain/authorization/controller/AuthController.java b/src/main/java/com/yello/server/domain/authorization/controller/AuthController.java index 93df7e50..f1d68052 100644 --- a/src/main/java/com/yello/server/domain/authorization/controller/AuthController.java +++ b/src/main/java/com/yello/server/domain/authorization/controller/AuthController.java @@ -25,8 +25,8 @@ import com.yello.server.global.common.annotation.ServiceToken; import com.yello.server.global.common.dto.BaseResponse; import com.yello.server.infrastructure.slack.annotation.SlackSignUpNotification; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import lombok.val; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/yello/server/domain/authorization/dto/request/OnBoardingFriendRequest.java b/src/main/java/com/yello/server/domain/authorization/dto/request/OnBoardingFriendRequest.java index 1c2bfacc..106d6bc2 100644 --- a/src/main/java/com/yello/server/domain/authorization/dto/request/OnBoardingFriendRequest.java +++ b/src/main/java/com/yello/server/domain/authorization/dto/request/OnBoardingFriendRequest.java @@ -1,7 +1,7 @@ package com.yello.server.domain.authorization.dto.request; +import jakarta.validation.constraints.NotNull; import java.util.List; -import javax.validation.constraints.NotNull; import lombok.Builder; @Builder diff --git a/src/main/java/com/yello/server/domain/authorization/dto/request/SignUpRequest.java b/src/main/java/com/yello/server/domain/authorization/dto/request/SignUpRequest.java index e9dbaaa5..a89088f6 100644 --- a/src/main/java/com/yello/server/domain/authorization/dto/request/SignUpRequest.java +++ b/src/main/java/com/yello/server/domain/authorization/dto/request/SignUpRequest.java @@ -2,9 +2,9 @@ import com.yello.server.domain.user.entity.Gender; import com.yello.server.domain.user.entity.Social; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; import java.util.List; -import javax.validation.constraints.Email; -import javax.validation.constraints.NotNull; import lombok.Builder; @Builder diff --git a/src/main/java/com/yello/server/domain/authorization/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/yello/server/domain/authorization/exception/CustomAuthenticationEntryPoint.java index 1b2c73b0..3261ab46 100644 --- a/src/main/java/com/yello/server/domain/authorization/exception/CustomAuthenticationEntryPoint.java +++ b/src/main/java/com/yello/server/domain/authorization/exception/CustomAuthenticationEntryPoint.java @@ -5,10 +5,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yello.server.global.common.dto.BaseResponse; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; 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 6623b08d..b635332c 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 @@ -16,11 +16,11 @@ import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.security.SignatureException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import lombok.val; @@ -48,6 +48,8 @@ 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/statistics") || (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 04ae15c5..7689136b 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 @@ -2,12 +2,12 @@ import com.yello.server.domain.user.entity.User; import com.yello.server.domain.user.repository.UserRepository; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import lombok.val; @@ -41,7 +41,9 @@ 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") + || requestPath.startsWith("/api/v1/statistics")) { filterChain.doFilter(request, response); return; } diff --git a/src/main/java/com/yello/server/domain/authorization/service/AuthManager.java b/src/main/java/com/yello/server/domain/authorization/service/AuthManager.java index 89b2313d..8a1981e3 100644 --- a/src/main/java/com/yello/server/domain/authorization/service/AuthManager.java +++ b/src/main/java/com/yello/server/domain/authorization/service/AuthManager.java @@ -9,9 +9,11 @@ public interface AuthManager { User getSignedInUserByUuid(String uuid); - ServiceTokenVO registerToken(User user); + ServiceTokenVO issueToken(User user); - ServiceTokenVO setNewAccessToken(String refreshToken); + boolean isExpired(String token); + + String issueNewAccessToken(String refreshToken); void validateSignupRequest(SignUpRequest signUpRequest); diff --git a/src/main/java/com/yello/server/domain/authorization/service/AuthManagerImpl.java b/src/main/java/com/yello/server/domain/authorization/service/AuthManagerImpl.java index c24e4717..ca955750 100644 --- a/src/main/java/com/yello/server/domain/authorization/service/AuthManagerImpl.java +++ b/src/main/java/com/yello/server/domain/authorization/service/AuthManagerImpl.java @@ -1,10 +1,15 @@ package com.yello.server.domain.authorization.service; +import static com.yello.server.global.common.ErrorCode.ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION; import static com.yello.server.global.common.ErrorCode.DEVICE_TOKEN_CONFLICT_USER_EXCEPTION; import static com.yello.server.global.common.ErrorCode.NOT_SIGNIN_USER_EXCEPTION; import static com.yello.server.global.common.ErrorCode.UUID_CONFLICT_USER_EXCEPTION; import static com.yello.server.global.common.ErrorCode.YELLOID_CONFLICT_USER_EXCEPTION; +import com.yello.server.domain.admin.entity.AdminConfiguration; +import com.yello.server.domain.admin.entity.AdminConfigurationType; +import com.yello.server.domain.admin.exception.AdminConfigurationNotFoundException; +import com.yello.server.domain.admin.repository.AdminConfigurationRepository; import com.yello.server.domain.authorization.dto.ServiceTokenVO; import com.yello.server.domain.authorization.dto.request.SignUpRequest; import com.yello.server.domain.authorization.exception.NotSignedInException; @@ -15,7 +20,8 @@ import com.yello.server.domain.user.entity.User; import com.yello.server.domain.user.exception.UserConflictException; import com.yello.server.domain.user.repository.UserRepository; -import com.yello.server.infrastructure.redis.repository.TokenRepository; +import java.time.Duration; +import java.util.List; import java.util.Optional; import lombok.Builder; import lombok.RequiredArgsConstructor; @@ -26,11 +32,11 @@ @RequiredArgsConstructor public class AuthManagerImpl implements AuthManager { - private final FriendRepository friendRepository; + private final AdminConfigurationRepository adminConfigurationRepository; private final CooldownRepository cooldownRepository; - private final UserRepository userRepository; - private final TokenRepository tokenRepository; + private final FriendRepository friendRepository; private final TokenProvider tokenProvider; + private final UserRepository userRepository; @Override public User getSignedInUserByUuid(String uuid) { @@ -42,34 +48,59 @@ public User getSignedInUserByUuid(String uuid) { } @Override - public ServiceTokenVO registerToken(User user) { - ServiceTokenVO newUserTokens = tokenProvider.createServiceToken( + public ServiceTokenVO issueToken(User user) { + final List accessTokenTime = adminConfigurationRepository.findConfigurations( + AdminConfigurationType.ACCESS_TOKEN_TIME); + final List refreshTokenTime = adminConfigurationRepository.findConfigurations( + AdminConfigurationType.REFRESH_TOKEN_TIME); + + if (accessTokenTime.isEmpty() || refreshTokenTime.isEmpty()) { + throw new AdminConfigurationNotFoundException(ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION); + } + + final String accessToken = tokenProvider.createAccessToken( + user.getId(), + user.getUuid(), + Duration.ofMinutes(Long.parseLong(accessTokenTime.get(0).getValue())) + ); + final String refreshToken = tokenProvider.createAccessToken( user.getId(), - user.getUuid() + user.getUuid(), + Duration.ofMinutes(Long.parseLong(refreshTokenTime.get(0).getValue())) ); - tokenRepository.set(user.getId(), newUserTokens); - return newUserTokens; + + return ServiceTokenVO.of(accessToken, refreshToken); + } + + @Override + public boolean isExpired(String token) { + return tokenProvider.isExpired(token); } @Override - public ServiceTokenVO setNewAccessToken(String refreshToken) { + public String issueNewAccessToken(String refreshToken) { final Long userId = tokenProvider.getUserId(refreshToken); final String uuid = tokenProvider.getUserUuid(refreshToken); + final List accessTokenTime = adminConfigurationRepository.findConfigurations( + AdminConfigurationType.ACCESS_TOKEN_TIME); userRepository.getById(userId); userRepository.getByUuid(uuid); + if (accessTokenTime.isEmpty()) { + throw new AdminConfigurationNotFoundException(ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION); + } + + final String newAccessToken = tokenProvider.createAccessToken( + userId, + uuid, + Duration.ofMinutes(Long.parseLong(accessTokenTime.get(0).getValue())) + ); - final String newAccessToken = tokenProvider.createAccessToken(userId, uuid); - final ServiceTokenVO token = ServiceTokenVO.of(newAccessToken, refreshToken); - tokenRepository.set(userId, token); - return token; + return newAccessToken; } @Override public void validateSignupRequest(SignUpRequest signUpRequest) { - // 회원가입 로그 (이슈 해결 후 제거) - System.out.println("회원가입 : " + signUpRequest.toString()); - userRepository.findByUuidNotFiltered(signUpRequest.uuid()) .ifPresent(action -> { throw new UserConflictException(UUID_CONFLICT_USER_EXCEPTION); 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 f65badfa..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 @@ -3,7 +3,9 @@ import static com.yello.server.global.common.ErrorCode.TOKEN_ALL_EXPIRED_AUTH_EXCEPTION; import static com.yello.server.global.common.ErrorCode.TOKEN_NOT_EXPIRED_AUTH_EXCEPTION; import static com.yello.server.global.common.ErrorCode.YELLOID_REQUIRED_EXCEPTION; +import static com.yello.server.global.common.util.ConstantUtil.GlobalZoneId; import static com.yello.server.global.common.util.ConstantUtil.RECOMMEND_POINT; +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; import com.yello.server.domain.authorization.dto.ServiceTokenVO; import com.yello.server.domain.authorization.dto.kakao.KakaoTokenInfo; @@ -27,16 +29,20 @@ import com.yello.server.domain.group.entity.UserGroupType; import com.yello.server.domain.group.repository.UserGroupRepository; import com.yello.server.domain.user.entity.User; +import com.yello.server.domain.user.entity.UserData; +import com.yello.server.domain.user.entity.UserDataType; +import com.yello.server.domain.user.repository.UserDataRepository; import com.yello.server.domain.user.repository.UserRepository; import com.yello.server.domain.vote.service.VoteManager; import com.yello.server.global.common.factory.PaginationFactory; import com.yello.server.global.common.manager.ConnectionManager; import com.yello.server.infrastructure.firebase.service.NotificationService; import com.yello.server.infrastructure.rabbitmq.repository.MessageQueueRepository; +import jakarta.validation.constraints.NotNull; +import java.time.ZonedDateTime; import java.util.List; import java.util.Objects; import java.util.Optional; -import javax.validation.constraints.NotNull; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -51,19 +57,17 @@ @Transactional(readOnly = true) public class AuthService { - private final UserRepository userRepository; - private final UserGroupRepository userGroupRepository; - private final FriendRepository friendRepository; - private final CooldownRepository cooldownRepository; - private final MessageQueueRepository messageQueueRepository; - private final AuthManager authManager; - private final FriendManager friendManager; private final ConnectionManager connectionManager; - private final VoteManager voteManager; - private final TokenProvider tokenProvider; - + private final CooldownRepository cooldownRepository; + private final FriendManager friendManager; + private final FriendRepository friendRepository; + private final MessageQueueRepository messageQueueRepository; private final NotificationService notificationService; + private final UserDataRepository userDataRepository; + private final UserGroupRepository userGroupRepository; + private final UserRepository userRepository; + private final VoteManager voteManager; @Transactional public OAuthResponse oauthLogin(OAuthRequest oAuthRequest) { @@ -71,7 +75,7 @@ public OAuthResponse oauthLogin(OAuthRequest oAuthRequest) { connectionManager.getKakaoTokenInfo(oAuthRequest.accessToken()); final User user = authManager.getSignedInUserByUuid(response.getBody().id().toString()); - final ServiceTokenVO serviceTokenVO = authManager.registerToken(user); + final ServiceTokenVO serviceTokenVO = authManager.issueToken(user); final Boolean isResigned = authManager.renewUserData(user); user.setDeviceToken(oAuthRequest.deviceToken()); @@ -96,7 +100,7 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { this.recommendUser(signUpRequest.recommendId(), signUpRequest.yelloId()); - final ServiceTokenVO signUpToken = authManager.registerToken(newUser); + final ServiceTokenVO signUpToken = authManager.issueToken(newUser); this.makeFriend(newUser, signUpRequest.friends()); @@ -109,10 +113,21 @@ public void recommendUser(String recommendYelloId, String userYelloId) { if (recommendYelloId != null && !recommendYelloId.isEmpty()) { User recommendedUser = userRepository.getByYelloId(recommendYelloId); User user = userRepository.getByYelloId(userYelloId); + final Optional recommended = userDataRepository.findByUserIdAndTag(recommendedUser.getId(), + 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); + + userDataRepository.save(UserData.of( + UserDataType.RECOMMENDED, + ZonedDateTime.now(GlobalZoneId).format(ISO_OFFSET_DATE_TIME), + recommendedUser + )); + } notificationService.sendRecommendNotification(user, recommendedUser); @@ -172,8 +187,8 @@ public DepartmentSearchResponse findGroupDepartmentBySchoolNameContaining(String @Transactional public ServiceTokenVO reIssueToken(@NotNull ServiceTokenVO tokens) { - boolean isAccessTokenExpired = tokenProvider.isExpired(tokens.accessToken()); - boolean isRefreshTokenExpired = tokenProvider.isExpired(tokens.refreshToken()); + boolean isAccessTokenExpired = authManager.isExpired(tokens.accessToken()); + boolean isRefreshTokenExpired = authManager.isExpired(tokens.refreshToken()); if (isAccessTokenExpired) { @@ -181,8 +196,8 @@ public ServiceTokenVO reIssueToken(@NotNull ServiceTokenVO tokens) { throw new NotExpiredTokenForbiddenException(TOKEN_ALL_EXPIRED_AUTH_EXCEPTION); } - final String refreshToken = tokens.refreshToken(); - return authManager.setNewAccessToken(refreshToken); + String newAccessToken = authManager.issueNewAccessToken(tokens.refreshToken()); + return ServiceTokenVO.of(newAccessToken, tokens.refreshToken()); } throw new NotExpiredTokenForbiddenException(TOKEN_NOT_EXPIRED_AUTH_EXCEPTION); diff --git a/src/main/java/com/yello/server/domain/authorization/service/TokenJwtProvider.java b/src/main/java/com/yello/server/domain/authorization/service/TokenJwtProvider.java index f510a2b7..8d9d0b42 100644 --- a/src/main/java/com/yello/server/domain/authorization/service/TokenJwtProvider.java +++ b/src/main/java/com/yello/server/domain/authorization/service/TokenJwtProvider.java @@ -1,21 +1,22 @@ package com.yello.server.domain.authorization.service; import static io.jsonwebtoken.SignatureAlgorithm.HS256; -import static java.time.Duration.ofDays; -import static java.time.Duration.ofMinutes; -import com.yello.server.domain.authorization.dto.ServiceTokenVO; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.security.SignatureException; +import java.time.Duration; import java.util.Date; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +/** + * 해당 객체는 secretKey를 제외한 State를 가지지 않도록 합니다. + */ @Log4j2 @Component public class TokenJwtProvider implements TokenProvider { @@ -23,9 +24,6 @@ public class TokenJwtProvider implements TokenProvider { public static final String ACCESS_TOKEN = "accessToken"; public static final String REFRESH_TOKEN = "refreshToken"; - private static final Long ACCESS_TOKEN_VALID_TIME = ofMinutes(30).toMillis(); - private static final Long REFRESH_TOKEN_VALID_TIME = ofDays(14).toMillis(); - public String secretKey; public TokenJwtProvider(@Value("${spring.jwt.secret}") String secretKey) { @@ -76,25 +74,16 @@ public boolean isExpired(String token) { } @Override - public String createAccessToken(Long userId, String uuid) { - return createJwt(userId, uuid, ACCESS_TOKEN_VALID_TIME, ACCESS_TOKEN); + public String createAccessToken(Long userId, String uuid, Duration duration) { + return createJwt(userId, uuid, duration, ACCESS_TOKEN); } @Override - public String createRefreshToken(Long userId, String uuid) { - return createJwt(userId, uuid, REFRESH_TOKEN_VALID_TIME, REFRESH_TOKEN); + public String createRefreshToken(Long userId, String uuid, Duration duration) { + return createJwt(userId, uuid, duration, REFRESH_TOKEN); } - @Override - public ServiceTokenVO createServiceToken(Long userId, String uuid) { - return ServiceTokenVO.of( - createAccessToken(userId, uuid), - createRefreshToken(userId, uuid) - ); - } - - @Override - public String createJwt(Long userId, String uuid, Long tokenValidTime, String tokenType) { + private String createJwt(Long userId, String uuid, Duration duration, String tokenType) { Claims claims = Jwts.claims() .setSubject(uuid) .setId(String.valueOf(userId)); @@ -103,7 +92,7 @@ public String createJwt(Long userId, String uuid, Long tokenValidTime, String to .setClaims(claims) .setHeaderParam("type", tokenType) .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + tokenValidTime)) + .setExpiration(new Date(System.currentTimeMillis() + duration.toMillis())) .signWith(HS256, secretKey.getBytes()) .compact(); } diff --git a/src/main/java/com/yello/server/domain/authorization/service/TokenProvider.java b/src/main/java/com/yello/server/domain/authorization/service/TokenProvider.java index ac02f2ee..e25bd2e5 100644 --- a/src/main/java/com/yello/server/domain/authorization/service/TokenProvider.java +++ b/src/main/java/com/yello/server/domain/authorization/service/TokenProvider.java @@ -1,6 +1,6 @@ package com.yello.server.domain.authorization.service; -import com.yello.server.domain.authorization.dto.ServiceTokenVO; +import java.time.Duration; public interface TokenProvider { @@ -10,11 +10,8 @@ public interface TokenProvider { boolean isExpired(String token); - String createAccessToken(Long userId, String uuid); + String createAccessToken(Long userId, String uuid, Duration duration); - String createRefreshToken(Long userId, String uuid); + String createRefreshToken(Long userId, String uuid, Duration duration); - ServiceTokenVO createServiceToken(Long userId, String uuid); - - String createJwt(Long userId, String uuid, Long tokenValidTime, String tokenType); } diff --git a/src/main/java/com/yello/server/domain/cooldown/entity/Cooldown.java b/src/main/java/com/yello/server/domain/cooldown/entity/Cooldown.java index 99d87952..6b1a05fc 100644 --- a/src/main/java/com/yello/server/domain/cooldown/entity/Cooldown.java +++ b/src/main/java/com/yello/server/domain/cooldown/entity/Cooldown.java @@ -4,22 +4,24 @@ import static com.yello.server.global.common.util.ConstantUtil.TIMER_TIME; import com.yello.server.domain.user.entity.User; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import java.time.LocalDateTime; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.Where; import org.springframework.data.annotation.CreatedDate; @@ -45,6 +47,7 @@ public class Cooldown { @OneToOne(fetch = FetchType.LAZY) @JoinColumn(nullable = false, name = "userId") + @OnDelete(action = OnDeleteAction.CASCADE) private User user; private String messageId; 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 new file mode 100644 index 00000000..bc64ee6d --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/controller/EventController.java @@ -0,0 +1,107 @@ +package com.yello.server.domain.event.controller; + +import static com.yello.server.global.common.ErrorCode.ADMOB_URI_BAD_REQUEST_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.EMPTY_QUERY_STRING_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.GET_IS_POSSIBLE_ADMOB_SUCCESS; +import static com.yello.server.global.common.SuccessCode.REWARD_ADMOB_SUCCESS; +import static com.yello.server.global.common.util.ConstantUtil.IdempotencyKeyHeader; + +import com.fasterxml.jackson.core.JsonProcessingException; +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; +import com.yello.server.domain.event.dto.response.GetIsPossibleAdmob; +import com.yello.server.domain.event.exception.EventBadRequestException; +import com.yello.server.domain.event.exception.EventNotFoundException; +import com.yello.server.domain.event.service.EventService; +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.http.HttpStatus; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class EventController { + + private final EventService eventService; + + @GetMapping("/v1/event") + public BaseResponse> getEvents(@AccessTokenUser User user) + throws JsonProcessingException { + val data = eventService.getEvents(user.getId()); + return BaseResponse.success(EVENT_NOTICE_SUCCESS, data); + } + + @PostMapping("/v1/event") + public BaseResponse joinEvent(@AccessTokenUser User user, HttpServletRequest requestServlet, + @RequestBody EventJoinRequest request) { + UUID uuidIdempotencyKey = + UuidFactory.checkUuid(requestServlet.getHeader(IdempotencyKeyHeader)); + eventService.joinEvent(user.getId(), uuidIdempotencyKey, request); + return BaseResponse.success(EVENT_JOIN_SUCCESS); + } + + @PostMapping("/v1/event/reward") + public BaseResponse rewardEvent(@AccessTokenUser User user, + HttpServletRequest requestServlet) throws JsonProcessingException { + UUID uuidIdempotencyKey = + UuidFactory.checkUuid(requestServlet.getHeader(IdempotencyKeyHeader)); + val data = eventService.rewardEvent(user.getId(), uuidIdempotencyKey); + return BaseResponse.success(EVENT_REWARD_SUCCESS, data); + } + + @GetMapping("/v1/admob/verify") + public ResponseEntity verifyAdmob(HttpServletRequest request) { + URI uri; + System.out.println(request.getQueryString() + " alalalalalal"); + if (request.getQueryString()==null) { + throw new EventNotFoundException(EMPTY_QUERY_STRING_EXCEPTION); + } + + try { + 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); + } + System.out.println("URIIIII : " + uri); + eventService.verifyAdmobReward(uri, request); + + 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); + } + + @GetMapping("/v1/admob/possible/{tag}") + public BaseResponse getIsPossibleAdmob(@AccessTokenUser User user, + @PathVariable("tag") String tag) { + val data = eventService.getIsPossibleAdmob(user.getId(), tag); + return BaseResponse.success(GET_IS_POSSIBLE_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/dto/request/EventJoinRequest.java b/src/main/java/com/yello/server/domain/event/dto/request/EventJoinRequest.java new file mode 100644 index 00000000..446a71de --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/dto/request/EventJoinRequest.java @@ -0,0 +1,10 @@ +package com.yello.server.domain.event.dto.request; + +import lombok.Builder; + +@Builder +public record EventJoinRequest( + String tag +) { + +} diff --git a/src/main/java/com/yello/server/domain/event/dto/response/EventResponse.java b/src/main/java/com/yello/server/domain/event/dto/response/EventResponse.java new file mode 100644 index 00000000..cababeb6 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/dto/response/EventResponse.java @@ -0,0 +1,95 @@ +package com.yello.server.domain.event.dto.response; + +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yello.server.domain.event.entity.Event; +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 jakarta.annotation.Nullable; +import java.time.OffsetTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record EventResponse( + EventType tag, + String startDate, + String endDate, + String title, + String subTitle, + List animationList, + @Nullable EventResponse.EventRewardVO eventReward +) { + + public static EventResponse of(Event event, @Nullable EventTime eventTime, + @Nullable List eventRewardMappingList) + throws JsonProcessingException { + // animation + ObjectMapper objectMapper = new ObjectMapper(); + final List animationList = objectMapper.readValue(event.getAnimation(), + new TypeReference>() { + }); + + EventResponse.EventRewardVO eventRewardVO = eventTime == null || eventRewardMappingList == null ? null + : EventResponse.EventRewardVO.of(eventTime, eventRewardMappingList); + + return EventResponse.builder() + .tag(event.getTag()) + .startDate(event.getStartDate().format(ISO_OFFSET_DATE_TIME)) + .endDate(event.getEndDate().format(ISO_OFFSET_DATE_TIME)) + .title(event.getTitle()) + .subTitle(event.getSubTitle()) + .animationList(animationList) + .eventReward(eventRewardVO) + .build(); + } + + @Builder + public record EventRewardVO( + OffsetTime startTime, + OffsetTime endTime, + Long rewardCount, + List eventRewardItem + ) { + + public static EventRewardVO of(EventTime eventTime, List eventRewardMappingList) { + List eventRewardItemList = eventRewardMappingList.stream() + .map(EventRewardItemVO::of).toList(); + + return EventRewardVO.builder() + .startTime(eventTime.getStartTime()) + .endTime(eventTime.getEndTime()) + .rewardCount(eventTime.getRewardCount()) + .eventRewardItem(eventRewardItemList) + .build(); + } + } + + @Builder + public record EventRewardItemVO( + String tag, + String eventRewardTitle, + String eventRewardImage, + Long maxRewardValue, + Long minRewardValue, + Integer eventRewardProbability, + String randomTag + ) { + + public static EventRewardItemVO of(EventRewardMapping eventRewardMapping) { + return EventRewardItemVO.builder() + .tag(eventRewardMapping.getEventReward().getTag()) + .eventRewardTitle(eventRewardMapping.getEventReward().getTitle()) + .eventRewardImage(eventRewardMapping.getEventReward().getImage()) + .maxRewardValue(eventRewardMapping.getEventReward().getMaxRewardValue()) + .minRewardValue(eventRewardMapping.getEventReward().getMinRewardValue()) + .eventRewardProbability(eventRewardMapping.getEventRewardProbability()) + .randomTag(eventRewardMapping.getEventRandom().getRandomTag()) + .build(); + } + } +} diff --git a/src/main/java/com/yello/server/domain/event/dto/response/EventRewardResponse.java b/src/main/java/com/yello/server/domain/event/dto/response/EventRewardResponse.java new file mode 100644 index 00000000..f7ea8670 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/dto/response/EventRewardResponse.java @@ -0,0 +1,22 @@ +package com.yello.server.domain.event.dto.response; + +import com.yello.server.domain.event.entity.EventInstanceReward; +import lombok.Builder; + +@Builder +public record EventRewardResponse( + String rewardTag, + Long rewardValue, + String rewardTitle, + String rewardImage +) { + + public static EventRewardResponse of(EventInstanceReward eventInstanceReward) { + return EventRewardResponse.builder() + .rewardTag(eventInstanceReward.getRewardTag()) + .rewardValue(eventInstanceReward.getRewardValue()) + .rewardTitle(eventInstanceReward.getRewardTitle()) + .rewardImage(eventInstanceReward.getRewardImage()) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/event/dto/response/GetIsPossibleAdmob.java b/src/main/java/com/yello/server/domain/event/dto/response/GetIsPossibleAdmob.java new file mode 100644 index 00000000..dd5a07d4 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/dto/response/GetIsPossibleAdmob.java @@ -0,0 +1,19 @@ +package com.yello.server.domain.event.dto.response; + +import com.yello.server.domain.user.entity.UserData; +import com.yello.server.global.common.factory.TimeFactory; +import lombok.Builder; + +@Builder +public record GetIsPossibleAdmob( + String createdAt, + Boolean isPossible +) { + public static GetIsPossibleAdmob of(UserData userAdmob) { + return GetIsPossibleAdmob.builder() + .createdAt(userAdmob.getValue()) + .isPossible(userAdmob.isPossible()) + .build(); + } + +} diff --git a/src/main/java/com/yello/server/domain/event/entity/Event.java b/src/main/java/com/yello/server/domain/event/entity/Event.java new file mode 100644 index 00000000..da002ac8 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/Event.java @@ -0,0 +1,62 @@ +package com.yello.server.domain.event.entity; + +import com.yello.server.global.common.dto.AuditingTimeEntity; +import com.yello.server.global.common.entity.ZonedDateTimeConverter; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import java.time.ZonedDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + uniqueConstraints = { + @UniqueConstraint( + name = "tag_unique", + columnNames = {"tag"} + ) + } +) +public class Event extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + @Convert(converter = EventTypeConverter.class) + private EventType tag; + + @Column(nullable = false) + @Convert(converter = ZonedDateTimeConverter.class) + private ZonedDateTime startDate; + + @Column(nullable = false) + @Convert(converter = ZonedDateTimeConverter.class) + private ZonedDateTime endDate; + + @Column + private String title; + + @Column + private String subTitle; + + /** + * Lottie를 저장하기 위한 컬럼 [{ ...json1... }, { ...json2... }] + */ + @Column(columnDefinition = "MEDIUMTEXT") + private String animation; +} 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 new file mode 100644 index 00000000..92cbecce --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventHistory.java @@ -0,0 +1,51 @@ +package com.yello.server.domain.event.entity; + +import com.yello.server.domain.user.entity.User; +import com.yello.server.global.common.dto.AuditingTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventHistory extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "userId") + @OnDelete(action = OnDeleteAction.CASCADE) + private User user; + + @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 new file mode 100644 index 00000000..29f83cb3 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventInstance.java @@ -0,0 +1,66 @@ +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; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +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; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventInstance { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventHistoryId") + @OnDelete(action = OnDeleteAction.CASCADE) + private EventHistory eventHistory; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventTimeId") + @OnDelete(action = OnDeleteAction.RESTRICT) + private EventTime eventTime; + + @Column(nullable = false) + @Convert(converter = ZonedDateTimeConverter.class) + private ZonedDateTime instanceDate; + + @Column + @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 new file mode 100644 index 00000000..d707371a --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventInstanceReward.java @@ -0,0 +1,58 @@ +package com.yello.server.domain.event.entity; + +import com.yello.server.global.common.dto.AuditingTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventInstanceReward extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventInstanceId") + @OnDelete(action = OnDeleteAction.CASCADE) + private EventInstance eventInstance; + + @Column(nullable = false) + private String rewardTag; + + @Column(nullable = false) + @Builder.Default + private Long rewardValue = 0L; + + @Column + private String rewardTitle; + + @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/EventRandom.java b/src/main/java/com/yello/server/domain/event/entity/EventRandom.java new file mode 100644 index 00000000..7e021a6b --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventRandom.java @@ -0,0 +1,33 @@ +package com.yello.server.domain.event.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventRandom { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String randomTag; + + /** + * x의 정의역 [0,1] y의 정의역 [minValue, maxValue] 예시) RANDOM [{ x: 0.5, y: 80 }, { x : 1, y: 20}] FIXED [{ x: 1, y: 100}] + */ + @Column + private String probabilityPointList; +} 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 new file mode 100644 index 00000000..ecd963c9 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventReward.java @@ -0,0 +1,61 @@ +package com.yello.server.domain.event.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + uniqueConstraints = { + @UniqueConstraint( + name = "tag_unique", + columnNames = {"tag"} + ) + } +) +public class EventReward { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String tag; + + @Column + @Builder.Default + private Long maxRewardValue = 0L; + + @Column + @Builder.Default + private Long minRewardValue = 0L; + + @Column + private String title; + + @Column + private String image; + + @Column + private String rewardTitle; + + @Column + private String rewardImage; + + public void updateMinRewardValue(Long rewardValue) { + this.minRewardValue = rewardValue; + } +} diff --git a/src/main/java/com/yello/server/domain/event/entity/EventRewardMapping.java b/src/main/java/com/yello/server/domain/event/entity/EventRewardMapping.java new file mode 100644 index 00000000..cbc9922e --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventRewardMapping.java @@ -0,0 +1,47 @@ +package com.yello.server.domain.event.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventRewardMapping { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventTimeId") + @OnDelete(action = OnDeleteAction.RESTRICT) + private EventTime eventTime; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventRewardId") + @OnDelete(action = OnDeleteAction.RESTRICT) + private EventReward eventReward; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventRandomId") + @OnDelete(action = OnDeleteAction.RESTRICT) + private EventRandom eventRandom; + + @Column(columnDefinition = "int NOT NULL CHECK (event_reward_probability BETWEEN 0 and 100)") + private Integer eventRewardProbability; +} diff --git a/src/main/java/com/yello/server/domain/event/entity/EventTime.java b/src/main/java/com/yello/server/domain/event/entity/EventTime.java new file mode 100644 index 00000000..7eaf6bd1 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventTime.java @@ -0,0 +1,49 @@ +package com.yello.server.domain.event.entity; + +import com.yello.server.global.common.entity.OffsetTimeConverter; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import java.time.OffsetTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class EventTime { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "eventId") + @OnDelete(action = OnDeleteAction.RESTRICT) + private Event event; + + @Column(columnDefinition = "varchar(30) NOT NULL") + @Convert(converter = OffsetTimeConverter.class) + private OffsetTime startTime; + + @Column(columnDefinition = "varchar(30) NOT NULL") + @Convert(converter = OffsetTimeConverter.class) + private OffsetTime endTime; + + @Column + @Builder.Default + private Long rewardCount = 1L; +} diff --git a/src/main/java/com/yello/server/domain/event/entity/EventType.java b/src/main/java/com/yello/server/domain/event/entity/EventType.java new file mode 100644 index 00000000..e4ce8750 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventType.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 EventType { + ADMOB_POINT("ADMOB_POINT"), + ADMOB_MULTIPLE_POINT("ADMOB_MULTIPLE_POINT"), + LUNCH_EVENT("LUNCH_EVENT"); + + private final String initial; + + public static EventType fromCode(String dbData) { + return Arrays.stream(EventType.values()) + .filter(v -> v.getInitial().equals(dbData)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static EventType fromName(String name) { + return Arrays.stream(EventType.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/EventTypeConverter.java b/src/main/java/com/yello/server/domain/event/entity/EventTypeConverter.java new file mode 100644 index 00000000..33bf2d3d --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/entity/EventTypeConverter.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 EventTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(EventType type) { + if (type == null) { + return null; + } + return type.name(); + } + + @Override + public EventType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return EventType.fromName(dbData); + } +} 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/EventBadRequestException.java b/src/main/java/com/yello/server/domain/event/exception/EventBadRequestException.java new file mode 100644 index 00000000..caf16dd6 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/exception/EventBadRequestException.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 EventBadRequestException extends CustomException { + + public EventBadRequestException(ErrorCode error) { + super(error, "[EventBadRequestException] " + error.getMessage()); + } +} 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/exception/EventNotFoundException.java b/src/main/java/com/yello/server/domain/event/exception/EventNotFoundException.java new file mode 100644 index 00000000..62632116 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/exception/EventNotFoundException.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 EventNotFoundException extends CustomException { + + public EventNotFoundException(ErrorCode error) { + super(error, "[EventNotFoundException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventHistoryJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventHistoryJpaRepository.java new file mode 100644 index 00000000..f3e2369e --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventHistoryJpaRepository.java @@ -0,0 +1,14 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventHistory; +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface EventHistoryJpaRepository extends JpaRepository { + + @Query("select e from EventHistory e where e.idempotencyKey = ?1") + Optional findTopByIdempotencyKey(UUID idempotencyKey); + +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventInstanceJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventInstanceJpaRepository.java new file mode 100644 index 00000000..93947f3b --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventInstanceJpaRepository.java @@ -0,0 +1,19 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventHistory; +import com.yello.server.domain.event.entity.EventInstance; +import com.yello.server.domain.event.entity.EventTime; +import com.yello.server.domain.user.entity.User; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface EventInstanceJpaRepository extends JpaRepository { + + @Query("select e from EventInstance e where e.eventHistory = ?1") + Optional findTopByEventHistory(EventHistory eventHistory); + + @Query("select e from EventInstance e, EventHistory eh where e.eventHistory = eh and e.eventTime = ?1 and eh.user = ?2") + List findAllByEventTimeAndUser(EventTime eventTime, User user); +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventInstanceRewardJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventInstanceRewardJpaRepository.java new file mode 100644 index 00000000..155ac578 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventInstanceRewardJpaRepository.java @@ -0,0 +1,8 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventInstanceReward; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EventInstanceRewardJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventJpaRepository.java new file mode 100644 index 00000000..97eec741 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventJpaRepository.java @@ -0,0 +1,11 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.Event; +import com.yello.server.domain.event.entity.EventType; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EventJpaRepository extends JpaRepository { + + Optional findTopByTag(EventType type); +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventRandomJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventRandomJpaRepository.java new file mode 100644 index 00000000..85c82443 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventRandomJpaRepository.java @@ -0,0 +1,10 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventRandom; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EventRandomJpaRepository extends JpaRepository { + + Optional findTopByRandomTag(String randomTag); +} 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 new file mode 100644 index 00000000..18177b61 --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventRepository.java @@ -0,0 +1,48 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.Event; +import com.yello.server.domain.event.entity.EventHistory; +import com.yello.server.domain.event.entity.EventInstance; +import com.yello.server.domain.event.entity.EventInstanceReward; +import com.yello.server.domain.event.entity.EventRandom; +import com.yello.server.domain.event.entity.EventReward; +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.user.entity.User; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface EventRepository { + + EventHistory save(EventHistory newEventHistory); + + EventInstance save(EventInstance newEventInstance); + + EventInstanceReward save(EventInstanceReward newEventInstanceReward); + + List findAll(); + + List findAllByEventId(Long eventId); + + List findAllByEventTimeId(Long eventTimeId); + + List findRewardAll(); + + List findInstanceAllByEventTimeAndUser(EventTime eventTime, User user); + + Event getByTag(EventType tag); + + EventReward getRewardById(Long eventRewardId); + + EventRandom getRandomByRandomTag(String randomTag); + + 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 new file mode 100644 index 00000000..af3c2d6e --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventRepositoryImpl.java @@ -0,0 +1,122 @@ +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; +import com.yello.server.domain.event.entity.EventInstanceReward; +import com.yello.server.domain.event.entity.EventRandom; +import com.yello.server.domain.event.entity.EventReward; +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.exception.EventNotFoundException; +import com.yello.server.domain.user.entity.User; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class EventRepositoryImpl implements EventRepository { + + private final EventHistoryJpaRepository eventHistoryJpaRepository; + private final EventInstanceJpaRepository eventInstanceJpaRepository; + private final EventInstanceRewardJpaRepository eventInstanceRewardJpaRepository; + private final EventJpaRepository eventJpaRepository; + private final EventRandomJpaRepository eventRandomJpaRepository; + private final EventRewardJpaRepository eventRewardJpaRepository; + private final EventRewardMappingJpaRepository eventRewardMappingJpaRepository; + private final EventTimeJpaRepository eventTimeJpaRepository; + private final JPAQueryFactory jpaQueryFactory; + + @Override + public EventHistory save(EventHistory newEventHistory) { + return eventHistoryJpaRepository.save(newEventHistory); + } + + @Override + public EventInstance save(EventInstance newEventInstance) { + return eventInstanceJpaRepository.save(newEventInstance); + } + + @Override + public EventInstanceReward save(EventInstanceReward newEventInstanceReward) { + return eventInstanceRewardJpaRepository.save(newEventInstanceReward); + } + + @Override + public List findAll() { + return eventJpaRepository.findAll(); + } + + @Override + public List findAllByEventId(Long eventId) { + return eventTimeJpaRepository.findAllByEventId(eventId); + } + + @Override + public List findAllByEventTimeId(Long eventTimeId) { + return eventRewardMappingJpaRepository.findAllByEventTimeId(eventTimeId); + } + + @Override + public List findRewardAll() { + return eventRewardJpaRepository.findAll(); + } + + @Override + public List findInstanceAllByEventTimeAndUser(EventTime eventTime, User user) { + return eventInstanceJpaRepository.findAllByEventTimeAndUser(eventTime, user); + } + + @Override + public Event getByTag(EventType tag) { + return eventJpaRepository.findTopByTag(tag) + .orElseThrow(() -> new EventNotFoundException(EVENT_NOT_FOUND_EXCEPTION)); + } + + @Override + public EventReward getRewardById(Long eventRewardId) { + return eventRewardJpaRepository.findById(eventRewardId) + .orElseThrow(() -> new EventNotFoundException(EVENT_NOT_FOUND_EXCEPTION)); + } + + @Override + public EventRandom getRandomByRandomTag(String randomTag) { + return eventRandomJpaRepository.findTopByRandomTag(randomTag) + .orElseThrow(() -> new EventNotFoundException(EVENT_RANDOM_NOT_FOUND_EXCEPTION)); + } + + @Override + public Optional findHistoryByIdempotencyKey(UUID idempotencyKey) { + return eventHistoryJpaRepository.findTopByIdempotencyKey(idempotencyKey); + } + + @Override + 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/repository/EventRewardJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventRewardJpaRepository.java new file mode 100644 index 00000000..15b4a21f --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventRewardJpaRepository.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventReward; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface EventRewardJpaRepository extends JpaRepository { + + @Query("select e from EventReward e where e.tag = ?1") + Optional findByTag(String tag); + +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventRewardMappingJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventRewardMappingJpaRepository.java new file mode 100644 index 00000000..753f755d --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventRewardMappingJpaRepository.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventRewardMapping; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface EventRewardMappingJpaRepository extends JpaRepository { + + @Query("select e from EventRewardMapping e where e.eventTime.id = ?1") + List findAllByEventTimeId(Long eventTimeId); + +} diff --git a/src/main/java/com/yello/server/domain/event/repository/EventTimeJpaRepository.java b/src/main/java/com/yello/server/domain/event/repository/EventTimeJpaRepository.java new file mode 100644 index 00000000..2d56ee7f --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/repository/EventTimeJpaRepository.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.event.repository; + +import com.yello.server.domain.event.entity.EventTime; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface EventTimeJpaRepository extends JpaRepository { + + @Query("select e from EventTime e where e.event.id = ?1") + List findAllByEventId(Long eventId); + +} 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 new file mode 100644 index 00000000..09ac1eeb --- /dev/null +++ b/src/main/java/com/yello/server/domain/event/service/EventService.java @@ -0,0 +1,414 @@ +package com.yello.server.domain.event.service; + +import static com.yello.server.domain.user.entity.UserDataType.fromCode; +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; +import static com.yello.server.global.common.ErrorCode.IDEMPOTENCY_KEY_CONFLICT_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.IDEMPOTENCY_KEY_NOT_FOUND_EXCEPTION; +import static com.yello.server.global.common.factory.TimeFactory.minusTime; +import static com.yello.server.global.common.factory.TimeFactory.toDateFormattedString; +import static com.yello.server.global.common.util.ConstantUtil.ADMOB_SHOP_TIME; +import static com.yello.server.global.common.util.ConstantUtil.GlobalZoneId; + +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; +import com.yello.server.domain.event.dto.response.GetIsPossibleAdmob; +import com.yello.server.domain.event.entity.Event; +import com.yello.server.domain.event.entity.EventHistory; +import com.yello.server.domain.event.entity.EventInstance; +import com.yello.server.domain.event.entity.EventInstanceReward; +import com.yello.server.domain.event.entity.EventRandom; +import com.yello.server.domain.event.entity.EventReward; +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.entity.UserData; +import com.yello.server.domain.user.repository.UserDataRepository; +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.LocalDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class EventService { + + private final EventRepository eventRepository; + private final ObjectMapper objectMapper; + private final UserRepository userRepository; + private final UserDataRepository userDataRepository; + + public List getEvents(Long userId) throws JsonProcessingException { + // exception + final User user = userRepository.getById(userId); + + // logic + ZonedDateTime now = ZonedDateTime.now(GlobalZoneId); + OffsetTime nowTime = now.toOffsetDateTime().toOffsetTime(); + List result = new ArrayList<>(); + + // 현재 날짜에 유효한 이벤트. + final List eventList = eventRepository.findAll().stream() + .filter(event -> now.isAfter(event.getStartDate()) && now.isBefore(event.getEndDate())) + .toList(); + + for (Event event : eventList) { + // 현재 시각에 유효한 이벤트 시간대 + 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()) { + final EventTime eventTime = eventTimeList.get(0); + + // 현재 시각이 이벤트 시간에 유효하고, 남은 보상 카운트가 0인 이력 + eventInstanceList.addAll( + eventRepository.findInstanceAllByEventTimeAndUser( + eventTime, user) + .stream() + .filter(eventInstance -> + eventInstance.getInstanceDate().isAfter(event.getStartDate()) + && eventInstance.getInstanceDate().isBefore(event.getEndDate()) + && nowTime.isAfter(eventInstance.getEventTime().getStartTime()) + && nowTime.isBefore(eventInstance.getEventTime().getEndTime()) + && eventInstance.getRemainEventCount()==0 + ) + .toList() + ); + } + + // 해당 변수로 클라가 이벤트를 띄울지 판단하게 됨. + // 이벤트 시간대가 있고, 보상 카운트가 0인 참여이력의 숫자가 RewardCount 보다 적을 때 + // k번 이벤트 참여는 보상 카운트로 구현한다. + boolean isEventAvailable = + !eventTimeList.isEmpty() && (eventTimeList.get(0).getRewardCount() + > eventInstanceList.size()); + + final EventTime eventTime = isEventAvailable ? eventTimeList.get(0) : null; + final List eventRewardMappingList = + isEventAvailable ? eventRepository.findAllByEventTimeId(eventTime.getId()) : null; + + result.add(EventResponse.of(event, eventTime, eventRewardMappingList)); + } + + return result; + } + + @Transactional + public void joinEvent(Long userId, UUID uuidIdempotencyKey, EventJoinRequest request) { + // exception + final User user = userRepository.getById(userId); + final Optional eventHistory = + eventRepository.findHistoryByIdempotencyKey(uuidIdempotencyKey); + if (eventHistory.isPresent()) { + throw new EventBadRequestException(IDEMPOTENCY_KEY_CONFLICT_EXCEPTION); + } + + // logic + ZonedDateTime now = ZonedDateTime.now(GlobalZoneId); + OffsetTime nowTime = now.toOffsetDateTime().toOffsetTime(); + final Event event = eventRepository.getByTag(EventType.fromCode(request.tag())); + + // 이벤트 날짜에 유효해야함. + if (!(now.isAfter(event.getStartDate()) && now.isBefore(event.getEndDate()))) { + throw new EventBadRequestException(EVENT_DATE_BAD_REQUEST_EXCEPTION); + } + + // 이벤트 시간대에 유효해야함. + 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); + } + EventTime eventTime = eventTimeList.get(0); + + final EventHistory newEventHistory = eventRepository.save(EventHistory.builder() + .idempotencyKey(uuidIdempotencyKey) + .user(user) + .build()); + + eventRepository.save(EventInstance.builder() + .eventHistory(newEventHistory) + .eventTime(eventTime) + .instanceDate(now) + .remainEventCount(1L) + .build()); + } + + @Transactional + public EventRewardResponse rewardEvent(Long userId, UUID uuidIdempotencyKey) + throws JsonProcessingException { + // exception + final User user = userRepository.getById(userId); + 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()); + ZonedDateTime now = ZonedDateTime.now(GlobalZoneId); + OffsetTime nowTime = now.toOffsetDateTime().toOffsetTime(); + + // 이벤트 참여 요청은 1일 동안만 유효하다. + final Duration duration = Duration.between(now, eventInstance.get().getInstanceDate()); + if (duration.compareTo(Duration.ofDays(1L)) >= 1) { + throw new EventBadRequestException(EVENT_DATE_BAD_REQUEST_EXCEPTION); + } + + // 이벤트 시각이 유효해야한다. + if (!(nowTime.isAfter(eventInstance.get().getEventTime().getStartTime()) + && nowTime.isBefore(eventInstance.get().getEventTime().getEndTime()))) { + throw new EventBadRequestException(EVENT_TIME_BAD_REQUEST_EXCEPTION); + } + + if (eventInstance.get().getRemainEventCount() <= 0L) { + throw new EventBadRequestException(EVENT_COUNT_BAD_REQUEST_EXCEPTION); + } + + 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()); + + /** + * TODO 그냥 문자열로 저장하고 있는데, 어떻게 해야 enum의 문제를 극복하면서 switch와 함꼐 사용할 수 있을지 고민해야함. + */ + if (randomRewardMapping.getEventReward().getTag().equals("TICKET")) { + user.addTicketCount((int) randomValue); + } else if (randomRewardMapping.getEventReward().getTag().equals("POINT")) { + user.addPointBySubscribe((int) randomValue); + } + eventInstance.get().subRemainEventCount(1L); + return EventRewardResponse.of(eventInstanceReward); + } + + @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)); + + // user-data cooldown 추가 + UserData userAdmob = + userDataRepository.findByUserIdAndTag(userId, fromCode(request.rewardType())) + .orElseGet(() -> userDataRepository.save(UserData.of(fromCode(request.rewardType()), + toDateFormattedString(LocalDateTime.now()), user))); + + userAdmob.setValue(toDateFormattedString(LocalDateTime.now())); + + return EventRewardResponse.of(rewardInstance); + } + + public GetIsPossibleAdmob getIsPossibleAdmob(Long userId, String tag) { + final User user = userRepository.getById(userId); + UserData userAdmob = userDataRepository.findByUserIdAndTag(userId, fromCode(tag)) + .orElse(UserData.of(fromCode(tag), + toDateFormattedString(minusTime(LocalDateTime.now(), ADMOB_SHOP_TIME)), user)); + return GetIsPossibleAdmob.of(userAdmob); + } + + 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) + .sum(); + + // 0에서 totalProbability 사이의 무작위 정수를 생성합니다. + int randomValue = new Random().nextInt(totalProbability); + + // 누적 확률을 계산하며 무작위 값이 어느 구간에 속하는지 찾습니다. + int cumulativeProbability = 0; + for (EventRewardMapping eventRewardMapping : eventRewardMappingList) { + cumulativeProbability += eventRewardMapping.getEventRewardProbability(); + if (randomValue < cumulativeProbability) { + return eventRewardMapping; + } + } + + return eventRewardMappingList.get(0); + } + + private long selectRandomValue(@NotNull EventRewardMapping eventRewardMapping) + throws JsonProcessingException { + EventReward eventReward = eventRewardMapping.getEventReward(); + EventRandom eventRandom = eventRewardMapping.getEventRandom(); + + List probabilityPoints = + objectMapper.readValue(eventRandom.getProbabilityPointList(), + new TypeReference>() { + } + ); + + return calculateRewardValue(probabilityPoints, eventReward.getMinRewardValue(), + eventReward.getMaxRewardValue()); + } + + private long calculateRewardValue(List points, long minRewardValue, + long maxRewardValue) { + // 무작위 x 값을 생성합니다. + double x = new Random().nextDouble(); + + // x 값이 속하는 구간을 찾습니다. + 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, + maxRewardValue); + } + } + + // x 값이 마지막 구간에 속하는 경우 + if (x >= points.get(points.size() - 1).getX()) { + return randomRewardValue(points.get(points.size() - 1).getY(), 1.0, minRewardValue, + maxRewardValue); + } + + // 이 부분은 도달할 수 없지만 혹시 모를 오류를 대비해 예외를 던집니다. + throw new IllegalArgumentException("Could not find a matching range for x: " + x); + } + + private long randomRewardValue(double y1, double y2, long minRewardValue, long maxRewardValue) { + // y1과 y2 사이의 무작위 y 값을 생성합니다. + double y = y1 + new Random().nextDouble() * (y2 - y1); + + // 보상값을 계산합니다. + return (long) ((maxRewardValue - minRewardValue) * y + minRewardValue); + } + + @Getter + private static class ProbabilityPoint { + + private double x; + private double y; + } +} diff --git a/src/main/java/com/yello/server/domain/friend/controller/FriendController.java b/src/main/java/com/yello/server/domain/friend/controller/FriendController.java index 85838714..95d3ddcc 100644 --- a/src/main/java/com/yello/server/domain/friend/controller/FriendController.java +++ b/src/main/java/com/yello/server/domain/friend/controller/FriendController.java @@ -18,8 +18,8 @@ import com.yello.server.global.common.annotation.AccessTokenUser; import com.yello.server.global.common.dto.BaseResponse; import com.yello.server.infrastructure.firebase.service.NotificationService; +import jakarta.validation.Valid; import java.util.List; -import javax.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.val; import org.springframework.web.bind.annotation.DeleteMapping; diff --git a/src/main/java/com/yello/server/domain/friend/entity/Friend.java b/src/main/java/com/yello/server/domain/friend/entity/Friend.java index 51a6b826..75698d95 100644 --- a/src/main/java/com/yello/server/domain/friend/entity/Friend.java +++ b/src/main/java/com/yello/server/domain/friend/entity/Friend.java @@ -2,22 +2,24 @@ import com.yello.server.domain.user.entity.User; import com.yello.server.global.common.dto.AuditingTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import java.time.LocalDateTime; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.Where; @Getter @@ -42,10 +44,12 @@ public class Friend extends AuditingTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(nullable = false, name = "user") + @OnDelete(action = OnDeleteAction.CASCADE) private User user; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(nullable = false, name = "target") + @OnDelete(action = OnDeleteAction.CASCADE) private User target; @Column diff --git a/src/main/java/com/yello/server/domain/friend/service/FriendService.java b/src/main/java/com/yello/server/domain/friend/service/FriendService.java index 9f907c75..db988fec 100644 --- a/src/main/java/com/yello/server/domain/friend/service/FriendService.java +++ b/src/main/java/com/yello/server/domain/friend/service/FriendService.java @@ -1,16 +1,7 @@ package com.yello.server.domain.friend.service; -import static com.yello.server.global.common.ErrorCode.EXIST_FRIEND_EXCEPTION; -import static com.yello.server.global.common.util.ConstantUtil.YELLO_FEMALE; -import static com.yello.server.global.common.util.ConstantUtil.YELLO_MALE; - import com.yello.server.domain.friend.dto.request.KakaoRecommendRequest; -import com.yello.server.domain.friend.dto.response.FriendResponse; -import com.yello.server.domain.friend.dto.response.FriendShuffleResponse; -import com.yello.server.domain.friend.dto.response.FriendsResponse; -import com.yello.server.domain.friend.dto.response.RecommendFriendResponse; -import com.yello.server.domain.friend.dto.response.SearchFriendResponse; -import com.yello.server.domain.friend.dto.response.SearchFriendVO; +import com.yello.server.domain.friend.dto.response.*; import com.yello.server.domain.friend.entity.Friend; import com.yello.server.domain.friend.exception.FriendException; import com.yello.server.domain.friend.repository.FriendRepository; @@ -20,12 +11,6 @@ import com.yello.server.domain.vote.repository.VoteRepository; import com.yello.server.domain.vote.service.VoteManager; import com.yello.server.global.common.factory.PaginationFactory; -import java.lang.Character.UnicodeBlock; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -33,6 +18,13 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.lang.Character.UnicodeBlock; +import java.util.*; + +import static com.yello.server.global.common.ErrorCode.EXIST_FRIEND_EXCEPTION; +import static com.yello.server.global.common.util.ConstantUtil.YELLO_FEMALE; +import static com.yello.server.global.common.util.ConstantUtil.YELLO_MALE; + @Service @Builder @RequiredArgsConstructor @@ -47,13 +39,13 @@ public class FriendService { public FriendsResponse findAllFriends(Pageable pageable, Long userId) { final Page friendsData = friendRepository.findAllFriendsByUserId(pageable, userId); List friends = friendsData.stream() - .map(friend -> { - User targetUser = friend.getTarget(); - Integer friendCount = friendRepository.countAllByUserId(targetUser.getId()); - Integer yelloCount = voteRepository.countAllByReceiverUserId(targetUser.getId()); - return UserResponse.of(targetUser, yelloCount, friendCount); - }) - .toList(); + .map(friend -> { + User targetUser = friend.getTarget(); + Integer friendCount = friendRepository.countAllByUserId(targetUser.getId()); + Integer yelloCount = voteRepository.countAllByReceiverUserId(targetUser.getId()); + return UserResponse.of(targetUser, yelloCount, friendCount); + }) + .toList(); return FriendsResponse.of(friendsData.getTotalElements(), friends); } @@ -85,10 +77,10 @@ public RecommendFriendResponse findAllRecommendSchoolFriends(Pageable pageable, Integer size = userRepository.countAllByGroupNameFilteredByNotFriend(userId, user.getGroup().getGroupName()); List recommendFriends = - userRepository.findAllByGroupNameFilteredByNotFriend(userId, user.getGroup().getGroupName(), pageable) - .stream() - .map(FriendResponse::of) - .toList(); + userRepository.findAllByGroupNameFilteredByNotFriend(userId, user.getGroup().getGroupName(), pageable) + .stream() + .map(FriendResponse::of) + .toList(); return RecommendFriendResponse.of(size, recommendFriends); } @@ -106,25 +98,25 @@ public void deleteFriend(Long userId, Long targetId) { } public RecommendFriendResponse findAllRecommendKakaoFriends(Pageable pageable, Long userId, - KakaoRecommendRequest request) { + KakaoRecommendRequest request) { final User user = userRepository.getById(userId); List kakaoFriends = Arrays.stream(request.friendKakaoId()) - .filter(userRepository::existsByUuid) - .map(userRepository::getByUuid) - .filter(friend -> !friendRepository.existsByUserAndTarget(user.getId(), friend.getId())) - .toList(); + .filter(userRepository::existsByUuid) + .map(userRepository::getByUuid) + .filter(friend -> !friendRepository.existsByUserAndTarget(user.getId(), friend.getId())) + .toList(); List pageList = PaginationFactory.getPage(kakaoFriends, pageable) - .stream() - .map(FriendResponse::of) - .toList(); + .stream() + .map(FriendResponse::of) + .toList(); return RecommendFriendResponse.of(kakaoFriends.size(), pageList); } public SearchFriendResponse searchFriend(Long userId, Pageable pageable, - String keyword) { + String keyword) { final User user = userRepository.getById(userId); final String groupName = user.getGroup().getGroupName(); List uuidList = Arrays.asList(YELLO_FEMALE, YELLO_MALE); @@ -137,23 +129,23 @@ public SearchFriendResponse searchFriend(Long userId, Pageable pageable, if (!isEnglish(keyword)) { friendList.addAll( - userRepository.findAllByGroupContainingName(groupName, keyword, uuidList)); + userRepository.findAllByGroupContainingName(groupName, keyword, uuidList)); friendList.addAll( - userRepository.findAllByOtherGroupContainingName(groupName, keyword, uuidList)); - + userRepository.findAllByOtherGroupContainingName(groupName, keyword, uuidList)); + friendList.addAll(userRepository.findAllByGroupNameContainingAndFriendListNotContaining(keyword, uuidList, friendList)); } else { friendList.addAll( - userRepository.findAllByGroupContainingYelloId(groupName, keyword, uuidList)); + userRepository.findAllByGroupContainingYelloId(groupName, keyword, uuidList)); friendList.addAll( - userRepository.findAllByOtherGroupContainingYelloId(groupName, keyword, uuidList)); + userRepository.findAllByOtherGroupContainingYelloId(groupName, keyword, uuidList)); } List pageList = PaginationFactory.getPage(friendList, pageable) - .stream() - .filter(friend -> !userId.equals(friend.getId())) - .map(friend -> SearchFriendVO.of(friend, - friendRepository.existsByUserAndTarget(userId, friend.getId()))) - .toList(); + .stream() + .filter(friend -> !userId.equals(friend.getId())) + .map(friend -> SearchFriendVO.of(friend, + friendRepository.existsByUserAndTarget(userId, friend.getId()))) + .toList(); return SearchFriendResponse.of(friendList.size(), pageList); } diff --git a/src/main/java/com/yello/server/domain/group/entity/UserGroup.java b/src/main/java/com/yello/server/domain/group/entity/UserGroup.java index e795209b..cc572a5f 100644 --- a/src/main/java/com/yello/server/domain/group/entity/UserGroup.java +++ b/src/main/java/com/yello/server/domain/group/entity/UserGroup.java @@ -1,14 +1,13 @@ package com.yello.server.domain.group.entity; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Index; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -22,9 +21,6 @@ @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table( - indexes = { - @Index(name = "idx__group__name", columnList = "groupName") - }, uniqueConstraints = { @UniqueConstraint( name = "user_group__sub_group_name__unique", diff --git a/src/main/java/com/yello/server/domain/group/entity/UserGroupData.java b/src/main/java/com/yello/server/domain/group/entity/UserGroupData.java index a025ffae..ac4df791 100644 --- a/src/main/java/com/yello/server/domain/group/entity/UserGroupData.java +++ b/src/main/java/com/yello/server/domain/group/entity/UserGroupData.java @@ -1,19 +1,21 @@ package com.yello.server.domain.group.entity; import com.yello.server.global.common.dto.AuditingTimeEntity; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Getter @Entity @@ -28,6 +30,7 @@ public class UserGroupData extends AuditingTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "groupName", referencedColumnName = "groupName") + @OnDelete(action = OnDeleteAction.RESTRICT) private UserGroup group; @Convert(converter = UserGroupDataTagConterver.class) diff --git a/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTag.java b/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTag.java index ddb3a8b8..239eddf1 100644 --- a/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTag.java +++ b/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTag.java @@ -1,6 +1,8 @@ package com.yello.server.domain.group.entity; -import java.text.MessageFormat; +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; @@ -11,17 +13,19 @@ public enum UserGroupDataTag { ADDRESS("ADDRESS"), POPULATION("POPULATION"); - private final String intial; + private final String initial; public static UserGroupDataTag fromCode(String dbData) { return Arrays.stream(UserGroupDataTag.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 그룹 정보 key 타입 입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static UserGroupDataTag fromName(String name) { + return Arrays.stream(UserGroupDataTag.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/group/entity/UserGroupDataTagConterver.java b/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTagConterver.java index ed3239eb..0086d0c9 100644 --- a/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTagConterver.java +++ b/src/main/java/com/yello/server/domain/group/entity/UserGroupDataTagConterver.java @@ -1,7 +1,7 @@ package com.yello.server.domain.group.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(UserGroupDataTag userGroupType) { if (userGroupType == null) { return null; } - return userGroupType.getIntial(); + return userGroupType.name(); } @Override @@ -21,11 +21,7 @@ public UserGroupDataTag convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return UserGroupDataTag.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return UserGroupDataTag.fromName(dbData); } } diff --git a/src/main/java/com/yello/server/domain/group/entity/UserGroupType.java b/src/main/java/com/yello/server/domain/group/entity/UserGroupType.java index 6bb26723..0b782406 100644 --- a/src/main/java/com/yello/server/domain/group/entity/UserGroupType.java +++ b/src/main/java/com/yello/server/domain/group/entity/UserGroupType.java @@ -1,6 +1,8 @@ package com.yello.server.domain.group.entity; -import java.text.MessageFormat; +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; @@ -13,18 +15,19 @@ public enum UserGroupType { MIDDLE_SCHOOL("MIDDLE_SCHOOL"), SOPT("SOPT"); - private final String intial; + private final String initial; public static UserGroupType fromCode(String dbData) { return Arrays.stream(UserGroupType.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 그룹 타입 입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static UserGroupType fromName(String name) { + return Arrays.stream(UserGroupType.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/group/entity/UserGroupTypeConverter.java b/src/main/java/com/yello/server/domain/group/entity/UserGroupTypeConverter.java index 09527fc0..76a24212 100644 --- a/src/main/java/com/yello/server/domain/group/entity/UserGroupTypeConverter.java +++ b/src/main/java/com/yello/server/domain/group/entity/UserGroupTypeConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.group.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(UserGroupType userGroupType) { if (userGroupType == null) { return null; } - return userGroupType.getIntial(); + return userGroupType.name(); } @Override @@ -21,11 +21,7 @@ public UserGroupType convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return UserGroupType.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return UserGroupType.fromName(dbData); } } diff --git a/src/main/java/com/yello/server/domain/group/repository/UserGroupJpaRepository.java b/src/main/java/com/yello/server/domain/group/repository/UserGroupJpaRepository.java index 1c154d99..5a6c901c 100644 --- a/src/main/java/com/yello/server/domain/group/repository/UserGroupJpaRepository.java +++ b/src/main/java/com/yello/server/domain/group/repository/UserGroupJpaRepository.java @@ -11,6 +11,8 @@ public interface UserGroupJpaRepository extends JpaRepository { + List findAllByGroupName(String groupName); + @Query("select count (distinct(s.groupName)) from UserGroup s " + "where s.groupName " + "like CONCAT('%',:groupName,'%' )" + diff --git a/src/main/java/com/yello/server/domain/group/repository/UserGroupRepository.java b/src/main/java/com/yello/server/domain/group/repository/UserGroupRepository.java index f5d4f504..65bfaba3 100644 --- a/src/main/java/com/yello/server/domain/group/repository/UserGroupRepository.java +++ b/src/main/java/com/yello/server/domain/group/repository/UserGroupRepository.java @@ -14,6 +14,8 @@ public interface UserGroupRepository { Optional findById(Long id); + List findAllByGroupName(String groupName); + Integer countDistinctGroupNameContaining(String groupName, UserGroupType userGroupType); List findDistinctGroupNameContaining(String groupName, UserGroupType userGroupType, Pageable pageable); diff --git a/src/main/java/com/yello/server/domain/group/repository/UserGroupRepositoryImpl.java b/src/main/java/com/yello/server/domain/group/repository/UserGroupRepositoryImpl.java index f1612d82..d94c1f0c 100644 --- a/src/main/java/com/yello/server/domain/group/repository/UserGroupRepositoryImpl.java +++ b/src/main/java/com/yello/server/domain/group/repository/UserGroupRepositoryImpl.java @@ -36,6 +36,11 @@ public Optional findById(Long id) { return userGroupJpaRepository.findById(id); } + @Override + public List findAllByGroupName(String groupName) { + return userGroupJpaRepository.findAllByGroupName(groupName); + } + @Override public Integer countDistinctGroupNameContaining(String groupName, UserGroupType userGroupType) { return userGroupJpaRepository.countDistinctGroupNameContaining(groupName, userGroupType); diff --git a/src/main/java/com/yello/server/domain/keyword/entity/Keyword.java b/src/main/java/com/yello/server/domain/keyword/entity/Keyword.java index 83941cd4..2014b69d 100644 --- a/src/main/java/com/yello/server/domain/keyword/entity/Keyword.java +++ b/src/main/java/com/yello/server/domain/keyword/entity/Keyword.java @@ -1,19 +1,21 @@ package com.yello.server.domain.keyword.entity; import com.yello.server.domain.question.entity.Question; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Getter @Entity @@ -31,6 +33,7 @@ public class Keyword { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "questionId") + @OnDelete(action = OnDeleteAction.RESTRICT) private Question question; public static Keyword of(String keywordName, Question question) { diff --git a/src/main/java/com/yello/server/domain/notice/controller/NoticeController.java b/src/main/java/com/yello/server/domain/notice/controller/NoticeController.java new file mode 100644 index 00000000..51b9be99 --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/controller/NoticeController.java @@ -0,0 +1,30 @@ +package com.yello.server.domain.notice.controller; + + +import static com.yello.server.global.common.SuccessCode.READ_NOTICE_SUCCESS; + +import com.yello.server.domain.notice.dto.NoticeDataResponse; +import com.yello.server.domain.notice.service.NoticeService; +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 lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("api/v1") +public class NoticeController { + + private final NoticeService noticeService; + + @GetMapping("/notice/{tag}") + public BaseResponse findNotice(@AccessTokenUser User user, @PathVariable String tag) { + val data = noticeService.findNotice(user.getId(), tag); + return BaseResponse.success(READ_NOTICE_SUCCESS, data); + } +} diff --git a/src/main/java/com/yello/server/domain/notice/dto/NoticeDataResponse.java b/src/main/java/com/yello/server/domain/notice/dto/NoticeDataResponse.java new file mode 100644 index 00000000..70d38374 --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/dto/NoticeDataResponse.java @@ -0,0 +1,31 @@ +package com.yello.server.domain.notice.dto; + +import static com.yello.server.global.common.factory.TimeFactory.toYearAndMonthFormattedString; + +import com.yello.server.domain.notice.entity.Notice; +import lombok.Builder; + +@Builder +public record NoticeDataResponse( + String imageUrl, + String redirectUrl, + String startDate, + String endDate, + boolean isAvailable, + String type, + String title +) { + + public static NoticeDataResponse of(Notice notice, Boolean isAvailable) { + return NoticeDataResponse.builder() + .imageUrl(notice.getImageUrl()) + .redirectUrl(notice.getRedirectUrl()) + .startDate(toYearAndMonthFormattedString(notice.getStartDate().toLocalDateTime())) + .endDate(toYearAndMonthFormattedString(notice.getEndDate().toLocalDateTime())) + .isAvailable(isAvailable) + .type(notice.getTag().getInitial()) + .title(notice.getTitle()) + .build(); + } + +} diff --git a/src/main/java/com/yello/server/domain/notice/entity/Notice.java b/src/main/java/com/yello/server/domain/notice/entity/Notice.java new file mode 100644 index 00000000..e70e27ba --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/entity/Notice.java @@ -0,0 +1,52 @@ +package com.yello.server.domain.notice.entity; + +import com.yello.server.global.common.dto.AuditingTimeEntity; +import com.yello.server.global.common.entity.ZonedDateTimeConverter; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import java.time.ZonedDateTime; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Notice extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String imageUrl; + + @Column(nullable = false) + private String redirectUrl; + + @Column(nullable = false) + @Convert(converter = ZonedDateTimeConverter.class) + private ZonedDateTime startDate; + + @Column(nullable = false) + @Convert(converter = ZonedDateTimeConverter.class) + private ZonedDateTime endDate; + + @Column(nullable = false) + private Boolean isAvailable; + + @Column(nullable = false) + @Convert(converter = NoticeTypeConverter.class) + private NoticeType tag; + + @Column(nullable = false) + private String title; +} \ No newline at end of file diff --git a/src/main/java/com/yello/server/domain/notice/entity/NoticeType.java b/src/main/java/com/yello/server/domain/notice/entity/NoticeType.java new file mode 100644 index 00000000..1634e588 --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/entity/NoticeType.java @@ -0,0 +1,32 @@ +package com.yello.server.domain.notice.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 NoticeType { + NOTICE("NOTICE"), + BANNER("BANNER"), + PROFILE_BANNER("PROFILE-BANNER"); + + private final String initial; + + public static NoticeType fromCode(String dbData) { + return Arrays.stream(NoticeType.values()) + .filter(v -> v.getInitial().equals(dbData)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static NoticeType fromName(String name) { + return Arrays.stream(NoticeType.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/notice/entity/NoticeTypeConverter.java b/src/main/java/com/yello/server/domain/notice/entity/NoticeTypeConverter.java new file mode 100644 index 00000000..169b9ea3 --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/entity/NoticeTypeConverter.java @@ -0,0 +1,27 @@ +package com.yello.server.domain.notice.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.extern.log4j.Log4j2; + +@Converter +@Log4j2 +public class NoticeTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(NoticeType type) { + if (type == null) { + return null; + } + return type.name(); + } + + @Override + public NoticeType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return NoticeType.fromName(dbData); + } +} diff --git a/src/main/java/com/yello/server/domain/notice/exception/NoticeNotFoundException.java b/src/main/java/com/yello/server/domain/notice/exception/NoticeNotFoundException.java new file mode 100644 index 00000000..e53268ac --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/exception/NoticeNotFoundException.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.notice.exception; + +import com.yello.server.global.common.ErrorCode; +import com.yello.server.global.exception.CustomException; +import lombok.Getter; + +@Getter +public class NoticeNotFoundException extends CustomException { + + public NoticeNotFoundException(ErrorCode error) { + super(error, "[NoticeNotFoundException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/domain/notice/repository/NoticeJpaRepository.java b/src/main/java/com/yello/server/domain/notice/repository/NoticeJpaRepository.java new file mode 100644 index 00000000..3652fa1a --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/repository/NoticeJpaRepository.java @@ -0,0 +1,8 @@ +package com.yello.server.domain.notice.repository; + +import com.yello.server.domain.notice.entity.Notice; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface NoticeJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/yello/server/domain/notice/repository/NoticeRepository.java b/src/main/java/com/yello/server/domain/notice/repository/NoticeRepository.java new file mode 100644 index 00000000..78db8741 --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/repository/NoticeRepository.java @@ -0,0 +1,19 @@ +package com.yello.server.domain.notice.repository; + +import com.yello.server.domain.notice.entity.Notice; +import com.yello.server.domain.notice.entity.NoticeType; +import java.util.List; +import java.util.Optional; + +public interface NoticeRepository { + + Optional findTopNotice(NoticeType tag); + + List findAll(); + + Notice getById(Long id); + + Notice save(Notice notice); + + void update(Notice newNotice); +} diff --git a/src/main/java/com/yello/server/domain/notice/repository/NoticeRepositoryImpl.java b/src/main/java/com/yello/server/domain/notice/repository/NoticeRepositoryImpl.java new file mode 100644 index 00000000..fab23ec4 --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/repository/NoticeRepositoryImpl.java @@ -0,0 +1,70 @@ +package com.yello.server.domain.notice.repository; + +import static com.yello.server.domain.notice.entity.QNotice.notice; +import static com.yello.server.global.common.ErrorCode.NOT_FOUND_NOTICE_EXCEPTION; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.yello.server.domain.notice.entity.Notice; +import com.yello.server.domain.notice.entity.NoticeType; +import com.yello.server.domain.notice.exception.NoticeNotFoundException; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NoticeRepositoryImpl implements NoticeRepository { + + private final JPAQueryFactory jpaQueryFactory; + private final NoticeJpaRepository noticeJpaRepository; + + @Override + public Optional findTopNotice(NoticeType tag) { + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime zonedDateTime = ZonedDateTime.now(zoneId); + return Optional.ofNullable(jpaQueryFactory + .selectFrom(notice) + .where(notice.isAvailable.eq(true) + .and(notice.startDate.loe(zonedDateTime)) + .and(notice.endDate.goe(zonedDateTime)) + .and(notice.tag.eq(tag))) + .orderBy(notice.endDate.desc()) + .fetchFirst()); + } + + @Override + public List findAll() { + return jpaQueryFactory.selectFrom(notice).fetch(); + } + + @Override + public Notice getById(Long id) { + return noticeJpaRepository.findById(id) + .orElseThrow(() -> new NoticeNotFoundException(NOT_FOUND_NOTICE_EXCEPTION)); + } + + @Override + public Notice save(Notice notice) { + return noticeJpaRepository.save(notice); + } + + @Override + public void update(Notice newNotice) { + jpaQueryFactory.update(notice) + .set(notice.imageUrl, newNotice.getImageUrl()) + .set(notice.redirectUrl, newNotice.getRedirectUrl()) + .set(notice.startDate, newNotice.getStartDate()) + .set(notice.endDate, newNotice.getEndDate()) + .set(notice.isAvailable, newNotice.getIsAvailable()) + .set(notice.tag, newNotice.getTag()) + .set(notice.title, newNotice.getTitle()) + .where(notice.id.eq(newNotice.getId())) + .execute(); + } + +} diff --git a/src/main/java/com/yello/server/domain/notice/service/NoticeService.java b/src/main/java/com/yello/server/domain/notice/service/NoticeService.java new file mode 100644 index 00000000..9829c94e --- /dev/null +++ b/src/main/java/com/yello/server/domain/notice/service/NoticeService.java @@ -0,0 +1,40 @@ +package com.yello.server.domain.notice.service; + +import static com.yello.server.global.common.factory.TimeFactory.compareNowAndEndData; + +import com.yello.server.domain.notice.dto.NoticeDataResponse; +import com.yello.server.domain.notice.entity.Notice; +import com.yello.server.domain.notice.entity.NoticeType; +import com.yello.server.domain.notice.repository.NoticeRepository; +import com.yello.server.domain.user.repository.UserRepository; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Builder +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class NoticeService { + + private final NoticeRepository noticeRepository; + private final UserRepository userRepository; + + public NoticeDataResponse findNotice(Long userId, String tag) { + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime now = ZonedDateTime.now(zoneId); + userRepository.findById(userId); + Notice noticeData = + noticeRepository.findTopNotice(NoticeType.fromCode(tag)).orElseGet( + () -> Notice.builder().imageUrl("").redirectUrl("").title("").tag( + NoticeType.fromCode(tag)).endDate(now) + .startDate(now).isAvailable(false).build()); + return NoticeDataResponse.of(noticeData, + compareNowAndEndData(noticeData.getEndDate()) && noticeData.getIsAvailable()); + } + + +} diff --git a/src/main/java/com/yello/server/domain/pay/entity/Pay.java b/src/main/java/com/yello/server/domain/pay/entity/Pay.java index a1d73a58..47463c7d 100644 --- a/src/main/java/com/yello/server/domain/pay/entity/Pay.java +++ b/src/main/java/com/yello/server/domain/pay/entity/Pay.java @@ -1,18 +1,20 @@ package com.yello.server.domain.pay.entity; import com.yello.server.domain.user.entity.User; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Getter @@ -29,6 +31,7 @@ public class Pay { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "userId") + @OnDelete(action = OnDeleteAction.CASCADE) private User user; public static Pay of(Integer optionIndex, User user) { diff --git a/src/main/java/com/yello/server/domain/purchase/entity/Gateway.java b/src/main/java/com/yello/server/domain/purchase/entity/Gateway.java index 59e77f88..7b1c9030 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/Gateway.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/Gateway.java @@ -1,6 +1,8 @@ package com.yello.server.domain.purchase.entity; -import java.text.MessageFormat; +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; @@ -11,18 +13,19 @@ public enum Gateway { GOOGLE("google"), APPLE("apple"); - private final String intial; + private final String initial; public static Gateway fromCode(String dbData) { return Arrays.stream(Gateway.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 게이트웨이입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static Gateway fromName(String name) { + return Arrays.stream(Gateway.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/purchase/entity/GatewayConverter.java b/src/main/java/com/yello/server/domain/purchase/entity/GatewayConverter.java index e0612836..f8dce446 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/GatewayConverter.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/GatewayConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.purchase.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(Gateway gateway) { if (gateway == null) { return null; } - return gateway.getIntial(); + return gateway.name(); } @Override @@ -21,11 +21,7 @@ public Gateway convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return Gateway.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return Gateway.fromName(dbData); } } diff --git a/src/main/java/com/yello/server/domain/purchase/entity/ProductType.java b/src/main/java/com/yello/server/domain/purchase/entity/ProductType.java index a8e9f81d..cccccf99 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/ProductType.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/ProductType.java @@ -1,5 +1,6 @@ package com.yello.server.domain.purchase.entity; +import static com.yello.server.global.common.ErrorCode.ENUM_BAD_REQUEST_EXCEPTION; import static com.yello.server.global.common.util.ConstantUtil.FIVE_TICKET_ID; import static com.yello.server.global.common.util.ConstantUtil.GOOGLE_FIVE_TICKET_ID; import static com.yello.server.global.common.util.ConstantUtil.GOOGLE_ONE_TICKET_ID; @@ -9,7 +10,7 @@ import static com.yello.server.global.common.util.ConstantUtil.TWO_TICKET_ID; import static com.yello.server.global.common.util.ConstantUtil.YELLO_PLUS_ID; -import java.text.MessageFormat; +import com.yello.server.global.exception.EnumIllegalArgumentException; import java.util.Arrays; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -23,14 +24,20 @@ public enum ProductType { FIVE_TICKET("five_ticket"), TEST("test"); - private final String intial; + private final String initial; public static ProductType fromCode(String dbData) { return Arrays.stream(ProductType.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 상품타입입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static ProductType fromName(String name) { + return Arrays.stream(ProductType.values()) + .filter(v -> v.name().equals(name)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } public static ProductType getProductType(String productId) { @@ -51,8 +58,4 @@ public static Integer getTicketAmount(ProductType productType) { default -> null; }; } - - public String intial() { - return intial; - } } diff --git a/src/main/java/com/yello/server/domain/purchase/entity/ProductTypeConverter.java b/src/main/java/com/yello/server/domain/purchase/entity/ProductTypeConverter.java index 9f9788b2..de21eed2 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/ProductTypeConverter.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/ProductTypeConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.purchase.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(ProductType productType) { if (productType == null) { return null; } - return productType.getIntial(); + return productType.name(); } @Override @@ -21,11 +21,7 @@ public ProductType convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return ProductType.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return ProductType.fromName(dbData); } } diff --git a/src/main/java/com/yello/server/domain/purchase/entity/Purchase.java b/src/main/java/com/yello/server/domain/purchase/entity/Purchase.java index 356c4a8c..f1741ca1 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/Purchase.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/Purchase.java @@ -3,23 +3,25 @@ import com.yello.server.domain.user.entity.User; import com.yello.server.global.common.dto.AuditingTimeEntity; import com.yello.server.global.common.util.ConstantUtil; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Getter @@ -53,6 +55,7 @@ public class Purchase extends AuditingTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "userId") + @OnDelete(action = OnDeleteAction.SET_NULL) private User user; @Column(nullable = false) diff --git a/src/main/java/com/yello/server/domain/purchase/entity/PurchaseState.java b/src/main/java/com/yello/server/domain/purchase/entity/PurchaseState.java index 9c97a1f5..8b7c6837 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/PurchaseState.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/PurchaseState.java @@ -1,6 +1,8 @@ package com.yello.server.domain.purchase.entity; -import java.text.MessageFormat; +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; @@ -13,17 +15,19 @@ public enum PurchaseState { PAUSED("PAUSED"), INACTIVE("INACTIVE"); - private final String intial; + private final String initial; public static PurchaseState fromCode(String dbData) { return Arrays.stream(PurchaseState.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 결제 상태입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static PurchaseState fromName(String name) { + return Arrays.stream(PurchaseState.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/purchase/entity/PurchaseStateConverter.java b/src/main/java/com/yello/server/domain/purchase/entity/PurchaseStateConverter.java index d27759a6..9ae9c6ad 100644 --- a/src/main/java/com/yello/server/domain/purchase/entity/PurchaseStateConverter.java +++ b/src/main/java/com/yello/server/domain/purchase/entity/PurchaseStateConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.purchase.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(PurchaseState purchaseState) { if (purchaseState == null) { return null; } - return purchaseState.getIntial(); + return purchaseState.name(); } @Override @@ -21,12 +21,7 @@ public PurchaseState convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return PurchaseState.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } - } + return PurchaseState.fromName(dbData); + } } diff --git a/src/main/java/com/yello/server/domain/purchase/repository/PurchaseJpaRepository.java b/src/main/java/com/yello/server/domain/purchase/repository/PurchaseJpaRepository.java index 831d590e..db35e4fb 100644 --- a/src/main/java/com/yello/server/domain/purchase/repository/PurchaseJpaRepository.java +++ b/src/main/java/com/yello/server/domain/purchase/repository/PurchaseJpaRepository.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface PurchaseJpaRepository extends JpaRepository { @@ -17,4 +19,11 @@ public interface PurchaseJpaRepository extends JpaRepository { Optional findTopByUserAndProductTypeOrderByCreatedAtDesc(User user, ProductType productType); + + @Query("select p from Purchase p " + + "where p.state = 'ACTIVE' " + + "and p.user = :user " + + "and p.productType = 'yello_plus' " + + "order by p.updatedAt DESC") + Optional findTopByStateAndUser(@Param("user") User user); } diff --git a/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepository.java b/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepository.java index 57d6fee5..2862d41d 100644 --- a/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepository.java +++ b/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepository.java @@ -22,4 +22,6 @@ Optional findTopByUserAndProductTypeOrderByCreatedAtDesc(User user, ProductType productType); void delete(Purchase purchase); + + Purchase getTopByStateAndUserId(User user); } diff --git a/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepositoryImpl.java b/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepositoryImpl.java index daaae69f..15487d9e 100644 --- a/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepositoryImpl.java +++ b/src/main/java/com/yello/server/domain/purchase/repository/PurchaseRepositoryImpl.java @@ -1,8 +1,12 @@ package com.yello.server.domain.purchase.repository; +import static com.yello.server.global.common.ErrorCode.NOT_FOUND_USER_SUBSCRIBE_EXCEPTION; + import com.yello.server.domain.purchase.entity.ProductType; import com.yello.server.domain.purchase.entity.Purchase; +import com.yello.server.domain.purchase.exception.PurchaseNotFoundException; import com.yello.server.domain.user.entity.User; +import com.yello.server.global.common.ErrorCode; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -50,4 +54,10 @@ public Optional findTopByUserAndProductTypeOrderByCreatedAtDesc(User u public void delete(Purchase purchase) { purchaseJpaRepository.delete(purchase); } + + @Override + public Purchase getTopByStateAndUserId(User user) { + return purchaseJpaRepository.findTopByStateAndUser(user) + .orElseThrow(() -> new PurchaseNotFoundException(NOT_FOUND_USER_SUBSCRIBE_EXCEPTION)); + } } diff --git a/src/main/java/com/yello/server/domain/purchase/service/PurchaseManager.java b/src/main/java/com/yello/server/domain/purchase/service/PurchaseManager.java index 98f9d78a..f7ea7855 100644 --- a/src/main/java/com/yello/server/domain/purchase/service/PurchaseManager.java +++ b/src/main/java/com/yello/server/domain/purchase/service/PurchaseManager.java @@ -34,5 +34,6 @@ void handleAppleTransactionError(ResponseEntity respons SlackAppleNotificationResponse checkPurchaseDataByAppleSignedPayload(String payload); - void reSubscribeApple(AppleNotificationPayloadVO payloadVO); + void reSubscribeApple(AppleNotificationPayloadVO payloadVO, String notificationType); + void expiredSubscribe(AppleNotificationPayloadVO payloadVO); } 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 4a6168e3..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 @@ -3,9 +3,7 @@ import static com.yello.server.global.common.ErrorCode.APPLE_TOKEN_SERVER_EXCEPTION; import static com.yello.server.global.common.ErrorCode.GOOGLE_SUBSCRIPTIONS_SUBSCRIPTION_EXCEPTION; import static com.yello.server.global.common.ErrorCode.NOT_FOUND_TRANSACTION_EXCEPTION; -import static com.yello.server.global.common.util.ConstantUtil.REFUND_FIVE_TICKET; -import static com.yello.server.global.common.util.ConstantUtil.REFUND_ONE_TICKET; -import static com.yello.server.global.common.util.ConstantUtil.REFUND_TWO_TICKET; +import static com.yello.server.global.common.util.ConstantUtil.*; import com.fasterxml.jackson.databind.ObjectMapper; import com.yello.server.domain.purchase.dto.apple.AppleNotificationPayloadVO; @@ -24,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; @@ -79,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(); @@ -100,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) @@ -118,16 +114,21 @@ public void changeSubscriptionStatus(AppleNotificationPayloadVO payloadVO) { .orElseThrow(() -> new PurchaseNotFoundException(NOT_FOUND_TRANSACTION_EXCEPTION)); User user = purchaseData.purchase().getUser(); - if (payloadVO.subtype().equals(ConstantUtil.APPLE_SUBTYPE_AUTO_RENEW_DISABLED) + if(payloadVO.subtype().equals(APPLE_SUBTYPE_AUTO_RENEW_ENABLED)) { + user.setSubscribe(Subscribe.ACTIVE); + purchase.setPurchaseState(PurchaseState.ACTIVE); + return; + } + + if (payloadVO.subtype().equals(APPLE_SUBTYPE_AUTO_RENEW_DISABLED) && !user.getSubscribe().equals(Subscribe.NORMAL)) { user.setSubscribe(Subscribe.CANCELED); purchase.setPurchaseState(PurchaseState.CANCELED); + return; } - if (payloadVO.subtype().equals(ConstantUtil.APPLE_SUBTYPE_VOLUNTARY)) { - user.setSubscribe(Subscribe.NORMAL); - purchase.setPurchaseState(PurchaseState.PAUSED); - } + user.setSubscribe(Subscribe.CANCELED); + purchase.setPurchaseState(PurchaseState.CANCELED); } @Override @@ -141,48 +142,40 @@ public void refundAppleInApp(AppleNotificationPayloadVO payloadVO) { switch (purchaseData.purchase().getProductType()) { case YELLO_PLUS -> { user.setSubscribe(Subscribe.NORMAL); - purchase.setPurchaseState(PurchaseState.INACTIVE); + purchase.setPurchaseState(PurchaseState.PAUSED); } case ONE_TICKET -> { validateTicketCount(REFUND_ONE_TICKET, user); - purchase.setPurchaseState(PurchaseState.INACTIVE); + purchase.setPurchaseState(PurchaseState.PAUSED); } case TWO_TICKET -> { validateTicketCount(REFUND_TWO_TICKET, user); - purchase.setPurchaseState(PurchaseState.INACTIVE); + purchase.setPurchaseState(PurchaseState.PAUSED); } case FIVE_TICKET -> { validateTicketCount(REFUND_FIVE_TICKET, user); - purchase.setPurchaseState(PurchaseState.INACTIVE); + purchase.setPurchaseState(PurchaseState.PAUSED); } } } @Override - public SlackAppleNotificationResponse checkPurchaseDataByAppleSignedPayload(String payload) { - AppleNotificationPayloadVO payloadVO = decodeApplePayload(payload); - Purchase purchase = decodeAppleNotificationData(payloadVO.data().signedTransactionInfo()); - - return SlackAppleNotificationResponse.of(payloadVO, purchase); - } - - @Override - public void reSubscribeApple(AppleNotificationPayloadVO payloadVO) { + public void reSubscribeApple(AppleNotificationPayloadVO payloadVO, String notificationType) { - if (!payloadVO.subtype().equals(ConstantUtil.APPLE_SUBTYPE_RESUBSCRIBE)) { + if (notificationType.equals(APPLE_NOTIFICATION_SUBSCRIBED) && !payloadVO.subtype().equals(APPLE_SUBTYPE_RESUBSCRIBE)) { return; } AppleJwsTransactionResponse appleJwtDecode = - decodeAppleDataPayload(payloadVO.data().signedTransactionInfo()); + decodeAppleDataPayload(payloadVO.data().signedTransactionInfo()); Purchase purchase = - purchaseRepository.findByTransactionId(appleJwtDecode.originalTransactionId()) - .orElseThrow(() -> new PurchaseConflictException(NOT_FOUND_TRANSACTION_EXCEPTION)); + purchaseRepository.findByTransactionId(appleJwtDecode.originalTransactionId()) + .orElseThrow(() -> new PurchaseConflictException(NOT_FOUND_TRANSACTION_EXCEPTION)); Purchase reSubscribePurchase = - createSubscribe(purchase.getUser(), Gateway.APPLE, appleJwtDecode.transactionId(), null, - PurchaseState.ACTIVE, appleJwtDecode.toString()); + createSubscribe(purchase.getUser(), Gateway.APPLE, appleJwtDecode.transactionId(), null, + PurchaseState.ACTIVE, appleJwtDecode.toString()); purchase.setPurchaseState(PurchaseState.INACTIVE); reSubscribePurchase.setPurchaseState(PurchaseState.ACTIVE); @@ -190,6 +183,25 @@ public void reSubscribeApple(AppleNotificationPayloadVO payloadVO) { purchaseRepository.save(reSubscribePurchase); } + @Override + public void expiredSubscribe(AppleNotificationPayloadVO payloadVO) { + ApplePurchaseVO purchaseData = getPurchaseData(payloadVO); + Purchase purchase = + purchaseRepository.findByTransactionId(purchaseData.transactionId()) + .orElseThrow(() -> new PurchaseNotFoundException(NOT_FOUND_TRANSACTION_EXCEPTION)); + User user = purchaseData.purchase().getUser(); + user.setSubscribe(Subscribe.NORMAL); + purchase.setPurchaseState(PurchaseState.INACTIVE); + } + + @Override + public SlackAppleNotificationResponse checkPurchaseDataByAppleSignedPayload(String payload) { + AppleNotificationPayloadVO payloadVO = decodeApplePayload(payload); + Purchase purchase = decodeAppleNotificationData(payloadVO.data().signedTransactionInfo()); + + return SlackAppleNotificationResponse.of(payloadVO, purchase); + } + public void validateTicketCount(int ticketCount, User user) { if (user.getTicketCount() >= ticketCount) { user.addTicketCount(-Math.abs(ticketCount)); @@ -209,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/purchase/service/PurchaseService.java b/src/main/java/com/yello/server/domain/purchase/service/PurchaseService.java index 197cf608..7001db8a 100644 --- a/src/main/java/com/yello/server/domain/purchase/service/PurchaseService.java +++ b/src/main/java/com/yello/server/domain/purchase/service/PurchaseService.java @@ -20,16 +20,7 @@ import static com.yello.server.global.common.ErrorCode.NOT_FOUND_TRANSACTION_EXCEPTION; import static com.yello.server.global.common.ErrorCode.PURCHASE_TOKEN_NOT_FOUND_PURCHASE_EXCEPTION; import static com.yello.server.global.common.ErrorCode.SUBSCRIBE_ACTIVE_EXCEPTION; -import static com.yello.server.global.common.util.ConstantUtil.APPLE_NOTIFICATION_CONSUMPTION_REQUEST; -import static com.yello.server.global.common.util.ConstantUtil.APPLE_NOTIFICATION_EXPIRED; -import static com.yello.server.global.common.util.ConstantUtil.APPLE_NOTIFICATION_REFUND; -import static com.yello.server.global.common.util.ConstantUtil.APPLE_NOTIFICATION_SUBSCRIBED; -import static com.yello.server.global.common.util.ConstantUtil.APPLE_NOTIFICATION_SUBSCRIPTION_STATUS_CHANGE; -import static com.yello.server.global.common.util.ConstantUtil.APPLE_NOTIFICATION_TEST; -import static com.yello.server.global.common.util.ConstantUtil.FIVE_TICKET_ID; -import static com.yello.server.global.common.util.ConstantUtil.ONE_TICKET_ID; -import static com.yello.server.global.common.util.ConstantUtil.TWO_TICKET_ID; -import static com.yello.server.global.common.util.ConstantUtil.YELLO_PLUS_ID; +import static com.yello.server.global.common.util.ConstantUtil.*; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -336,20 +327,24 @@ public void appleNotification(AppleNotificationRequest request) { }); switch (payloadVO.notificationType()) { - case APPLE_NOTIFICATION_CONSUMPTION_REQUEST: - break; - case APPLE_NOTIFICATION_SUBSCRIPTION_STATUS_CHANGE, APPLE_NOTIFICATION_EXPIRED: + case APPLE_NOTIFICATION_SUBSCRIPTION_STATUS_CHANGE -> { purchaseManager.changeSubscriptionStatus(payloadVO); - break; - case APPLE_NOTIFICATION_REFUND: + } + case APPLE_NOTIFICATION_EXPIRED ->{ + purchaseManager.expiredSubscribe(payloadVO); + } + case APPLE_NOTIFICATION_REFUND -> { purchaseManager.refundAppleInApp(payloadVO); - break; - case APPLE_NOTIFICATION_SUBSCRIBED: - purchaseManager.reSubscribeApple(payloadVO); - case APPLE_NOTIFICATION_TEST: + } + case APPLE_NOTIFICATION_SUBSCRIBED, APPLE_NOTIFICATION_DID_RENEW -> { + purchaseManager.reSubscribeApple(payloadVO, payloadVO.notificationType()); + } + case APPLE_NOTIFICATION_TEST -> { return; - default: + } + default -> { throw new PurchaseNotFoundException(NOT_FOUND_NOTIFICATION_TYPE_EXCEPTION); + } } } diff --git a/src/main/java/com/yello/server/domain/question/entity/Question.java b/src/main/java/com/yello/server/domain/question/entity/Question.java index 31556db1..7f0483a6 100644 --- a/src/main/java/com/yello/server/domain/question/entity/Question.java +++ b/src/main/java/com/yello/server/domain/question/entity/Question.java @@ -1,17 +1,17 @@ package com.yello.server.domain.question.entity; import com.yello.server.domain.keyword.entity.Keyword; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.OneToMany; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -50,7 +50,7 @@ public Question(String nameHead, String nameFoot, String keywordHead, String key private static String deleteBracket(String target) { val slashIndex = target.indexOf('/'); - return slashIndex!=-1 ? target.substring(slashIndex + 1) : target; + return slashIndex != -1 ? target.substring(slashIndex + 1) : target; } public static Question of(String nameHead, String nameFoot, String keywordHead, String keywordFoot) { @@ -68,18 +68,18 @@ public void addKeyword(Keyword keyword) { public String toNotificationSentence() { final String nameFootPart = deleteBracket(this.nameFoot); - final String nameHeadPart = (this.nameHead!=null) ? MessageFormat.format("{0} ", this.nameHead) : ""; - final String keywordHeadPart = (this.keywordHead!=null) ? MessageFormat.format(" {0}", this.keywordHead) : ""; + final String nameHeadPart = (this.nameHead != null) ? MessageFormat.format("{0} ", this.nameHead) : ""; + final String keywordHeadPart = (this.keywordHead != null) ? MessageFormat.format(" {0}", this.keywordHead) : ""; return MessageFormat.format("{0}너{1}{2} ...", nameHeadPart, nameFootPart, keywordHeadPart); } @Override public boolean equals(Object o) { - if (this==o) { + if (this == o) { return true; } - if (o==null || getClass()!=o.getClass()) { + if (o == null || getClass() != o.getClass()) { return false; } Question question = (Question) o; diff --git a/src/main/java/com/yello/server/domain/question/entity/QuestionGroupType.java b/src/main/java/com/yello/server/domain/question/entity/QuestionGroupType.java index c8e003a6..84a3ded4 100644 --- a/src/main/java/com/yello/server/domain/question/entity/QuestionGroupType.java +++ b/src/main/java/com/yello/server/domain/question/entity/QuestionGroupType.java @@ -3,15 +3,15 @@ import com.yello.server.domain.group.entity.UserGroupType; import com.yello.server.domain.group.entity.UserGroupTypeConverter; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; diff --git a/src/main/java/com/yello/server/domain/statistics/controller/StatisticsController.java b/src/main/java/com/yello/server/domain/statistics/controller/StatisticsController.java new file mode 100644 index 00000000..fb0b7fc0 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/controller/StatisticsController.java @@ -0,0 +1,45 @@ +package com.yello.server.domain.statistics.controller; + +import static com.yello.server.global.common.SuccessCode.READ_USER_GROUP_SCHOOL_ATTACK_STATISTICS_SUCCESS; +import static com.yello.server.global.common.factory.PaginationFactory.createPageable; + +import com.yello.server.domain.statistics.dto.SchoolAttackStatisticsVO; +import com.yello.server.domain.statistics.dto.response.StatisticsUserGroupSchoolAttackResponse; +import com.yello.server.domain.statistics.service.StatisticsService; +import com.yello.server.global.common.dto.BaseResponse; +import lombok.RequiredArgsConstructor; +import lombok.val; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/statistics") +public class StatisticsController { + + private final StatisticsService statisticsService; + + @GetMapping("/user-group/school-attack") + public BaseResponse getSchoolAttackStatistics( + @RequestParam(value = "page") Integer page) { + val data = statisticsService.getSchoolAttackStatistics(createPageable(page, 10)); + return BaseResponse.success(READ_USER_GROUP_SCHOOL_ATTACK_STATISTICS_SUCCESS, data); + } + + @GetMapping("/user-group/school-attack/{groupName}") + public BaseResponse getSchoolAttackStatisticsByGroupName( + @PathVariable(value = "groupName") String groupName) { + val data = statisticsService.getSchoolAttackStatisticsByGroupName(groupName); + return BaseResponse.success(READ_USER_GROUP_SCHOOL_ATTACK_STATISTICS_SUCCESS, data); + } + + @GetMapping("/user-group/school-attack/group-name/{groupName}") + public BaseResponse getSchoolAttackStatisticsLikeGroupName( + @PathVariable(value = "groupName") String groupName, @RequestParam(value = "page") Integer page) { + val data = statisticsService.getSchoolAttackStatisticsLikeGroupName(groupName, createPageable(page, 10)); + return BaseResponse.success(READ_USER_GROUP_SCHOOL_ATTACK_STATISTICS_SUCCESS, data); + } +} diff --git a/src/main/java/com/yello/server/domain/statistics/dto/NewStatisticsUserGroupVO.java b/src/main/java/com/yello/server/domain/statistics/dto/NewStatisticsUserGroupVO.java new file mode 100644 index 00000000..b5b3f195 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/dto/NewStatisticsUserGroupVO.java @@ -0,0 +1,10 @@ +package com.yello.server.domain.statistics.dto; + +public record NewStatisticsUserGroupVO( + String userGroupName, + Long userCount, + Long voteCount, + Long rankNumber +) { + +} diff --git a/src/main/java/com/yello/server/domain/statistics/dto/SchoolAttackStatisticsVO.java b/src/main/java/com/yello/server/domain/statistics/dto/SchoolAttackStatisticsVO.java new file mode 100644 index 00000000..843ac304 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/dto/SchoolAttackStatisticsVO.java @@ -0,0 +1,35 @@ +package com.yello.server.domain.statistics.dto; + +import com.yello.server.domain.statistics.entity.StatisticsUserGroup; +import lombok.Builder; + +@Builder +public record SchoolAttackStatisticsVO( + String userGroupName, + Long userCount, + Long voteCount, + Long score, + Long rankNumber, + Long prevUserCount, + Long prevVoteCount, + Long prevScore, + Long prevRankNumber +) { + + public static SchoolAttackStatisticsVO of(StatisticsUserGroup statisticsUserGroup) { + final Long score = statisticsUserGroup.getUserCount() + 2 * statisticsUserGroup.getVoteCount(); + final Long prevScore = statisticsUserGroup.getPrevUserCount() + 2 * statisticsUserGroup.getPrevVoteCount(); + + return SchoolAttackStatisticsVO.builder() + .userGroupName(statisticsUserGroup.getGroupName()) + .userCount(statisticsUserGroup.getUserCount()) + .voteCount(statisticsUserGroup.getVoteCount()) + .score(score) + .rankNumber(statisticsUserGroup.getRankNumber()) + .prevUserCount(statisticsUserGroup.getPrevUserCount()) + .prevVoteCount(statisticsUserGroup.getVoteCount()) + .prevScore(prevScore) + .prevRankNumber(statisticsUserGroup.getPrevRankNumber()) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/statistics/dto/response/StatisticsUserGroupSchoolAttackResponse.java b/src/main/java/com/yello/server/domain/statistics/dto/response/StatisticsUserGroupSchoolAttackResponse.java new file mode 100644 index 00000000..679912a3 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/dto/response/StatisticsUserGroupSchoolAttackResponse.java @@ -0,0 +1,16 @@ +package com.yello.server.domain.statistics.dto.response; + +import com.yello.server.domain.statistics.dto.SchoolAttackStatisticsVO; +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; + +@Builder +public record StatisticsUserGroupSchoolAttackResponse( + Long pageCount, + Long totalCount, + LocalDateTime updatedAt, + List statisticsList +) { + +} diff --git a/src/main/java/com/yello/server/domain/statistics/entity/StatisticsUserGroup.java b/src/main/java/com/yello/server/domain/statistics/entity/StatisticsUserGroup.java new file mode 100644 index 00000000..baaf672b --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/entity/StatisticsUserGroup.java @@ -0,0 +1,95 @@ +package com.yello.server.domain.statistics.entity; + +import com.yello.server.global.common.dto.AuditingTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.DynamicInsert; + +@Entity +@Getter +@SuperBuilder +@DynamicInsert +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + uniqueConstraints = { + @UniqueConstraint( + name = "statistics_user_group__group_name__unique", + columnNames = {"groupName"} + ), + } +) +public class StatisticsUserGroup extends AuditingTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + @JoinColumn( + name = "groupName", + foreignKey = @ForeignKey( + value = ConstraintMode.NO_CONSTRAINT, + foreignKeyDefinition = "FOREIGN KEY (group_name) REFERENCES user_group (group_name)" + /** + * NOTE ForeignKey Constraint는 직접 설정해주어야한다. + */ + ) + ) + private String groupName; + + @Column + private Long voteCount; + + @Column + private Long userCount; + + @Column + private Long alpha; + + @Column + private Long rankNumber; + + @Column + private Long prevVoteCount; + + @Column + private Long prevUserCount; + + @Column + private Long prevAlpha; + + @Column + private Long prevRankNumber; + + public void update(Long voteCount, Long userCount, Long alpha, Long rankNumber, Long prevVoteCount, + Long prevUserCount, + Long prevAlpha, Long prevRankNumber) { + this.voteCount = voteCount; + this.userCount = userCount; + this.alpha = alpha; + this.rankNumber = rankNumber; + this.prevVoteCount = prevVoteCount; + this.prevUserCount = prevUserCount; + this.prevAlpha = prevAlpha; + this.prevRankNumber = prevRankNumber; + } + + public void updateRankNumber(Long rankNumber, Long prevRankNumber) { + this.rankNumber = rankNumber; + this.prevRankNumber = prevRankNumber; + } +} diff --git a/src/main/java/com/yello/server/domain/statistics/exception/StatisticsNotFoundException.java b/src/main/java/com/yello/server/domain/statistics/exception/StatisticsNotFoundException.java new file mode 100644 index 00000000..b6a57116 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/exception/StatisticsNotFoundException.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.statistics.exception; + +import com.yello.server.global.common.ErrorCode; +import com.yello.server.global.exception.CustomException; +import lombok.Getter; + +@Getter +public class StatisticsNotFoundException extends CustomException { + + public StatisticsNotFoundException(ErrorCode error) { + super(error, "[StatisticsNotFoundException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/domain/statistics/repository/StatisticsRepository.java b/src/main/java/com/yello/server/domain/statistics/repository/StatisticsRepository.java new file mode 100644 index 00000000..7cfa05ab --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/repository/StatisticsRepository.java @@ -0,0 +1,29 @@ +package com.yello.server.domain.statistics.repository; + +import com.yello.server.domain.statistics.dto.NewStatisticsUserGroupVO; +import com.yello.server.domain.statistics.entity.StatisticsUserGroup; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Pageable; + +public interface StatisticsRepository { + + Optional findByUserGroupName(String userGroupName); + + StatisticsUserGroup getByUserGroupName(String groupName); + + StatisticsUserGroup save(StatisticsUserGroup statistics); + + List getUserGroupNewStatistics(LocalDateTime voteStartAt); + + Long countSchoolAttackStatistics(); + + Long countSchoolAttackStatisticsContaining(String groupName); + + LocalDateTime getSchoolAttackLastUpdatedAt(); + + List getSchoolAttackStatistics(Pageable pageable); + + List getSchoolAttackStatisticsContaining(String groupName, Pageable pageable); +} diff --git a/src/main/java/com/yello/server/domain/statistics/repository/StatisticsRepositoryImpl.java b/src/main/java/com/yello/server/domain/statistics/repository/StatisticsRepositoryImpl.java new file mode 100644 index 00000000..652a9b0f --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/repository/StatisticsRepositoryImpl.java @@ -0,0 +1,66 @@ +package com.yello.server.domain.statistics.repository; + +import static com.yello.server.global.common.ErrorCode.STATISTICS_NOT_FOUND_EXCEPTION; + +import com.yello.server.domain.statistics.dto.NewStatisticsUserGroupVO; +import com.yello.server.domain.statistics.entity.StatisticsUserGroup; +import com.yello.server.domain.statistics.exception.StatisticsNotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class StatisticsRepositoryImpl implements StatisticsRepository { + + private final StatisticsUserGroupJpaRepository userGroupJpaRepository; + + @Override + public Optional findByUserGroupName(String userGroupName) { + return userGroupJpaRepository.findByGroupName(userGroupName); + } + + @Override + public StatisticsUserGroup getByUserGroupName(String groupName) { + return userGroupJpaRepository.findByGroupName(groupName) + .orElseThrow(() -> new StatisticsNotFoundException(STATISTICS_NOT_FOUND_EXCEPTION)); + } + + @Override + public StatisticsUserGroup save(StatisticsUserGroup statistics) { + return userGroupJpaRepository.save(statistics); + } + + @Override + public List getUserGroupNewStatistics(LocalDateTime voteStartAt) { + return userGroupJpaRepository.getNewUserGroupStatistics(voteStartAt); + } + + @Override + public Long countSchoolAttackStatistics() { + return userGroupJpaRepository.countSchoolAttackStatistics(); + } + + @Override + public Long countSchoolAttackStatisticsContaining(String groupName) { + return userGroupJpaRepository.countSchoolAttackStatisticsContaining(groupName); + } + + @Override + public LocalDateTime getSchoolAttackLastUpdatedAt() { + return userGroupJpaRepository.getSchoolAttackLastUpdatedAt(); + } + + @Override + public List getSchoolAttackStatistics(Pageable pageable) { + return userGroupJpaRepository.getSchoolAttackStatistics(pageable); + } + + @Override + public List getSchoolAttackStatisticsContaining(String groupName, Pageable pageable) { + return userGroupJpaRepository.getSchoolAttackStatisticsContaining(groupName, pageable); + } +} diff --git a/src/main/java/com/yello/server/domain/statistics/repository/StatisticsUserGroupJpaRepository.java b/src/main/java/com/yello/server/domain/statistics/repository/StatisticsUserGroupJpaRepository.java new file mode 100644 index 00000000..3fd19dc9 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/repository/StatisticsUserGroupJpaRepository.java @@ -0,0 +1,67 @@ +package com.yello.server.domain.statistics.repository; + +import com.yello.server.domain.statistics.dto.NewStatisticsUserGroupVO; +import com.yello.server.domain.statistics.entity.StatisticsUserGroup; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface StatisticsUserGroupJpaRepository extends JpaRepository { + + Optional findByGroupName(String groupName); + + @Query("SELECT " + + " new com.yello.server.domain.statistics.dto.NewStatisticsUserGroupVO( " + + " userGroup.groupName, " + + " COALESCE(user_count_table.user_count, 0), " + + " COALESCE(vote_count_table.vote_count, 0)," + + " RANK() OVER (ORDER BY COALESCE(user_count_table.user_count, 0) + COALESCE(vote_count_table.vote_count, 0) * 2 DESC) ) " + + "FROM " + + " UserGroup userGroup " + + "LEFT JOIN " + + " (SELECT " + + " ug.groupName AS group_name, " + + " COUNT(user.id) AS user_count " + + " FROM " + + " User user, UserGroup ug " + + " WHERE " + + " user.group.id = ug.id " + + " AND user.deletedAt IS NULL " + + " GROUP BY " + + " ug.groupName) AS user_count_table ON userGroup.groupName = user_count_table.group_name " + + "LEFT JOIN " + + " (SELECT " + + " ug.groupName AS group_name, " + + " COUNT(vote.id) AS vote_count " + + " FROM " + + " User user " + + " LEFT JOIN " + + " Vote vote ON user.id = vote.sender.id " + + " INNER JOIN " + + " UserGroup ug ON user.group.id = ug.id" + + " WHERE vote.createdAt >= ?1 " + + " GROUP BY " + + " ug.groupName) AS vote_count_table ON userGroup.groupName = vote_count_table.group_name " + + "GROUP BY userGroup.groupName, user_count_table.group_name, vote_count_table.group_name " + + "ORDER BY " + + " user_count_table.user_count DESC, vote_count_table.vote_count DESC") + List getNewUserGroupStatistics(LocalDateTime voteStartAt); + + @Query("select count(sug) from StatisticsUserGroup sug") + Long countSchoolAttackStatistics(); + + @Query("select count(sug) from StatisticsUserGroup sug where sug.groupName like CONCAT('%',?1,'%' )") + Long countSchoolAttackStatisticsContaining(String groupName); + + @Query("select sug.updatedAt from StatisticsUserGroup sug ORDER BY sug.updatedAt DESC LIMIT 1") + LocalDateTime getSchoolAttackLastUpdatedAt(); + + @Query("SELECT sug FROM StatisticsUserGroup sug ORDER BY sug.rankNumber") + List getSchoolAttackStatistics(Pageable pageable); + + @Query("SELECT sug FROM StatisticsUserGroup sug where sug.groupName like CONCAT('%',?1,'%' ) ORDER BY sug.rankNumber") + List getSchoolAttackStatisticsContaining(String groupName, Pageable pageable); +} diff --git a/src/main/java/com/yello/server/domain/statistics/service/StatisticsService.java b/src/main/java/com/yello/server/domain/statistics/service/StatisticsService.java new file mode 100644 index 00000000..e0391778 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/service/StatisticsService.java @@ -0,0 +1,96 @@ +package com.yello.server.domain.statistics.service; + +import com.yello.server.domain.statistics.dto.NewStatisticsUserGroupVO; +import com.yello.server.domain.statistics.dto.SchoolAttackStatisticsVO; +import com.yello.server.domain.statistics.dto.response.StatisticsUserGroupSchoolAttackResponse; +import com.yello.server.domain.statistics.entity.StatisticsUserGroup; +import com.yello.server.domain.statistics.repository.StatisticsRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StatisticsService { + + private final StatisticsRepository statisticsRepository; + + @Transactional + public void writeUserGroupStatistics() { + final LocalDateTime voteStartAt = LocalDateTime.of(2023, 12, 1, 0, 0, 0); + final List newStatistics = statisticsRepository.getUserGroupNewStatistics( + voteStartAt); + + for (NewStatisticsUserGroupVO newStatistic : newStatistics) { + final Optional groupStatistics = statisticsRepository.findByUserGroupName( + newStatistic.userGroupName()); + + if (groupStatistics.isEmpty()) { + statisticsRepository.save(StatisticsUserGroup.builder() + .groupName(newStatistic.userGroupName()) + .userCount(newStatistic.userCount()) + .voteCount(newStatistic.voteCount()) + .alpha(0L) + .rankNumber(newStatistic.rankNumber()) + .prevUserCount(0L) + .prevVoteCount(0L) + .prevAlpha(0L) + .prevRankNumber(0L) + .build()); + } else { + groupStatistics.get().update( + newStatistic.voteCount(), + newStatistic.userCount(), + 0L, + newStatistic.rankNumber(), + groupStatistics.get().getVoteCount(), + groupStatistics.get().getUserCount(), + groupStatistics.get().getAlpha(), + groupStatistics.get().getRankNumber() + ); + } + } + } + + public StatisticsUserGroupSchoolAttackResponse getSchoolAttackStatistics(Pageable pageable) { + final List statistics = statisticsRepository.getSchoolAttackStatistics( + pageable); + final Long counted = statisticsRepository.countSchoolAttackStatistics(); + final LocalDateTime lastUpdatedAt = statisticsRepository.getSchoolAttackLastUpdatedAt(); + long pageSize = pageable.getPageSize(); + + return StatisticsUserGroupSchoolAttackResponse.builder() + .pageCount(counted % pageSize == 0 ? counted / pageSize : counted / pageSize + 1) + .totalCount(counted) + .updatedAt(lastUpdatedAt) + .statisticsList(statistics.stream().map(SchoolAttackStatisticsVO::of).toList()) + .build(); + } + + public SchoolAttackStatisticsVO getSchoolAttackStatisticsByGroupName(String groupName) { + return SchoolAttackStatisticsVO.of( + statisticsRepository.getByUserGroupName(groupName) + ); + } + + public StatisticsUserGroupSchoolAttackResponse getSchoolAttackStatisticsLikeGroupName(String groupName, + Pageable pageable) { + final List statistics = statisticsRepository.getSchoolAttackStatisticsContaining( + groupName, pageable); + final Long counted = statisticsRepository.countSchoolAttackStatisticsContaining(groupName); + final LocalDateTime lastUpdatedAt = statisticsRepository.getSchoolAttackLastUpdatedAt(); + long pageSize = pageable.getPageSize(); + + return StatisticsUserGroupSchoolAttackResponse.builder() + .pageCount(counted % pageSize == 0 ? counted / pageSize : counted / pageSize + 1) + .totalCount(counted) + .updatedAt(lastUpdatedAt) + .statisticsList(statistics.stream().map(SchoolAttackStatisticsVO::of).toList()) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/statistics/task/StatisticsTask.java b/src/main/java/com/yello/server/domain/statistics/task/StatisticsTask.java new file mode 100644 index 00000000..2da6f3f4 --- /dev/null +++ b/src/main/java/com/yello/server/domain/statistics/task/StatisticsTask.java @@ -0,0 +1,19 @@ +package com.yello.server.domain.statistics.task; + +import com.yello.server.domain.statistics.service.StatisticsService; +import com.yello.server.global.common.util.ConstantUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StatisticsTask { + + private final StatisticsService statisticsService; + + @Scheduled(cron = "0 */30 * * * *", zone = ConstantUtil.GlobalZoneIdLabel) + public void writeUserGroupStatistics() { + statisticsService.writeUserGroupStatistics(); + } +} diff --git a/src/main/java/com/yello/server/domain/user/controller/UserController.java b/src/main/java/com/yello/server/domain/user/controller/UserController.java index 49ebff1d..d3705f9d 100644 --- a/src/main/java/com/yello/server/domain/user/controller/UserController.java +++ b/src/main/java/com/yello/server/domain/user/controller/UserController.java @@ -1,13 +1,24 @@ package com.yello.server.domain.user.controller; import static com.yello.server.global.common.SuccessCode.DELETE_USER_SUCCESS; +import static com.yello.server.global.common.SuccessCode.READ_USER_DATA_SUCCESS; +import static com.yello.server.global.common.SuccessCode.READ_USER_SUBSCRIBE_SUCCESS; import static com.yello.server.global.common.SuccessCode.READ_USER_SUCCESS; import static com.yello.server.global.common.SuccessCode.UPDATE_DEVICE_TOKEN_USER_SUCCESS; +import static com.yello.server.global.common.SuccessCode.UPDATE_USER_DATA_SUCCESS; +import static com.yello.server.global.common.SuccessCode.UPDATE_USER_DETAIL_SUCCESS; +import com.yello.server.domain.user.dto.request.UserDataUpdateRequest; +import com.yello.server.domain.user.dto.request.UserDeleteReasonRequest; import com.yello.server.domain.user.dto.request.UserDeviceTokenRequest; +import com.yello.server.domain.user.dto.request.UserUpdateRequest; +import com.yello.server.domain.user.dto.response.UserDataResponse; import com.yello.server.domain.user.dto.response.UserDetailResponse; +import com.yello.server.domain.user.dto.response.UserDetailV2Response; import com.yello.server.domain.user.dto.response.UserResponse; +import com.yello.server.domain.user.dto.response.UserSubscribeDetailResponse; import com.yello.server.domain.user.entity.User; +import com.yello.server.domain.user.entity.UserDataType; import com.yello.server.domain.user.service.UserService; import com.yello.server.global.common.annotation.AccessTokenUser; import com.yello.server.global.common.dto.BaseResponse; @@ -17,6 +28,7 @@ import org.springframework.web.bind.annotation.DeleteMapping; 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.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -24,24 +36,30 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/user") +@RequestMapping("/api") public class UserController { private final UserService userService; - @GetMapping + @GetMapping("/v1/user") public BaseResponse findUser(@AccessTokenUser User user) { val data = userService.findMyProfile(user.getId()); return BaseResponse.success(READ_USER_SUCCESS, data); } - @GetMapping("/{userId}") + @GetMapping("/v2/user") + public BaseResponse getUser(@AccessTokenUser User user) { + val data = userService.getUserDetailV2(user.getId()); + return BaseResponse.success(READ_USER_SUCCESS, data); + } + + @GetMapping("/v1/user/{userId}") public BaseResponse findUserById(@PathVariable Long userId) { val data = userService.findUserById(userId); return BaseResponse.success(READ_USER_SUCCESS, data); } - @PutMapping("/device") + @PutMapping("/v1/user/device") public BaseResponse putUserDeviceToken( @AccessTokenUser User user, @RequestBody UserDeviceTokenRequest request @@ -50,9 +68,41 @@ public BaseResponse putUserDeviceToken( return BaseResponse.success(UPDATE_DEVICE_TOKEN_USER_SUCCESS, data); } - @DeleteMapping + @DeleteMapping("/v1/user") public BaseResponse deleteUser(@AccessTokenUser User user) { userService.delete(user); return BaseResponse.success(DELETE_USER_SUCCESS); } + + @GetMapping("/v1/user/subscribe") + public BaseResponse getUserSubscribe(@AccessTokenUser User user) { + val data = userService.getUserSubscribe(user.getId()); + return BaseResponse.success(READ_USER_SUBSCRIBE_SUCCESS, data); + } + + @PostMapping("/v1/user") + public BaseResponse postUser(@AccessTokenUser User user, @RequestBody UserUpdateRequest request) { + userService.updateUserProfile(user.getId(), request); + return BaseResponse.success(UPDATE_USER_DETAIL_SUCCESS); + } + + @DeleteMapping("/v2/user") + public BaseResponse deleteUserWithReason(@AccessTokenUser User user, @RequestBody + UserDeleteReasonRequest request) { + userService.deleteUserWithReason(user.getId(), request); + return BaseResponse.success(DELETE_USER_SUCCESS); + } + + @GetMapping("/v1/user/data/{tag}") + public BaseResponse getUserData(@AccessTokenUser User user, @PathVariable("tag") String tag) { + val data = userService.readUserData(user.getId(), UserDataType.fromCode(tag)); + return BaseResponse.success(READ_USER_DATA_SUCCESS, data); + } + + @PostMapping("/v1/user/data/{tag}") + public BaseResponse updateUserData(@AccessTokenUser User user, @PathVariable("tag") String tag, + @RequestBody UserDataUpdateRequest request) { + userService.updateUserData(user.getId(), UserDataType.fromCode(tag), request); + return BaseResponse.success(UPDATE_USER_DATA_SUCCESS); + } } diff --git a/src/main/java/com/yello/server/domain/user/dto/request/UserDataUpdateRequest.java b/src/main/java/com/yello/server/domain/user/dto/request/UserDataUpdateRequest.java new file mode 100644 index 00000000..e119a168 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/dto/request/UserDataUpdateRequest.java @@ -0,0 +1,10 @@ +package com.yello.server.domain.user.dto.request; + +import lombok.Builder; + +@Builder +public record UserDataUpdateRequest( + String value +) { + +} diff --git a/src/main/java/com/yello/server/domain/user/dto/request/UserDeleteReasonRequest.java b/src/main/java/com/yello/server/domain/user/dto/request/UserDeleteReasonRequest.java new file mode 100644 index 00000000..36aa8d43 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/dto/request/UserDeleteReasonRequest.java @@ -0,0 +1,10 @@ +package com.yello.server.domain.user.dto.request; + +import lombok.Builder; + +@Builder +public record UserDeleteReasonRequest( + String value +) { + +} diff --git a/src/main/java/com/yello/server/domain/user/dto/request/UserUpdateRequest.java b/src/main/java/com/yello/server/domain/user/dto/request/UserUpdateRequest.java new file mode 100644 index 00000000..33a5fc94 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/dto/request/UserUpdateRequest.java @@ -0,0 +1,21 @@ +package com.yello.server.domain.user.dto.request; + +import com.yello.server.domain.user.entity.User; +import lombok.Builder; + +@Builder +public record UserUpdateRequest( + String name, + String yelloId, + String gender, + String email, + String profileImageUrl, + Long groupId, + Integer groupAdmissionYear +) { + + public boolean groupInfoEquals(User user) { + return groupId.equals(user.getGroup().getId()) && + groupAdmissionYear.equals(user.getGroupAdmissionYear()); + } +} diff --git a/src/main/java/com/yello/server/domain/user/dto/response/UserDataResponse.java b/src/main/java/com/yello/server/domain/user/dto/response/UserDataResponse.java new file mode 100644 index 00000000..1e3af6f9 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/dto/response/UserDataResponse.java @@ -0,0 +1,56 @@ +package com.yello.server.domain.user.dto.response; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; + +import com.yello.server.domain.user.entity.User; +import com.yello.server.domain.user.entity.UserDataType; +import com.yello.server.global.common.util.ConstantUtil; +import java.time.ZonedDateTime; +import java.time.format.DateTimeParseException; +import javax.annotation.Nullable; +import lombok.Builder; + +@Builder +public record UserDataResponse( + String tag, + String value +) { + + public static UserDataResponse of(User user, UserDataType tag, @Nullable String value) + throws DateTimeParseException { + String resultValue = ""; + + switch (tag) { + case ACCOUNT_UPDATED_AT -> { + if (value == null) { + resultValue = String.format( + "%s|%s|%s", + true, + null, + user.getCreatedAt().format(ISO_LOCAL_DATE) + ); + } else { + final ZonedDateTime current = ZonedDateTime.now(ConstantUtil.GlobalZoneId); + final ZonedDateTime updatedAt = ZonedDateTime.parse(value, ISO_OFFSET_DATE_TIME); + boolean isUpdatable = current.getYear() - updatedAt.getYear() >= 1; + + resultValue = String.format( + "%s|%s|%s", + isUpdatable, + updatedAt.format(ISO_LOCAL_DATE), + user.getCreatedAt().format(ISO_LOCAL_DATE) + ); + } + } + case RECOMMENDED, WITHDRAW_REASON -> { + resultValue = value; + } + } + + return UserDataResponse.builder() + .tag(tag.name()) + .value(resultValue) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/user/dto/response/UserDetailV2Response.java b/src/main/java/com/yello/server/domain/user/dto/response/UserDetailV2Response.java new file mode 100644 index 00000000..4104b114 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/dto/response/UserDetailV2Response.java @@ -0,0 +1,61 @@ +package com.yello.server.domain.user.dto.response; + +import com.yello.server.domain.group.entity.UserGroup; +import com.yello.server.domain.user.entity.User; +import lombok.Builder; + +@Builder +public record UserDetailV2Response( + /* Default */ + Long userId, + String name, + String yelloId, + String gender, + String email, + String profileImageUrl, + /* Device */ + String social, + String uuid, + String deviceToken, + /* Group */ + Long groupId, + String group, + String groupType, + String groupName, + String subGroupName, + Integer groupAdmissionYear, + /* Domain */ + Long recommendCount, + Integer ticketCount, + Integer point, + String subscribe, + Integer yelloCount, + Integer friendCount +) { + + public static UserDetailV2Response of(User user, UserGroup userGroup, Integer yelloCount, Integer friendCount) { + return UserDetailV2Response.builder() + .userId(user.getId()) + .name(user.getName()) + .yelloId(user.getYelloId()) + .gender(user.getGender().getInitial()) + .email(user.getEmail()) + .profileImageUrl(user.getProfileImage()) + .social(user.getSocial().toString()) + .uuid(user.getUuid()) + .deviceToken(user.getDeviceToken()) + .groupId(userGroup.getId()) + .group(user.toGroupString()) + .groupType(userGroup.getUserGroupType().getInitial()) + .groupName(userGroup.getGroupName()) + .subGroupName(userGroup.getSubGroupName()) + .groupAdmissionYear(user.getGroupAdmissionYear()) + .recommendCount(user.getRecommendCount()) + .ticketCount(user.getTicketCount()) + .point(user.getPoint()) + .subscribe(user.getSubscribe().getInitial()) + .yelloCount(yelloCount) + .friendCount(friendCount) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/user/dto/response/UserSubscribeDetailResponse.java b/src/main/java/com/yello/server/domain/user/dto/response/UserSubscribeDetailResponse.java new file mode 100644 index 00000000..41372b47 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/dto/response/UserSubscribeDetailResponse.java @@ -0,0 +1,24 @@ +package com.yello.server.domain.user.dto.response; + +import static com.yello.server.global.common.factory.TimeFactory.toYearAndMonthFormattedString; +import static com.yello.server.global.common.util.ConstantUtil.SUBSCRIBE_DAYS; + +import com.yello.server.domain.purchase.entity.Purchase; +import lombok.Builder; + +@Builder +public record UserSubscribeDetailResponse( + Long id, + String subscribe, + String expiredDate +) { + + public static UserSubscribeDetailResponse of(Purchase purchase) { + return UserSubscribeDetailResponse.builder() + .id(purchase.getUser().getId()) + .subscribe(purchase.getUser().getSubscribe().getInitial()) + .expiredDate( + toYearAndMonthFormattedString(purchase.getUpdatedAt().plusDays(SUBSCRIBE_DAYS))) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/user/entity/Gender.java b/src/main/java/com/yello/server/domain/user/entity/Gender.java index 4737b737..19605d74 100644 --- a/src/main/java/com/yello/server/domain/user/entity/Gender.java +++ b/src/main/java/com/yello/server/domain/user/entity/Gender.java @@ -1,6 +1,8 @@ package com.yello.server.domain.user.entity; -import java.text.MessageFormat; +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; @@ -11,21 +13,23 @@ public enum Gender { MALE("M"), FEMALE("F"); - private final String intial; + private final String initial; public static Gender fromCode(String dbData) { return Arrays.stream(Gender.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 성별입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static Gender fromName(String name) { + return Arrays.stream(Gender.values()) + .filter(v -> v.name().equals(name)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } public Gender reverse() { - return "M".equals(this.intial) ? FEMALE : MALE; + return "M".equals(this.initial) ? FEMALE : MALE; } } diff --git a/src/main/java/com/yello/server/domain/user/entity/GenderConverter.java b/src/main/java/com/yello/server/domain/user/entity/GenderConverter.java index 1c0b2b63..95ded004 100644 --- a/src/main/java/com/yello/server/domain/user/entity/GenderConverter.java +++ b/src/main/java/com/yello/server/domain/user/entity/GenderConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.user.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(Gender gender) { if (gender == null) { return null; } - return gender.getIntial(); + return gender.name(); } @Override @@ -21,11 +21,7 @@ public Gender convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return Gender.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return Gender.fromName(dbData); } } diff --git a/src/main/java/com/yello/server/domain/user/entity/Social.java b/src/main/java/com/yello/server/domain/user/entity/Social.java index 709574f6..85154977 100644 --- a/src/main/java/com/yello/server/domain/user/entity/Social.java +++ b/src/main/java/com/yello/server/domain/user/entity/Social.java @@ -1,6 +1,8 @@ package com.yello.server.domain.user.entity; -import java.text.MessageFormat; +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; @@ -11,18 +13,19 @@ public enum Social { KAKAO("Kakao"), APPLE("Apple"); - private final String intial; + private final String initial; public static Social fromCode(String dbData) { return Arrays.stream(Social.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("존재하지 않는 소셜입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static Social fromName(String name) { + return Arrays.stream(Social.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/user/entity/SocialConverter.java b/src/main/java/com/yello/server/domain/user/entity/SocialConverter.java index d1671424..d4d8a65f 100644 --- a/src/main/java/com/yello/server/domain/user/entity/SocialConverter.java +++ b/src/main/java/com/yello/server/domain/user/entity/SocialConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.user.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(Social social) { if (social == null) { return null; } - return social.getIntial(); + return social.name(); } @Override @@ -21,11 +21,7 @@ public Social convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return Social.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return Social.fromName(dbData); } } diff --git a/src/main/java/com/yello/server/domain/user/entity/Subscribe.java b/src/main/java/com/yello/server/domain/user/entity/Subscribe.java index 11fa3cd8..a9e3d2c8 100644 --- a/src/main/java/com/yello/server/domain/user/entity/Subscribe.java +++ b/src/main/java/com/yello/server/domain/user/entity/Subscribe.java @@ -1,6 +1,8 @@ package com.yello.server.domain.user.entity; -import java.text.MessageFormat; +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; @@ -12,18 +14,19 @@ public enum Subscribe { ACTIVE("active"), CANCELED("canceled"); - private final String intial; + private final String initial; public static Subscribe fromCode(String dbData) { return Arrays.stream(Subscribe.values()) - .filter(v -> v.getIntial().equals(dbData)) + .filter(v -> v.getInitial().equals(dbData)) .findAny() - .orElseThrow(() -> new IllegalArgumentException( - MessageFormat.format("잘못된 구독입니다. {0}", dbData))); + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); } - public String intial() { - return intial; + public static Subscribe fromName(String name) { + return Arrays.stream(Subscribe.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/user/entity/SubscribeConverter.java b/src/main/java/com/yello/server/domain/user/entity/SubscribeConverter.java index 97e83cb2..7dc12eac 100644 --- a/src/main/java/com/yello/server/domain/user/entity/SubscribeConverter.java +++ b/src/main/java/com/yello/server/domain/user/entity/SubscribeConverter.java @@ -1,7 +1,7 @@ package com.yello.server.domain.user.entity; -import javax.persistence.AttributeConverter; -import javax.persistence.Converter; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; import lombok.extern.log4j.Log4j2; @Converter @@ -13,7 +13,7 @@ public String convertToDatabaseColumn(Subscribe subscribe) { if (subscribe == null) { return null; } - return subscribe.getIntial(); + return subscribe.name(); } @Override @@ -21,11 +21,7 @@ public Subscribe convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } - try { - return Subscribe.fromCode(dbData); - } catch (IllegalArgumentException exception) { - log.error("failure to convert cause unexpected code" + dbData + exception); - throw exception; - } + + return Subscribe.fromName(dbData); } } 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 cb7bdd9d..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 @@ -3,27 +3,30 @@ import com.yello.server.domain.admin.dto.request.AdminUserDetailRequest; import com.yello.server.domain.authorization.dto.request.SignUpRequest; import com.yello.server.domain.group.entity.UserGroup; +import com.yello.server.domain.user.dto.request.UserUpdateRequest; import com.yello.server.global.common.dto.AuditingTimeEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import jakarta.validation.constraints.Email; import java.time.LocalDateTime; import java.util.Objects; -import javax.persistence.Column; -import javax.persistence.Convert; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.Table; -import javax.persistence.UniqueConstraint; -import javax.validation.constraints.Email; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.ColumnDefault; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Getter @Entity @@ -67,8 +70,8 @@ public class User extends AuditingTimeEntity { private Gender gender; @Column(nullable = false) - @ColumnDefault("200") - private Integer point; + @Builder.Default + private Integer point = 200; @Column(nullable = false) @Convert(converter = SocialConverter.class) @@ -84,6 +87,7 @@ public class User extends AuditingTimeEntity { private LocalDateTime deletedAt; @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.RESTRICT) @JoinColumn(name = "groupId") private UserGroup group; @@ -103,9 +107,9 @@ public class User extends AuditingTimeEntity { private String deviceToken; @Column(nullable = false) - @ColumnDefault("normal") @Convert(converter = SubscribeConverter.class) - private Subscribe subscribe; + @Builder.Default + private Subscribe subscribe = Subscribe.NORMAL; public static User of(SignUpRequest signUpRequest, UserGroup group) { return User.builder() @@ -128,16 +132,6 @@ public static User of(SignUpRequest signUpRequest, UserGroup group) { .build(); } - public void delete() { - this.deletedAt = LocalDateTime.now(); - this.point = 0; - this.deviceToken = null; - } - - public void renew() { - this.deletedAt = null; - } - public void update(AdminUserDetailRequest request) { this.recommendCount = request.recommendCount(); this.name = request.name(); @@ -154,7 +148,28 @@ public void update(AdminUserDetailRequest request) { this.subscribe = request.subscribe(); } - public void addPoint(Integer point) { + public void update(UserUpdateRequest request, Gender gender, UserGroup userGroup) { + this.name = request.name(); + this.yelloId = request.yelloId(); + this.gender = gender; + this.email = request.email(); + this.profileImage = request.profileImageUrl(); + this.group = userGroup; + this.groupAdmissionYear = request.groupAdmissionYear(); + } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + this.point = 0; + this.deviceToken = null; + } + + public void renew() { + this.deletedAt = null; + } + + + public void addPointBySubscribe(Integer point) { if (this.getSubscribe() == Subscribe.NORMAL) { this.point += point; return; @@ -165,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/user/entity/UserData.java b/src/main/java/com/yello/server/domain/user/entity/UserData.java new file mode 100644 index 00000000..4802b2cb --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/entity/UserData.java @@ -0,0 +1,67 @@ +package com.yello.server.domain.user.entity; + +import static com.yello.server.global.common.factory.TimeFactory.getSecondsBetween; +import static com.yello.server.global.common.util.ConstantUtil.ADMOB_TIMER_TIME; + +import com.yello.server.global.common.factory.TimeFactory; +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +@Getter +@Entity +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table +public class UserData { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.CASCADE) + @JoinColumn(name = "userId") + private User user; + + @Column(nullable = false) + @Convert(converter = UserDataTypeConverter.class) + private UserDataType tag; + + @Column(nullable = false) + private String value; + + public static UserData of(UserDataType tag, String value, User user) { + return UserData.builder() + .tag(tag) + .user(user) + .value(value) + .build(); + } + + public void setValue(String value) { + this.value = value; + } + + public Boolean isPossible(){ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return TimeFactory.getSecondsBetween(LocalDateTime.now(), LocalDateTime.parse(this.value, formatter)) >= ADMOB_TIMER_TIME; + } +} diff --git a/src/main/java/com/yello/server/domain/user/entity/UserDataType.java b/src/main/java/com/yello/server/domain/user/entity/UserDataType.java new file mode 100644 index 00000000..789510b7 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/entity/UserDataType.java @@ -0,0 +1,40 @@ +package com.yello.server.domain.user.entity; + +import static com.yello.server.global.common.ErrorCode.ENUM_BAD_REQUEST_EXCEPTION; + +import com.yello.server.global.exception.EnumIllegalArgumentException; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.util.Arrays; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum UserDataType { + /** + * ACCOUNT_UPDATE_AT, RECOMMENDED DotaTimeFormatter.ISO_OFFSET_DATE_TIME 으로 저장해주세요 + */ + WITHDRAW_REASON(String.class, "withdraw-reason"), + ACCOUNT_UPDATED_AT(ZonedDateTime.class, "account-updated-at"), + RECOMMENDED(ZonedDateTime.class, "recommended"), + ADMOB_POINT(LocalDateTime.class, "ADMOB_POINT"), + ADMOB_MULTIPLE_POINT(LocalDateTime.class, "ADMOB_MULTIPLE_POINT"); + + private final Class classType; + private final String initial; + + public static UserDataType fromCode(String dbData) { + return Arrays.stream(UserDataType.values()) + .filter(v -> v.getInitial().equals(dbData)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static UserDataType fromName(String name) { + return Arrays.stream(UserDataType.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/user/entity/UserDataTypeConverter.java b/src/main/java/com/yello/server/domain/user/entity/UserDataTypeConverter.java new file mode 100644 index 00000000..69173534 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/entity/UserDataTypeConverter.java @@ -0,0 +1,25 @@ +package com.yello.server.domain.user.entity; + +import jakarta.persistence.AttributeConverter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class UserDataTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(UserDataType userData) { + if (userData == null) { + return null; + } + return userData.name(); + } + + @Override + public UserDataType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return UserDataType.fromName(dbData); + } +} diff --git a/src/main/java/com/yello/server/domain/user/exception/UserDataNotFoundException.java b/src/main/java/com/yello/server/domain/user/exception/UserDataNotFoundException.java new file mode 100644 index 00000000..afed4757 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/exception/UserDataNotFoundException.java @@ -0,0 +1,13 @@ +package com.yello.server.domain.user.exception; + +import com.yello.server.global.common.ErrorCode; +import com.yello.server.global.exception.CustomException; +import lombok.Getter; + +@Getter +public class UserDataNotFoundException extends CustomException { + + public UserDataNotFoundException(ErrorCode error) { + super(error, "[UserDataNotFoundException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/domain/user/repository/UserDataJpaRepository.java b/src/main/java/com/yello/server/domain/user/repository/UserDataJpaRepository.java new file mode 100644 index 00000000..5177e7b7 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/repository/UserDataJpaRepository.java @@ -0,0 +1,8 @@ +package com.yello.server.domain.user.repository; + +import com.yello.server.domain.user.entity.UserData; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserDataJpaRepository extends JpaRepository { + +} diff --git a/src/main/java/com/yello/server/domain/user/repository/UserDataRepository.java b/src/main/java/com/yello/server/domain/user/repository/UserDataRepository.java new file mode 100644 index 00000000..9543b160 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/repository/UserDataRepository.java @@ -0,0 +1,16 @@ +package com.yello.server.domain.user.repository; + +import com.yello.server.domain.user.entity.UserData; +import com.yello.server.domain.user.entity.UserDataType; +import java.util.Optional; + +public interface UserDataRepository { + + UserData save(UserData userData); + + void update(Long userId, UserDataType tag, String value); + + UserData getByUserIdAndTag(Long userId, UserDataType tag); + + Optional findByUserIdAndTag(Long userId, UserDataType tag); +} diff --git a/src/main/java/com/yello/server/domain/user/repository/UserDataRepositoryImpl.java b/src/main/java/com/yello/server/domain/user/repository/UserDataRepositoryImpl.java new file mode 100644 index 00000000..f24e9658 --- /dev/null +++ b/src/main/java/com/yello/server/domain/user/repository/UserDataRepositoryImpl.java @@ -0,0 +1,59 @@ +package com.yello.server.domain.user.repository; + + +import static com.yello.server.domain.user.entity.QUserData.userData; +import static com.yello.server.global.common.ErrorCode.USER_DATA_NOT_FOUND_EXCEPTION; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.yello.server.domain.user.entity.UserData; +import com.yello.server.domain.user.entity.UserDataType; +import com.yello.server.domain.user.exception.UserDataNotFoundException; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserDataRepositoryImpl implements UserDataRepository { + + private final JPAQueryFactory jpaQueryFactory; + private final UserDataJpaRepository userDataJpaRepository; + + @Override + public UserData save(UserData userData) { + return userDataJpaRepository.save(userData); + } + + @Override + public void update(Long userId, UserDataType tag, String value) { + jpaQueryFactory.update(userData) + .set(userData.value, value) + .where(userData.user.id.eq(userId)) + .where(userData.tag.eq(tag)) + .execute(); + } + + @Override + public UserData getByUserIdAndTag(Long userId, UserDataType tag) { + final Optional data = Optional.ofNullable(jpaQueryFactory.selectFrom(userData) + .where(userData.user.id.eq(userId)) + .where(userData.tag.eq(tag)) + .fetchFirst()); + + if (data.isEmpty()) { + throw new UserDataNotFoundException(USER_DATA_NOT_FOUND_EXCEPTION); + } + + return data.get(); + } + + @Override + public Optional findByUserIdAndTag(Long userId, UserDataType tag) { + return Optional.ofNullable(jpaQueryFactory.selectFrom(userData) + .where(userData.user.id.eq(userId)) + .where(userData.tag.eq(tag)) + .fetchFirst()); + } +} diff --git a/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java b/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java index 1d5eaab1..01d64b09 100644 --- a/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java +++ b/src/main/java/com/yello/server/domain/user/repository/UserJpaRepository.java @@ -1,115 +1,125 @@ package com.yello.server.domain.user.repository; import com.yello.server.domain.user.entity.User; -import java.util.List; -import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; +import java.util.Optional; + public interface UserJpaRepository extends JpaRepository { @Query("select u from User u " + - "where u.id = :id " + - "and u.deletedAt is null") + "where u.id = :id " + + "and u.deletedAt is null") Optional findById(@Param("id") Long id); @Query("select u from User u " + - "where u.id = :id") + "where u.id = :id") Optional findByIdNotFiltered(@Param("id") Long id); @Query("select u from User u " + - "where u.uuid = :uuid") + "where u.uuid = :uuid") Optional findByUuid(@Param("uuid") String uuid); @Query("select u from User u " + - "where u.uuid = :uuid") + "where u.uuid = :uuid") Optional findByUuidNotFiltered(@Param("uuid") String uuid); @Query("select case when count(u) > 0 then true else false end from User u " + - "where u.uuid = :uuid " + - "and u.deletedAt is null") + "where u.uuid = :uuid " + + "and u.deletedAt is null") boolean existsByUuid(@Param("uuid") String uuid); @Query("select u from User u " + - "where u.yelloId = :yelloId " + - "and u.deletedAt is null") + "where u.yelloId = :yelloId " + + "and u.deletedAt is null") Optional findByYelloId(@Param("yelloId") String yelloId); @Query("select u from User u " + - "where u.yelloId = :yelloId") + "where u.yelloId = :yelloId") Optional findByYelloIdNotFiltered(@Param("yelloId") String yelloId); @Query("select u from User u, UserGroup g " + - "where u.group.id = g.id " + - "and g.id = :groupId " + - "and u.deletedAt is null") + "where u.group.id = g.id " + + "and g.id = :groupId " + + "and u.deletedAt is null") List findAllByGroupId(@Param("groupId") Long groupId); @Query("select count (u) from User u, UserGroup g " + - "where u.group.id = g.id " + - "and g.groupName = :groupName " + - "and u.id <> :userId " + - "and u.id not in (select f.target.id from Friend f where :userId = f.user.id and f.target.deletedAt is null) " - + - "and u.deletedAt is null") + "where u.group.id = g.id " + + "and g.groupName = :groupName " + + "and u.id <> :userId " + + "and u.id not in (select f.target.id from Friend f where :userId = f.user.id and f.target.deletedAt is null) " + + + "and u.deletedAt is null") Integer countAllByGroupNameFilteredByNotFriend(@Param("userId") Long userId, @Param("groupName") String groupName); @Query("select u from User u, UserGroup g " + - "where u.group.id = g.id " + - "and g.groupName = :groupName " + - "and u.id <> :userId " + - "and u.id not in (select f.target.id from Friend f where :userId = f.user.id and f.target.deletedAt is null) " - + - "and u.deletedAt is null") + "where u.group.id = g.id " + + "and g.groupName = :groupName " + + "and u.id <> :userId " + + "and u.id not in (select f.target.id from Friend f where :userId = f.user.id and f.target.deletedAt is null) " + + + "and u.deletedAt is null") List findAllByGroupNameFilteredByNotFriend(@Param("userId") Long userId, - @Param("groupName") String groupName, Pageable pageable); + @Param("groupName") String groupName, Pageable pageable); @Query("select u from User u " - + "where u.group.groupName = :groupName " - + "and u.uuid not in :uuidList " - + "and u.name like CONCAT('%', :keyword, '%') " - + "and u.deletedAt is null " - + "order by u.name ASC ") + + "where u.group.groupName = :groupName " + + "and u.uuid not in :uuidList " + + "and u.name like CONCAT('%', :keyword, '%') " + + "and u.deletedAt is null " + + "order by u.name ASC ") List findAllByGroupContainingName(@Param("groupName") String groupName, - @Param("keyword") String keyword, @Param("uuidList") List uuidList); + @Param("keyword") String keyword, @Param("uuidList") List uuidList); @Query("select u from User u " - + "where u.group.groupName <> :groupName " - + "and u.uuid not in :uuidList " - + "and u.name like CONCAT('%', :keyword, '%') " - + "and u.deletedAt is null " - + "order by u.groupAdmissionYear DESC ") + + "where u.group.groupName <> :groupName " + + "and u.uuid not in :uuidList " + + "and u.name like CONCAT('%', :keyword, '%') " + + "and u.deletedAt is null " + + "order by u.groupAdmissionYear DESC ") List findAllByOtherGroupContainingName(@Param("groupName") String groupName, - @Param("keyword") String keyword, @Param("uuidList") List uuidList); + @Param("keyword") String keyword, @Param("uuidList") List uuidList); + + @Query("select u from User u " + + "where u.group.groupName like CONCAT('%', :keyword, '%') " + + "and u.uuid not in :uuidList " + + "and u not in :friendList " + + "and u.deletedAt is null " + + "order by u.name ASC ") + List findAllByGroupContaining(@Param("keyword") String keyword, @Param("uuidList") List uuidList, @Param("friendList") List friendList); + @Query("select u from User u " - + "where u.group.groupName = :groupName " - + "and u.uuid not in :uuidList " - + "and LOWER(u.yelloId) like LOWER(CONCAT('%', :keyword, '%')) " - + "and u.deletedAt is null " - + "order by u.yelloId ASC ") + + "where u.group.groupName = :groupName " + + "and u.uuid not in :uuidList " + + "and LOWER(u.yelloId) like LOWER(CONCAT('%', :keyword, '%')) " + + "and u.deletedAt is null " + + "order by u.yelloId ASC ") List findAllByGroupContainingYelloId(@Param("groupName") String groupName, - @Param("keyword") String keyword, @Param("uuidList") List uuidList); + @Param("keyword") String keyword, @Param("uuidList") List uuidList); @Query("select u from User u " - + "where u.group.groupName <> :groupName " - + "and u.uuid not in :uuidList " - + "and LOWER(u.yelloId) like LOWER(CONCAT('%', :keyword, '%')) " - + "and u.deletedAt is null " - + "order by u.groupAdmissionYear DESC ") + + "where u.group.groupName <> :groupName " + + "and u.uuid not in :uuidList " + + "and LOWER(u.yelloId) like LOWER(CONCAT('%', :keyword, '%')) " + + "and u.deletedAt is null " + + "order by u.groupAdmissionYear DESC ") List findAllByOtherGroupContainingYelloId(@Param("groupName") String groupName, - @Param("keyword") String keyword, @Param("uuidList") List uuidList); + @Param("keyword") String keyword, @Param("uuidList") List uuidList); @Query("select u from User u " - + "where u.deviceToken = :deviceToken " - + "and u.deletedAt is null") + + "where u.deviceToken = :deviceToken " + + "and u.deletedAt is null") Optional findByDeviceToken(@Param("deviceToken") String deviceToken); @Query("select u from User u " + - "where u.deviceToken = :deviceToken") + "where u.deviceToken = :deviceToken") Optional findByDeviceTokenNotFiltered(@Param("deviceToken") String deviceToken); Long countAllByYelloIdContaining(String yelloId); @@ -117,10 +127,10 @@ List findAllByOtherGroupContainingYelloId(@Param("groupName") String group Long countAllByNameContaining(String name); @Query("select u from User u " - + "where LOWER(u.yelloId) like LOWER(CONCAT('%', :yelloId, '%'))") + + "where LOWER(u.yelloId) like LOWER(CONCAT('%', :yelloId, '%'))") Page findAllByYelloIdContaining(Pageable pageable, @Param("yelloId") String yelloId); @Query("select u from User u " - + "where LOWER(u.name) like LOWER(CONCAT('%', :name, '%'))") + + "where LOWER(u.name) like LOWER(CONCAT('%', :name, '%'))") Page findAllByNameContaining(Pageable pageable, @Param("name") String name); } diff --git a/src/main/java/com/yello/server/domain/user/repository/UserRepository.java b/src/main/java/com/yello/server/domain/user/repository/UserRepository.java index 66df3633..2ef67768 100644 --- a/src/main/java/com/yello/server/domain/user/repository/UserRepository.java +++ b/src/main/java/com/yello/server/domain/user/repository/UserRepository.java @@ -60,6 +60,8 @@ List findAllByGroupContainingYelloId(String groupName, String keyword, List findAllByOtherGroupContainingYelloId(String groupName, String keyword, List uuidList); + List findAllByGroupNameContainingAndFriendListNotContaining(String keyword, List uuidList, List friendList); + Long count(); Long countAllByYelloIdContaining(String yelloId); diff --git a/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java b/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java index a56bc042..aa1e8df4 100644 --- a/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java +++ b/src/main/java/com/yello/server/domain/user/repository/UserRepositoryImpl.java @@ -153,6 +153,11 @@ public List findAllByOtherGroupContainingYelloId(String groupName, String return userJpaRepository.findAllByOtherGroupContainingYelloId(groupName, keyword, uuidList); } + @Override + public List findAllByGroupNameContainingAndFriendListNotContaining(String keyword, List uuidList, List friendList) { + return userJpaRepository.findAllByGroupContaining(keyword, uuidList, friendList); + } + @Override public Long count() { return userJpaRepository.count(); diff --git a/src/main/java/com/yello/server/domain/user/service/UserManagerImpl.java b/src/main/java/com/yello/server/domain/user/service/UserManagerImpl.java index c7de0f2f..84d6c587 100644 --- a/src/main/java/com/yello/server/domain/user/service/UserManagerImpl.java +++ b/src/main/java/com/yello/server/domain/user/service/UserManagerImpl.java @@ -23,7 +23,7 @@ public class UserManagerImpl implements UserManager { @Override public User getOfficialUser(Gender gender) { final String uuid = - "M".equals(gender.getIntial()) ? OFFICIAL_FEMALE_ID : OFFICIAL_MALE_ID; + "M".equals(gender.getInitial()) ? OFFICIAL_FEMALE_ID : OFFICIAL_MALE_ID; return userRepository.findByUuid(uuid) .orElseGet(() -> userRepository.save(makeOfficialUser(OFFICIAL_NAME, uuid, gender.reverse())) @@ -35,7 +35,7 @@ public List getOfficialUsers() { List users = new ArrayList<>(); users.add(getOfficialUser(Gender.FEMALE)); users.add(getOfficialUser(Gender.MALE)); - + return users; } diff --git a/src/main/java/com/yello/server/domain/user/service/UserService.java b/src/main/java/com/yello/server/domain/user/service/UserService.java index 38cfde58..4fec5687 100644 --- a/src/main/java/com/yello/server/domain/user/service/UserService.java +++ b/src/main/java/com/yello/server/domain/user/service/UserService.java @@ -1,22 +1,43 @@ package com.yello.server.domain.user.service; +import static com.yello.server.domain.user.entity.UserDataType.ACCOUNT_UPDATED_AT; +import static com.yello.server.domain.user.entity.UserDataType.WITHDRAW_REASON; import static com.yello.server.global.common.ErrorCode.DEVICE_TOKEN_CONFLICT_USER_EXCEPTION; +import static com.yello.server.global.common.ErrorCode.USER_DATA_INVALID_ARGUMENT_EXCEPTION; import com.yello.server.domain.admin.repository.UserAdminRepository; import com.yello.server.domain.cooldown.entity.Cooldown; import com.yello.server.domain.cooldown.repository.CooldownRepository; import com.yello.server.domain.friend.entity.Friend; import com.yello.server.domain.friend.repository.FriendRepository; +import com.yello.server.domain.group.entity.UserGroup; +import com.yello.server.domain.group.repository.UserGroupRepository; +import com.yello.server.domain.purchase.entity.Purchase; import com.yello.server.domain.purchase.repository.PurchaseRepository; +import com.yello.server.domain.user.dto.request.UserDataUpdateRequest; +import com.yello.server.domain.user.dto.request.UserDeleteReasonRequest; import com.yello.server.domain.user.dto.request.UserDeviceTokenRequest; +import com.yello.server.domain.user.dto.request.UserUpdateRequest; +import com.yello.server.domain.user.dto.response.UserDataResponse; import com.yello.server.domain.user.dto.response.UserDetailResponse; +import com.yello.server.domain.user.dto.response.UserDetailV2Response; import com.yello.server.domain.user.dto.response.UserResponse; +import com.yello.server.domain.user.dto.response.UserSubscribeDetailResponse; +import com.yello.server.domain.user.entity.Gender; import com.yello.server.domain.user.entity.User; +import com.yello.server.domain.user.entity.UserData; +import com.yello.server.domain.user.entity.UserDataType; import com.yello.server.domain.user.exception.UserConflictException; +import com.yello.server.domain.user.exception.UserException; +import com.yello.server.domain.user.repository.UserDataRepository; import com.yello.server.domain.user.repository.UserRepository; import com.yello.server.domain.vote.repository.VoteRepository; import com.yello.server.global.common.dto.EmptyObject; -import com.yello.server.infrastructure.redis.repository.TokenRepository; +import com.yello.server.global.common.util.ConstantUtil; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; import lombok.Builder; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -28,13 +49,14 @@ @Transactional(readOnly = true) public class UserService { - private final UserRepository userRepository; - private final FriendRepository friendRepository; - private final VoteRepository voteRepository; private final CooldownRepository cooldownRepository; - private final TokenRepository tokenRepository; + private final FriendRepository friendRepository; private final PurchaseRepository purchaseRepository; private final UserAdminRepository userAdminRepository; + private final UserDataRepository userDataRepository; + private final UserGroupRepository userGroupRepository; + private final UserRepository userRepository; + private final VoteRepository voteRepository; public UserDetailResponse findMyProfile(Long userId) { final User user = userRepository.getById(userId); @@ -44,6 +66,14 @@ public UserDetailResponse findMyProfile(Long userId) { return UserDetailResponse.of(user, yelloCount, friendCount); } + public UserDetailV2Response getUserDetailV2(Long userId) { + final User user = userRepository.getById(userId); + final Integer yelloCount = voteRepository.countAllByReceiverUserId(user.getId()); + final Integer friendCount = friendRepository.findAllByUserId(user.getId()).size(); + + return UserDetailV2Response.of(user, user.getGroup(), yelloCount, friendCount); + } + public UserResponse findUserById(Long userId) { final User user = userRepository.getById(userId); final Integer yelloCount = voteRepository.countAllByReceiverUserId(user.getId()); @@ -78,4 +108,90 @@ public void delete(User user) { cooldownRepository.findByUserId(target.getId()) .ifPresent(Cooldown::delete); } + + public UserSubscribeDetailResponse getUserSubscribe(Long userId) { + final User user = userRepository.getById(userId); + final Purchase purchase = purchaseRepository.getTopByStateAndUserId(user); + + return UserSubscribeDetailResponse.of(purchase); + } + + @Transactional + public void deleteUserWithReason(Long userId, UserDeleteReasonRequest request) { + final User target = userRepository.getById(userId); + target.delete(); + + friendRepository.findAllByUserId(target.getId()) + .forEach(Friend::delete); + + friendRepository.findAllByTargetId(target.getId()) + .forEach(Friend::delete); + + cooldownRepository.findByUserId(target.getId()) + .ifPresent(Cooldown::delete); + + userDataRepository.save(UserData.of(WITHDRAW_REASON, request.value(), target)); + + } + + @Transactional + public void updateUserProfile(Long userId, UserUpdateRequest request) { + // exception + final User user = userRepository.getById(userId); + final UserGroup userGroup = userGroupRepository.getById(request.groupId()); + final Gender gender = Gender.fromCode(request.gender()); + final Optional userData = userDataRepository.findByUserIdAndTag(userId, + UserDataType.ACCOUNT_UPDATED_AT); + + // logic + if (!request.groupInfoEquals(user)) { + if (userData.isPresent()) { + userData.get().setValue( + ZonedDateTime.now(ConstantUtil.GlobalZoneId).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) + ); + } else { + userDataRepository.save(UserData.of( + ACCOUNT_UPDATED_AT, + ZonedDateTime.now(ConstantUtil.GlobalZoneId).format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + user + )); + } + } + user.update(request, gender, userGroup); + } + + public UserDataResponse readUserData(Long userId, UserDataType tag) { + // exception + final User user = userRepository.getById(userId); + final Optional userData = userDataRepository.findByUserIdAndTag(userId, tag); + + return UserDataResponse.of(user, tag, userData.isPresent() ? userData.get().getValue() : null); + } + + @Transactional + public void updateUserData(Long userId, UserDataType tag, UserDataUpdateRequest request) { + // exception + final User user = userRepository.getById(userId); + final Optional userData = userDataRepository.findByUserIdAndTag(userId, tag); + final DateTimeFormatter zonedDateFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + String savedValue = null; + + if (tag.getClassType().equals(String.class)) { + savedValue = request.value(); + } else if (tag.getClassType().equals(ZonedDateTime.class)) { + try { + savedValue = ZonedDateTime + .parse(request.value(), zonedDateFormatter) + .format(zonedDateFormatter); + } catch (DateTimeParseException exception) { + throw new UserException(USER_DATA_INVALID_ARGUMENT_EXCEPTION); + } + } + + if (userData.isPresent()) { + userDataRepository.update(userId, tag, savedValue); + } else { + userDataRepository.save(UserData.of(tag, savedValue, user)); + } + } } diff --git a/src/main/java/com/yello/server/domain/vote/controller/VoteController.java b/src/main/java/com/yello/server/domain/vote/controller/VoteController.java index 7b8796a1..bba2f0a3 100644 --- a/src/main/java/com/yello/server/domain/vote/controller/VoteController.java +++ b/src/main/java/com/yello/server/domain/vote/controller/VoteController.java @@ -16,9 +16,11 @@ import com.yello.server.domain.vote.dto.response.VoteAvailableResponse; import com.yello.server.domain.vote.dto.response.VoteCreateResponse; import com.yello.server.domain.vote.dto.response.VoteDetailResponse; +import com.yello.server.domain.vote.dto.response.VoteFriendAndUserResponse; import com.yello.server.domain.vote.dto.response.VoteFriendResponse; import com.yello.server.domain.vote.dto.response.VoteListResponse; import com.yello.server.domain.vote.dto.response.VoteUnreadCountResponse; +import com.yello.server.domain.vote.entity.VoteType; import com.yello.server.domain.vote.service.VoteService; import com.yello.server.global.common.SuccessCode; import com.yello.server.global.common.annotation.AccessTokenUser; @@ -37,56 +39,69 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("api/v1/vote") +@RequestMapping("api") @RequiredArgsConstructor public class VoteController { private final VoteService voteService; private final NotificationService notificationService; - @GetMapping - public BaseResponse findAllMyVotes(@RequestParam Integer page, @AccessTokenUser User user) { + @GetMapping("/v1/vote") + public BaseResponse findAllMyVotes(@RequestParam Integer page, + @AccessTokenUser User user) { val data = voteService.findAllVotes(user.getId(), createPageableLimitTen(page)); return BaseResponse.success(READ_VOTE_SUCCESS, data); } - @GetMapping("/count") + @GetMapping("/v1/vote/count") public BaseResponse getUnreadVoteCount(@AccessTokenUser User user) { val data = voteService.getUnreadVoteCount(user.getId()); return BaseResponse.success(READ_VOTE_SUCCESS, data); } - @GetMapping("/friend") - public BaseResponse findAllFriendVotes(@RequestParam Integer page, @AccessTokenUser User user) { + @GetMapping("/v1/vote/friend") + public BaseResponse findAllFriendVotes(@RequestParam Integer page, + @AccessTokenUser User user) { val data = voteService.findAllFriendVotes(user.getId(), createPageableLimitTen(page)); return BaseResponse.success(READ_VOTE_SUCCESS, data); } - @GetMapping("/{voteId}") - public BaseResponse findVote(@PathVariable Long voteId, @AccessTokenUser User user) { + @GetMapping("/v1/vote/{voteId}") + public BaseResponse findVote(@PathVariable Long voteId, + @AccessTokenUser User user) { val data = voteService.findVoteById(voteId, user.getId()); return BaseResponse.success(READ_VOTE_SUCCESS, data); } - @PatchMapping("/{voteId}/keyword") - public BaseResponse checkKeyword(@PathVariable Long voteId, @AccessTokenUser User user) { + @GetMapping("/v2/vote/friend") + public BaseResponse findAllFriendVotesWithType( + @RequestParam("page") Integer page, @RequestParam(value = "type", required = false) String type, + @AccessTokenUser User user) { + val data = voteService.findAllFriendVotesWithType(user.getId(), createPageableLimitTen(page), type); + return BaseResponse.success(READ_VOTE_SUCCESS, data); + } + + @PatchMapping("/v1/vote/{voteId}/keyword") + public BaseResponse checkKeyword(@PathVariable Long voteId, + @AccessTokenUser User user) { val keywordCheckResponse = voteService.checkKeyword(user.getId(), voteId); return BaseResponse.success(CHECK_KEYWORD_SUCCESS, keywordCheckResponse); } - @GetMapping("/question") - public BaseResponse> findVoteQuestions(@AccessTokenUser User user) { + @GetMapping("/v1/vote/question") + public BaseResponse> findVoteQuestions( + @AccessTokenUser User user) { val data = voteService.findVoteQuestionList(user.getId()); return BaseResponse.success(READ_YELLO_VOTE_SUCCESS, data); } - @GetMapping("/available") + @GetMapping("/v1/vote/available") public BaseResponse checkVoteAvailable(@AccessTokenUser User user) { val data = voteService.checkVoteAvailable(user.getId()); return BaseResponse.success(READ_YELLO_START_SUCCESS, data); } - @PostMapping + @PostMapping("/v1/vote") public BaseResponse createVote( @AccessTokenUser User user, @RequestBody CreateVoteRequest request @@ -98,14 +113,16 @@ public BaseResponse createVote( return BaseResponse.success(CREATE_VOTE_SUCCESS, response); } - @PatchMapping("/{voteId}/name") - public BaseResponse revealNameHint(@AccessTokenUser User user, @PathVariable Long voteId) { + @PatchMapping("/v1/vote/{voteId}/name") + public BaseResponse revealNameHint(@AccessTokenUser User user, + @PathVariable Long voteId) { val data = voteService.revealNameHint(user.getId(), voteId); return BaseResponse.success(SuccessCode.REVEAL_NAME_HINT_SUCCESS, data); } - @PatchMapping("/{voteId}/fullname") - public BaseResponse revealFullName(@AccessTokenUser User user, @PathVariable Long voteId) { + @PatchMapping("/v1/vote/{voteId}/fullname") + public BaseResponse revealFullName(@AccessTokenUser User user, + @PathVariable Long voteId) { val data = voteService.revealFullName(user.getId(), voteId); return BaseResponse.success(SuccessCode.REVEAL_NAME_SUCCESS, data); } diff --git a/src/main/java/com/yello/server/domain/vote/dto/response/VoteFriendAndUserResponse.java b/src/main/java/com/yello/server/domain/vote/dto/response/VoteFriendAndUserResponse.java new file mode 100644 index 00000000..a5409340 --- /dev/null +++ b/src/main/java/com/yello/server/domain/vote/dto/response/VoteFriendAndUserResponse.java @@ -0,0 +1,20 @@ +package com.yello.server.domain.vote.dto.response; + +import java.util.List; +import lombok.Builder; + +@Builder +public record VoteFriendAndUserResponse( + Long totalCount, + List friendVotes + +) { + + public static VoteFriendAndUserResponse of(Long totalCount, List friendVotes) { + return VoteFriendAndUserResponse.builder() + .totalCount(totalCount) + .friendVotes(friendVotes) + .build(); + } +} + diff --git a/src/main/java/com/yello/server/domain/vote/dto/response/VoteFriendAndUserVO.java b/src/main/java/com/yello/server/domain/vote/dto/response/VoteFriendAndUserVO.java new file mode 100644 index 00000000..c4d953a0 --- /dev/null +++ b/src/main/java/com/yello/server/domain/vote/dto/response/VoteFriendAndUserVO.java @@ -0,0 +1,46 @@ +package com.yello.server.domain.vote.dto.response; + +import static com.yello.server.global.common.factory.TimeFactory.toFormattedString; + +import com.yello.server.domain.vote.entity.Vote; +import lombok.Builder; + +@Builder +public record VoteFriendAndUserVO( + Long id, + Long senderId, + String senderName, + String senderGender, + String senderYelloId, + String senderProfileImage, + Long receiverId, + String receiverName, + String receiverGender, + String receiverYelloId, + String receiverProfileImage, + VoteContentVO vote, + Boolean isHintUsed, + String createdAt, + Boolean isUserSenderVote +) { + + public static VoteFriendAndUserVO of(Vote vote, Boolean isUserSenderVote) { + return VoteFriendAndUserVO.builder() + .id(vote.getId()) + .senderId(vote.getSender().getId()) + .senderName(vote.getSender().getName()) + .senderGender(vote.getSender().getGender().name()) + .senderYelloId(vote.getSender().getYelloId()) + .senderProfileImage(vote.getSender().getProfileImage()) + .receiverId(vote.getReceiver().getId()) + .receiverGender(vote.getReceiver().getGender().name()) + .receiverName(vote.getReceiver().getName()) + .receiverYelloId(vote.getReceiver().getYelloId()) + .receiverProfileImage(vote.getReceiver().getProfileImage()) + .vote(VoteContentVO.of(vote)) + .isHintUsed(vote.getIsAnswerRevealed()) + .createdAt(toFormattedString(vote.getCreatedAt())) + .isUserSenderVote(isUserSenderVote) + .build(); + } +} diff --git a/src/main/java/com/yello/server/domain/vote/entity/Vote.java b/src/main/java/com/yello/server/domain/vote/entity/Vote.java index a8b3b265..fb30b63f 100644 --- a/src/main/java/com/yello/server/domain/vote/entity/Vote.java +++ b/src/main/java/com/yello/server/domain/vote/entity/Vote.java @@ -3,21 +3,24 @@ import com.yello.server.domain.question.entity.Question; import com.yello.server.domain.user.entity.User; import com.yello.server.global.common.dto.AuditingTimeEntity; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; import org.hibernate.annotations.ColumnDefault; import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Getter @@ -38,23 +41,26 @@ public class Vote extends AuditingTimeEntity { @Column(nullable = false) private Integer nameHint; - @ColumnDefault("false") @Column(nullable = false) - private Boolean isAnswerRevealed; + @Builder.Default + private Boolean isAnswerRevealed = false; - @ColumnDefault("false") @Column(nullable = false) - private Boolean isRead; + @Builder.Default + private Boolean isRead = false; @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.SET_NULL) @JoinColumn(name = "senderId") private User sender; @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.SET_NULL) @JoinColumn(name = "receiverId") private User receiver; @ManyToOne(fetch = FetchType.LAZY) + @OnDelete(action = OnDeleteAction.RESTRICT) @JoinColumn(name = "questionId") private Question question; diff --git a/src/main/java/com/yello/server/domain/vote/entity/VoteType.java b/src/main/java/com/yello/server/domain/vote/entity/VoteType.java new file mode 100644 index 00000000..59b095e7 --- /dev/null +++ b/src/main/java/com/yello/server/domain/vote/entity/VoteType.java @@ -0,0 +1,30 @@ +package com.yello.server.domain.vote.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 VoteType { + SEND("send"); + + private final String initial; + + public static VoteType fromCode(String value) { + return Arrays.stream(VoteType.values()) + .filter(v -> v.getInitial().equals(value)) + .findAny() + .orElseThrow(() -> new EnumIllegalArgumentException(ENUM_BAD_REQUEST_EXCEPTION)); + } + + public static VoteType fromName(String name) { + return Arrays.stream(VoteType.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/vote/entity/VoteTypeConverter.java b/src/main/java/com/yello/server/domain/vote/entity/VoteTypeConverter.java new file mode 100644 index 00000000..472d350e --- /dev/null +++ b/src/main/java/com/yello/server/domain/vote/entity/VoteTypeConverter.java @@ -0,0 +1,27 @@ +package com.yello.server.domain.vote.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.extern.log4j.Log4j2; + +@Converter +@Log4j2 +public class VoteTypeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(VoteType voteType) { + if (voteType == null) { + return null; + } + return voteType.name(); + } + + @Override + public VoteType convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + return VoteType.fromName(dbData); + } +} diff --git a/src/main/java/com/yello/server/domain/vote/repository/VoteJpaRepository.java b/src/main/java/com/yello/server/domain/vote/repository/VoteJpaRepository.java index 757c8fde..4455e0c8 100644 --- a/src/main/java/com/yello/server/domain/vote/repository/VoteJpaRepository.java +++ b/src/main/java/com/yello/server/domain/vote/repository/VoteJpaRepository.java @@ -16,14 +16,14 @@ public interface VoteJpaRepository extends JpaRepository { + "where v.receiver.id = :userId " + "and v.receiver.deletedAt is null " + "and v.sender.deletedAt is null " - + "and v.isRead is false") + + "and v.isRead = false") Integer countUnreadByReceiverUserId(@Param("userId") Long userId); @Query("select count(v) from Vote v " + "where v.receiver.deviceToken = :deviceToken " + "and v.receiver.deletedAt is null " + "and v.sender.deletedAt is null " - + "and v.isRead is false") + + "and v.isRead = false") Integer countUnreadByReceiverDeviceToken(@Param("deviceToken") String deviceToken); @Query("select v from Vote v " @@ -35,6 +35,7 @@ public interface VoteJpaRepository extends JpaRepository { @Query("select v from Vote v where v.receiver.id in " + "(select f.target.id from Friend f where f.user.id = :userId and f.deletedAt is null) " + + "and v.nameHint != -3 " + "and v.sender.deletedAt is null " + "and v.receiver.deletedAt is null " + "order by v.createdAt desc") @@ -42,6 +43,7 @@ public interface VoteJpaRepository extends JpaRepository { @Query("select count(v) from Vote v where v.receiver.id in " + "(select f.target.id from Friend f where f.user.id = :userId and f.deletedAt is null) " + + "and v.nameHint != -3 " + "and v.sender.deletedAt is null " + "and v.receiver.deletedAt is null " + "order by v.createdAt desc") @@ -50,22 +52,22 @@ public interface VoteJpaRepository extends JpaRepository { @Query("select count(v) from Vote v " + "where v.receiver.id = :userId " - + "and v.isRead is true " + + "and v.isRead = true " + "and v.receiver.deletedAt is null " + "and v.sender.deletedAt is null") Integer countReadByReceiverUserId(@Param("userId") Long userId); @Query("select count(v) from Vote v " + "where v.receiver.id = :userId " - + "and v.isRead is true " - + "and v.isAnswerRevealed is true " + + "and v.isRead = true " + + "and v.isAnswerRevealed = true " + "and v.receiver.deletedAt is null " + "and v.sender.deletedAt is null") Integer countOpenKeywordByReceiverUserId(@Param("userId") Long userId); @Query("select count(v) from Vote v " + "where v.receiver.id = :userId " - + "and v.isRead is true " + + "and v.isRead = true " + "and v.nameHint in (0,1) " + "and v.receiver.deletedAt is null " + "and v.sender.deletedAt is null") @@ -74,7 +76,7 @@ public interface VoteJpaRepository extends JpaRepository { @Query("select count(v) from Vote v " + "where v.receiver.id = :userId " - + "and v.isRead is true " + + "and v.isRead = true " + "and v.nameHint = -2 " + "and v.receiver.deletedAt is null " + "and v.sender.deletedAt is null") diff --git a/src/main/java/com/yello/server/domain/vote/repository/VoteRepository.java b/src/main/java/com/yello/server/domain/vote/repository/VoteRepository.java index 9880ef01..4c689bf1 100644 --- a/src/main/java/com/yello/server/domain/vote/repository/VoteRepository.java +++ b/src/main/java/com/yello/server/domain/vote/repository/VoteRepository.java @@ -32,4 +32,8 @@ public interface VoteRepository { Integer countOpenNameByReceiverUserId(Long userId); Integer countOpenFullNameByReceiverUserId(Long userId); + + List findUserSendReceivedByFriends(Long userId, Pageable pageable); + + Long countUserSendReceivedByFriends(Long userId); } diff --git a/src/main/java/com/yello/server/domain/vote/repository/VoteRepositoryImpl.java b/src/main/java/com/yello/server/domain/vote/repository/VoteRepositoryImpl.java index b25312ac..97a6245d 100644 --- a/src/main/java/com/yello/server/domain/vote/repository/VoteRepositoryImpl.java +++ b/src/main/java/com/yello/server/domain/vote/repository/VoteRepositoryImpl.java @@ -1,7 +1,11 @@ package com.yello.server.domain.vote.repository; +import static com.yello.server.domain.friend.entity.QFriend.friend; +import static com.yello.server.domain.vote.entity.QVote.vote; import static com.yello.server.global.common.ErrorCode.NOT_FOUND_VOTE_EXCEPTION; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; import com.yello.server.domain.vote.entity.Vote; import com.yello.server.domain.vote.exception.VoteNotFoundException; import java.util.List; @@ -17,6 +21,7 @@ public class VoteRepositoryImpl implements VoteRepository { private final VoteJpaRepository voteJpaRepository; + private final JPAQueryFactory jpaQueryFactory; @Override public Vote save(Vote vote) { @@ -83,4 +88,42 @@ public Integer countOpenNameByReceiverUserId(Long userId) { public Integer countOpenFullNameByReceiverUserId(Long userId) { return voteJpaRepository.countOpenFullNameByReceiverUserId(userId); } + + @Override + public List findUserSendReceivedByFriends(Long userId, Pageable pageable) { + return jpaQueryFactory.selectFrom(vote) + .where(vote.sender.id.eq(userId) + .and(vote.receiver.id.in( + JPAExpressions + .select(friend.target.id) + .from(friend) + .where(friend.user.id.eq(userId) + .and(friend.deletedAt.isNull()) + ))) + .and(vote.nameHint.ne(-3)) + .and(vote.sender.deletedAt.isNull()) + .and(vote.receiver.deletedAt.isNull())) + .orderBy(vote.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + } + + @Override + public Long countUserSendReceivedByFriends(Long userId) { + return jpaQueryFactory.select(vote.count()) + .from(vote) + .where(vote.sender.id.eq(userId) + .and(vote.receiver.id.in( + JPAExpressions + .select(friend.target.id) + .from(friend) + .where(friend.user.id.eq(userId) + .and(friend.deletedAt.isNull()) + ))) + .and(vote.nameHint.ne(-3)) + .and(vote.sender.deletedAt.isNull()) + .and(vote.receiver.deletedAt.isNull())) + .fetchOne(); + } } 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 d9d87def..7c23eb3b 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 @@ -5,6 +5,7 @@ import static com.yello.server.global.common.ErrorCode.LACK_TICKET_COUNT_EXCEPTION; import static com.yello.server.global.common.ErrorCode.LACK_USER_EXCEPTION; 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.CHECK_FULL_NAME; import static com.yello.server.global.common.util.ConstantUtil.COOL_DOWN_TIME; @@ -32,12 +33,15 @@ import com.yello.server.domain.vote.dto.response.VoteCountVO; import com.yello.server.domain.vote.dto.response.VoteCreateVO; import com.yello.server.domain.vote.dto.response.VoteDetailResponse; +import com.yello.server.domain.vote.dto.response.VoteFriendAndUserResponse; +import com.yello.server.domain.vote.dto.response.VoteFriendAndUserVO; import com.yello.server.domain.vote.dto.response.VoteFriendResponse; import com.yello.server.domain.vote.dto.response.VoteFriendVO; import com.yello.server.domain.vote.dto.response.VoteListResponse; import com.yello.server.domain.vote.dto.response.VoteResponse; import com.yello.server.domain.vote.dto.response.VoteUnreadCountResponse; import com.yello.server.domain.vote.entity.Vote; +import com.yello.server.domain.vote.entity.VoteType; import com.yello.server.domain.vote.exception.VoteForbiddenException; import com.yello.server.domain.vote.exception.VoteNotFoundException; import com.yello.server.domain.vote.repository.VoteRepository; @@ -51,6 +55,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; @Service @Builder @@ -107,12 +112,37 @@ public VoteFriendResponse findAllFriendVotes(Long userId, Pageable pageable) { final Integer totalCount = voteRepository.countAllReceivedByFriends(userId); final List list = voteRepository.findAllReceivedByFriends(userId, pageable) .stream() - .filter(vote -> vote.getNameHint()!=-3) .map(VoteFriendVO::of) .toList(); return VoteFriendResponse.of(totalCount, list); } + public VoteFriendAndUserResponse findAllFriendVotesWithType(Long userId, Pageable pageable, String voteType) { + + if(!StringUtils.hasText(voteType)) { + final Long totalCount = Long.valueOf(voteRepository.countAllReceivedByFriends(userId)); + final List list = voteRepository.findAllReceivedByFriends(userId, pageable) + .stream() + .map(vote -> VoteFriendAndUserVO.of(vote, vote.getSender().getId().equals(userId))) + .toList(); + return VoteFriendAndUserResponse.of(totalCount, list); + } + + switch(VoteType.fromCode(voteType)) { + case SEND -> { + final Long totalCount = voteRepository.countUserSendReceivedByFriends(userId); + List list = + voteRepository.findUserSendReceivedByFriends(userId, pageable) + .stream() + .map(vote -> VoteFriendAndUserVO.of(vote, vote.getSender().getId().equals(userId))) + .toList(); + return VoteFriendAndUserResponse.of(totalCount,list); + } + } + + throw new VoteForbiddenException(WRONG_VOTE_TYPE_FORBIDDEN); + } + @Transactional public KeywordCheckResponse checkKeyword(Long userId, Long voteId) { final Vote vote = voteRepository.getById(voteId); @@ -177,7 +207,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 fc426e59..f27e7a9e 100644 --- a/src/main/java/com/yello/server/global/common/ErrorCode.java +++ b/src/main/java/com/yello/server/global/common/ErrorCode.java @@ -37,6 +37,15 @@ public enum ErrorCode { APPLE_IN_APP_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "존재하지 않는 영수증입니다."), METHOD_ARGUMENT_TYPE_MISMATCH_EXCEPTION(BAD_REQUEST, "입력한 값의 타입이 올바르지 않습니다."), USER_ADMIN_BAD_REQUEST_EXCEPTION(BAD_REQUEST, "검색 필드가 없습니다."), + 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_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 @@ -64,6 +73,8 @@ public enum ErrorCode { "유효하지 않는 Google OAuth 2.0 refreshToken입니다. DBA에게 문의해주세요."), 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 @@ -89,6 +100,18 @@ public enum ErrorCode { USER_ADMIN_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 Admin이 존재하지 않습니다."), NOT_EQUAL_TRANSACTION_EXCEPTION(NOT_FOUND, "동일하지 않은 거래입니다."), NOT_FOUND_NOTIFICATION_TYPE_EXCEPTION(NOT_FOUND, "존재하지 않는 알림 타입 입니다"), + NOT_FOUND_USER_SUBSCRIBE_EXCEPTION(NOT_FOUND, "유저의 구독정보가 존재하지 않습니다."), + NOT_FOUND_NOTICE_EXCEPTION(NOT_FOUND, "공지가 존재하지 않습니다."), + ADMIN_CONFIGURATION_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 설정이 존재하지 않습니다."), + USER_DATA_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 유저 데이터가 존재하지 않습니다."), + EVENT_REWARD_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 EventReward가 존재하지 않습니다."), + EVENT_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 Event가 존재하지 않습니다."), + 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가 존재하지 않습니다."), + EMPTY_QUERY_STRING_EXCEPTION(NOT_FOUND, "query string 값이 null 입니다."), + STATISTICS_NOT_FOUND_EXCEPTION(NOT_FOUND, "해당 통계가 존재하지 않습니다."), /** * 409 CONFLICT @@ -99,6 +122,7 @@ public enum ErrorCode { DEVICE_TOKEN_CONFLICT_USER_EXCEPTION(CONFLICT, "이미 존재하는 deviceToken 입니다."), SUBSCRIBE_ACTIVE_EXCEPTION(CONFLICT, "이미 옐로플러스 구독한 유저입니다."), GOOGLE_SUBSCRIPTIONS_SUBSCRIPTION_EXCEPTION(CONFLICT, "이미 적용한 영수증입니다."), + IDEMPOTENCY_KEY_CONFLICT_EXCEPTION(CONFLICT, "이미 존재하는 멱등키 입니다."), /** * 500 INTERNAL SERVER ERROR 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 9a4e078f..d6ea3b9c 100644 --- a/src/main/java/com/yello/server/global/common/SuccessCode.java +++ b/src/main/java/com/yello/server/global/common/SuccessCode.java @@ -37,16 +37,34 @@ public enum SuccessCode { VERIFY_RECEIPT_SUCCESS(OK, "인앱결제 검증에 성공했습니다."), READ_USER_ADMIN_SUCCESS(OK, "어드민 페이지 유저 조회에 성공하였습니다."), READ_USER_DETAIL_ADMIN_SUCCESS(OK, "어드민 페이지 유저 상세 조회에 성공하였습니다."), + READ_USER_DATA_SUCCESS(OK, "유저 데이터 조회에 성공하였습니다."), UPDATE_USER_DETAIL_ADMIN_SUCCESS(OK, "어드민 페이지 유저 상세 정보 수정에 성공하였습니다."), + UPDATE_USER_DETAIL_SUCCESS(OK, "유저 상세 정보 수정에 성공하였습니다."), + UPDATE_USER_DATA_SUCCESS(OK, "유저 데이터 수정에 성공하였습니다."), READ_COOLDOWN_ADMIN_SUCCESS(OK, "어드민 페이지 쿨다운 조회에 성공하였습니다."), - DELETE_USER_ADMIN_SUCCESS(OK, "어드민 권환으로 유저 삭제에 성공하였습니다."), - DELETE_COOLDOWN_ADMIN_SUCCESS(OK, "어드민 권환으로 쿨다운 삭제에 성공하였습니다."), + DELETE_USER_ADMIN_SUCCESS(OK, "어드민 권한으로 유저 삭제에 성공하였습니다."), + DELETE_COOLDOWN_ADMIN_SUCCESS(OK, "어드민 권한으로 쿨다운 삭제에 성공하였습니다."), POST_APPLE_NOTIFICATION_SUCCESS(OK, "apple 알림 처리에 성공하였습니다."), POST_GOOGLE_NOTIFICATION_SUCCESS(OK, "google 알림 처리에 성공하였습니다."), READ_QUESTION_ADMIN_SUCCESS(OK, "어드민 페이지 질문 조회에 성공하였습니다."), READ_QUESTION_DETAIL_ADMIN_SUCCESS(OK, "어드민 페이지 질문 상세 조회에 성공하였습니다."), - DELETE_QUESTION_ADMIN_SUCCESS(OK, "어드민 권환으로 질문지 삭제에 성공하였습니다."), + DELETE_QUESTION_ADMIN_SUCCESS(OK, "어드민 권한으로 질문지 삭제에 성공하였습니다."), CLASS_NAME_SEARCH_BY_SCHOOL_NAME_SCHOOL_SUCCESS(OK, "학반 검색에 성공했습니다."), + READ_USER_SUBSCRIBE_SUCCESS(OK, "구독 정보 조회에 성공하였습니다."), + READ_NOTICE_SUCCESS(OK, "공지 조회에 성공하였습니다."), + CONFIGURATION_READ_ADMIN_SUCCESS(OK, "어드민 권한으로 설정 조회에 성공하였습니다."), + CONFIGURATION_UPDATE_ADMIN_SUCCESS(OK, "어드민 권한으로 설정 수정에 성공하였습니다."), + NOTICE_CREATE_ADMIN_SUCCESS(OK, "어드민 권한으로 공지 생성에 성공하였습니다."), + NOTICE_READ_ADMIN_SUCCESS(OK, "어드민 권한으로 공지 조회에 성공하였습니다."), + NOTICE_UPDATE_DETAIL_ADMIN_SUCCESS(OK, "어드민 권한으로 공지 수정에 성공하였습니다."), + EVENT_CREATE_ADMIN_SUCCESS(OK, "어드민 권한으로 이벤트 생성에 성공하였습니다."), + EVENT_NOTICE_SUCCESS(OK, "이벤트 전체 조회에 성공하였습니다."), + EVENT_REWARD_CREATE_ADMIN_SUCCESS(OK, "어드민 권한으로 이벤트 보상 생성에 성공하였습니다."), + EVENT_JOIN_SUCCESS(OK, "이벤트 참여에 성공하였습니다."), + EVENT_REWARD_SUCCESS(OK, "이벤트 보상에 성공하였습니다."), + VERIFY_ADMOB_SSV_SUCCESS(OK, "Admob ssv 검증에 성공하였습니다."), + GET_IS_POSSIBLE_ADMOB_SUCCESS(OK, "광고 보고 포인트 얻기 가능 여부 조회에 성공했습니다."), + READ_USER_GROUP_SCHOOL_ATTACK_STATISTICS_SUCCESS(OK, "School Attack 통계 조회에 성공하였습니다."), /** * 201 CREATED @@ -61,11 +79,12 @@ 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; - private final String message; + String message; public int getHttpStatusCode() { return httpStatus.value(); diff --git a/src/main/java/com/yello/server/global/common/dto/AuditingTimeEntity.java b/src/main/java/com/yello/server/global/common/dto/AuditingTimeEntity.java index 6fbc28bd..76221f23 100644 --- a/src/main/java/com/yello/server/global/common/dto/AuditingTimeEntity.java +++ b/src/main/java/com/yello/server/global/common/dto/AuditingTimeEntity.java @@ -1,8 +1,8 @@ package com.yello.server.global.common.dto; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; import java.time.LocalDateTime; -import javax.persistence.EntityListeners; -import javax.persistence.MappedSuperclass; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/yello/server/global/common/dto/MultiReadHttpServletRequest.java b/src/main/java/com/yello/server/global/common/dto/MultiReadHttpServletRequest.java index 73dd0dc1..be95d707 100644 --- a/src/main/java/com/yello/server/global/common/dto/MultiReadHttpServletRequest.java +++ b/src/main/java/com/yello/server/global/common/dto/MultiReadHttpServletRequest.java @@ -1,5 +1,9 @@ package com.yello.server.global.common.dto; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -7,10 +11,6 @@ import java.io.InputStreamReader; import java.util.Enumeration; import java.util.Map; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; import org.apache.tomcat.util.http.fileupload.IOUtils; public class MultiReadHttpServletRequest extends HttpServletRequestWrapper { @@ -65,7 +65,7 @@ public String toString() { .append(headers.get(key)) .append("\n"); }); - + return builder.toString(); } diff --git a/src/main/java/com/yello/server/global/common/entity/GoogleToken.java b/src/main/java/com/yello/server/global/common/entity/GoogleToken.java index 28e19cb1..43bf7ce5 100644 --- a/src/main/java/com/yello/server/global/common/entity/GoogleToken.java +++ b/src/main/java/com/yello/server/global/common/entity/GoogleToken.java @@ -1,10 +1,10 @@ package com.yello.server.global.common.entity; import com.yello.server.global.common.dto.AuditingTimeEntity; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/yello/server/global/common/entity/OffsetTimeConverter.java b/src/main/java/com/yello/server/global/common/entity/OffsetTimeConverter.java new file mode 100644 index 00000000..f6cbafbc --- /dev/null +++ b/src/main/java/com/yello/server/global/common/entity/OffsetTimeConverter.java @@ -0,0 +1,28 @@ +package com.yello.server.global.common.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.time.OffsetTime; +import java.time.format.DateTimeFormatter; + +@Converter +public class OffsetTimeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(OffsetTime offsetTime) { + if (offsetTime == null) { + return null; + } + + return offsetTime.format(DateTimeFormatter.ISO_OFFSET_TIME); + } + + @Override + public OffsetTime convertToEntityAttribute(String sqlTimestamp) { + if (sqlTimestamp == null) { + return null; + } + + return OffsetTime.parse(sqlTimestamp, DateTimeFormatter.ISO_OFFSET_TIME); + } +} diff --git a/src/main/java/com/yello/server/global/common/entity/ZonedDateTimeConverter.java b/src/main/java/com/yello/server/global/common/entity/ZonedDateTimeConverter.java new file mode 100644 index 00000000..d8b31b86 --- /dev/null +++ b/src/main/java/com/yello/server/global/common/entity/ZonedDateTimeConverter.java @@ -0,0 +1,28 @@ +package com.yello.server.global.common.entity; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +@Converter +public class ZonedDateTimeConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ZonedDateTime zonedDateTime) { + if (zonedDateTime == null) { + return null; + } + + return zonedDateTime.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + + @Override + public ZonedDateTime convertToEntityAttribute(String sqlTimestamp) { + if (sqlTimestamp == null) { + return null; + } + + return ZonedDateTime.parse(sqlTimestamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } +} 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/TimeFactory.java b/src/main/java/com/yello/server/global/common/factory/TimeFactory.java index 662d3e04..93102ccc 100644 --- a/src/main/java/com/yello/server/global/common/factory/TimeFactory.java +++ b/src/main/java/com/yello/server/global/common/factory/TimeFactory.java @@ -2,6 +2,8 @@ import java.time.Duration; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; public class TimeFactory { @@ -51,4 +53,20 @@ public static LocalDateTime plusTime(LocalDateTime localDateTime, int time) { public static LocalDateTime minusTime(LocalDateTime localDateTime, int time) { return localDateTime.minusMinutes(time); } + + public static String toYearAndMonthFormattedString(LocalDateTime localDateTime) { + if (localDateTime == null) { + return ""; + } + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + return localDateTime.format(dateTimeFormatter); + } + + public static Boolean compareNowAndEndData(ZonedDateTime zonedDateTime) { + ZoneId zoneId = ZoneId.of("Asia/Seoul"); + ZonedDateTime now = ZonedDateTime.now(zoneId); + + return now.isBefore(zonedDateTime); + } + } 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 44213c44..d02e01d4 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 @@ -1,7 +1,12 @@ package com.yello.server.global.common.util; +import java.time.ZoneId; + public class ConstantUtil { + public static final String GlobalZoneIdLabel = "Asia/Seoul"; + public static final ZoneId GlobalZoneId = ZoneId.of(GlobalZoneIdLabel); + public static final String IdempotencyKeyHeader = "IdempotencyKey"; public static final int RANDOM_COUNT = 4; public static final int VOTE_COUNT = 8; public static final long TIMER_TIME = 2400L; @@ -57,15 +62,20 @@ public class ConstantUtil { public static final String APPLE_NOTIFICATION_EXPIRED = "EXPIRED"; public static final String APPLE_NOTIFICATION_TEST = "TEST"; public static final String APPLE_NOTIFICATION_SUBSCRIBED = "SUBSCRIBED"; + public static final String APPLE_NOTIFICATION_DID_RENEW = "DID_RENEW"; public static final String APPLE_SUBTYPE_AUTO_RENEW_DISABLED = "AUTO_RENEW_DISABLED"; public static final String APPLE_SUBTYPE_VOLUNTARY = "VOLUNTARY"; public static final String APPLE_SUBTYPE_AUTO_RENEW_ENABLED = "AUTO_RENEW_ENABLED"; + public static final String APPLE_SUBTYPE_BILLING_RECOVERY = "BILLING_RECOVERY"; public static final String APPLE_SUBTYPE_RESUBSCRIBE = "RESUBSCRIBE"; public static final int REFUND_ONE_TICKET = 1; public static final int REFUND_TWO_TICKET = 2; public static final int REFUND_FIVE_TICKET = 5; public static final int NO_FRIEND_COUNT = 0; - + public static final int SUBSCRIBE_DAYS = 7; + public static final int PLUS_BASIC_TIME = 0; + public static final int ADMOB_SHOP_TIME = 60; + public static final long ADMOB_TIMER_TIME = 3600L; private ConstantUtil() { throw new IllegalStateException(); diff --git a/src/main/java/com/yello/server/global/configuration/QueryDslConfiguration.java b/src/main/java/com/yello/server/global/configuration/QueryDslConfiguration.java new file mode 100644 index 00000000..24cc3417 --- /dev/null +++ b/src/main/java/com/yello/server/global/configuration/QueryDslConfiguration.java @@ -0,0 +1,22 @@ +package com.yello.server.global.configuration; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@RequiredArgsConstructor +public class QueryDslConfiguration { + + @PersistenceContext + private final EntityManager entityManager; + + @Bean + public JPAQueryFactory queryFactory() { + return new JPAQueryFactory(entityManager); + } + +} diff --git a/src/main/java/com/yello/server/global/configuration/SchedulerConfiguration.java b/src/main/java/com/yello/server/global/configuration/SchedulerConfiguration.java new file mode 100644 index 00000000..b605de95 --- /dev/null +++ b/src/main/java/com/yello/server/global/configuration/SchedulerConfiguration.java @@ -0,0 +1,10 @@ +package com.yello.server.global.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulerConfiguration { + +} diff --git a/src/main/java/com/yello/server/global/configuration/ThreadPoolConfiguration.java b/src/main/java/com/yello/server/global/configuration/ThreadPoolConfiguration.java index c3fd5551..0024fb87 100644 --- a/src/main/java/com/yello/server/global/configuration/ThreadPoolConfiguration.java +++ b/src/main/java/com/yello/server/global/configuration/ThreadPoolConfiguration.java @@ -1,7 +1,9 @@ package com.yello.server.global.configuration; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -9,6 +11,8 @@ public class ThreadPoolConfiguration { @Bean + @Primary + @Qualifier("threadPoolTaskExecutor") public TaskExecutor threadPoolTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); 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 4524a428..ff643451 100644 --- a/src/main/java/com/yello/server/global/exception/ControllerExceptionAdvice.java +++ b/src/main/java/com/yello/server/global/exception/ControllerExceptionAdvice.java @@ -6,10 +6,10 @@ import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; import static org.springframework.http.HttpStatus.FORBIDDEN; -import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import static org.springframework.http.HttpStatus.NOT_FOUND; import static org.springframework.http.HttpStatus.UNAUTHORIZED; +import com.yello.server.domain.admin.exception.AdminConfigurationNotFoundException; import com.yello.server.domain.admin.exception.UserAdminBadRequestException; import com.yello.server.domain.admin.exception.UserAdminNotFoundException; import com.yello.server.domain.authorization.exception.AuthBadRequestException; @@ -20,20 +20,23 @@ import com.yello.server.domain.authorization.exception.NotSignedInException; 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; import com.yello.server.domain.group.exception.GroupNotFoundException; +import com.yello.server.domain.notice.exception.NoticeNotFoundException; import com.yello.server.domain.purchase.exception.AppleBadRequestException; -import com.yello.server.domain.purchase.exception.AppleTokenServerErrorException; import com.yello.server.domain.purchase.exception.GoogleBadRequestException; import com.yello.server.domain.purchase.exception.GoogleTokenNotFoundException; -import com.yello.server.domain.purchase.exception.GoogleTokenServerErrorException; import com.yello.server.domain.purchase.exception.PurchaseConflictException; import com.yello.server.domain.purchase.exception.PurchaseException; import com.yello.server.domain.purchase.exception.PurchaseNotFoundException; import com.yello.server.domain.purchase.exception.SubscriptionConflictException; import com.yello.server.domain.question.exception.QuestionException; import com.yello.server.domain.question.exception.QuestionNotFoundException; +import com.yello.server.domain.statistics.exception.StatisticsNotFoundException; import com.yello.server.domain.user.exception.UserBadRequestException; import com.yello.server.domain.user.exception.UserConflictException; import com.yello.server.domain.user.exception.UserException; @@ -41,10 +44,8 @@ import com.yello.server.domain.vote.exception.VoteForbiddenException; import com.yello.server.domain.vote.exception.VoteNotFoundException; import com.yello.server.global.common.dto.BaseResponse; -import com.yello.server.infrastructure.redis.exception.RedisException; -import com.yello.server.infrastructure.redis.exception.RedisNotFoundException; import com.yello.server.infrastructure.slack.factory.SlackWebhookMessageFactory; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import net.gpedro.integrations.slack.SlackApi; import net.gpedro.integrations.slack.SlackMessage; import org.springframework.beans.factory.annotation.Qualifier; @@ -69,7 +70,7 @@ public class ControllerExceptionAdvice { public ControllerExceptionAdvice( @Qualifier("slackErrorApi") SlackApi slackErrorApi, SlackWebhookMessageFactory slackWebhookMessageFactory, - TaskExecutor taskExecutor + @Qualifier("threadPoolTaskExecutor") TaskExecutor taskExecutor ) { this.slackWebhookMessageFactory = slackWebhookMessageFactory; this.taskExecutor = taskExecutor; @@ -87,25 +88,6 @@ void handleException(HttpServletRequest request, Exception exception) throws Exc throw exception; } - /** - * 400 BAD REQUEST - */ - @ExceptionHandler({ - FriendException.class, - UserException.class, - AuthBadRequestException.class, - UserBadRequestException.class, - QuestionException.class, - PurchaseException.class, - GoogleBadRequestException.class, - AppleBadRequestException.class, - UserAdminBadRequestException.class - }) - public ResponseEntity BadRequestException(CustomException exception) { - return ResponseEntity.status(BAD_REQUEST) - .body(BaseResponse.error(exception.getError(), exception.getMessage())); - } - @ExceptionHandler({ // @Valid 오류 Catch MethodArgumentNotValidException.class @@ -149,6 +131,28 @@ public ResponseEntity BadRequestException( METHOD_ARGUMENT_TYPE_MISMATCH_EXCEPTION.getMessage())); } + /** + * 400 BAD REQUEST + */ + @ExceptionHandler({ + FriendException.class, + UserException.class, + AuthBadRequestException.class, + UserBadRequestException.class, + QuestionException.class, + PurchaseException.class, + GoogleBadRequestException.class, + AppleBadRequestException.class, + UserAdminBadRequestException.class, + EnumIllegalArgumentException.class, + IllegalArgumentException.class + }) + public ResponseEntity BadRequestException(CustomException exception) { + return ResponseEntity.status(BAD_REQUEST) + .body(BaseResponse.error(exception.getError(), exception.getMessage())); + } + + /** * 401 UNAUTHORIZED */ @@ -170,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) @@ -186,10 +191,14 @@ public ResponseEntity ForbiddenException(CustomException exception GroupNotFoundException.class, FriendNotFoundException.class, QuestionNotFoundException.class, - RedisNotFoundException.class, PurchaseNotFoundException.class, GoogleTokenNotFoundException.class, - UserAdminNotFoundException.class + UserAdminNotFoundException.class, + NoticeNotFoundException.class, + AdminConfigurationNotFoundException.class, + EventNotFoundException.class, + EventBadRequestException.class, + StatisticsNotFoundException.class }) public ResponseEntity NotFoundException(CustomException exception) { return ResponseEntity.status(NOT_FOUND) @@ -208,17 +217,4 @@ public ResponseEntity ConflictException(CustomException exception) return ResponseEntity.status(CONFLICT) .body(BaseResponse.error(exception.getError(), exception.getMessage())); } - - /** - * 500 INTERNAL SERVER ERROR - */ - @ExceptionHandler({ - RedisException.class, - GoogleTokenServerErrorException.class, - AppleTokenServerErrorException.class - }) - public ResponseEntity InternalServerException(CustomException exception) { - return ResponseEntity.status(INTERNAL_SERVER_ERROR) - .body(BaseResponse.error(exception.getError(), exception.getMessage())); - } } diff --git a/src/main/java/com/yello/server/global/exception/EnumIllegalArgumentException.java b/src/main/java/com/yello/server/global/exception/EnumIllegalArgumentException.java new file mode 100644 index 00000000..4d76a603 --- /dev/null +++ b/src/main/java/com/yello/server/global/exception/EnumIllegalArgumentException.java @@ -0,0 +1,12 @@ +package com.yello.server.global.exception; + +import com.yello.server.global.common.ErrorCode; +import lombok.Getter; + +@Getter +public class EnumIllegalArgumentException extends CustomException { + + public EnumIllegalArgumentException(ErrorCode error) { + super(error, "[EnumIllegalArgumentException] " + error.getMessage()); + } +} diff --git a/src/main/java/com/yello/server/global/exception/ExceptionHandlerFilter.java b/src/main/java/com/yello/server/global/exception/ExceptionHandlerFilter.java index 66be1ebf..40153f02 100644 --- a/src/main/java/com/yello/server/global/exception/ExceptionHandlerFilter.java +++ b/src/main/java/com/yello/server/global/exception/ExceptionHandlerFilter.java @@ -7,11 +7,11 @@ import com.yello.server.global.common.ErrorCode; import com.yello.server.global.common.dto.BaseResponse; import com.yello.server.global.common.dto.MultiReadHttpServletRequest; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.springframework.web.filter.OncePerRequestFilter; public class ExceptionHandlerFilter extends OncePerRequestFilter { @@ -23,7 +23,7 @@ protected void doFilterInternal( FilterChain filterChain ) throws ServletException, IOException { MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest(request); - + try { filterChain.doFilter(multiReadRequest, response); } catch (CustomException exception) { diff --git a/src/main/java/com/yello/server/infrastructure/client/AppleApiWebClient.java b/src/main/java/com/yello/server/infrastructure/client/AppleApiWebClient.java index a3626bbc..766150f8 100644 --- a/src/main/java/com/yello/server/infrastructure/client/AppleApiWebClient.java +++ b/src/main/java/com/yello/server/infrastructure/client/AppleApiWebClient.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; @@ -32,9 +33,9 @@ public ResponseEntity appleGetTransaction( ResponseEntity transactionResponse = getTransactionByWebClient(appleTransaction, APPLE_PRODUCTION_URL); - HttpStatus statusCode = transactionResponse.getStatusCode(); + HttpStatusCode statusCode = transactionResponse.getStatusCode(); - if (transactionResponse==null) { + if (transactionResponse == null) { throw new PurchaseException(NOT_FOUND_TRANSACTION_EXCEPTION); } if (statusCode.equals(HttpStatus.NOT_FOUND)) { diff --git a/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java b/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java index 07fb6d61..f452069b 100644 --- a/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java +++ b/src/main/java/com/yello/server/infrastructure/firebase/dto/request/NotificationMessage.java @@ -44,7 +44,7 @@ public static NotificationMessage toYelloNotificationContent(Vote vote) { final User sender = vote.getSender(); final String target = - Gender.MALE.getIntial().equals(sender.getGender().getIntial()) ? "남학생" : "여학생"; + Gender.MALE.getInitial().equals(sender.getGender().getInitial()) ? "남학생" : "여학생"; return NotificationMessage.builder() .title(MessageFormat.format("{0}이 쪽지를 보냈어요!", target)) .message(vote.getQuestion().toNotificationSentence()) diff --git a/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java b/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java index c92a36f6..b110e0c3 100644 --- a/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java +++ b/src/main/java/com/yello/server/infrastructure/firebase/service/NotificationFcmService.java @@ -10,7 +10,6 @@ import com.yello.server.infrastructure.firebase.dto.request.NotificationCustomMessage; import com.yello.server.infrastructure.firebase.dto.request.NotificationMessage; import com.yello.server.infrastructure.firebase.manager.FCMManager; -import com.yello.server.infrastructure.redis.repository.TokenRepository; import java.util.Objects; import lombok.Builder; import lombok.RequiredArgsConstructor; @@ -23,10 +22,9 @@ @RequiredArgsConstructor public class NotificationFcmService implements NotificationService { - private final UserRepository userRepository; - private final TokenRepository tokenRepository; private final FCMManager fcmManager; private final UserAdminRepository userAdminRepository; + private final UserRepository userRepository; @Override public void sendRecommendNotification(User user, User target) { diff --git a/src/main/java/com/yello/server/infrastructure/redis/configuration/RedisConfiguration.java b/src/main/java/com/yello/server/infrastructure/redis/configuration/RedisConfiguration.java deleted file mode 100644 index 73299136..00000000 --- a/src/main/java/com/yello/server/infrastructure/redis/configuration/RedisConfiguration.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.yello.server.infrastructure.redis.configuration; - -import com.yello.server.domain.authorization.dto.ServiceTokenVO; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializer; - -@Configuration -public class RedisConfiguration { - - @Value("${spring.redis.host}") - private String host; - - @Value("${spring.redis.port}") - private int port; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory(host, port); - } - - @Bean - public RedisTemplate redisTokenTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setKeySerializer(RedisSerializer.java(Long.class.getClassLoader())); - redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(ServiceTokenVO.class)); - redisTemplate.setConnectionFactory(redisConnectionFactory()); - return redisTemplate; - } - - @Bean - public ValueOperations tokenValueOperations() { - return redisTokenTemplate().opsForValue(); - } -} diff --git a/src/main/java/com/yello/server/infrastructure/redis/exception/RedisException.java b/src/main/java/com/yello/server/infrastructure/redis/exception/RedisException.java deleted file mode 100644 index d3588a42..00000000 --- a/src/main/java/com/yello/server/infrastructure/redis/exception/RedisException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.yello.server.infrastructure.redis.exception; - -import com.yello.server.global.common.ErrorCode; -import com.yello.server.global.exception.CustomException; -import lombok.Getter; - -@Getter -public class RedisException extends CustomException { - - public RedisException(ErrorCode error) { - super(error, "[RedisException] " + error.getMessage()); - } -} diff --git a/src/main/java/com/yello/server/infrastructure/redis/exception/RedisNotFoundException.java b/src/main/java/com/yello/server/infrastructure/redis/exception/RedisNotFoundException.java deleted file mode 100644 index 32a426ad..00000000 --- a/src/main/java/com/yello/server/infrastructure/redis/exception/RedisNotFoundException.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.yello.server.infrastructure.redis.exception; - -import com.yello.server.global.common.ErrorCode; -import com.yello.server.global.exception.CustomException; -import lombok.Getter; - -@Getter -public class RedisNotFoundException extends CustomException { - - public RedisNotFoundException(ErrorCode error) { - super(error, "[RedisNotFoundException] " + error.getMessage()); - } -} diff --git a/src/main/java/com/yello/server/infrastructure/redis/repository/TokenRepository.java b/src/main/java/com/yello/server/infrastructure/redis/repository/TokenRepository.java deleted file mode 100644 index c81438f5..00000000 --- a/src/main/java/com/yello/server/infrastructure/redis/repository/TokenRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.yello.server.infrastructure.redis.repository; - -import com.yello.server.domain.authorization.dto.ServiceTokenVO; - -public interface TokenRepository { - - void set(Long key, ServiceTokenVO value); - - ServiceTokenVO get(Long key); - -} diff --git a/src/main/java/com/yello/server/infrastructure/redis/repository/TokenRepositoryImpl.java b/src/main/java/com/yello/server/infrastructure/redis/repository/TokenRepositoryImpl.java deleted file mode 100644 index 2ffd4689..00000000 --- a/src/main/java/com/yello/server/infrastructure/redis/repository/TokenRepositoryImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.yello.server.infrastructure.redis.repository; - -import com.yello.server.domain.authorization.dto.ServiceTokenVO; -import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.stereotype.Repository; - -@Repository -@RequiredArgsConstructor -public class TokenRepositoryImpl implements TokenRepository { - - private final ValueOperations redisTokenRepository; - - @Override - public void set(Long key, ServiceTokenVO value) { - redisTokenRepository.set(key, value); - } - - @Override - public ServiceTokenVO get(Long key) { - return redisTokenRepository.get(key); - } - -} diff --git a/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackApplePurchaseNotificationAspect.java b/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackApplePurchaseNotificationAspect.java index b7400a90..88d34c67 100644 --- a/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackApplePurchaseNotificationAspect.java +++ b/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackApplePurchaseNotificationAspect.java @@ -1,7 +1,7 @@ package com.yello.server.infrastructure.slack.aspect; import com.yello.server.infrastructure.slack.factory.SlackWebhookMessageFactory; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import net.gpedro.integrations.slack.SlackApi; import net.gpedro.integrations.slack.SlackMessage; import org.aspectj.lang.ProceedingJoinPoint; @@ -24,7 +24,7 @@ public class SlackApplePurchaseNotificationAspect { public SlackApplePurchaseNotificationAspect( @Qualifier("slackApplePurchaseNotificationApi") SlackApi slackApplePurchaseApi, SlackWebhookMessageFactory slackWebhookMessageFactory, - TaskExecutor taskExecutor) { + @Qualifier("threadPoolTaskExecutor") TaskExecutor taskExecutor) { this.slackApplePurchaseApi = slackApplePurchaseApi; this.slackWebhookMessageFactory = slackWebhookMessageFactory; this.taskExecutor = taskExecutor; diff --git a/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackPurchaseNotificationAspect.java b/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackPurchaseNotificationAspect.java index 1a1d9853..9586cccb 100644 --- a/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackPurchaseNotificationAspect.java +++ b/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackPurchaseNotificationAspect.java @@ -1,7 +1,7 @@ package com.yello.server.infrastructure.slack.aspect; import com.yello.server.infrastructure.slack.factory.SlackWebhookMessageFactory; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import net.gpedro.integrations.slack.SlackApi; import net.gpedro.integrations.slack.SlackMessage; import org.aspectj.lang.ProceedingJoinPoint; @@ -24,7 +24,7 @@ public class SlackPurchaseNotificationAspect { public SlackPurchaseNotificationAspect( @Qualifier("slackPurchaseApi") SlackApi slackPurchaseApi, SlackWebhookMessageFactory slackWebhookMessageFactory, - TaskExecutor taskExecutor) { + @Qualifier("threadPoolTaskExecutor") TaskExecutor taskExecutor) { this.slackPurchaseApi = slackPurchaseApi; this.slackWebhookMessageFactory = slackWebhookMessageFactory; this.taskExecutor = taskExecutor; diff --git a/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackSignUpNotificationAspect.java b/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackSignUpNotificationAspect.java index 03b50b12..e5af18da 100644 --- a/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackSignUpNotificationAspect.java +++ b/src/main/java/com/yello/server/infrastructure/slack/aspect/SlackSignUpNotificationAspect.java @@ -1,7 +1,7 @@ package com.yello.server.infrastructure.slack.aspect; import com.yello.server.infrastructure.slack.factory.SlackWebhookMessageFactory; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequest; import net.gpedro.integrations.slack.SlackApi; import net.gpedro.integrations.slack.SlackMessage; import org.aspectj.lang.ProceedingJoinPoint; @@ -24,7 +24,7 @@ public class SlackSignUpNotificationAspect { public SlackSignUpNotificationAspect( @Qualifier("slackSignUpApi") SlackApi slackSignUpApi, SlackWebhookMessageFactory slackWebhookMessageFactory, - TaskExecutor taskExecutor) { + @Qualifier("threadPoolTaskExecutor") TaskExecutor taskExecutor) { this.slackSignUpApi = slackSignUpApi; this.slackWebhookMessageFactory = slackWebhookMessageFactory; this.taskExecutor = taskExecutor; diff --git a/src/main/java/com/yello/server/infrastructure/slack/factory/SlackWebhookMessageFactory.java b/src/main/java/com/yello/server/infrastructure/slack/factory/SlackWebhookMessageFactory.java index 8d164f94..82b19f0c 100644 --- a/src/main/java/com/yello/server/infrastructure/slack/factory/SlackWebhookMessageFactory.java +++ b/src/main/java/com/yello/server/infrastructure/slack/factory/SlackWebhookMessageFactory.java @@ -8,6 +8,7 @@ import com.yello.server.domain.user.repository.UserRepository; import com.yello.server.global.common.factory.TimeFactory; import com.yello.server.infrastructure.slack.dto.response.SlackAppleNotificationResponse; +import jakarta.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -17,7 +18,6 @@ import java.util.Collections; import java.util.List; import java.util.Optional; -import javax.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import net.gpedro.integrations.slack.SlackAttachment; import net.gpedro.integrations.slack.SlackField; @@ -39,10 +39,9 @@ public class SlackWebhookMessageFactory { private static final String APPLE_PURCHASE_ALARM_TITLE = "애플이 결제 관련 알림을 보냈습니다."; private static final String APPLE_PURCHASE_ALARM_USERNAME = "애플 뱅크 알림"; - - private final UserRepository userRepository; - private final TokenProvider tokenProvider; private final PurchaseManager purchaseManager; + private final TokenProvider tokenProvider; + private final UserRepository userRepository; private static String getRequestBody(HttpServletRequest request) throws IOException { @@ -52,7 +51,7 @@ private static String getRequestBody(HttpServletRequest request) throws IOExcept try { InputStream inputStream = request.getInputStream(); - if (inputStream!=null) { + if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[128]; int bytesRead = -1; @@ -63,7 +62,7 @@ private static String getRequestBody(HttpServletRequest request) throws IOExcept } catch (IOException ex) { throw ex; } finally { - if (bufferedReader!=null) { + if (bufferedReader != null) { try { bufferedReader.close(); } catch (IOException ex) { @@ -173,10 +172,10 @@ private List generateSlackFieldList( HttpServletRequest request ) throws IOException { final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); - final String token = authHeader==null ? "null" : authHeader.substring("Bearer ".length()); - final Long userId = authHeader==null ? -1L : tokenProvider.getUserId(token); + final String token = authHeader == null ? "null" : authHeader.substring("Bearer ".length()); + final Long userId = authHeader == null ? -1L : tokenProvider.getUserId(token); final Optional user = - authHeader==null ? Optional.empty() : userRepository.findById(userId); + authHeader == null ? Optional.empty() : userRepository.findById(userId); final String yelloId = user.isPresent() ? user.get().getYelloId() : "null"; final String deviceToken = user.isPresent() ? user.get().getDeviceToken() : "null"; diff --git a/src/main/resources/static/docs/add-friend.html b/src/main/resources/static/docs/add-friend.html index c11c538f..64bb0f9c 100644 --- a/src/main/resources/static/docs/add-friend.html +++ b/src/main/resources/static/docs/add-friend.html @@ -4,25 +4,23 @@ - + 친구 추가하기 + + + +
+
+

상점에서 보상형 광고 가능한지 여부

+
+
+

요청

+
+
+
GET /api/v1/admob/possible/ADMOB_POINT HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+
+
+
+
+

요청 파라미터

+ + ++++ + + + + + + + + + + + + +
Table 1. /api/v1/admob/possible/{tag}
ParameterDescription

tag

보상형 광고의 종류

+
+
+
tag -> ADMOB_POINT
+
+
+
+
    +
  • +

    보상형 광고 다른곳에서 사용할 수도 있으므로 tag로 어떤 곳에서 사용하고 있는곳인지 tag로 명시

    +
  • +
  • +

    현재는 상점에 있는 보상형 광고 (ADMOB_POINT를 tag에 요청)

    +
  • +
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "광고 보고 포인트 얻기 가능 여부 조회에 성공했습니다.",
+  "data" : {
+    "createdAt" : "2024-01-10 01:12:00",
+    "isPossible" : true
+  }
+}
+
+
+
+
+

NOTE

+ +
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.17 API 릴리즈

    +
  • +
  • +

    2024.02.16 명세서 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/check-keyword.html b/src/main/resources/static/docs/check-keyword.html index 97598443..b9840ce7 100644 --- a/src/main/resources/static/docs/check-keyword.html +++ b/src/main/resources/static/docs/check-keyword.html @@ -4,25 +4,23 @@ - + 투표 키워드 확인하기 + + + +
+
+

내 정보 조회하기 V2

+
+
+

요청

+
+
+
GET /api/v2/user HTTP/1.1
+Authorization: Bearer your-access-token
+
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "유저 조회에 성공했습니다.",
+  "data" : {
+    "userId" : 1,
+    "name" : "name1",
+    "yelloId" : "yelloId1",
+    "gender" : "M",
+    "email" : "test1@test.com",
+    "profileImageUrl" : "test image",
+    "social" : "KAKAO",
+    "uuid" : "1",
+    "deviceToken" : "deviceToken#1",
+    "groupId" : 1,
+    "group" : "테스트 그룹 1 테스트 하위 그룹 1 20학번",
+    "groupType" : "UNIVERSITY",
+    "groupName" : "테스트 그룹 1",
+    "subGroupName" : "테스트 하위 그룹 1",
+    "groupAdmissionYear" : 20,
+    "recommendCount" : 0,
+    "ticketCount" : 0,
+    "point" : 200,
+    "subscribe" : "normal",
+    "yelloCount" : 100,
+    "friendCount" : 200
+  }
+}
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "userId": Long

    +
  • +
  • +

    "name": String

    +
  • +
  • +

    "yelloId": String

    +
  • +
  • +

    "gender": "M" | "F"

    +
  • +
  • +

    "email": String

    +
  • +
  • +

    "profileImageUrl": String

    +
  • +
  • +

    "social": "KAKAO" | "APPLE"

    +
  • +
  • +

    "uuid": String(10)

    +
  • +
  • +

    "deviceToken": String

    +
  • +
  • +

    "groupId": Long

    +
  • +
  • +

    "group": String

    +
  • +
  • +

    "groupType": "UNIVERSITY" | "HIGH_SCHOOL" | "MIDDLE_SCHOOL" | "SOPT"

    +
  • +
  • +

    "groupName": String

    +
  • +
  • +

    "subGroupName": String

    +
  • +
  • +

    "groupAdmissionYear": Integer

    +
  • +
  • +

    "recommendCount": Long

    +
  • +
  • +

    "ticketCount": Integer

    +
  • +
  • +

    "point": Integer

    +
  • +
  • +

    "subscribe": "normal" | "active" | "canceled"

    +
  • +
  • +

    "yelloCount": Integer

    +
  • +
  • +

    "friendCount": Integer

    +
  • +
+
+
+
+

Note

+
+
    +
  • +

    내 정보 조회하기 V1가 제공했던 단편적인 정보를 보완하기 위해 설계된 API입니다.

    +
  • +
  • +

    Authroization 헤더로 제공된 JWT Token에 담긴 유저의 정보가 응답으로 주어집니다.

    +
  • +
  • +

    유저 정보가 필요하면 해당 API를 사용하세요!

    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.01.07 첫 릴리즈

    +
  • +
  • +

    2024.01.09 필드 타입 추가

    +
  • +
  • +

    2024.01.30 groupId 필드 추가

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/check-user.html b/src/main/resources/static/docs/check-user.html index 4b0acfd6..80656ce5 100644 --- a/src/main/resources/static/docs/check-user.html +++ b/src/main/resources/static/docs/check-user.html @@ -4,25 +4,23 @@ - -내 정보 조회하기 + +내 정보 조회하기 V1 + + + +
+
+

이벤트 참여

+
+
+

요청

+
+
+
POST /api/v1/event HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+IdempotencyKey: 0397b5f3-ecdc-47d6-b5d7-2b1afcf00e87
+Content-Length: 27
+
+{
+  "tag" : "LUNCH_EVENT"
+}
+
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "이벤트 참여에 성공하였습니다."
+}
+
+
+
+
+

주의

+
+
    +
  • +

    "tag": "LUNCH_EVENT"

    +
  • +
+
+
+
+

NOTE

+
+
    +
  • +

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

    +
    +
      +
    • +

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

      +
    • +
    +
    +
  • +
  • +

    주의사항

    +
    +
      +
    • +

      tag 요청값에 해당하는 이벤트의 날짜와 시간이 모두 유효해야함. +(뭔가 에러나면 서버요청 ㄱㄱ)

      +
    • +
    • +

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

      +
    • +
    +
    +
  • +
  • +

    ADMOB

    +
    +
      +
    • +

      광고를 시청하기 전, 해당 API를 호출.

      +
    • +
    • +

      ADMOB 서버에 ServerSideVerificationOptions의 customData에 동일한 멱등성 키를 넘겨주세요.

      +
    • +
    +
    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.07 릴리즈

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/create-vote.html b/src/main/resources/static/docs/create-vote.html index cb8ac7e9..b971d0f0 100644 --- a/src/main/resources/static/docs/create-vote.html +++ b/src/main/resources/static/docs/create-vote.html @@ -4,25 +4,23 @@ - + 투표 생성하기 + + + +
+
+

탈퇴 & 사유 저장 v2

+
+
+

요청

+
+
+
DELETE /api/v2/user HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+Content-Length: 37
+
+{
+  "value" : "오류가 많아서"
+}
+
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "유저 탈퇴에 성공했습니다."
+}
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "value": String

    +
    +
      +
    • +

      value는 탈퇴 사유를 보내주시면 됩니다.

      +
    • +
    +
    +
  • +
+
+
+

필드 타입

+
+
+
+

NOTE

+
+
    +
  • +

    AccessToken에 해당하는 User의 탈퇴 처리 및 탈퇴 사유를 저장하는 API입니다.

    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.01.27 API 릴리즈

    +
  • +
  • +

    2024.01.09 명세 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/delete-user.html b/src/main/resources/static/docs/delete-user.html index 63e884e2..71b09cdf 100644 --- a/src/main/resources/static/docs/delete-user.html +++ b/src/main/resources/static/docs/delete-user.html @@ -4,25 +4,23 @@ - + 유저 탈퇴하기 + + + +
+
+

유저 프로필 수정

+
+
+

요청

+
+
+
POST /api/v1/user HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+Content-Length: 195
+
+{
+  "name" : "after",
+  "yelloId" : "afterupdate",
+  "gender" : "M",
+  "email" : "afterupdate@yello.com",
+  "profileImageUrl" : "https://after.com",
+  "groupId" : 1,
+  "groupAdmissionYear" : 24
+}
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "name": String

    +
  • +
  • +

    "yelloId": String

    +
  • +
  • +

    "gender": "M" | "F"

    +
  • +
  • +

    "email": String

    +
  • +
  • +

    "profileImageUrl": String

    +
  • +
  • +

    "groupId": Long

    +
    +
      +
    • +

      대학교 검색 또는 고등학교 검색 API를 이용하여 유저가 선택한 groupId를 입력해주세요.

      +
    • +
    • +

      해당 groupId에 해당하는 group이 고등학교면, 해당 유저는 고등학생 / 대학교면 대학생이 됩니다.

      +
    • +
    +
    +
  • +
  • +

    "groupAdmissionYear": Integer

    +
    +
      +
    • +

      대학생이면 학번 / 고등학생이면 '반(class)'를 넣어주세요.

      +
    • +
    +
    +
  • +
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "유저 상세 정보 수정에 성공하였습니다."
+}
+
+
+
+

필드 타입

+
+
+
+

NOTE

+
+
    +
  • +

    포인트 / 구독정보 / 로그인 정보와 같이 user-pure하지 않은 정보는 수정할 수 없도록 설계하였습니다.

    +
    +
      +
    • +

      해당 정보 수정API는 도메인 별로 만들 예정입니다.

      +
    • +
    +
    +
  • +
  • +

    비즈니스 로직인 '1년에 1회 수정 가능하다'라는 조건과 상관없이 여러번 호출하여 유저 정보 수정이 가능합니다.

    +
    +
      +
    • +

      해당 비즈니스 로직을 만족하기 위해서 프로필 수정 가능 여부 조회를 같이 사용해주세요.

      +
      +
        +
      • +

        UserGroup(groupId, groupAdmissionYear)에 대한 정보가 유저의 기존 정보와 달라졌을때 프로필 수정 가능 여부 가 갱신됩니다.

        +
      • +
      +
      +
    • +
    +
    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.02 groupId, groupAdmissionYear 변경에 따른 제약조건 추가

    +
  • +
  • +

    2024.01.31 릴리즈

    +
  • +
  • +

    2024.01.09 명세 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/find-event.html b/src/main/resources/static/docs/find-event.html new file mode 100644 index 00000000..292228db --- /dev/null +++ b/src/main/resources/static/docs/find-event.html @@ -0,0 +1,728 @@ + + + + + + + +이벤트 조회 + + + + + +
+
+

이벤트 조회

+
+
+

요청

+
+
+
GET /api/v1/event HTTP/1.1
+Authorization: Bearer your-access-token
+
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "이벤트 전체 조회에 성공하였습니다.",
+  "data" : [ {
+    "tag" : "LUNCH_EVENT",
+    "startDate" : "2024-01-01T00:00:00+09:00",
+    "endDate" : "2024-12-31T00:00:00+09:00",
+    "title" : "점심 시간 깜짝 선물!",
+    "subTitle" : "평일 12-14시 최대 1회까지 참여 가능",
+    "animationList" : [ {
+      "v" : "로티1"
+    }, {
+      "v" : "로티2"
+    } ],
+    "eventReward" : {
+      "startTime" : "12:00+09:00",
+      "endTime" : "14:00+09:00",
+      "rewardCount" : 1,
+      "eventRewardItem" : [ {
+        "tag" : "POINT",
+        "eventRewardTitle" : "최대 200P",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/coin-stack.svg",
+        "maxRewardValue" : 200,
+        "minRewardValue" : 10,
+        "eventRewardProbability" : 10,
+        "randomTag" : "FIXED"
+      }, {
+        "tag" : "TICKET",
+        "eventRewardTitle" : "열람권 1개",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/key.svg",
+        "maxRewardValue" : 1,
+        "minRewardValue" : 1,
+        "eventRewardProbability" : 90,
+        "randomTag" : "RANDOM"
+      } ]
+    }
+  }, {
+    "tag" : "ADMOB_POINT",
+    "startDate" : "2024-01-01T00:00:00+09:00",
+    "endDate" : "2024-12-31T00:00:00+09:00",
+    "title" : "ADMOB 광고입니다.",
+    "subTitle" : "ADMOB 광고는 영구 사용 가능 설정입니다",
+    "animationList" : [ ],
+    "eventReward" : {
+      "startTime" : "00:00+09:00",
+      "endTime" : "23:59:59.000999999+09:00",
+      "rewardCount" : 1,
+      "eventRewardItem" : [ {
+        "tag" : "ADMOB_POINT",
+        "eventRewardTitle" : "광고 보고 포인트 얻기",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/coin-stack.svg",
+        "maxRewardValue" : 50,
+        "minRewardValue" : 50,
+        "eventRewardProbability" : 100,
+        "randomTag" : "FIXED"
+      } ]
+    }
+  } ]
+}
+
+
+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "이벤트 전체 조회에 성공하였습니다.",
+  "data" : [ {
+    "tag" : "LUNCH_EVENT",
+    "startDate" : "2024-01-01T00:00:00+09:00",
+    "endDate" : "2024-12-31T00:00:00+09:00",
+    "title" : "점심 시간 깜짝 선물!",
+    "subTitle" : "평일 12-14시 최대 1회까지 참여 가능",
+    "animationList" : [ {
+      "v" : "로티1"
+    }, {
+      "v" : "로티2"
+    } ],
+    "eventReward" : {
+      "startTime" : "22:00+09:00",
+      "endTime" : "00:00+09:00",
+      "rewardCount" : 1,
+      "eventRewardItem" : [ {
+        "tag" : "POINT",
+        "eventRewardTitle" : "최대 200P",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/coin-stack.svg",
+        "maxRewardValue" : 200,
+        "minRewardValue" : 10,
+        "eventRewardProbability" : 40,
+        "randomTag" : "FIXED"
+      }, {
+        "tag" : "TICKET",
+        "eventRewardTitle" : "열람권 1개",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/key.svg",
+        "maxRewardValue" : 1,
+        "minRewardValue" : 1,
+        "eventRewardProbability" : 60,
+        "randomTag" : "RANDOM"
+      } ]
+    }
+  }, {
+    "tag" : "ADMOB_POINT",
+    "startDate" : "2024-01-01T00:00:00+09:00",
+    "endDate" : "2024-12-31T00:00:00+09:00",
+    "title" : "ADMOB 광고입니다.",
+    "subTitle" : "ADMOB 광고는 영구 사용 가능 설정입니다",
+    "animationList" : [ ],
+    "eventReward" : {
+      "startTime" : "00:00+09:00",
+      "endTime" : "23:59:59.000999999+09:00",
+      "rewardCount" : 1,
+      "eventRewardItem" : [ {
+        "tag" : "ADMOB_POINT",
+        "eventRewardTitle" : "광고 보고 포인트 얻기",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/coin-stack.svg",
+        "maxRewardValue" : 50,
+        "minRewardValue" : 50,
+        "eventRewardProbability" : 100,
+        "randomTag" : "FIXED"
+      } ]
+    }
+  } ]
+}
+
+
+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "이벤트 전체 조회에 성공하였습니다.",
+  "data" : [ {
+    "tag" : "LUNCH_EVENT",
+    "startDate" : "2024-01-01T00:00:00+09:00",
+    "endDate" : "2024-12-31T00:00:00+09:00",
+    "title" : "점심 시간 깜짝 선물!",
+    "subTitle" : "평일 12-14시 최대 1회까지 참여 가능",
+    "animationList" : [ {
+      "v" : "로티1"
+    }, {
+      "v" : "로티2"
+    } ],
+    "eventReward" : null
+  }, {
+    "tag" : "ADMOB_POINT",
+    "startDate" : "2024-01-01T00:00:00+09:00",
+    "endDate" : "2024-12-31T00:00:00+09:00",
+    "title" : "ADMOB 광고입니다.",
+    "subTitle" : "ADMOB 광고는 영구 사용 가능 설정입니다",
+    "animationList" : [ ],
+    "eventReward" : {
+      "startTime" : "00:00+09:00",
+      "endTime" : "23:59:59.000999999+09:00",
+      "rewardCount" : 1,
+      "eventRewardItem" : [ {
+        "tag" : "ADMOB_POINT",
+        "eventRewardTitle" : "광고 보고 포인트 얻기",
+        "eventRewardImage" : "https://storage.googleapis.com/yelloworld/image/coin-stack.svg",
+        "maxRewardValue" : 50,
+        "minRewardValue" : 50,
+        "eventRewardProbability" : 100,
+        "randomTag" : "FIXED"
+      } ]
+    }
+  } ]
+}
+
+
+
+
+

주의

+
+
    +
  • +

    data: Response[]

    +
  • +
  • +

    Response

    +
  • +
  • +

    tag : "LUNCH_EVENT"

    +
    +
      +
    • +

      LUNCH_EVENT에 해당하는 *Response*가 없으면 Render 해주지 말아주세요

      +
    • +
    +
    +
  • +
  • +

    startDate : "2024-01-01T00:00:00+09:00"

    +
  • +
  • +

    endDate : "2024-12-31T00:00:00+09:00"

    +
  • +
  • +

    title : "점심 시간 깜짝 선물!"

    +
  • +
  • +

    subTitle : "평일 12-14시 최대 1회까지 참여 가능"

    +
  • +
  • +

    animationList : string[]

    +
    +
      +
    • +

      URL이 들어감.

      +
    • +
    +
    +
  • +
  • +

    eventReward: EventReward | null

    +
    +
      +
    • +

      해당 필드가 null일 시, 이벤트 보여주지 않도록 해주세요.

      +
    • +
    +
    +
  • +
+
+
+
+

NOTE

+
+
    +
  • +

    ! LUNCH_EVENT에 해당하는 Response가 없거나, LUNCH_EVENT Response의 eventReward가 null이면, 메인화면 접속시, 이벤트 화면을 띄워주지마세요.

    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.06 릴리즈

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/find-friend-votes-v2.html b/src/main/resources/static/docs/find-friend-votes-v2.html new file mode 100644 index 00000000..c799b0c0 --- /dev/null +++ b/src/main/resources/static/docs/find-friend-votes-v2.html @@ -0,0 +1,688 @@ + + + + + + + +친구 투표 전체 조회 v2 + + + + + +
+
+

친구 투표 전체 조회 v2

+
+
+

요청

+
+
+
GET /api/v2/vote/friend?page=0&type=send HTTP/1.1
+Authorization: Bearer your-access-token
+
+
+
+
+

요청 파라미터

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지네이션 페이지 번호

type

쪽지 유형 선택

+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "투표 조회에 성공했습니다.",
+  "data" : {
+    "totalCount" : 1,
+    "friendVotes" : [ {
+      "id" : 1,
+      "senderId" : 1,
+      "senderName" : "name1",
+      "senderGender" : "MALE",
+      "senderYelloId" : "yelloId1",
+      "senderProfileImage" : "test image",
+      "receiverId" : 2,
+      "receiverName" : "name2",
+      "receiverGender" : "MALE",
+      "receiverYelloId" : "yelloId2",
+      "receiverProfileImage" : "test image",
+      "vote" : {
+        "nameHead" : "나는",
+        "nameFoot" : "와",
+        "keywordHead" : "멋진",
+        "keyword" : "test",
+        "keywordFoot" : "에서 놀고싶어"
+      },
+      "isHintUsed" : false,
+      "createdAt" : "0초 전",
+      "isUserSenderVote" : true
+    } ]
+  }
+}
+
+
+ ++++ + + + + + + +

type

조회할 쪽지 종류 (null → 모든쪽지, send→ 보낸쪽지)

+
+

필드 타입

+
+
+
    +
  • +

    "totalCount": Integer

    +
  • +
  • +

    "friendVotes": FriendVote[]

    +
  • +
  • +

    "isUserSenderVote" : Boolean (내가 보냈는지 여부)

    +
  • +
  • +

    FriendVote

    +
    +
      +
    • +

      "id": Long

      +
    • +
    • +

      "senderId" : Long

      +
    • +
    • +

      "senderName" : String

      +
    • +
    • +

      "senderYelloId" : String

      +
    • +
    • +

      "senderGender": "MALE" | "FEMALE"

      +
    • +
    • +

      "senderProfileImage" : String

      +
    • +
    • +

      "receiverId" : Long

      +
    • +
    • +

      "receiverName": String

      +
    • +
    • +

      "receiverYelloId" : String

      +
    • +
    • +

      "receiverGender": "MALE" | "FEMALE"

      +
    • +
    • +

      "receiverProfileImage": String

      +
    • +
    • +

      "vote": Vote

      +
    • +
    • +

      "isHintUsed": Boolean

      +
    • +
    • +

      "createdAt": "{0}초 전" | "{0}분 전" | "{0}시간 전" | "{0}일 전"

      +
    • +
    +
    +
  • +
  • +

    Vote

    +
    +
      +
    • +

      "nameHead": String

      +
    • +
    • +

      "nameFoot": String

      +
    • +
    • +

      "keywordHead": String

      +
    • +
    • +

      "keyword": String

      +
    • +
    • +

      "keywordFoot": String

      +
    • +
    +
    +
  • +
+
+
+
+

Excpetion

+
+
    +
  • +

    잘못된 type을 queryString에 보내는 경우

    +
  • +
+
+
+
+
{
+    "status": 403,
+    "message": "[VoteForbiddenException] 잘못된 투표 유형입니다."
+}
+
+
+
+
+

NOTE

+
+
    +
  • +

    모든 종류의 쪽지를 조회할 때 /api/v1/vote/friend?page=0 으로 요청해주세요

    +
    +
      +
    • +

      type을 명시하지 마세요

      +
    • +
    +
    +
  • +
  • +

    내가 보낸 쪽지를 조회할 때 /api/v1/vote/friend?page=0&type=send 으로 요청해주세요

    +
  • +
  • +

    senderGender 필드가 다른 API와 일관되지 못한점 미안해요 ㅠ

    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2924.01.30 API 릴리즈

    +
  • +
  • +

    2024.01.26 필드 명세 업데이트

    +
  • +
  • +

    2024.01.09 type 명세 업데이트

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/find-friend-votes.html b/src/main/resources/static/docs/find-friend-votes.html index 21065f52..8a905b95 100644 --- a/src/main/resources/static/docs/find-friend-votes.html +++ b/src/main/resources/static/docs/find-friend-votes.html @@ -4,25 +4,23 @@ - -친구 투표 전체 조회 + +친구 투표 전체 조회 (최신버전 → v2 확인) + + + + +그룹 추천 친구 조회하기 + +
-
-

그룹 추천 친구 조회하기

-
-
-

요청

-
-
-
GET /api/v1/friend/recommend/userGroup?page=0 HTTP/1.1
+
+

그룹 추천 친구 조회하기

+
+
+

요청

+
+
+
GET /api/v1/friend/recommend/school?page=0 HTTP/1.1
 Authorization: Bearer your-access-token
-
-
-
-
-

요청 파라미터

- - - - - - - - - - - - - - - - - -
ParameterDescription

page -

페이지네이션 페이지 번호

-
-
-
-

응답

-
-
+
+
+
+
+

요청 파라미터

+ ++++ + + + + + + + + + + + + +
ParameterDescription

page

페이지네이션 페이지 번호

+
+
+

응답

+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
 Content-Type: application/json
 
 {
@@ -2191,22 +491,21 @@ 

응답

"friends" : [ { "id" : 1, "name" : "name1", - "group" : "테스트 대학교 1 테스트 학과 1 20학번", + "group" : "테스트 그룹 1 테스트 하위 그룹 1 20학번", "profileImage" : "test image" } ] } }
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file diff --git a/src/main/resources/static/docs/find-kakao-friends.html b/src/main/resources/static/docs/find-kakao-friends.html index fe65ed1e..e9255507 100644 --- a/src/main/resources/static/docs/find-kakao-friends.html +++ b/src/main/resources/static/docs/find-kakao-friends.html @@ -4,25 +4,23 @@ - + 카카오 추천 친구 조회하기 + + + +
+
+

공지 조회

+
+
+

요청

+
+
+
GET /api/v1/notice/NOTICE HTTP/1.1
+Authorization: Bearer your-access-token
+
+
+
+
+

요청 파라미터

+ + ++++ + + + + + + + + + + + + +
Table 1. /api/v1/notice/{tag}
ParameterDescription

tag

공지의 종류

+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "공지 조회에 성공하였습니다.",
+  "data" : {
+    "imageUrl" : "imageUrl",
+    "redirectUrl" : "redirectUrl",
+    "startDate" : "2023-12-29",
+    "endDate" : "2024-01-04",
+    "isAvailable" : false,
+    "type" : "NOTICE",
+    "title" : "notice title"
+  }
+}
+
+
+
+

path variable

+
+
+
    +
  • +

    tag(ENUM 값) → "NOTICE" | "BANNER | "PROFILE-BANNER"

    +
    +
      +
    • +

      NOTICE : 진입시 공지

      +
    • +
    • +

      BANNER : 내 쪽지 배너

      +
    • +
    • +

      PROFILE-BANNER : 프로필 배너

      +
    • +
    +
    +
  • +
+
+
+
+

주의

+
+

유효한 날짜의 공지가 존재하지 않는 경우

+
+
+

→ isAvailable은 false로 오고 날짜 제외한 나머지 값은 빈값으로 전달

+
+
+
+
{
+    "status": 200,
+    "message": "공지 조회에 성공하였습니다.",
+    "data": {
+    "imageUrl": "",
+    "redirectUrl": "",
+    "startDate": "2024-01-27",
+    "endDate": "2024-01-27",
+    "isAvailable": false,
+    "type": null,
+    "title": ""
+}
+
+
+
+
+

NOTE

+
+
    +
  • +

    공지 정보를 조회하는 API입니다.

    +
  • +
  • +

    isAvailable true일 때, 유효한 1개의 공지를 반환합니다.

    +
    +
      +
    • +

      요구사항에 따라 여러개의 공지를 반환할 수 있도록 염두하고 있습니다.

      +
    • +
    • +

      반환되는 공지를 무조건 View에 띄워주시면 되겠습니다.

      +
    • +
    +
    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.01.29 API tag 추가 및 수정

    +
  • +
  • +

    2024.01.26 API 릴리즈

    +
  • +
  • +

    2024.01.09 명세 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/find-onboarding-friends.html b/src/main/resources/static/docs/find-onboarding-friends.html index 3e3723d7..f53d6a2a 100644 --- a/src/main/resources/static/docs/find-onboarding-friends.html +++ b/src/main/resources/static/docs/find-onboarding-friends.html @@ -4,25 +4,23 @@ - + 가입한 친구 목록 불러오기 - + + + + +YELL:O API 문서 + + + +
- +
+

APIs

+
+ + + + + +
+

Pay API

+
+ +
+
+ + +
- - - - + + \ 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 c5f4577b..79046842 100644 --- a/src/main/resources/static/docs/login.html +++ b/src/main/resources/static/docs/login.html @@ -4,25 +4,23 @@ - + 소셜 로그인 + + + +
+
+

유저 구독 정보

+
+
+

요청

+
+
+
GET /api/v1/user/subscribe HTTP/1.1
+Authorization: Bearer your-access-token
+
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "구독 정보 조회에 성공하였습니다.",
+  "data" : {
+    "id" : 1,
+    "subscribe" : "normal",
+    "expiredDate" : "2024-01-08"
+  }
+}
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "id": Long

    +
  • +
  • +

    "subscribe": "normal" | "active" | "canceled"

    +
  • +
  • +

    "expireDate": String(10)

    +
    +
      +
    • +

      YYYY-MM-DD (ISO-8601)

      +
    • +
    +
    +
  • +
+
+
+
+

Note

+ +
+
+

CHANGELOG

+
+
    +
  • +

    2024.01.25 expiredDate 필드명 및 subscribe 소문자 오류 수정

    +
  • +
  • +

    2024.01.23 API 릴리즈

    +
  • +
  • +

    2024.01.09 명세 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/reissue-token.html b/src/main/resources/static/docs/reissue-token.html index 20957548..666fa820 100644 --- a/src/main/resources/static/docs/reissue-token.html +++ b/src/main/resources/static/docs/reissue-token.html @@ -4,25 +4,23 @@ - + 토큰 재발급 + + + +
+
+

이벤트 참여

+
+
+

요청

+
+
+
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/reward-event.html b/src/main/resources/static/docs/reward-event.html new file mode 100644 index 00000000..94e6598a --- /dev/null +++ b/src/main/resources/static/docs/reward-event.html @@ -0,0 +1,582 @@ + + + + + + + +공지 조회 + + + + + +
+
+

공지 조회

+
+
+

요청

+
+
+
POST /api/v1/event/reward HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+IdempotencyKey: 87552f7c-9b62-4b12-b567-1bd062b09288
+
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "이벤트 보상에 성공하였습니다.",
+  "data" : {
+    "rewardTag" : "TICKET",
+    "rewardValue" : 1,
+    "rewardTitle" : "열람권 1개를 얻었어요!",
+    "rewardImage" : "https://storage.googleapis.com/yelloworld/image/ticket-reward.svg"
+  }
+}
+
+
+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "이벤트 보상에 성공하였습니다.",
+  "data" : {
+    "rewardTag" : "POINT",
+    "rewardValue" : 79,
+    "rewardTitle" : "79 포인트를 얻었어요!",
+    "rewardImage" : "https://storage.googleapis.com/yelloworld/image/coin-reward.svg"
+  }
+}
+
+
+
+
+

주의

+
+
    +
  • +

    "rewardTag": "TICKET" | "POINT"

    +
  • +
  • +

    "rewardValue": Long

    +
  • +
  • +

    "rewardTitle": String

    +
  • +
  • +

    "rewardImage": String

    +
  • +
+
+
+
+

NOTE

+
+
    +
  • +

    Header에 이벤트 참여에 입력했던 멱등키를 넣어주세요.

    +
    +
      +
    • +

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

      +
    • +
    +
    +
  • +
  • +

    ADMOB

    +
    +
      +
    • +

      광고를 시청한 후, 해당 API를 호출.

      +
    • +
    • +

      ADMOB 서버에 ServerSideVerificationOptions의 customData에 동일한 멱등성 키를 넘겨주세요.

      +
    • +
    • +

      이벤트 참여에 넣어준 멱등키와 동일하여 검증이되면, 정상 보상, 그렇지 않으면 이상으로 판단하여 400번대 에러

      +
    • +
    +
    +
  • +
  • +

    보상

    +
    +
      +
    • +

      지금 랜덤 보상이 구현안되있는데, 차후 서버에서 보상 처리하겠음. +클라측에서는 View과 종속적인 값만 처리하면 됨.

      +
    • +
    +
    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.07 릴리즈

    +
  • +
+
+
+
+
+
+ + + \ 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 740d483b..2534ed88 100644 --- a/src/main/resources/static/docs/search-department.html +++ b/src/main/resources/static/docs/search-department.html @@ -1,2195 +1,493 @@ - - - - - 대학교 학과 검색하기 - - + + + + +대학교 학과 검색하기 + +
-
-

대학교 학과 검색하기

-
-
-

요청

-
-
-
GET /api/v1/auth/userGroup/department?page=0&userGroup=userGroup+name+here&keyword=keyword+here HTTP/1.1
-
-
-
-
-

요청 파라미터

- - - - - - - - - - - - - - - - - - - - - - - - - -
ParameterDescription

page -

페이지네이션 페이지 번호

-

- userGroup

학교 이름

keyword -

검색할 쿼리

-
-
-

응답

-
-
+
+

대학교 학과 검색하기

+
+
+

요청

+
+
+
GET /api/v1/auth/group/univ/department?page=0&name=school+name+here&keyword=keyword+here HTTP/1.1
+
+
+
+
+

요청 파라미터

+ ++++ + + + + + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지네이션 페이지 번호

name

학교 이름

keyword

검색할 쿼리

+
+
+

응답

+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
 Content-Type: application/json
 
 {
@@ -2199,21 +497,20 @@ 

응답

"totalCount" : 0, "groupList" : [ { "groupId" : 1, - "departmentName" : "테스트 학과 1" + "departmentName" : "테스트 하위 그룹 1" } ] } }
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file diff --git a/src/main/resources/static/docs/search-friend.html b/src/main/resources/static/docs/search-friend.html index faa74135..8bf480d9 100644 --- a/src/main/resources/static/docs/search-friend.html +++ b/src/main/resources/static/docs/search-friend.html @@ -4,25 +4,23 @@ - + 친구 검색하기 + + + +
+
+

고등학교 이름으로 학반 검색하기

+
+
+

요청

+
+
+
GET /api/v1/auth/group/high/class?name=school+name+here&keyword=keyword+here HTTP/1.1
+
+
+
+
+

요청 파라미터

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

name

학교 이름

keyword

검색할 쿼리

+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "학반 검색에 성공했습니다.",
+  "data" : {
+    "groupId" : 1
+  }
+}
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/search-high-name.html b/src/main/resources/static/docs/search-high-name.html new file mode 100644 index 00000000..0aa8cd5f --- /dev/null +++ b/src/main/resources/static/docs/search-high-name.html @@ -0,0 +1,509 @@ + + + + + + + +고등학교 이름 검색하기 + + + + + +
+
+

고등학교 이름 검색하기

+
+
+

요청

+
+
+
GET /api/v1/auth/group/high/name?page=0&keyword=keyword+here HTTP/1.1
+
+
+
+
+

요청 파라미터

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지네이션 페이지 번호

keyword

검색할 쿼리

+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "학교 이름 검색에 성공했습니다.",
+  "data" : {
+    "totalCount" : 0,
+    "groupNameList" : [ "groupName" ]
+  }
+}
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/search-school.html b/src/main/resources/static/docs/search-school.html index bb65a5eb..7527575c 100644 --- a/src/main/resources/static/docs/search-school.html +++ b/src/main/resources/static/docs/search-school.html @@ -1,2211 +1,509 @@ - - - - - 대학교 검색하기 - - + + + + +대학교 검색하기 + +
-
-

대학교 검색하기

-
-
-

요청

-
-
-
GET /api/v1/auth/userGroup?page=0&keyword=keyword+here HTTP/1.1
-
-
-
-
-

요청 파라미터

- - - - - - - - - - - - - - - - - - - - - -
ParameterDescription

page -

페이지네이션 페이지 번호

-

keyword -

검색할 쿼리

-
-
-

응답

-
-
+
+

대학교 검색하기

+
+
+

요청

+
+
+
GET /api/v1/auth/group/univ/name?page=0&keyword=keyword+here HTTP/1.1
+
+
+
+
+

요청 파라미터

+ ++++ + + + + + + + + + + + + + + + + +
ParameterDescription

page

페이지네이션 페이지 번호

keyword

검색할 쿼리

+
+
+

응답

+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
 Content-Type: application/json
 
 {
   "status" : 200,
-  "message" : "학교 검색에 성공했습니다.",
+  "message" : "학교 이름 검색에 성공했습니다.",
   "data" : {
     "totalCount" : 0,
     "groupNameList" : [ "groupName" ]
   }
 }
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file diff --git a/src/main/resources/static/docs/shuffle-friends.html b/src/main/resources/static/docs/shuffle-friends.html index 6dd6752a..8bea2e6e 100644 --- a/src/main/resources/static/docs/shuffle-friends.html +++ b/src/main/resources/static/docs/shuffle-friends.html @@ -4,25 +4,23 @@ - + 셔플한 친구 조회하기 + + + +
+
+

프로필 수정 가능 여부 조회

+
+
+

요청

+
+
+
GET /api/v1/user/data/account-updated-at HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "TAG": "account-updated-at" | "recommended" | "withdraw-reason"

    +
  • +
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "유저 데이터 조회에 성공하였습니다.",
+  "data" : {
+    "tag" : "account-updated-at",
+    "value" : "false|2024-01-31|2024-01-31"
+  }
+}
+
+
+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "유저 데이터 조회에 성공하였습니다.",
+  "data" : {
+    "tag" : "account-updated-at",
+    "value" : "true|null|2024-01-31"
+  }
+}
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "tag": "ACCOUNT_UPDATED_AT" | "RECOMMENDED" | "WITHDRAW_REASON"

    +
  • +
  • +

    "value": String

    +
    +
      +
    • +

      ACCOUNT_UPDATED_AT일 때, {boolean}|{updated_at}|{created_at} 를 반환합니다.

      +
      +
        +
      • +

        boolean 및 updated_at 값을 parse하여 사용해주세요.

        +
      • +
      • +

        예시) "false|2024-01-31|2024-01-31"

        +
      • +
      • +

        "updated_at": "YYYY-mm-dd" | null

        +
      • +
      +
      +
    • +
    +
    +
  • +
+
+
+
+

NOTE

+
+
    +
  • +

    User의 다양한 정보를 조회하는 범용적인 API입니다.

    +
  • +
  • +

    account-updated-at 을 통해 AccessToken에 해당하는 User의 프로필 수정 가능 여부 조회를 조회하세요.

    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.02.10 응답 케이스 추가

    +
  • +
  • +

    2024.01.30 릴리즈

    +
  • +
  • +

    2024.01.09 명세 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/user-data-post.html b/src/main/resources/static/docs/user-data-post.html new file mode 100644 index 00000000..c545b94d --- /dev/null +++ b/src/main/resources/static/docs/user-data-post.html @@ -0,0 +1,553 @@ + + + + + + + +유저 기타 정보 저장 (명세) + + + + + +
+
+

유저 기타 정보 저장 (명세)

+
+
+

요청

+
+
+
POST /api/v1/user/data/withdraw-reason HTTP/1.1
+Content-Type: application/json;charset=UTF-8
+Authorization: Bearer your-access-token
+Content-Length: 34
+
+{
+  "value" : "바꿀 값임요"
+}
+
+
+
+

필드 타입

+
+
+
    +
  • +

    "TAG": "withdraw-reason" | "account-update-at" | "recommended"

    +
  • +
  • +

    "value": String

    +
    +
      +
    • +

      withdraw-reason 일 때, 255byte 이내의 string

      +
    • +
    • +

      account-update-at 또는 recommended 일 때, ISO-8601 + ZoneInfo

      +
      +
        +
      • +

        예시) 2011-12-03T10:15:30+01:00

        +
      • +
      +
      +
    • +
    +
    +
  • +
+
+
+
+

응답

+
+
+
HTTP/1.1 200 OK
+Vary: Origin
+Vary: Access-Control-Request-Method
+Vary: Access-Control-Request-Headers
+Content-Type: application/json
+
+{
+  "status" : 200,
+  "message" : "유저 데이터 수정에 성공하였습니다."
+}
+
+
+
+
+

NOTE

+
+
    +
  • +

    account-update-atrecommended 기능은 유저 정보 수정회원가입에 통합되어있습니다

    +
    +
      +
    • +

      정말 필요에 의해 값을 수정해야하는 경우만 사용해주세요.

      +
    • +
    +
    +
  • +
  • +

    User의 다양한 정보를 저장하는 API로 범용적인 확장할 예정입니다.

    +
  • +
+
+
+
+

CHANGELOG

+
+
    +
  • +

    2024.01.31 릴리즈

    +
  • +
  • +

    2024.01.30 탈퇴 v2 분리로 인한 명세 업데이트

    +
  • +
  • +

    2024.01.09 명세 작성

    +
  • +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/static/docs/validate-yelloid.html b/src/main/resources/static/docs/validate-yelloid.html index 89a3508a..8d8486d9 100644 --- a/src/main/resources/static/docs/validate-yelloid.html +++ b/src/main/resources/static/docs/validate-yelloid.html @@ -4,25 +4,23 @@ - + 옐로 아이디 중복 확인