From 1b43baf12d2610291daa29158bce779e19ef9b8b Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 21 Jul 2023 19:45:23 +0900 Subject: [PATCH 01/94] =?UTF-8?q?CodeDeploy=20=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- appspec.yml | 18 ++++++++++++++++++ scripts/kill_process.sh | 16 ++++++++++++++++ scripts/run_process.sh | 12 ++++++++++++ ...veApplication.kt => SodaLiveApplication.kt} | 4 ++-- 4 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 appspec.yml create mode 100644 scripts/kill_process.sh create mode 100644 scripts/run_process.sh rename src/main/kotlin/kr/co/vividnext/sodalive/{SodaliveApplication.kt => SodaLiveApplication.kt} (74%) diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..77ea8fe --- /dev/null +++ b/appspec.yml @@ -0,0 +1,18 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ec2-user + overwrite: yes + +hooks: + ApplicationStart: + - location: scripts/run_process.sh # ApplicationStart 단계에서 해당 파일을 실행해라 + timeout: 60 + runas: ec2-user + + ApplicationStop: + - location: scripts/kill_process.sh # ApplicationStart 단계에서 해당 파일을 실행해라 + timeout: 100 + runas: ec2-user diff --git a/scripts/kill_process.sh b/scripts/kill_process.sh new file mode 100644 index 0000000..14ff76d --- /dev/null +++ b/scripts/kill_process.sh @@ -0,0 +1,16 @@ +#!/bin/bash +BUILD_JAR=$(ls /home/ec2-user/build/libs/*.jar) # jar가 위치하는 곳 +JAR_NAME=$(basename $BUILD_JAR) +echo "> build 파일명: $JAR_NAME" >> /home/ec2-user/deploy.log + +echo "> 현재 실행중인 애플리케이션 pid 확인" >> /home/ec2-user/deploy.log +CURRENT_PID=$(pgrep -f $JAR_NAME) + +if [ -z $CURRENT_PID ] +then + echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다." >> /home/ec2-user/deploy.log +else + echo "> kill -15 $CURRENT_PID" + kill -15 $CURRENT_PID + sleep 5 +fi diff --git a/scripts/run_process.sh b/scripts/run_process.sh new file mode 100644 index 0000000..c0826ba --- /dev/null +++ b/scripts/run_process.sh @@ -0,0 +1,12 @@ +#!/bin/bash +BUILD_JAR=$(ls /home/ec2-user/build/libs/*.jar) # jar가 위치하는 곳 +JAR_NAME=$(basename $BUILD_JAR) + +echo "> build 파일 복사" >> /home/ec2-user/deploy.log +DEPLOY_PATH=/home/ec2-user/ +cp $BUILD_JAR $DEPLOY_PATH + +DEPLOY_JAR=$DEPLOY_PATH$JAR_NAME +echo "> DEPLOY_JAR 배포" >> /home/ec2-user/deploy.log +chmod +x $DEPLOY_JAR +nohup java -jar $DEPLOY_JAR >> /home/ec2-user/deploy.log 2> /dev/null < /dev/null & diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/SodaliveApplication.kt b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt similarity index 74% rename from src/main/kotlin/kr/co/vividnext/sodalive/SodaliveApplication.kt rename to src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt index 94e947f..1a8e541 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/SodaliveApplication.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt @@ -4,8 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication -class SodaliveApplication +class SodaLiveApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } From 72a94bb311a47542fcbea0c6c146d5c2f26c9db0 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 21 Jul 2023 19:49:34 +0900 Subject: [PATCH 02/94] =?UTF-8?q?lint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 262c8b2..b8d1b4b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "2.7.14" - id("io.spring.dependency-management") version "1.0.15.RELEASE" + id("org.springframework.boot") version "2.7.14" + id("io.spring.dependency-management") version "1.0.15.RELEASE" val kotlinVersion = "1.6.21" kotlin("jvm") version kotlinVersion @@ -18,42 +18,42 @@ version = "0.0.1-SNAPSHOT" val querydslVersion = "5.0.0" java { - sourceCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_11 } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-data-redis") - implementation("org.springframework.boot:spring-boot-starter-security") - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-data-redis") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-reflect") // querydsl (추가 설정) implementation("com.querydsl:querydsl-jpa:$querydslVersion") kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa") kapt("org.springframework.boot:spring-boot-configuration-processor") - developmentOnly("org.springframework.boot:spring-boot-devtools") - runtimeOnly("com.h2database:h2") - runtimeOnly("com.mysql:mysql-connector-j") - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.springframework.security:spring-security-test") + developmentOnly("org.springframework.boot:spring-boot-devtools") + runtimeOnly("com.h2database:h2") + runtimeOnly("com.mysql:mysql-connector-j") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") } tasks.withType { - kotlinOptions { - freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "11" - } + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "11" + } } tasks.withType { - useJUnitPlatform() + useJUnitPlatform() } tasks.getByName("jar") { From 9265dc7f6afbb4b651f4441cc9c39617f9cb8e85 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 21 Jul 2023 19:54:50 +0900 Subject: [PATCH 03/94] =?UTF-8?q?lint=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt index 1a8e541..807674a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt @@ -7,5 +7,5 @@ import org.springframework.boot.runApplication class SodaLiveApplication fun main(args: Array) { - runApplication(*args) + runApplication(*args) } From 23506e79f15620d2d0ad88ef9d3070741ff28fc1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 21 Jul 2023 20:22:14 +0900 Subject: [PATCH 04/94] =?UTF-8?q?application.yml=EB=A1=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/{application.properties => application.yml} | 0 .../kr/co/vividnext/sodalive/SodaliveApplicationTests.kt | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) rename src/main/resources/{application.properties => application.yml} (100%) diff --git a/src/main/resources/application.properties b/src/main/resources/application.yml similarity index 100% rename from src/main/resources/application.properties rename to src/main/resources/application.yml diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt b/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt index d5738ce..b2933c7 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt @@ -6,8 +6,7 @@ import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class SodaliveApplicationTests { - @Test - fun contextLoads() { - } - + @Test + fun contextLoads() { + } } From f81f07bd05b1e2f733f296c1ad44c21faa386a34 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 23 Jul 2023 03:26:17 +0900 Subject: [PATCH 05/94] =?UTF-8?q?=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=9C=A0=EC=A0=80=20API=20-=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8,=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85,=20?= =?UTF-8?q?=EA=B3=84=EC=A0=95=EC=A0=95=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + build.gradle.kts | 16 ++ .../vividnext/sodalive/SodaLiveApplication.kt | 2 + .../vividnext/sodalive/aws/s3/S3Uploader.kt | 34 +++ .../vividnext/sodalive/common/ApiResponse.kt | 22 ++ .../vividnext/sodalive/common/BaseEntity.kt | 30 +++ .../sodalive/common/SodaException.kt | 3 + .../sodalive/common/SodaExceptionHandler.kt | 60 +++++ .../sodalive/configs/AmazonS3Config.kt | 29 +++ .../sodalive/configs/QueryDslConfig.kt | 18 ++ .../vividnext/sodalive/configs/RedisConfig.kt | 30 +++ .../sodalive/configs/SecurityConfig.kt | 74 ++++++ .../vividnext/sodalive/configs/WebConfig.kt | 19 ++ .../sodalive/jwt/JwtAccessDeniedHandler.kt | 18 ++ .../jwt/JwtAuthenticationEntryPoint.kt | 18 ++ .../kr/co/vividnext/sodalive/jwt/JwtFilter.kt | 45 ++++ .../vividnext/sodalive/jwt/TokenProvider.kt | 133 +++++++++++ .../kr/co/vividnext/sodalive/member/Member.kt | 104 +++++++++ .../sodalive/member/MemberAdapter.kt | 10 + .../sodalive/member/MemberController.kt | 37 +++ .../sodalive/member/MemberRepository.kt | 16 ++ .../sodalive/member/MemberService.kt | 215 ++++++++++++++++++ .../sodalive/member/SignUpValidator.kt | 40 ++++ .../co/vividnext/sodalive/member/auth/Auth.kt | 30 +++ .../member/info/GetMemberInfoResponse.kt | 12 + .../sodalive/member/login/LoginRequest.kt | 8 + .../sodalive/member/login/LoginResponse.kt | 9 + .../member/notification/MemberNotification.kt | 29 +++ .../sodalive/member/signUp/SignUpRequest.kt | 13 ++ .../member/stipulation/Stipulation.kt | 14 ++ .../member/stipulation/StipulationAgree.kt | 30 +++ .../stipulation/StipulationRepository.kt | 10 + .../sodalive/member/token/MemberToken.kt | 11 + .../member/token/MemberTokenRepository.kt | 7 + .../kr/co/vividnext/sodalive/utils/Utils.kt | 34 +++ src/main/resources/application.yml | 66 ++++++ 36 files changed, 1247 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt diff --git a/.gitignore b/.gitignore index 5218d57..67ee418 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ HELP.md .gradle +.envrc build/ !**/src/main/**/build/ !**/src/test/**/build/ diff --git a/build.gradle.kts b/build.gradle.kts index b8d1b4b..905258f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,11 +33,21 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + // jwt + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + // querydsl (추가 설정) implementation("com.querydsl:querydsl-jpa:$querydslVersion") kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa") kapt("org.springframework.boot:spring-boot-configuration-processor") + // aws + implementation("com.amazonaws:aws-java-sdk-ses:1.12.380") + implementation("com.amazonaws:aws-java-sdk-s3:1.12.380") + implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") @@ -45,6 +55,12 @@ dependencies { testImplementation("org.springframework.security:spring-security-test") } +allOpen { + annotation("javax.persistence.Entity") + annotation("javax.persistence.MappedSuperclass") + annotation("javax.persistence.Embeddable") +} + tasks.withType { kotlinOptions { freeCompilerArgs += "-Xjsr305=strict" diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt index 807674a..bb4472f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/SodaLiveApplication.kt @@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.scheduling.annotation.EnableAsync @SpringBootApplication +@EnableAsync class SodaLiveApplication fun main(args: Array) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt b/src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt new file mode 100644 index 0000000..bd3486d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.aws.s3 + +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.model.ObjectMetadata +import com.amazonaws.services.s3.model.PutObjectRequest +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.io.InputStream + +@Component +class S3Uploader(private val amazonS3Client: AmazonS3Client) { + private val logger = LoggerFactory.getLogger(this::class.java) + + fun upload( + inputStream: InputStream, + bucket: String, + filePath: String, + metadata: ObjectMetadata? = null + ): String { + putS3(inputStream, bucket, filePath, metadata) + return filePath + } + + private fun putS3( + inputStream: InputStream, + bucket: String, + filePath: String, + metadata: ObjectMetadata? + ): String { + amazonS3Client.putObject(PutObjectRequest(bucket, filePath, inputStream, metadata)) + logger.info("파일이 업로드 되었습니다.") + return amazonS3Client.getUrl(bucket, filePath).toString() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt new file mode 100644 index 0000000..daf3890 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/ApiResponse.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.common + +data class ApiResponse( + val success: Boolean, + val message: String? = null, + val data: T? = null, + val errorProperty: String? = null +) { + companion object { + fun ok(data: T? = null, message: String? = null) = ApiResponse( + success = true, + message = message, + data = data + ) + + fun error(message: String? = null, errorProperty: String? = null) = ApiResponse( + success = false, + message = message, + errorProperty = errorProperty + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt new file mode 100644 index 0000000..bf2b11d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.common + +import java.time.LocalDateTime +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.MappedSuperclass +import javax.persistence.PrePersist +import javax.persistence.PreUpdate + +@MappedSuperclass +abstract class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + var updatedAt: LocalDateTime? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + updatedAt = LocalDateTime.now() + } + + @PreUpdate + fun preUpdate() { + updatedAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt new file mode 100644 index 0000000..cf3cb9e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.common + +class SodaException(message: String, val errorProperty: String? = null) : RuntimeException(message) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt new file mode 100644 index 0000000..4b9aa7f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt @@ -0,0 +1,60 @@ +package kr.co.vividnext.sodalive.common + +import org.slf4j.LoggerFactory +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.authentication.BadCredentialsException +import org.springframework.security.authentication.InternalAuthenticationServiceException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.multipart.MaxUploadSizeExceededException + +@RestControllerAdvice +class SodaExceptionHandler { + private val logger = LoggerFactory.getLogger(this::class.java) + + @ExceptionHandler(SodaException::class) + fun handleSudaException(e: SodaException) = run { + logger.error("API error", e) + ApiResponse.error( + message = e.message, + errorProperty = e.errorProperty + ) + } + + @ExceptionHandler(MaxUploadSizeExceededException::class) + fun handleMaxUploadSizeExceededException(e: MaxUploadSizeExceededException) = run { + logger.error("API error", e) + ApiResponse.error(message = "파일용량은 최대 1024MB까지 저장할 수 있습니다.") + } + + @ExceptionHandler(AccessDeniedException::class) + fun handleAccessDeniedException(e: AccessDeniedException) = run { + logger.error("API error", e) + ApiResponse.error(message = "권한이 없습니다.") + } + + @ExceptionHandler(InternalAuthenticationServiceException::class) + fun handleInternalAuthenticationServiceException(e: InternalAuthenticationServiceException) = run { + logger.error("API error", e) + ApiResponse.error("로그인 정보를 확인해주세요.") + } + + @ExceptionHandler(BadCredentialsException::class) + fun handleBadCredentialsException(e: BadCredentialsException) = run { + logger.error("API error", e) + ApiResponse.error("로그인 정보를 확인해주세요.") + } + + @ExceptionHandler(DataIntegrityViolationException::class) + fun handleDataIntegrityViolationException(e: DataIntegrityViolationException) = run { + logger.error("API error", e) + ApiResponse.error("이미 등록되어 있습니다.") + } + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception) = run { + logger.error("API error", e) + ApiResponse.error("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt new file mode 100644 index 0000000..2e8f95d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AmazonS3Config.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.configs + +import com.amazonaws.auth.AWSStaticCredentialsProvider +import com.amazonaws.auth.BasicAWSCredentials +import com.amazonaws.services.s3.AmazonS3Client +import com.amazonaws.services.s3.AmazonS3ClientBuilder +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class AmazonS3Config( + @Value("\${cloud.aws.credentials.access-key}") + private val accessKey: String, + @Value("\${cloud.aws.credentials.secret-key}") + private val secretKey: String, + @Value("\${cloud.aws.region.static}") + private val region: String +) { + + @Bean + fun amazonS3Client(): AmazonS3Client { + val awsCredentials = BasicAWSCredentials(accessKey, secretKey) + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(AWSStaticCredentialsProvider(awsCredentials)) + .build() as AmazonS3Client + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt new file mode 100644 index 0000000..dde8c84 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/QueryDslConfig.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.configs + +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import javax.persistence.EntityManager +import javax.persistence.PersistenceContext + +@Configuration +class QueryDslConfig( + @PersistenceContext + private val entityManager: EntityManager +) { + @Bean + fun jpaQueryFactory(): JPAQueryFactory { + return JPAQueryFactory(entityManager) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt new file mode 100644 index 0000000..22480ee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.configs + +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.repository.configuration.EnableRedisRepositories + +@Configuration +@EnableRedisRepositories +class RedisConfig( + @Value("\${spring.redis.host}") + private val host: String, + @Value("\${spring.redis.port}") + private val port: Int +) { + @Bean + fun redisConnectionFactory(): RedisConnectionFactory { + return LettuceConnectionFactory(host, port) + } + + @Bean + fun redisTemplate(): RedisTemplate<*, *> { + val redisTemplate: RedisTemplate<*, *> = RedisTemplate() + redisTemplate.setConnectionFactory(redisConnectionFactory()) + return redisTemplate + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt new file mode 100644 index 0000000..aae4472 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -0,0 +1,74 @@ +package kr.co.vividnext.sodalive.configs + +import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler +import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint +import kr.co.vividnext.sodalive.jwt.JwtFilter +import kr.co.vividnext.sodalive.jwt.TokenProvider +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.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.builders.WebSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +class SecurityConfig( + private val tokenProvider: TokenProvider, + private val accessDeniedHandler: JwtAccessDeniedHandler, + private val authenticationEntryPoint: JwtAuthenticationEntryPoint +) { + @Bean + fun passwordEncoder(): PasswordEncoder { + return BCryptPasswordEncoder() + } + + @Bean + fun webSecurityCustomizer(): WebSecurityCustomizer { + return WebSecurityCustomizer { web: WebSecurity -> + web + .ignoring() + .antMatchers("/h2-console/**", "/favicon.ico", "/error") + } + } + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + val jwtFilter = JwtFilter(tokenProvider) + + return http + .cors() + .and() + .csrf().disable() + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler) + .and() + .headers() + .frameOptions() + .sameOrigin() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/member/check/email").permitAll() + .antMatchers("/member/check/nickname").permitAll() + .antMatchers("/member/signup").permitAll() + .antMatchers("/member/login").permitAll() + .antMatchers("/member/forgot-password").permitAll() + .antMatchers("/stplat/terms_of_service").permitAll() + .antMatchers("/stplat/privacy_policy").permitAll() + .anyRequest().authenticated() + .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) + .build() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt new file mode 100644 index 0000000..ffbb936 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.configs + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig : WebMvcConfigurer { + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins( + "http://localhost:8888", + "https://test-admin.sodalive.net", + "https://admin.sodalive.net" + ) + .allowedMethods("*") + .allowCredentials(true) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt new file mode 100644 index 0000000..d0e9a8b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAccessDeniedHandler.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.jwt + +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.stereotype.Component +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class JwtAccessDeniedHandler : AccessDeniedHandler { + override fun handle( + request: HttpServletRequest, + response: HttpServletResponse, + accessDeniedException: AccessDeniedException + ) { + response.sendError(HttpServletResponse.SC_FORBIDDEN) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt new file mode 100644 index 0000000..72b5ec6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtAuthenticationEntryPoint.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.jwt + +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.stereotype.Component +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +@Component +class JwtAuthenticationEntryPoint : AuthenticationEntryPoint { + override fun commence( + request: HttpServletRequest, + response: HttpServletResponse, + authException: AuthenticationException + ) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt new file mode 100644 index 0000000..cef7213 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/JwtFilter.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.jwt + +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.util.StringUtils +import org.springframework.web.filter.OncePerRequestFilter +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class JwtFilter(private val tokenProvider: TokenProvider) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + val jwt = resolveToken(request) + val requestURI = request.requestURI + + if (jwt != null && StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { + val authentication = tokenProvider.getAuthentication(jwt) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("Security Context에 '${authentication.name}' 인증정보를 저장했습니다, uri: $requestURI") + } else { + logger.debug("유효한 JWT 토큰이 없습니다., uri: $requestURI") + if (response.status != 200) { + response.status = 401 + } + } + + filterChain.doFilter(request, response) + } + + private fun resolveToken(request: HttpServletRequest): String? { + val bearerToken = request.getHeader(AUTHORIZATION_HEADER) + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7) + } + + return null + } + + companion object { + const val AUTHORIZATION_HEADER = "Authorization" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt new file mode 100644 index 0000000..0c12ad9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt @@ -0,0 +1,133 @@ +package kr.co.vividnext.sodalive.jwt + +import io.jsonwebtoken.ExpiredJwtException +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.MalformedJwtException +import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.UnsupportedJwtException +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.MemberAdapter +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.token.MemberToken +import kr.co.vividnext.sodalive.member.token.MemberTokenRepository +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.InitializingBean +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.stereotype.Component +import java.security.Key +import java.util.Date +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.write + +// 토큰의 생성, 유효성 검증 담당 클래스 +@Component +class TokenProvider( + @Value("\${jwt.secret}") + private val secret: String, + @Value("\${jwt.token-validity-in-seconds}") + private val tokenValidityInSeconds: Long, + private val repository: MemberRepository, + private val tokenRepository: MemberTokenRepository +) : InitializingBean { + + private val logger = LoggerFactory.getLogger(TokenProvider::class.java) + private val tokenValidityInMilliseconds: Long = tokenValidityInSeconds * 1000 + private val tokenLocks: MutableMap = mutableMapOf() + + private lateinit var key: Key + + override fun afterPropertiesSet() { + val keyBytes = Decoders.BASE64.decode(secret) + this.key = Keys.hmacShaKeyFor(keyBytes) + } + + fun createToken(authentication: Authentication, memberId: Long): String { + val authorities = authentication.authorities + .joinToString(separator = ",", transform = GrantedAuthority::getAuthority) + + val now = Date().time + val validity = Date(now + tokenValidityInMilliseconds) + + val token = Jwts.builder() + .setSubject(memberId.toString()) + .claim(AUTHORITIES_KEY, authorities) + .signWith(key, SignatureAlgorithm.HS512) + .setExpiration(validity) + .compact() + + val lock = getOrCreateLock(memberId = memberId) + lock.write { + val memberToken = tokenRepository.findByIdOrNull(memberId) + ?: MemberToken(id = memberId, listOf()) + + val memberTokenSet = memberToken.tokenList.toMutableSet() + memberTokenSet.add(token) + memberToken.tokenList = memberTokenSet.toList() + tokenRepository.save(memberToken) + } + + return token + } + + fun getAuthentication(token: String): Authentication { + val claims = Jwts + .parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .body + + val authorities = claims[AUTHORITIES_KEY].toString().split(",").map { SimpleGrantedAuthority(it) } + val memberToken = tokenRepository.findByIdOrNull(id = claims.subject.toLong()) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (!memberToken.tokenList.contains(token)) throw SodaException("로그인 정보를 확인해주세요.") + + val member = repository.findByIdOrNull(id = claims.subject.toLong()) + ?: throw SodaException("로그인 정보를 확인해주세요.") + val principal = MemberAdapter(member) + + return UsernamePasswordAuthenticationToken(principal, token, authorities) + } + + fun validateToken(token: String): Boolean { + try { + Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + + return true + } catch (e: SecurityException) { + logger.info("잘못된 JWT 서명입니다.") + } catch (e: MalformedJwtException) { + logger.info("잘못된 JWT 서명입니다.") + } catch (e: ExpiredJwtException) { + logger.info("만료된 JWT 서명입니다.") + } catch (e: UnsupportedJwtException) { + logger.info("지원되지 않는 JWT 서명입니다.") + } catch (e: IllegalArgumentException) { + logger.info("JWT 토큰이 잘못되었습니다.") + } catch (e: SignatureException) { + logger.info("잘못된 JWT 서명입니다.") + } + + return false + } + + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { + return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } + } + + companion object { + private const val AUTHORITIES_KEY = "auth" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt new file mode 100644 index 0000000..e836a94 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -0,0 +1,104 @@ +package kr.co.vividnext.sodalive.member + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.auth.Auth +import kr.co.vividnext.sodalive.member.notification.MemberNotification +import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.OneToMany +import javax.persistence.OneToOne + +@Entity +data class Member( + val email: String, + var password: String, + var nickname: String, + var profileImage: String? = null, + + @Enumerated(value = EnumType.STRING) + var gender: Gender = Gender.NONE, + + @Enumerated(value = EnumType.STRING) + var role: MemberRole = MemberRole.USER, + + var isActive: Boolean = true, + + var container: String = "web" +) : BaseEntity() { + @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL]) + val stipulationAgrees: MutableList = mutableListOf() + + @OneToOne(mappedBy = "member", fetch = FetchType.LAZY) + var notification: MemberNotification? = null + + @OneToOne(mappedBy = "member", fetch = FetchType.LAZY) + var auth: Auth? = null + + // 소개 + @Column(columnDefinition = "TEXT") + var introduce = "" + + // SNS + var instagramUrl = "" + var youtubeUrl = "" + var websiteUrl = "" + var blogUrl = "" + + var pushToken: String? = null + + // 화폐 + private var pgChargeCan: Int = 0 + private var pgRewardCan: Int = 0 + private var googleChargeCan: Int = 0 + private var googleRewardCan: Int = 0 + private var appleChargeCan: Int = 0 + private var appleRewardCan: Int = 0 + + fun getChargeCan(container: String): Int { + return when (container) { + "ios" -> appleChargeCan + pgChargeCan + "aos" -> googleChargeCan + pgChargeCan + else -> pgChargeCan + } + } + + fun getRewardCan(container: String): Int { + return when (container) { + "ios" -> appleRewardCan + pgRewardCan + "aos" -> googleRewardCan + pgRewardCan + else -> pgRewardCan + } + } + + fun charge(chargeCan: Int, rewardCan: Int, container: String) { + when (container) { + "ios" -> { + appleChargeCan = chargeCan + appleRewardCan = rewardCan + } + + "aos" -> { + googleChargeCan = chargeCan + googleRewardCan = rewardCan + } + + else -> { + pgChargeCan = chargeCan + pgRewardCan = rewardCan + } + } + } +} + +enum class Gender { + MALE, FEMALE, NONE +} + +enum class MemberRole { + ADMIN, BOT, USER, CREATOR, AGENT +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt new file mode 100644 index 0000000..ce42b37 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.member + +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.User + +class MemberAdapter(val member: Member) : User( + member.email, + member.password, + listOf(SimpleGrantedAuthority("ROLE_${member.role.name}")) +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt new file mode 100644 index 0000000..6bbec6a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.member + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.login.LoginRequest +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +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.RequestParam +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/member") +class MemberController(private val service: MemberService) { + @PostMapping("/signup") + fun signUp( + @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, + @RequestPart("request") requestString: String + ) = service.signUp(profileImage, requestString) + + @PostMapping("/login") + fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest) + + @GetMapping("/info") + fun getMemberInfo( + @RequestParam container: String?, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getMemberInfo(member, container ?: "web")) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt new file mode 100644 index 0000000..d5d41ce --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.member + +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberRepository : JpaRepository, MemberQueryRepository { + fun findByEmail(email: String): Member? + fun findByNickname(nickname: String): Member? +} + +interface MemberQueryRepository + +@Repository +class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt new file mode 100644 index 0000000..263db54 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -0,0 +1,215 @@ +package kr.co.vividnext.sodalive.member + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.jwt.TokenProvider +import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse +import kr.co.vividnext.sodalive.member.login.LoginRequest +import kr.co.vividnext.sodalive.member.login.LoginResponse +import kr.co.vividnext.sodalive.member.signUp.SignUpRequest +import kr.co.vividnext.sodalive.member.stipulation.Stipulation +import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree +import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository +import kr.co.vividnext.sodalive.member.stipulation.StipulationIds +import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.core.userdetails.UserDetailsService +import org.springframework.security.core.userdetails.UsernameNotFoundException +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +@Transactional(readOnly = true) +class MemberService( + private val repository: MemberRepository, + private val stipulationRepository: StipulationRepository, + private val stipulationAgreeRepository: StipulationAgreeRepository, + + private val s3Uploader: S3Uploader, + private val validator: SignUpValidator, + private val tokenProvider: TokenProvider, + private val passwordEncoder: PasswordEncoder, + private val authenticationManagerBuilder: AuthenticationManagerBuilder, + + private val objectMapper: ObjectMapper, + + @Value("\${cloud.aws.s3.bucket}") + private val s3Bucket: String +) : UserDetailsService { + @Transactional + fun signUp( + profileImage: MultipartFile?, + requestString: String + ): ApiResponse { + val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + val request = objectMapper.readValue(requestString, SignUpRequest::class.java) + if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) { + throw SodaException("약관에 동의하셔야 회원가입이 가능합니다.") + } + + validatePassword(request.password) + duplicateCheckEmail(request.email) + duplicateCheckNickname(request.nickname) + + val member = createMember(request) + member.profileImage = uploadProfileImage(profileImage = profileImage, memberId = member.id!!) + agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy) + + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = login(request.email, request.password)) + } + + fun login(request: LoginRequest): ApiResponse { + return ApiResponse.ok( + message = "로그인 되었습니다.", + data = login(request.email, request.password, request.isAdmin, request.isCreator) + ) + } + + fun getMemberInfo(member: Member, container: String): GetMemberInfoResponse { + return GetMemberInfoResponse( + can = member.getChargeCan(container) + member.getRewardCan(container), + isAuth = member.auth != null, + role = member.role, + messageNotice = member.notification?.message, + followingChannelLiveNotice = member.notification?.live, + followingChannelUploadContentNotice = member.notification?.uploadContent + ) + } + + private fun login( + email: String, + password: String, + isAdmin: Boolean = false, + isCreator: Boolean = false + ): LoginResponse { + val member = repository.findByEmail(email = email) ?: throw SodaException("로그인 정보를 확인해주세요.") + if (!member.isActive) { + throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.") + } + + if (isCreator && member.role != MemberRole.CREATOR) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + if (isAdmin && member.role != MemberRole.ADMIN) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val authenticationToken = UsernamePasswordAuthenticationToken(email, password) + val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken) + SecurityContextHolder.getContext().authentication = authentication + + val jwt = tokenProvider.createToken( + authentication = authentication, + memberId = member.id!! + ) + + return LoginResponse( + userId = member.id!!, + token = jwt, + nickname = member.nickname, + email = member.email, + profileImage = member.profileImage ?: "" + ) + } + + private fun uploadProfileImage(profileImage: MultipartFile?, memberId: Long): String { + return if (profileImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = profileImage.size + + s3Uploader.upload( + inputStream = profileImage.inputStream, + bucket = s3Bucket, + filePath = "profile/$memberId/${generateFileName(prefix = "$memberId-profile")}", + metadata = metadata + ) + } else { + "profile/default-profile.png" + } + } + + private fun agreeTermsOfServiceAndPrivacyPolicy( + member: Member, + stipulationTermsOfService: Stipulation, + stipulationPrivacyPolicy: Stipulation + ) { + val termsOfServiceAgree = StipulationAgree(true) + termsOfServiceAgree.member = member + termsOfServiceAgree.stipulation = stipulationTermsOfService + stipulationAgreeRepository.save(termsOfServiceAgree) + + val privacyPolicyAgree = StipulationAgree(true) + privacyPolicyAgree.member = member + privacyPolicyAgree.stipulation = stipulationPrivacyPolicy + stipulationAgreeRepository.save(privacyPolicyAgree) + } + + private fun createMember(request: SignUpRequest): Member { + val member = Member( + email = request.email, + password = passwordEncoder.encode(request.password), + nickname = request.nickname, + gender = request.gender, + container = request.container + ) + + return repository.save(member) + } + + private fun validatePassword(password: String) { + val passwordValidationMessage = validator.passwordValidation(password) + if (passwordValidationMessage.trim().isNotEmpty()) { + throw SodaException(passwordValidationMessage) + } + } + + fun duplicateCheckEmail(email: String): ApiResponse { + validateEmail(email) + repository.findByEmail(email)?.let { throw SodaException("이미 사용중인 이메일 입니다.", "email") } + return ApiResponse.ok(message = "사용 가능한 이메일 입니다.") + } + + private fun validateEmail(email: String) { + val emailValidationMessage = validator.emailValidation(email) + if (emailValidationMessage.trim().isNotEmpty()) { + throw SodaException(emailValidationMessage, "email") + } + } + + fun duplicateCheckNickname(nickname: String): ApiResponse { + validateNickname(nickname) + repository.findByNickname(nickname)?.let { throw SodaException("이미 사용중인 닉네임 입니다.", "nickname") } + return ApiResponse.ok(message = "사용 가능한 닉네임 입니다.") + } + + private fun validateNickname(nickname: String) { + val nicknameValidationMessage = validator.nicknameValidation(nickname) + if (nicknameValidationMessage.trim().isNotEmpty()) { + throw SodaException(nicknameValidationMessage, "nickname") + } + } + + override fun loadUserByUsername(username: String): UserDetails { + val member = repository.findByEmail(email = username) + ?: throw UsernameNotFoundException(username) + + return MemberAdapter(member) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt new file mode 100644 index 0000000..3d1c0ff --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt @@ -0,0 +1,40 @@ +package kr.co.vividnext.sodalive.member + +import org.springframework.stereotype.Component + +@Component +class SignUpValidator { + fun emailValidation(email: String): String { + val isNotValidEmail = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$" + .toRegex(RegexOption.IGNORE_CASE) + .matches(email) + .not() + + if (isNotValidEmail) { + return "올바른 이메일을 입력해 주세요" + } + + return "" + } + + fun nicknameValidation(nickname: String): String { + if (nickname.length < 2) { + return "닉네임은 2자 이상 입력해 주세요." + } + + return "" + } + + fun passwordValidation(password: String): String { + val isNotValidPassword = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d$@!%*#?&]{8,}$" + .toRegex() + .matches(password) + .not() + + if (isNotValidPassword) { + return "영문, 숫자 포함 8자 이상의 비밀번호를 입력해 주세요." + } + + return "" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt new file mode 100644 index 0000000..570729b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.member.auth + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class Auth( + @Column(nullable = false) + val name: String, + @Column(nullable = false) + val birth: String, + @Column(columnDefinition = "TEXT", nullable = false) + val uniqueCi: String, + @Column(columnDefinition = "TEXT", nullable = false) + val di: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinColumn(name = "member_id", nullable = true) + var member: Member? = null + set(value) { + value?.auth = this + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt new file mode 100644 index 0000000..2db88e6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/info/GetMemberInfoResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.member.info + +import kr.co.vividnext.sodalive.member.MemberRole + +data class GetMemberInfoResponse( + val can: Int, + val isAuth: Boolean, + val role: MemberRole, + val messageNotice: Boolean?, + val followingChannelLiveNotice: Boolean?, + val followingChannelUploadContentNotice: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt new file mode 100644 index 0000000..f49585f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.member.login + +data class LoginRequest( + val email: String, + val password: String, + val isAdmin: Boolean = false, + val isCreator: Boolean = false +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt new file mode 100644 index 0000000..667f06d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/login/LoginResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.member.login + +data class LoginResponse( + val userId: Long, + val token: String, + val nickname: String, + val email: String, + val profileImage: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt new file mode 100644 index 0000000..4c3007a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotification.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.member.notification + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class MemberNotification( + @Column(nullable = false) + var uploadContent: Boolean? = true, + + @Column(nullable = false) + var live: Boolean? = true, + + @Column(nullable = false) + var message: Boolean? = true +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + set(value) { + value?.notification = this + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt new file mode 100644 index 0000000..1d962ab --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.member.signUp + +import kr.co.vividnext.sodalive.member.Gender + +data class SignUpRequest( + val email: String, + val password: String, + val nickname: String, + val gender: Gender, + val isAgreeTermsOfService: Boolean, + val isAgreePrivacyPolicy: Boolean, + val container: String = "api" +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt new file mode 100644 index 0000000..e854861 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/Stipulation.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class Stipulation( + @Column(nullable = false) + val title: String, + @Column(columnDefinition = "TEXT", nullable = false) + var description: String, + var isActive: Boolean = true +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt new file mode 100644 index 0000000..e5504e9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationAgree.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class StipulationAgree( + val isAgree: Boolean +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + set(value) { + member?.stipulationAgrees?.add(this) + field = value + } + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "stipulation_id", nullable = false) + var stipulation: Stipulation? = null +} + +object StipulationIds { + const val TERMS_OF_SERVICE_ID = 1L + const val PRIVACY_POLICY_ID = 2L +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt new file mode 100644 index 0000000..54332b9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationRepository.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface StipulationRepository : JpaRepository + +@Repository +interface StipulationAgreeRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt new file mode 100644 index 0000000..514e0e8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.member.token + +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash + +@RedisHash("MemberToken") +data class MemberToken( + @Id + val id: Long, + var tokenList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt new file mode 100644 index 0000000..29808a2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberTokenRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.member.token + +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberTokenRepository : CrudRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt b/src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt new file mode 100644 index 0000000..c4a54df --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/utils/Utils.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.utils + +import java.nio.charset.StandardCharsets +import java.security.SecureRandom +import java.util.Base64 +import java.util.Random +import java.util.UUID + +fun generatePassword(length: Int): String { + val characterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()1234567890" + val random = SecureRandom() + val password = StringBuilder() + + for (i in 0 until length) { + val rIndex = random.nextInt(characterSet.length) + password.append(characterSet[rIndex]) + } + + return password.toString() +} + +fun generateFileName(prefix: String = ""): String { + val timestamp = System.currentTimeMillis() + val random = Random().nextInt(10000) + val uuid = UUID.randomUUID().toString() + + return if (prefix.isBlank()) { + Base64 + .getUrlEncoder() + .encodeToString("$uuid-$random-$timestamp".toByteArray(StandardCharsets.UTF_8)) + } else { + "$prefix-$uuid-$random-$timestamp" + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8b13789..c1b7712 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1 +1,67 @@ +server: + shutdown: graceful + +logging: + level: + com: + amazonaws: + util: + EC2MetadataUtils: error + +cloud: + aws: + credentials: + accessKey: ${APP_AWS_ACCESS_KEY} + secretKey: ${APP_AWS_SECRET_KEY} + s3: + bucket: ${S3_BUCKET} + cloudFront: + host: ${CLOUD_FRONT_HOST} + region: + static: ap-northeast-2 + stack: + auto: false + +jwt: + header: Authorization + token-validity-in-seconds: ${JWT_TOKEN_VALIDITY_TIME} + secret: ${JWT_SECRET} + +spring: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + + jpa: + hibernate: + ddl-auto: validate + database: mysql + + servlet: + multipart: + max-file-size: 1024MB + max-request-size: 1024MB +--- +spring: + config: + activate: + on-profile: local + + devtools: + restart: + enabled: true + livereload: + enabled: true + + jpa: + properties: + hibernate: + show_sql: true + format_sql: true From f525f19530e2e624c36c979d413b150b7084bfc0 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 24 Jul 2023 02:17:00 +0900 Subject: [PATCH 06/94] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/SodaliveApplicationTests.kt | 12 ------ src/test/resources/application.yml | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+), 12 deletions(-) delete mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt create mode 100644 src/test/resources/application.yml diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt b/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt deleted file mode 100644 index b2933c7..0000000 --- a/src/test/kotlin/kr/co/vividnext/sodalive/SodaliveApplicationTests.kt +++ /dev/null @@ -1,12 +0,0 @@ -package kr.co.vividnext.sodalive - -import org.junit.jupiter.api.Test -import org.springframework.boot.test.context.SpringBootTest - -@SpringBootTest -class SodaliveApplicationTests { - - @Test - fun contextLoads() { - } -} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..ac59f28 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,38 @@ +logging: + level: + com: + amazonaws: + util: + EC2MetadataUtils: error + +cloud: + aws: + credentials: + accessKey: ${APP_AWS_ACCESS_KEY} + secretKey: ${APP_AWS_SECRET_KEY} + s3: + bucket: ${S3_BUCKET} + cloudFront: + host: ${CLOUD_FRONT_HOST} + region: + static: ap-northeast-2 + stack: + auto: false + +jwt: + header: Authorization + token-validity-in-seconds: ${JWT_TOKEN_VALIDITY_TIME} + secret: ${JWT_SECRET} + +spring: + redis: + host: localhost + port: 6379 + + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + show_sql: true + format_sql: true From 53a64d9bd70706db4f637ffca9d315a9881fd0af Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 24 Jul 2023 03:43:10 +0900 Subject: [PATCH 07/94] hibernate-dialect = org.hibernate.dialect.MySQL8Dialect --- src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c1b7712..9db65d5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,6 +42,9 @@ spring: hibernate: ddl-auto: validate database: mysql + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect servlet: multipart: From ac09de91416cc9b7ae973737756679f448699c47 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 24 Jul 2023 14:40:41 +0900 Subject: [PATCH 08/94] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=ED=9B=84=20=EC=B4=88=EA=B8=B0=20=EC=95=8C=EB=A6=BC=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/member/MemberController.kt | 11 ++++++++ .../sodalive/member/MemberService.kt | 14 ++++++++++ .../MemberNotificationRepository.kt | 25 +++++++++++++++++ .../notification/MemberNotificationService.kt | 27 +++++++++++++++++++ .../UpdateNotificationSettingRequest.kt | 7 +++++ 5 files changed, 84 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/notification/UpdateNotificationSettingRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 6bbec6a..f9aa17c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.member import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.login.LoginRequest +import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping @@ -34,4 +35,14 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.getMemberInfo(member, container ?: "web")) } + + @PostMapping("/notification") + fun updateNotificationSettings( + @RequestBody request: UpdateNotificationSettingRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.updateNotificationSettings(request, member)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 263db54..55ed5d6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -9,6 +9,8 @@ import kr.co.vividnext.sodalive.jwt.TokenProvider import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.login.LoginResponse +import kr.co.vividnext.sodalive.member.notification.MemberNotificationService +import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequest import kr.co.vividnext.sodalive.member.stipulation.Stipulation import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree @@ -36,6 +38,8 @@ class MemberService( private val stipulationRepository: StipulationRepository, private val stipulationAgreeRepository: StipulationAgreeRepository, + private val memberNotificationService: MemberNotificationService, + private val s3Uploader: S3Uploader, private val validator: SignUpValidator, private val tokenProvider: TokenProvider, @@ -92,6 +96,16 @@ class MemberService( ) } + @Transactional + fun updateNotificationSettings(request: UpdateNotificationSettingRequest, member: Member) { + memberNotificationService.updateNotification( + live = request.live, + uploadContent = request.uploadContent, + message = request.message, + member = member + ) + } + private fun login( email: String, password: String, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationRepository.kt new file mode 100644 index 0000000..6d461d9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationRepository.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.member.notification + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MemberNotificationRepository : JpaRepository, MemberNotificationQueryRepository + +interface MemberNotificationQueryRepository { + fun getMemberNotification(memberId: Long): MemberNotification? +} + +@Repository +class MemberNotificationQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : MemberNotificationQueryRepository { + override fun getMemberNotification(memberId: Long): MemberNotification? { + return queryFactory + .selectFrom(memberNotification) + .where(memberNotification.member.id.eq(memberId)) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationService.kt new file mode 100644 index 0000000..d46fc53 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/MemberNotificationService.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.member.notification + +import kr.co.vividnext.sodalive.member.Member +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class MemberNotificationService(private val repository: MemberNotificationRepository) { + fun updateNotification( + live: Boolean? = null, + uploadContent: Boolean? = null, + message: Boolean? = null, + member: Member + ) { + var notification = repository.getMemberNotification(memberId = member.id!!) + if (notification == null) { + notification = MemberNotification(uploadContent, live, message) + notification.member = member + repository.save(notification) + } else { + if (live != null) notification.live = live + if (message != null) notification.message = message + if (uploadContent != null) notification.uploadContent = uploadContent + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/UpdateNotificationSettingRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/UpdateNotificationSettingRequest.kt new file mode 100644 index 0000000..e9bc210 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/notification/UpdateNotificationSettingRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.member.notification + +data class UpdateNotificationSettingRequest( + val live: Boolean?, + val message: Boolean?, + val uploadContent: Boolean? +) From 967d358a52bb2ba4a15cd1dd5dc6270ab1bbee5e Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 24 Jul 2023 14:50:09 +0900 Subject: [PATCH 09/94] =?UTF-8?q?gradle=20=EC=84=A4=EC=A0=95=20-=20daemon,?= =?UTF-8?q?=20configureondemand=20true=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37aef8d..f00b59d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,3 +4,5 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +org.gradle.daemon=true +org.gradle.configureondemand=true From 0580cdd2d66bf4b36a328e2f298da2a088dce1fa Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 24 Jul 2023 16:12:05 +0900 Subject: [PATCH 10/94] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20-=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 1 + .../vividnext/sodalive/member/{ => signUp}/SignUpValidator.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) rename src/main/kotlin/kr/co/vividnext/sodalive/member/{ => signUp}/SignUpValidator.kt (95%) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 55ed5d6..a905bbf 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.member.login.LoginResponse import kr.co.vividnext.sodalive.member.notification.MemberNotificationService import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequest +import kr.co.vividnext.sodalive.member.signUp.SignUpValidator import kr.co.vividnext.sodalive.member.stipulation.Stipulation import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpValidator.kt similarity index 95% rename from src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt rename to src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpValidator.kt index 3d1c0ff..38cbebb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignUpValidator.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpValidator.kt @@ -1,4 +1,4 @@ -package kr.co.vividnext.sodalive.member +package kr.co.vividnext.sodalive.member.signUp import org.springframework.stereotype.Component From ee124e258ea1179b55680856036a92c87328aa97 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 25 Jul 2023 03:05:54 +0900 Subject: [PATCH 11/94] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=ED=91=B8=EC=8B=9C?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/member/MemberController.kt | 17 +++++++++++++++++ .../sodalive/member/MemberRepository.kt | 14 ++++++++++++-- .../vividnext/sodalive/member/MemberService.kt | 14 ++++++++++++++ .../sodalive/member/PushTokenUpdateRequest.kt | 3 +++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/PushTokenUpdateRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index f9aa17c..425add6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingReq import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping 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 import org.springframework.web.bind.annotation.RequestParam @@ -45,4 +46,20 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.updateNotificationSettings(request, member)) } + + @PutMapping("/push-token/update") + fun updatePushToken( + @RequestBody pushTokenUpdateRequest: PushTokenUpdateRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.updatePushToken( + memberId = member.id!!, + pushToken = pushTokenUpdateRequest.pushToken, + container = pushTokenUpdateRequest.container + ) + ) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index d5d41ce..011d73a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.member import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -10,7 +11,16 @@ interface MemberRepository : JpaRepository, MemberQueryRepository fun findByNickname(nickname: String): Member? } -interface MemberQueryRepository +interface MemberQueryRepository { + fun findByPushToken(pushToken: String): List +} @Repository -class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository +class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository { + override fun findByPushToken(pushToken: String): List { + return queryFactory + .selectFrom(member) + .where(member.pushToken.eq(pushToken)) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index a905bbf..59e3eb0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -107,6 +107,20 @@ class MemberService( ) } + @Transactional + fun updatePushToken(memberId: Long, pushToken: String, container: String) { + val existsHavePushTokenMemberList = repository.findByPushToken(pushToken = pushToken) + for (existsHavePushTokenMember in existsHavePushTokenMemberList) { + existsHavePushTokenMember.pushToken = null + } + + val member = repository.findByIdOrNull(id = memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + member.pushToken = pushToken + member.container = container + } + private fun login( email: String, password: String, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/PushTokenUpdateRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/PushTokenUpdateRequest.kt new file mode 100644 index 0000000..a379f5f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/PushTokenUpdateRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.member + +data class PushTokenUpdateRequest(val pushToken: String, val container: String) From ee99dd31479b87d7c75dea3784be877296c9d47a Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 27 Jul 2023 06:24:23 +0900 Subject: [PATCH 12/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20-=20=EC=B6=94=EC=B2=9C=EB=9D=BC=EC=9D=B4=EB=B8=8C,?= =?UTF-8?q?=20=EC=B6=94=EC=B2=9C=EC=B1=84=EB=84=90,=20=EC=98=88=EC=95=BD?= =?UTF-8?q?=EC=A4=91=EC=9D=B8=20=EB=9D=BC=EC=9D=B4=EB=B8=8C,=20=EC=A7=84?= =?UTF-8?q?=ED=96=89=EC=A4=91=EC=9D=B8=20=EB=9D=BC=EC=9D=B4=EB=B8=8C,=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=B0=EB=84=88=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/event/Event.kt | 23 ++++ .../sodalive/event/EventController.kt | 13 ++ .../sodalive/event/EventRepository.kt | 35 ++++++ .../vividnext/sodalive/event/EventService.kt | 35 ++++++ .../sodalive/event/GetEventResponse.kt | 18 +++ .../recommend/GetRecommendChannelResponse.kt | 8 ++ .../recommend/GetRecommendLiveResponse.kt | 6 + .../live/recommend/LiveRecommendController.kt | 29 +++++ .../live/recommend/LiveRecommendRepository.kt | 100 +++++++++++++++ .../live/recommend/LiveRecommendService.kt | 35 ++++++ .../recommend/RecommendLiveCreatorBanner.kt | 28 +++++ .../sodalive/live/room/GetRoomListResponse.kt | 19 +++ .../vividnext/sodalive/live/room/LiveRoom.kt | 51 ++++++++ .../sodalive/live/room/LiveRoomController.kt | 29 +++++ .../sodalive/live/room/LiveRoomRepository.kt | 119 ++++++++++++++++++ .../sodalive/live/room/LiveRoomService.kt | 72 +++++++++++ .../room/donation/LiveRoomDonationMessage.kt | 10 ++ .../sodalive/live/room/info/LiveRoomInfo.kt | 101 +++++++++++++++ .../room/info/LiveRoomInfoRedisRepository.kt | 5 + .../sodalive/live/room/info/LiveRoomMember.kt | 21 ++++ 20 files changed, 757 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt new file mode 100644 index 0000000..f529a46 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.event + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class Event( + @Column(nullable = false) + var thumbnailImage: String, + @Column(nullable = true) + var detailImage: String?, + @Column(nullable = true) + var popupImage: String?, + @Column(nullable = true) + var link: String?, + @Column(nullable = true) + var title: String?, + @Column(nullable = false) + var isPopup: Boolean = false, + @Column(nullable = false) + var isActive: Boolean = true +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt new file mode 100644 index 0000000..fdbe64b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.event + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/event") +class EventController(private val service: EventService) { + @GetMapping + fun getEventList() = ApiResponse.ok(service.getEventList()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt new file mode 100644 index 0000000..9b45e2f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.event + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.event.QEvent.event +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface EventRepository : JpaRepository, EventQueryRepository + +interface EventQueryRepository { + fun getEventList(): List +} + +@Repository +class EventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : EventQueryRepository { + override fun getEventList(): List { + return queryFactory + .select( + QEventItem( + event.id, + event.title, + event.thumbnailImage, + event.detailImage, + event.popupImage, + event.link, + event.isPopup + ) + ) + .from(event) + .where(event.isActive.isTrue) + .orderBy(event.id.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt new file mode 100644 index 0000000..0469524 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.event + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class EventService( + private val repository: EventRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun getEventList(): GetEventResponse { + val eventList = repository.getEventList() + .asSequence() + .map { + if (!it.thumbnailImageUrl.startsWith("https://")) { + it.thumbnailImageUrl = "$cloudFrontHost/${it.thumbnailImageUrl}" + } + + if (it.detailImageUrl != null && !it.detailImageUrl!!.startsWith("https://")) { + it.detailImageUrl = "$cloudFrontHost/${it.detailImageUrl}" + } + + if (it.popupImageUrl != null && !it.popupImageUrl!!.startsWith("https://")) { + it.popupImageUrl = "$cloudFrontHost/${it.popupImageUrl}" + } + + it + } + .toList() + + return GetEventResponse(0, eventList) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt new file mode 100644 index 0000000..5a0b6ee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/GetEventResponse.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.event + +import com.querydsl.core.annotations.QueryProjection + +data class GetEventResponse( + val totalCount: Int, + val eventList: List +) + +data class EventItem @QueryProjection constructor( + val id: Long, + val title: String? = null, + var thumbnailImageUrl: String, + var detailImageUrl: String? = null, + var popupImageUrl: String? = null, + val link: String? = null, + val isPopup: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt new file mode 100644 index 0000000..587673d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendChannelResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.recommend + +data class GetRecommendChannelResponse( + val creatorId: Long, + val nickname: String, + val profileImageUrl: String, + val isOnAir: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt new file mode 100644 index 0000000..97f21fe --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetRecommendLiveResponse.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.recommend + +data class GetRecommendLiveResponse( + val imageUrl: String, + val creatorId: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt new file mode 100644 index 0000000..3c38eef --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.live.recommend + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class LiveRecommendController(private val service: LiveRecommendService) { + @GetMapping("/live/recommend") + fun getRecommendLive( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRecommendLive(member)) + } + + @GetMapping("/live/recommend/channel") + fun getRecommendChannelList( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRecommendChannelList(member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt new file mode 100644 index 0000000..7ebacf8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -0,0 +1,100 @@ +package kr.co.vividnext.sodalive.live.recommend + +import com.querydsl.core.types.Projections +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner +import kr.co.vividnext.sodalive.live.room.LiveRoomType +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { + fun getRecommendLive(memberId: Long, isAdult: Boolean): List { + val dateNow = LocalDateTime.now() + + var where = recommendLiveCreatorBanner.startDate.loe(dateNow) + .and(recommendLiveCreatorBanner.endDate.goe(dateNow)) + + if (!isAdult) { + where = where.and(recommendLiveCreatorBanner.isAdult.isFalse) + } + + return queryFactory + .select( + Projections.constructor( + GetRecommendLiveResponse::class.java, + recommendLiveCreatorBanner.image, + recommendLiveCreatorBanner.creator.id + ) + ) + .from(recommendLiveCreatorBanner) + .where(where) + .orderBy(recommendLiveCreatorBanner.orders.asc()) + .fetch() + } + + fun getOnAirRecommendChannelList( + memberId: Long, + isAdult: Boolean + ): List { + var where = member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + + if (!isAdult) { + where = where.and(liveRoom.isAdult.isFalse) + } + + return queryFactory + .select( + Projections.constructor( + GetRecommendChannelResponse::class.java, + member.id, + member.nickname, + member.profileImage, + Expressions.asBoolean(true) + ) + ) + .from(liveRoom) + .rightJoin(liveRoom.member, member) + .where( + where + .and(liveRoom.isActive.isTrue) + .and(liveRoom.type.ne(LiveRoomType.SECRET)) + .and(liveRoom.channelName.isNotNull) + .and(liveRoom.channelName.isNotEmpty) + ) + .groupBy(member.id) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .limit(20) + .fetch() + } + + fun getRecommendChannelList( + memberId: Long, + withOutCreatorList: List, + limit: Long + ): List { + val where = member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + + return queryFactory + .select( + Projections.constructor( + GetRecommendChannelResponse::class.java, + member.id, + member.nickname, + member.profileImage, + Expressions.asBoolean(false) + ) + ) + .from(member) + .where(where.and(member.id.notIn(withOutCreatorList))) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .limit(limit) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt new file mode 100644 index 0000000..9b400ae --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.live.recommend + +import kr.co.vividnext.sodalive.member.Member +import org.springframework.stereotype.Service + +@Service +class LiveRecommendService(private val repository: LiveRecommendRepository) { + + fun getRecommendLive(member: Member): List { + return repository.getRecommendLive( + memberId = member.id!!, + isAdult = member.auth != null + ) + } + + fun getRecommendChannelList(member: Member): List { + val onAirChannelList = repository.getOnAirRecommendChannelList(member.id!!, isAdult = member.auth != null) + + if (onAirChannelList.size >= 20) { + return onAirChannelList + } + + val onAirCreatorIdList = onAirChannelList.asSequence() + .map { it.creatorId } + .toList() + + val notOnAirCreatorList = repository.getRecommendChannelList( + member.id!!, + withOutCreatorList = onAirCreatorIdList, + limit = (20 - onAirChannelList.size).toLong() + ) + + return onAirChannelList + notOnAirCreatorList + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt new file mode 100644 index 0000000..78e823e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt @@ -0,0 +1,28 @@ +package kr.co.vividnext.sodalive.live.recommend + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class RecommendLiveCreatorBanner( + @Column(nullable = false) + var image: String, + @Column(nullable = false) + var startDate: LocalDateTime, + @Column(nullable = false) + var endDate: LocalDateTime, + @Column(nullable = false) + var isAdult: Boolean = false, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id", nullable = false) + var creator: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt new file mode 100644 index 0000000..84f8796 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.live.room + +data class GetRoomListResponse( + val roomId: Long, + val title: String, + val content: String, + val beginDateTime: String, + val numberOfParticipate: Int, + val numberOfPeople: Int, + val coverImageUrl: String, + val isAdult: Boolean, + val price: Int, + val tags: List, + val channelName: String?, + val managerNickname: String, + val managerId: Long, + val isReservation: Boolean, + val isPrivateRoom: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt new file mode 100644 index 0000000..2578439 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -0,0 +1,51 @@ +package kr.co.vividnext.sodalive.live.room + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class LiveRoom( + var title: String, + @Column(columnDefinition = "TEXT", nullable = false) + var notice: String, + var beginDateTime: LocalDateTime, + var numberOfPeople: Int, + var coverImage: String? = null, + var bgImage: String? = null, + var isAdult: Boolean, + val price: Int = 0, + @Enumerated(value = EnumType.STRING) + val type: LiveRoomType = LiveRoomType.OPEN, + @Column(nullable = true) + var password: String? = null +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + var channelName: String? = null + var isActive: Boolean = true +} + +enum class LiveRoomType { + // 공개 + OPEN, + + // 비공개 + PRIVATE, + + // 비밀방 + SECRET +} + +enum class LiveRoomStatus { + NOW, RESERVATION +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt new file mode 100644 index 0000000..ec927e4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.live.room + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/live/room") +class LiveRoomController(private val service: LiveRoomService) { + + @GetMapping + fun getRoomList( + @RequestParam timezone: String, + @RequestParam dateString: String? = null, + @RequestParam status: LiveRoomStatus, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRoomList(dateString, status, pageable, member, timezone)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt new file mode 100644 index 0000000..d3ec2d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -0,0 +1,119 @@ +package kr.co.vividnext.sodalive.live.room + +import com.querydsl.core.types.OrderSpecifier +import com.querydsl.core.types.Predicate +import com.querydsl.core.types.dsl.CaseBuilder +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Repository +interface LiveRoomRepository : JpaRepository, LiveRoomQueryRepository + +interface LiveRoomQueryRepository { + fun getLiveRoomList( + dateString: String?, + status: LiveRoomStatus, + pageable: Pageable, + member: Member, + timezone: String, + isAdult: Boolean + ): List +} + +class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { + override fun getLiveRoomList( + dateString: String?, + status: LiveRoomStatus, + pageable: Pageable, + member: Member, + timezone: String, + isAdult: Boolean + ): List { + var where: Predicate + + if (status == LiveRoomStatus.NOW) { + where = liveRoom.channelName.isNotNull + .and(liveRoom.channelName.isNotEmpty) + } else { + where = if (dateString != null) { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val date = LocalDate.parse(dateString, dateTimeFormatter).atStartOfDay() + .atZone(ZoneId.of(timezone)) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + liveRoom.beginDateTime.goe(date) + .and(liveRoom.beginDateTime.lt(date.plusDays(1))) + .and( + liveRoom.channelName.isNull + .or(liveRoom.channelName.isEmpty) + ) + } else { + liveRoom.beginDateTime.gt( + LocalDateTime.now() + .atZone(ZoneId.of(timezone)) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + ) + .and( + liveRoom.channelName.isNull + .or(liveRoom.channelName.isEmpty) + ) + } + } + + if (!isAdult) { + where = where.and(liveRoom.isAdult.isFalse) + } + + where = where.and(liveRoom.isActive.isTrue) + .and(liveRoom.member.isNotNull) + .and(liveRoom.type.ne(LiveRoomType.SECRET)) + + return queryFactory + .selectFrom(liveRoom) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .where(where) + .orderBy( + *orderByFieldAccountId( + memberId = member.id!!, + status = status, + offset = pageable.offset, + dateString = dateString + ) + ) + .fetch() + } + + private fun orderByFieldAccountId( + memberId: Long, + status: LiveRoomStatus, + offset: Long, + dateString: String? + ): Array> { + return if (status == LiveRoomStatus.NOW) { + arrayOf(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + } else if (status == LiveRoomStatus.RESERVATION && offset == 0L && dateString == null) { + arrayOf( + CaseBuilder() + .`when`(member.id.eq(memberId)).then(1) + .otherwise(2) + .asc(), + liveRoom.beginDateTime.asc() + ) + } else { + arrayOf(liveRoom.beginDateTime.asc()) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt new file mode 100644 index 0000000..24a41b8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -0,0 +1,72 @@ +package kr.co.vividnext.sodalive.live.room + +import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository +import kr.co.vividnext.sodalive.member.Member +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class LiveRoomService( + private val repository: LiveRoomRepository, + private val roomInfoRepository: LiveRoomInfoRedisRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val coverImageHost: String +) { + fun getRoomList( + dateString: String?, + status: LiveRoomStatus, + pageable: Pageable, + member: Member, + timezone: String + ): List { + return repository + .getLiveRoomList( + dateString = dateString, + status = status, + pageable = pageable, + member = member, + timezone = timezone, + isAdult = member.auth != null + ) + .asSequence() + .map { + val roomInfo = roomInfoRepository.findByIdOrNull(it.id!!) + + val beginDateTime = it.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetRoomListResponse( + roomId = it.id!!, + title = it.title, + content = it.notice, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + numberOfParticipate = (roomInfo?.listenerCount ?: 0) + + (roomInfo?.speakerCount ?: 0) + + (roomInfo?.managerCount ?: 0), + numberOfPeople = it.numberOfPeople, + isAdult = it.isAdult, + price = it.price, + channelName = it.channelName, + managerNickname = it.member!!.nickname, + managerId = it.member!!.id!!, + tags = listOf(), + coverImageUrl = if (it.coverImage!!.startsWith("https://")) { + it.coverImage!! + } else { + "$coverImageHost/${it.coverImage!!}" + }, + isReservation = false, + isPrivateRoom = it.type == LiveRoomType.PRIVATE + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt new file mode 100644 index 0000000..4655c93 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import java.util.UUID + +data class LiveRoomDonationMessage( + val uuid: String = UUID.randomUUID().toString(), + val nickname: String, + val coinMessage: String, + val donationMessage: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt new file mode 100644 index 0000000..d666ae2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt @@ -0,0 +1,101 @@ +package kr.co.vividnext.sodalive.live.room.info + +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash + +@RedisHash("live_room_info") +data class LiveRoomInfo( + @Id + val roomId: Long, + var speakerList: List = mutableListOf(), + var listenerList: List = mutableListOf(), + var managerList: List = mutableListOf(), + var donationMessageList: List = mutableListOf() +) { + var speakerCount = 0 + private set + + var listenerCount = 0 + private set + + var managerCount = 0 + private set + + fun addSpeaker(member: Member) { + val liveRoomMember = LiveRoomMember(member) + liveRoomMember.role = LiveRoomMemberRole.SPEAKER + + val speakerSet = speakerList.toMutableSet() + speakerSet.add(liveRoomMember) + speakerList = speakerSet.toList() + + setSpeakerCount() + } + + fun removeSpeaker(member: Member) { + (speakerList as MutableList).removeIf { it.id == member.id!! } + setSpeakerCount() + } + + private fun setSpeakerCount() { + speakerCount = speakerList.size + } + + fun addListener(member: Member) { + val liveRoomMember = LiveRoomMember(member) + liveRoomMember.role = LiveRoomMemberRole.LISTENER + + val listenerSet = listenerList.toMutableSet() + listenerSet.add(liveRoomMember) + listenerList = listenerSet.toList() + + setListenerCount() + } + + fun removeListener(member: Member) { + (listenerList as MutableList).removeIf { it.id == member.id!! } + setListenerCount() + } + + private fun setListenerCount() { + listenerCount = listenerList.size + } + + fun addManager(member: Member) { + val liveRoomMember = LiveRoomMember(member) + liveRoomMember.role = LiveRoomMemberRole.MANAGER + + val managerSet = managerList.toMutableSet() + managerSet.add(liveRoomMember) + managerList = managerSet.toList() + + setManagerCount() + } + + fun removeManager(member: Member) { + (managerList as MutableList).removeIf { it.id == member.id!! } + setManagerCount() + } + + private fun setManagerCount() { + managerCount = managerList.size + } + + fun addDonationMessage(nickname: String, coin: Int, donationMessage: String) { + val donationMessageSet = donationMessageList.toMutableSet() + donationMessageSet.add( + LiveRoomDonationMessage( + nickname = nickname, + coinMessage = "${coin}코인을 후원하셨습니다.", + donationMessage = donationMessage + ) + ) + donationMessageList = donationMessageSet.toList() + } + + fun removeDonationMessage(uuid: String) { + (donationMessageList as MutableList).removeIf { it.uuid == uuid } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt new file mode 100644 index 0000000..a39c911 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfoRedisRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.live.room.info + +import org.springframework.data.repository.CrudRepository + +interface LiveRoomInfoRedisRepository : CrudRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt new file mode 100644 index 0000000..cdca980 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.live.room.info + +import kr.co.vividnext.sodalive.member.Member + +data class LiveRoomMember( + val id: Long, + val nickname: String, + val profileImage: String +) { + var role = LiveRoomMemberRole.LISTENER + + constructor(member: Member) : this( + id = member.id!!, + nickname = member.nickname, + profileImage = member.profileImage ?: "profile/default-profile.png" + ) +} + +enum class LiveRoomMemberRole { + LISTENER, SPEAKER, MANAGER +} From 6174ec35230cabad8a31a5f6f4cadc9103fe63c5 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 27 Jul 2023 06:42:35 +0900 Subject: [PATCH 13/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20-=20=EC=98=88=EC=95=BD=EC=A4=91=EC=9D=B8=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=A0=95=EB=A0=AC=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index d3ec2d2..82e227d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -7,6 +7,7 @@ import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository @@ -82,6 +83,7 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L return queryFactory .selectFrom(liveRoom) + .innerJoin(liveRoom.member, QMember.member) .offset(pageable.offset) .limit(pageable.pageSize.toLong()) .where(where) From ab116bb45b8e87ee7eaf1b9794ceb6d1a96f9340 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 28 Jul 2023 14:38:53 +0900 Subject: [PATCH 14/94] =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=A9=94=EC=9D=B8=20-=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/member/MemberController.kt | 10 ++++++++ .../sodalive/member/MemberService.kt | 25 ++++++++++++++++++- .../sodalive/member/myPage/MyPageResponse.kt | 14 +++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/myPage/MyPageResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 425add6..30cfcb9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -62,4 +62,14 @@ class MemberController(private val service: MemberService) { ) ) } + + @GetMapping("/mypage") + fun getMyPage( + @RequestParam container: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getMyPage(member, container)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 59e3eb0..d6e32a9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.jwt.TokenProvider import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.login.LoginResponse +import kr.co.vividnext.sodalive.member.myPage.MyPageResponse import kr.co.vividnext.sodalive.member.notification.MemberNotificationService import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequest @@ -50,7 +51,10 @@ class MemberService( private val objectMapper: ObjectMapper, @Value("\${cloud.aws.s3.bucket}") - private val s3Bucket: String + private val s3Bucket: String, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String ) : UserDetailsService { @Transactional fun signUp( @@ -121,6 +125,25 @@ class MemberService( member.container = container } + fun getMyPage(member: Member, container: String): MyPageResponse { + return MyPageResponse( + nickname = member.nickname, + profileUrl = if (member.profileImage != null) { + "$cloudFrontHost/${member.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + chargeCan = member.getChargeCan(container = container), + rewardCan = member.getRewardCan(container = container), + youtubeUrl = member.youtubeUrl, + instagramUrl = member.instagramUrl, + websiteUrl = member.websiteUrl, + blogUrl = member.blogUrl, + liveReservationCount = 0, + isAuth = false + ) + } + private fun login( email: String, password: String, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/myPage/MyPageResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/myPage/MyPageResponse.kt new file mode 100644 index 0000000..d357f34 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/myPage/MyPageResponse.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.member.myPage + +data class MyPageResponse( + val nickname: String, + val profileUrl: String, + val chargeCan: Int, + val rewardCan: Int, + val youtubeUrl: String?, + val instagramUrl: String?, + val websiteUrl: String? = null, + val blogUrl: String? = null, + val liveReservationCount: Int, + val isAuth: Boolean +) From 5b3b3a767f8d96f6b5859128153db69704b5239d Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 28 Jul 2023 17:35:31 +0900 Subject: [PATCH 15/94] =?UTF-8?q?=EB=B3=B8=EC=9D=B8=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + .../co/vividnext/sodalive/member/auth/Auth.kt | 9 +-- .../sodalive/member/auth/AuthController.kt | 24 +++++++ .../sodalive/member/auth/AuthRepository.kt | 7 +++ .../sodalive/member/auth/AuthService.kt | 62 +++++++++++++++++++ .../sodalive/member/auth/AuthVerifyRequest.kt | 29 +++++++++ src/main/resources/application.yml | 4 ++ 7 files changed, 134 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthVerifyRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 905258f..f55ad63 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -48,6 +48,9 @@ dependencies { implementation("com.amazonaws:aws-java-sdk-s3:1.12.380") implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380") + // bootpay + implementation("io.github.bootpay:backend:2.2.1") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt index 570729b..1c66dfa 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/Auth.kt @@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.member.auth import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.member.Member -import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity import javax.persistence.FetchType @@ -18,10 +17,12 @@ data class Auth( @Column(columnDefinition = "TEXT", nullable = false) val uniqueCi: String, @Column(columnDefinition = "TEXT", nullable = false) - val di: String + val di: String, + @Column(nullable = false) + val gender: Int ) : BaseEntity() { - @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) - @JoinColumn(name = "member_id", nullable = true) + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) var member: Member? = null set(value) { value?.auth = this diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt new file mode 100644 index 0000000..764a24e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.member.auth + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +@RequestMapping("/auth") +class AuthController(private val service: AuthService) { + @PostMapping + fun authVerify( + @RequestBody request: AuthVerifyRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.verify(member, request)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthRepository.kt new file mode 100644 index 0000000..954dc91 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.member.auth + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AuthRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt new file mode 100644 index 0000000..76af74e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt @@ -0,0 +1,62 @@ +package kr.co.vividnext.sodalive.member.auth + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.bootpay.Bootpay +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate + +@Service +@Transactional(readOnly = true) +class AuthService( + private val repository: AuthRepository, + private val objectMapper: ObjectMapper, + + @Value("\${bootpay.application-id}") + private val bootpayApplicationId: String, + @Value("\${bootpay.private-key}") + private val bootpayPrivateKey: String +) { + @Transactional + fun verify(member: Member, request: AuthVerifyRequest) { + val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey) + try { + val token = bootpay.accessToken + if (token["error_code"] != null) throw SodaException("인증정보에 오류가 있습니다.\n다시 시도해 주세요.") + + val res = bootpay.certificate(request.receiptId) + val certificateResult = objectMapper.convertValue(res, AuthCertificateResult::class.java) + + if ( + certificateResult.status == 12 && + certificateResult.statusLocale == "본인인증완료" && + certificateResult.receiptId == request.receiptId + ) { + val certificate = certificateResult.authenticateData + val nowYear = LocalDate.now().year + val certificateYear = certificate.birth.substring(0, 4).toInt() + if (nowYear - certificateYear >= 19) { + val auth = Auth( + name = certificate.name, + birth = certificate.birth, + uniqueCi = certificate.unique, + di = certificate.di, + gender = certificate.gender + ) + auth.member = member + + repository.save(auth) + } else { + throw SodaException("19세 미만 인증 오류") + } + } else { + throw SodaException("인증정보에 오류가 있습니다.\n다시 시도해 주세요.") + } + } catch (e: Exception) { + throw SodaException("인증정보에 오류가 있습니다.\n다시 시도해 주세요.") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthVerifyRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthVerifyRequest.kt new file mode 100644 index 0000000..ea9590e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthVerifyRequest.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.member.auth + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AuthVerifyRequest( + @JsonProperty("receipt_id") + val receiptId: String, + @JsonProperty("version") + val version: String? +) + +data class AuthVerifyCertificate( + val name: String, + val birth: String, + val unique: String, + val di: String, + val gender: Int +) + +data class AuthCertificateResult( + @JsonProperty("receipt_id") + val receiptId: String, + @JsonProperty("status") + val status: Int, + @JsonProperty("status_locale") + val statusLocale: String, + @JsonProperty("authenticate_data") + val authenticateData: AuthVerifyCertificate +) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9db65d5..180b5dd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,6 +8,10 @@ logging: util: EC2MetadataUtils: error +bootpay: + applicationId: ${BOOTPAY_APPLICATION_ID} + privateKey: ${BOOTPAY_PRIVATE_KEY} + cloud: aws: credentials: From a3b3aa5b18e1791fc068952ec9e0ff7c63b69bef Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 28 Jul 2023 18:09:06 +0900 Subject: [PATCH 16/94] =?UTF-8?q?=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=A9=94=EC=9D=B8=20-=20=EB=B3=B8=EC=9D=B8?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20true/false=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index d6e32a9..1f5d324 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -140,7 +140,7 @@ class MemberService( websiteUrl = member.websiteUrl, blogUrl = member.blogUrl, liveReservationCount = 0, - isAuth = false + isAuth = member.auth != null ) } From c06de5f9f6f8011e68ae7643eced736ecec1f8c8 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 29 Jul 2023 04:15:22 +0900 Subject: [PATCH 17/94] =?UTF-8?q?=EC=BD=94=EC=9D=B8=20->=20=EC=BA=94=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/donation/LiveRoomDonationMessage.kt | 2 +- .../kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt index 4655c93..52bb4a1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationMessage.kt @@ -5,6 +5,6 @@ import java.util.UUID data class LiveRoomDonationMessage( val uuid: String = UUID.randomUUID().toString(), val nickname: String, - val coinMessage: String, + val canMessage: String, val donationMessage: String ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt index d666ae2..3946fbf 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt @@ -83,12 +83,12 @@ data class LiveRoomInfo( managerCount = managerList.size } - fun addDonationMessage(nickname: String, coin: Int, donationMessage: String) { + fun addDonationMessage(nickname: String, can: Int, donationMessage: String) { val donationMessageSet = donationMessageList.toMutableSet() donationMessageSet.add( LiveRoomDonationMessage( nickname = nickname, - coinMessage = "${coin}코인을 후원하셨습니다.", + canMessage = "${can}캔을 후원하셨습니다.", donationMessage = donationMessage ) ) From 7c8084bdd4834bfa89e3e958d4240094e6e2b3aa Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 29 Jul 2023 05:37:06 +0900 Subject: [PATCH 18/94] =?UTF-8?q?=EC=BD=94=EC=9D=B8=20=EC=B6=A9=EC=A0=84,?= =?UTF-8?q?=20=EC=BD=94=EC=9D=B8=20=EB=82=B4=EC=97=AD=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + .../kr/co/vividnext/sodalive/can/Can.kt | 20 ++ .../vividnext/sodalive/can/CanController.kt | 60 ++++++ .../vividnext/sodalive/can/CanRepository.kt | 96 +++++++++ .../co/vividnext/sodalive/can/CanResponse.kt | 11 + .../co/vividnext/sodalive/can/CanService.kt | 118 ++++++++++ .../can/GetCanChargeStatusResponseItem.kt | 7 + .../sodalive/can/GetCanStatusResponse.kt | 6 + .../can/GetCanUseStatusResponseItem.kt | 7 + .../vividnext/sodalive/can/charge/Charge.kt | 45 ++++ .../sodalive/can/charge/ChargeController.kt | 52 +++++ .../sodalive/can/charge/ChargeData.kt | 35 +++ .../sodalive/can/charge/ChargeRepository.kt | 7 + .../sodalive/can/charge/ChargeService.kt | 203 ++++++++++++++++++ .../sodalive/can/charge/ChargeStatus.kt | 8 + .../vividnext/sodalive/can/payment/Payment.kt | 41 ++++ .../sodalive/can/payment/PaymentGateway.kt | 5 + .../co/vividnext/sodalive/can/use/CanUsage.kt | 8 + .../co/vividnext/sodalive/can/use/UseCan.kt | 40 ++++ .../sodalive/can/use/UseCanCalculate.kt | 37 ++++ .../sodalive/configs/OkHttpConfig.kt | 22 ++ .../vividnext/sodalive/live/room/LiveRoom.kt | 5 + .../kr/co/vividnext/sodalive/member/Member.kt | 4 +- src/main/resources/application.yml | 4 + src/test/resources/application.yml | 4 + 25 files changed, 846 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/Can.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/CanResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanChargeStatusResponseItem.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanStatusResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanUseStatusResponseItem.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/payment/Payment.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/OkHttpConfig.kt diff --git a/build.gradle.kts b/build.gradle.kts index f55ad63..78ca8b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,6 +51,9 @@ dependencies { // bootpay implementation("io.github.bootpay:backend:2.2.1") + implementation("com.squareup.okhttp3:okhttp:4.9.3") + implementation("org.json:json:20230227") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/Can.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/Can.kt new file mode 100644 index 0000000..1f0f4f6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/Can.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.can + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated + +@Entity +data class Can( + var title: String, + var can: Int, + var rewardCan: Int, + var price: Int, + @Enumerated(value = EnumType.STRING) + var status: CanStatus +) : BaseEntity() + +enum class CanStatus { + SALE, END_OF_SALE +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt new file mode 100644 index 0000000..af53b78 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanController.kt @@ -0,0 +1,60 @@ +package kr.co.vividnext.sodalive.can + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/can") +class CanController(private val service: CanService) { + @GetMapping + fun getCans(): ApiResponse> { + return ApiResponse.ok(service.getCans()) + } + + @GetMapping("/status") + fun getCanStatus( + @RequestParam container: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.getCanStatus(member, container)) + } + + @GetMapping("/status/use") + fun getCanUseStatus( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + @RequestParam("timezone") timezone: String, + @RequestParam("container") container: String, + pageable: Pageable + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.getCanUseStatus(member, pageable, timezone, container)) + } + + @GetMapping("/status/charge") + fun getCanChargeStatus( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + @RequestParam("timezone") timezone: String, + @RequestParam("container") container: String, + pageable: Pageable + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.getCanChargeStatus(member, pageable, timezone, container)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt new file mode 100644 index 0000000..05bedff --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -0,0 +1,96 @@ +package kr.co.vividnext.sodalive.can + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.QCan.can1 +import kr.co.vividnext.sodalive.can.charge.Charge +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.charge.QCharge.charge +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.payment.QPayment.payment +import kr.co.vividnext.sodalive.can.use.QUseCan.useCan +import kr.co.vividnext.sodalive.can.use.UseCan +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.QMember +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface CanRepository : JpaRepository, CanQueryRepository + +interface CanQueryRepository { + fun findAllByStatus(status: CanStatus): List + fun getCanUseStatus(member: Member, pageable: Pageable): List + fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List +} + +@Repository +class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQueryRepository { + override fun findAllByStatus(status: CanStatus): List { + return queryFactory + .select( + QCanResponse( + can1.id, + can1.title, + can1.can, + can1.rewardCan, + can1.price + ) + ) + .from(can1) + .where(can1.status.eq(status)) + .orderBy(can1.can.asc()) + .fetch() + } + + override fun getCanUseStatus(member: Member, pageable: Pageable): List { + return queryFactory + .selectFrom(useCan) + .where(useCan.member.id.eq(member.id)) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .orderBy(useCan.id.desc()) + .fetch() + } + + override fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List { + val qMember = QMember.member + val chargeStatusCondition = when (container) { + "aos" -> { + charge.payment.paymentGateway.eq(PaymentGateway.PG) + .or(charge.payment.paymentGateway.eq(PaymentGateway.GOOGLE_IAP)) + } + + "ios" -> { + charge.payment.paymentGateway.eq(PaymentGateway.PG) + .or(charge.payment.paymentGateway.eq(PaymentGateway.APPLE_IAP)) + } + + else -> charge.payment.paymentGateway.eq(PaymentGateway.PG) + } + + return queryFactory + .selectFrom(charge) + .innerJoin(charge.member, qMember) + .innerJoin(charge.useCan, useCan) + .leftJoin(charge.payment, payment) + .where( + qMember.id.eq(member.id) + .and( + payment.status.eq(PaymentStatus.COMPLETE) + .or( + charge.status.eq(ChargeStatus.REFUND_CHARGE) + .and(useCan.isNotNull) + ) + .or(charge.status.eq(ChargeStatus.EVENT)) + .or(charge.status.eq(ChargeStatus.ADMIN)) + ) + .and(chargeStatusCondition) + ) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .orderBy(charge.id.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanResponse.kt new file mode 100644 index 0000000..e3c5d48 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanResponse.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.can + +import com.querydsl.core.annotations.QueryProjection + +data class CanResponse @QueryProjection constructor( + val id: Long, + val title: String, + val can: Int, + val rewardCan: Int, + val price: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt new file mode 100644 index 0000000..a8d99c8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanService.kt @@ -0,0 +1,118 @@ +package kr.co.vividnext.sodalive.can + +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class CanService(private val repository: CanRepository) { + fun getCans(): List { + return repository.findAllByStatus(status = CanStatus.SALE) + } + + fun getCanStatus(member: Member, container: String): GetCanStatusResponse { + return GetCanStatusResponse( + chargeCan = member.getChargeCan(container), + rewardCan = member.getRewardCan(container) + ) + } + + fun getCanUseStatus( + member: Member, + pageable: Pageable, + timezone: String, + container: String + ): List { + return repository.getCanUseStatus(member, pageable) + .filter { (it.can + it.rewardCan) > 0 } + .filter { + when (container) { + "aos" -> { + it.useCanCalculates.any { useCanCalculate -> + useCanCalculate.paymentGateway == PaymentGateway.PG || + useCanCalculate.paymentGateway == PaymentGateway.GOOGLE_IAP + } + } + + "ios" -> { + it.useCanCalculates.any { useCanCalculate -> + useCanCalculate.paymentGateway == PaymentGateway.PG || + useCanCalculate.paymentGateway == PaymentGateway.APPLE_IAP + } + } + + else -> it.useCanCalculates.any { useCanCalculate -> + useCanCalculate.paymentGateway == PaymentGateway.PG + } + } + } + .map { + val title: String = when (it.canUsage) { + CanUsage.DONATION -> { + "[후원] ${it.room!!.member!!.nickname}" + } + + CanUsage.LIVE -> { + "[라이브] ${it.room!!.title}" + } + + CanUsage.CHANGE_NICKNAME -> "닉네임 변경" + CanUsage.ORDER_CONTENT -> "콘텐츠 구매" + } + + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetCanUseStatusResponseItem( + title = title, + date = createdAt.format( + DateTimeFormatter.ofPattern("yyyy-MM-dd | HH:mm:ss") + ), + can = it.can + it.rewardCan + ) + } + } + + fun getCanChargeStatus( + member: Member, + pageable: Pageable, + timezone: String, + container: String + ): List { + return repository.getCanChargeStatus(member, pageable, container) + .map { + val canTitle = it.title ?: "" + val chargeMethod = when (it.status) { + ChargeStatus.CHARGE, ChargeStatus.EVENT -> { + it.payment!!.method ?: "" + } + + ChargeStatus.REFUND_CHARGE -> { + "환불" + } + + else -> { + "환불" + } + } + + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetCanChargeStatusResponseItem( + canTitle = canTitle, + date = createdAt.format( + DateTimeFormatter.ofPattern("yyyy-MM-dd | HH:mm:ss") + ), + chargeMethod = chargeMethod + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanChargeStatusResponseItem.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanChargeStatusResponseItem.kt new file mode 100644 index 0000000..5564e01 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanChargeStatusResponseItem.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can + +data class GetCanChargeStatusResponseItem( + val canTitle: String, + val date: String, + val chargeMethod: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanStatusResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanStatusResponse.kt new file mode 100644 index 0000000..401a551 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanStatusResponse.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.can + +data class GetCanStatusResponse( + val chargeCan: Int, + val rewardCan: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanUseStatusResponseItem.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanUseStatusResponseItem.kt new file mode 100644 index 0000000..bef3654 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/GetCanUseStatusResponseItem.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can + +data class GetCanUseStatusResponseItem( + val title: String, + val date: String, + val can: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt new file mode 100644 index 0000000..bb59e85 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.can.charge + +import kr.co.vividnext.sodalive.can.Can +import kr.co.vividnext.sodalive.can.payment.Payment +import kr.co.vividnext.sodalive.can.use.UseCan +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.CascadeType +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToOne + +@Entity +data class Charge( + var chargeCan: Int, + var rewardCan: Int, + @Enumerated(value = EnumType.STRING) + var status: ChargeStatus = ChargeStatus.CHARGE +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "can_id", nullable = true) + var can: Can? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinColumn(name = "payment_id", nullable = true) + var payment: Payment? = null + set(value) { + value?.charge = this + field = value + } + + @OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinColumn(name = "use_can_id", nullable = true) + var useCan: UseCan? = null + + var title: String? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt new file mode 100644 index 0000000..6f6291b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt @@ -0,0 +1,52 @@ +package kr.co.vividnext.sodalive.can.charge + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.User +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 +@RequestMapping("/charge") +class ChargeController(private val service: ChargeService) { + + @PostMapping + fun charge( + @RequestBody chargeRequest: ChargeRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.charge(member, chargeRequest)) + } + + @PostMapping("/verify") + fun verify( + @RequestBody verifyRequest: VerifyRequest, + @AuthenticationPrincipal user: User + ) = ApiResponse.ok(service.verify(user, verifyRequest)) + + @PostMapping("/apple") + fun appleCharge( + @RequestBody chargeRequest: AppleChargeRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.appleCharge(member, chargeRequest)) + } + + @PostMapping("/apple/verify") + fun appleVerify( + @RequestBody verifyRequest: AppleVerifyRequest, + @AuthenticationPrincipal user: User + ) = ApiResponse.ok(service.appleVerify(user, verifyRequest)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt new file mode 100644 index 0000000..7e7374a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.can.charge + +import com.fasterxml.jackson.annotation.JsonProperty +import kr.co.vividnext.sodalive.can.payment.PaymentGateway + +data class ChargeRequest(val canId: Long, val paymentGateway: PaymentGateway) + +data class ChargeResponse(val chargeId: Long) + +data class VerifyRequest( + @JsonProperty("receipt_id") + val receiptId: String, + @JsonProperty("order_id") + val orderId: String +) + +data class VerifyResult( + @JsonProperty("receipt_id") + val receiptId: String, + val method: String, + val status: Int, + val price: Int +) + +data class AppleChargeRequest( + val title: String, + val chargeCan: Int, + val paymentGateway: PaymentGateway, + var price: Double? = null, + var locale: String? = null +) + +data class AppleVerifyRequest(val receiptString: String, val chargeId: Long) + +data class AppleVerifyResponse(val status: Int) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt new file mode 100644 index 0000000..d9d02d6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can.charge + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ChargeRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt new file mode 100644 index 0000000..a9c7e21 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -0,0 +1,203 @@ +package kr.co.vividnext.sodalive.can.charge + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.bootpay.Bootpay +import kr.co.vividnext.sodalive.can.CanRepository +import kr.co.vividnext.sodalive.can.payment.Payment +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpHeaders +import org.springframework.security.core.userdetails.User +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class ChargeService( + private val chargeRepository: ChargeRepository, + private val canRepository: CanRepository, + private val memberRepository: MemberRepository, + private val objectMapper: ObjectMapper, + private val okHttpClient: OkHttpClient, + @Value("\${bootpay.application-id}") + private val bootpayApplicationId: String, + @Value("\${bootpay.private-key}") + private val bootpayPrivateKey: String, + @Value("\${apple.iap-verify-sandbox-url}") + private val appleInAppVerifySandBoxUrl: String, + @Value("\${apple.iap-verify-url}") + private val appleInAppVerifyUrl: String +) { + + @Transactional + fun charge(member: Member, request: ChargeRequest): ChargeResponse { + val can = canRepository.findByIdOrNull(request.canId) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + val charge = Charge(can.can, can.rewardCan) + charge.title = can.title + charge.member = member + charge.can = can + + val payment = Payment(paymentGateway = request.paymentGateway) + payment.price = can.price.toDouble() + charge.payment = payment + + chargeRepository.save(charge) + + return ChargeResponse(chargeId = charge.id!!) + } + + @Transactional + fun verify(user: User, verifyRequest: VerifyRequest) { + val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) + ?: throw SodaException("결제정보에 오류가 있습니다.") + val member = memberRepository.findByEmail(user.username) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (charge.payment!!.paymentGateway == PaymentGateway.PG) { + val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey) + + try { + bootpay.accessToken + val verifyResult = objectMapper.convertValue( + bootpay.getReceipt(verifyRequest.receiptId), + VerifyResult::class.java + ) + + if (verifyResult.status == 1 && verifyResult.price == charge.can?.price) { + charge.payment?.receiptId = verifyResult.receiptId + charge.payment?.method = verifyResult.method + charge.payment?.status = PaymentStatus.COMPLETE + member.charge(charge.chargeCan, charge.rewardCan, "pg") + } else { + throw SodaException("결제정보에 오류가 있습니다.") + } + } catch (e: Exception) { + throw SodaException("결제정보에 오류가 있습니다.") + } + } else { + throw SodaException("결제정보에 오류가 있습니다.") + } + } + + @Transactional + fun appleCharge(member: Member, request: AppleChargeRequest): ChargeResponse { + val charge = Charge(request.chargeCan, 0) + charge.title = request.title + charge.member = member + + val payment = Payment(paymentGateway = request.paymentGateway) + payment.price = if (request.price != null) { + request.price!! + } else { + 0.toDouble() + } + + payment.locale = request.locale + + charge.payment = payment + + chargeRepository.save(charge) + + return ChargeResponse(chargeId = charge.id!!) + } + + @Transactional + fun appleVerify(user: User, verifyRequest: AppleVerifyRequest) { + val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId) + ?: throw SodaException("결제정보에 오류가 있습니다.") + val member = memberRepository.findByEmail(user.username) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) { + // 검증로직 + if (requestRealServerVerify(verifyRequest)) { + charge.payment?.receiptId = verifyRequest.receiptString + charge.payment?.method = "애플(인 앱 결제)" + charge.payment?.status = PaymentStatus.COMPLETE + member.charge(charge.chargeCan, charge.rewardCan, "ios") + } else { + throw SodaException("결제정보에 오류가 있습니다.") + } + } else { + throw SodaException("결제정보에 오류가 있습니다.") + } + } + + private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean { + val body = JSONObject() + body.put("receipt-data", verifyRequest.receiptString) + val request = Request.Builder() + .url(appleInAppVerifyUrl) + .addHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .post(body.toString().toRequestBody("application/json".toMediaTypeOrNull())) + .build() + + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val responseString = response.body?.string() + if (responseString != null) { + val verifyResult = objectMapper.readValue(responseString, AppleVerifyResponse::class.java) + return when (verifyResult.status) { + 0 -> { + true + } + + 21007 -> { + requestSandboxServerVerify(verifyRequest) + } + + else -> { + throw SodaException("결제정보에 오류가 있습니다.") + } + } + } else { + throw SodaException("결제를 완료하지 못했습니다.") + } + } else { + throw SodaException("결제를 완료하지 못했습니다.") + } + } + + private fun requestSandboxServerVerify(verifyRequest: AppleVerifyRequest): Boolean { + val body = JSONObject() + body.put("receipt-data", verifyRequest.receiptString) + val request = Request.Builder() + .url(appleInAppVerifySandBoxUrl) + .addHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .post(body.toString().toRequestBody("application/json".toMediaTypeOrNull())) + .build() + + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val responseString = response.body?.string() + if (responseString != null) { + val verifyResult = objectMapper.readValue(responseString, AppleVerifyResponse::class.java) + return when (verifyResult.status) { + 0 -> { + true + } + + else -> { + throw SodaException("결제정보에 오류가 있습니다.") + } + } + } else { + throw SodaException("결제를 완료하지 못했습니다.") + } + } else { + throw SodaException("결제를 완료하지 못했습니다.") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt new file mode 100644 index 0000000..07d5c0f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.can.charge + +enum class ChargeStatus { + CHARGE, REFUND_CHARGE, EVENT, CANCEL, + + // 관리자 지급 + ADMIN +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/Payment.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/Payment.kt new file mode 100644 index 0000000..7438bbd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/Payment.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.can.payment + +import kr.co.vividnext.sodalive.can.charge.Charge +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.OneToOne + +@Entity +data class Payment( + @Enumerated(value = EnumType.STRING) + var status: PaymentStatus = PaymentStatus.REQUEST, + + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + val paymentGateway: PaymentGateway +) : BaseEntity() { + @OneToOne(mappedBy = "payment", fetch = FetchType.LAZY) + var charge: Charge? = null + + @Column(columnDefinition = "TEXT", nullable = true) + var receiptId: String? = null + var method: String? = null + + var price: Double = 0.toDouble() + var locale: String? = null +} + +enum class PaymentStatus { + // 결제요청 + REQUEST, + + // 결제완료 + COMPLETE, + + // 환불 + RETURN +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt new file mode 100644 index 0000000..8874c09 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.can.payment + +enum class PaymentGateway { + PG, GOOGLE_IAP, APPLE_IAP +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt new file mode 100644 index 0000000..5e53fdc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/CanUsage.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.can.use + +enum class CanUsage { + LIVE, + DONATION, + CHANGE_NICKNAME, + ORDER_CONTENT +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt new file mode 100644 index 0000000..9232846 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt @@ -0,0 +1,40 @@ +package kr.co.vividnext.sodalive.can.use + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.CascadeType +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany + +@Entity +data class UseCan( + @Enumerated(value = EnumType.STRING) + val canUsage: CanUsage, + + val can: Int, + + val rewardCan: Int, + + var isRefund: Boolean = false +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = true) + var room: LiveRoom? = null + set(value) { + value?.useCan?.add(this) + field = value + } + + @OneToMany(mappedBy = "useCan", cascade = [CascadeType.ALL]) + val useCanCalculates: MutableList = mutableListOf() +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt new file mode 100644 index 0000000..0982736 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.can.use + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class UseCanCalculate( + val can: Int, + + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + val paymentGateway: PaymentGateway, + + @Column(nullable = false) + @Enumerated(value = EnumType.STRING) + var status: UseCanCalculateStatus +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinColumn(name = "use_can_id", nullable = false) + var useCan: UseCan? = null + set(value) { + value?.useCanCalculates?.add(this) + field = value + } +} + +enum class UseCanCalculateStatus { + RECEIVED, CALCULATE_COMPLETE, REFUND +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/OkHttpConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/OkHttpConfig.kt new file mode 100644 index 0000000..6a5523a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/OkHttpConfig.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.configs + +import okhttp3.OkHttpClient +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import java.util.concurrent.TimeUnit + +@Configuration +class OkHttpConfig { + @Bean("okHttpClient") + fun okHttpClient(): OkHttpClient { + return OkHttpClient() + .newBuilder().apply { + // 서버 연결을 최대 60초 수행 + connectTimeout(60, TimeUnit.SECONDS) + // 서버 요청을 최대 60초 수행 + writeTimeout(60, TimeUnit.SECONDS) + // 서버 응답을 최대 60초 기다림 + readTimeout(60, TimeUnit.SECONDS) + }.build() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt index 2578439..5137e5c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -1,5 +1,6 @@ package kr.co.vividnext.sodalive.live.room +import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.member.Member import java.time.LocalDateTime @@ -9,6 +10,7 @@ import javax.persistence.EnumType import javax.persistence.Enumerated import javax.persistence.FetchType import javax.persistence.JoinColumn +import javax.persistence.OneToMany import javax.persistence.OneToOne @Entity @@ -31,6 +33,9 @@ data class LiveRoom( @JoinColumn(name = "member_id", nullable = false) var member: Member? = null + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) + var useCan: MutableList = mutableListOf() + var channelName: String? = null var isActive: Boolean = true } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index e836a94..a500c93 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -56,8 +56,8 @@ data class Member( private var pgRewardCan: Int = 0 private var googleChargeCan: Int = 0 private var googleRewardCan: Int = 0 - private var appleChargeCan: Int = 0 - private var appleRewardCan: Int = 0 + var appleChargeCan: Int = 0 + var appleRewardCan: Int = 0 fun getChargeCan(container: String): Int { return when (container) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 180b5dd..75ee46f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -12,6 +12,10 @@ bootpay: applicationId: ${BOOTPAY_APPLICATION_ID} privateKey: ${BOOTPAY_PRIVATE_KEY} +apple: + iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt + iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt + cloud: aws: credentials: diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index ac59f28..4cbecc3 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -5,6 +5,10 @@ logging: util: EC2MetadataUtils: error +apple: + iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt + iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt + cloud: aws: credentials: From f1610af6f6a159be0b2f16cb5aab6fbd8de0a011 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 29 Jul 2023 05:57:07 +0900 Subject: [PATCH 19/94] =?UTF-8?q?=EC=BD=94=EC=9D=B8=20=EC=B6=A9=EC=A0=84?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt index 05bedff..2220329 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -73,7 +73,7 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue return queryFactory .selectFrom(charge) .innerJoin(charge.member, qMember) - .innerJoin(charge.useCan, useCan) + .leftJoin(charge.useCan, useCan) .leftJoin(charge.payment, payment) .where( qMember.id.eq(member.id) From 036107d103a6b2a4aa61f69953e3ad3f6a870418 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 31 Jul 2023 02:04:32 +0900 Subject: [PATCH 20/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20-=20?= =?UTF-8?q?=EB=B0=A9=20=EB=A7=8C=EB=93=A4=EA=B8=B0,=20=ED=83=9C=EA=B7=B8?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D,=20=ED=83=9C=EA=B7=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/can/CanRepository.kt | 18 ++ .../sodalive/extensions/StringExtensions.kt | 9 + .../live/recommend/LiveRecommendRepository.kt | 2 - .../live/reservation/LiveReservation.kt | 27 +++ .../live/room/CreateLiveRoomResponse.kt | 6 + .../live/room/CreateSudaRoomRequest.kt | 15 ++ .../vividnext/sodalive/live/room/LiveRoom.kt | 13 +- .../sodalive/live/room/LiveRoomController.kt | 28 +++ .../sodalive/live/room/LiveRoomRepository.kt | 27 ++- .../sodalive/live/room/LiveRoomService.kt | 202 +++++++++++++++++- .../sodalive/live/room/LiveRoomTag.kt | 19 ++ .../live/room/detail/GetRoomDetailResponse.kt | 66 ++++++ .../sodalive/live/tag/CreateLiveTagRequest.kt | 3 + .../sodalive/live/tag/GetLiveTagResponse.kt | 9 + .../co/vividnext/sodalive/live/tag/LiveTag.kt | 17 ++ .../sodalive/live/tag/LiveTagController.kt | 35 +++ .../sodalive/live/tag/LiveTagRepository.kt | 40 ++++ .../sodalive/live/tag/LiveTagService.kt | 52 +++++ 18 files changed, 579 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt index 2220329..6c2f964 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -8,10 +8,13 @@ import kr.co.vividnext.sodalive.can.charge.QCharge.charge import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.can.payment.PaymentStatus import kr.co.vividnext.sodalive.can.payment.QPayment.payment +import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.QUseCan.useCan import kr.co.vividnext.sodalive.can.use.UseCan +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.QMember +import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -23,6 +26,7 @@ interface CanQueryRepository { fun findAllByStatus(status: CanStatus): List fun getCanUseStatus(member: Member, pageable: Pageable): List fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List + fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? } @Repository @@ -93,4 +97,18 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue .orderBy(charge.id.desc()) .fetch() } + + override fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? { + return queryFactory + .selectFrom(useCan) + .innerJoin(useCan.member, member) + .innerJoin(useCan.room, liveRoom) + .where( + member.id.eq(memberId) + .and(liveRoom.id.eq(roomId)) + .and(useCan.canUsage.eq(CanUsage.LIVE)) + ) + .orderBy(useCan.id.desc()) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt new file mode 100644 index 0000000..ff1bb02 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/StringExtensions.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.extensions + +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +fun String.convertLocalDateTime(format: String): LocalDateTime { + val dateTimeFormatter = DateTimeFormatter.ofPattern(format) + return LocalDateTime.parse(this, dateTimeFormatter) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 7ebacf8..7d1ebba 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -4,7 +4,6 @@ import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner -import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member @@ -63,7 +62,6 @@ class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { .where( where .and(liveRoom.isActive.isTrue) - .and(liveRoom.type.ne(LiveRoomType.SECRET)) .and(liveRoom.channelName.isNotNull) .and(liveRoom.channelName.isNotEmpty) ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt new file mode 100644 index 0000000..59cf6f2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservation.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.live.reservation + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToOne + +@Entity +data class LiveReservation( + var isActive: Boolean = true +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = true) + var room: LiveRoom? = null + set(value) { + value?.reservations!!.add(this) + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt new file mode 100644 index 0000000..24fb1be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateLiveRoomResponse.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.room + +data class CreateLiveRoomResponse( + val id: Long?, + val channelName: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt new file mode 100644 index 0000000..2df98d8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/CreateSudaRoomRequest.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.live.room + +data class CreateSudaRoomRequest( + val title: String, + val content: String, + val coverImageUrl: String? = null, + val isAdult: Boolean, + val tags: List, + val numberOfPeople: Int, + val beginDateTimeString: String? = null, + val price: Int = 0, + val timezone: String, + val type: LiveRoomType = LiveRoomType.OPEN, + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt index 5137e5c..85035a3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.reservation.LiveReservation import kr.co.vividnext.sodalive.member.Member import java.time.LocalDateTime +import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity import javax.persistence.EnumType @@ -33,9 +35,15 @@ data class LiveRoom( @JoinColumn(name = "member_id", nullable = false) var member: Member? = null + @OneToMany(mappedBy = "room", cascade = [CascadeType.ALL]) + var tags: MutableList = mutableListOf() + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) var useCan: MutableList = mutableListOf() + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) + var reservations: MutableList = mutableListOf() + var channelName: String? = null var isActive: Boolean = true } @@ -45,10 +53,7 @@ enum class LiveRoomType { OPEN, // 비공개 - PRIVATE, - - // 비밀방 - SECRET + PRIVATE } enum class LiveRoomStatus { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index ec927e4..c15d880 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -6,9 +6,13 @@ import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal 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.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/live/room") @@ -26,4 +30,28 @@ class LiveRoomController(private val service: LiveRoomService) { ApiResponse.ok(service.getRoomList(dateString, status, pageable, member, timezone)) } + + @PostMapping + fun createLiveRoom( + @RequestPart("coverImage") coverImage: MultipartFile?, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.createLiveRoom(coverImage, requestString, member)) + } + + @GetMapping("/detail/{id}") + fun getRoomDetail( + @PathVariable id: Long, + @RequestParam timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member != null) { + ApiResponse.ok(service.getRoomDetail(id, member, timezone)) + } else { + throw SodaException("로그인 정보를 확인해주세요.") + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index 82e227d..169ae95 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -5,6 +5,8 @@ import com.querydsl.core.types.Predicate import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.reservation.LiveReservation +import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.QMember @@ -29,6 +31,9 @@ interface LiveRoomQueryRepository { timezone: String, isAdult: Boolean ): List + + fun getLiveRoom(id: Long): LiveRoom? + fun getReservationList(roomId: Long): List } class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { @@ -79,7 +84,6 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L where = where.and(liveRoom.isActive.isTrue) .and(liveRoom.member.isNotNull) - .and(liveRoom.type.ne(LiveRoomType.SECRET)) return queryFactory .selectFrom(liveRoom) @@ -98,6 +102,27 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L .fetch() } + override fun getLiveRoom(id: Long): LiveRoom? { + return queryFactory + .selectFrom(liveRoom) + .where( + liveRoom.id.eq(id) + .and(liveRoom.isActive.isTrue) + ) + .fetchFirst() + } + + override fun getReservationList(roomId: Long): List { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(liveReservation.isActive.isTrue) + ) + .fetch() + } + private fun orderByFieldAccountId( memberId: Long, status: LiveRoomStatus, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 24a41b8..3115000 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -1,11 +1,24 @@ package kr.co.vividnext.sodalive.live.room +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.can.CanRepository +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository +import kr.co.vividnext.sodalive.live.tag.LiveTagRepository import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile +import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -14,8 +27,15 @@ class LiveRoomService( private val repository: LiveRoomRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository, + private val tagRepository: LiveTagRepository, + private val canRepository: CanRepository, + private val objectMapper: ObjectMapper, + private val s3Uploader: S3Uploader, + + @Value("\${cloud.aws.s3.bucket}") + private val coverImageBucket: String, @Value("\${cloud.aws.cloud-front.host}") - private val coverImageHost: String + private val cloudFrontHost: String ) { fun getRoomList( dateString: String?, @@ -61,7 +81,7 @@ class LiveRoomService( coverImageUrl = if (it.coverImage!!.startsWith("https://")) { it.coverImage!! } else { - "$coverImageHost/${it.coverImage!!}" + "$cloudFrontHost/${it.coverImage!!}" }, isReservation = false, isPrivateRoom = it.type == LiveRoomType.PRIVATE @@ -69,4 +89,182 @@ class LiveRoomService( } .toList() } + + fun createLiveRoom(coverImage: MultipartFile?, requestString: String, member: Member): CreateLiveRoomResponse { + val request = objectMapper.readValue(requestString, CreateSudaRoomRequest::class.java) + if (request.coverImageUrl == null && coverImage == null) { + throw SodaException("커버이미지를 선택해 주세요.") + } + + val now = LocalDateTime.now() + val beginDateTime = if (request.beginDateTimeString != null) { + request.beginDateTimeString.convertLocalDateTime("yyyy-MM-dd HH:mm") + .atZone(ZoneId.of(request.timezone)) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } else { + now + } + + if ( + request.beginDateTimeString != null && + beginDateTime < now.plusMinutes(30) + ) { + throw SodaException("현재시각 기준, 30분 이후부터 설정가능합니다.") + } + + if ( + request.type == LiveRoomType.PRIVATE && + (request.password == null || request.password.length != 6) + ) { + throw SodaException("방 입장 비밀번호 6자리를 입력해 주세요.") + } + + val room = LiveRoom( + title = request.title, + notice = request.content, + beginDateTime = beginDateTime, + numberOfPeople = request.numberOfPeople, + isAdult = request.isAdult, + price = request.price, + type = request.type, + password = request.password + ) + room.member = member + + if (request.beginDateTimeString == null) { + room.channelName = "SODA_LIVE_CHANNEL_" + + "${member.id!!}_${beginDateTime.year}_${beginDateTime.month}_${beginDateTime.dayOfMonth}_" + + "${beginDateTime.hour}_${beginDateTime.minute}" + } + + request.tags.forEach { + val tag = tagRepository.findByTag(it) + if (tag != null) { + room.tags.add(LiveRoomTag(room, tag)) + + if (tag.tag.contains("음담패설")) { + room.isAdult = true + } + } + } + + val createdRoom = repository.save(room) + // 이미지 업로드 + if (coverImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${room.id}-cover") + + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "live_room_cover/${room.id}/$coverImageFileName", + metadata = metadata + ) + + room.coverImage = coverImagePath + room.bgImage = coverImagePath + } else { + room.coverImage = request.coverImageUrl + room.bgImage = request.coverImageUrl + } + + return CreateLiveRoomResponse(createdRoom.id, createdRoom.channelName) + } + + fun getRoomDetail(roomId: Long, member: Member, timezone: String): GetRoomDetailResponse { + val room = repository.getLiveRoom(id = roomId) + ?: throw SodaException("이미 종료된 방입니다") + + if (room.isAdult && member.auth == null) { + throw SodaException("본인인증이 필요한 서비스 입니다.\n요즘라이브 마이페이지에서 본인인증 후 다시 이용해 주세요.") + } + + val beginDateTime = room.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + val response = GetRoomDetailResponse( + roomId = roomId, + title = room.title, + content = room.notice, + price = room.price, + tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(), + numberOfParticipantsTotal = room.numberOfPeople, + numberOfParticipants = 0, + channelName = room.channelName, + beginDateTime = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + isPaid = false, + isPrivateRoom = room.type == LiveRoomType.PRIVATE, + password = room.password + ) + response.manager = GetRoomDetailManager(room.member!!, cloudFrontHost = cloudFrontHost) + + if (!room.channelName.isNullOrBlank()) { + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + + if (roomInfo != null) { + response.isPaid = canRepository.isExistPaidLiveRoom( + memberId = member.id!!, + roomId = roomId + ) != null + + val users = roomInfo.speakerList + .asSequence() + .map { + GetRoomDetailUser( + it.id, + it.nickname, + "$cloudFrontHost/${it.profileImage}" + ) + }.toMutableList() + + users.addAll( + roomInfo.listenerList + .asSequence() + .map { + GetRoomDetailUser( + it.id, + it.nickname, + "$cloudFrontHost/${it.profileImage}" + ) + } + ) + + users.addAll( + roomInfo.managerList + .asSequence() + .map { + GetRoomDetailUser( + it.id, + it.nickname, + "$cloudFrontHost/${it.profileImage}" + ) + } + ) + + response.participatingUsers = users + response.numberOfParticipants = users.size + } + } else { + val reservationList = repository.getReservationList(roomId) + response.participatingUsers = reservationList + .asSequence() + .map { + if (it.member!!.id!! == member.id!!) { + response.isPaid = true + } + + GetRoomDetailUser(it.member!!, cloudFrontHost) + } + .toList() + response.numberOfParticipants = reservationList.size + } + + return response + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt new file mode 100644 index 0000000..c822931 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomTag.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.live.room + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.tag.LiveTag +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +class LiveRoomTag( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "live_room_id", nullable = false) + var room: LiveRoom, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "live_tag_id", nullable = false) + var tag: LiveTag +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt new file mode 100644 index 0000000..8a8a0d8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt @@ -0,0 +1,66 @@ +package kr.co.vividnext.sodalive.live.room.detail + +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole + +data class GetRoomDetailResponse( + val roomId: Long, + val price: Int, + val title: String, + val content: String, + var isPaid: Boolean, + val isPrivateRoom: Boolean, + val password: String?, + val tags: List, + val channelName: String?, + val beginDateTime: String, + var numberOfParticipants: Int, + val numberOfParticipantsTotal: Int +) { + var manager: GetRoomDetailManager? = null + var participatingUsers: List = listOf() +} + +data class GetRoomDetailManager( + val id: Long, + val nickname: String, + val introduce: String, + val youtubeUrl: String?, + val instagramUrl: String?, + val websiteUrl: String?, + val blogUrl: String?, + val profileImageUrl: String, + val isCreator: Boolean +) { + constructor(member: Member, cloudFrontHost: String) : this( + id = member.id!!, + nickname = member.nickname, + introduce = member.introduce, + youtubeUrl = member.youtubeUrl, + instagramUrl = member.instagramUrl, + websiteUrl = member.websiteUrl, + blogUrl = member.blogUrl, + profileImageUrl = if (member.profileImage != null) { + "$cloudFrontHost/${member.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + isCreator = member.role == MemberRole.CREATOR + ) +} + +data class GetRoomDetailUser( + val id: Long, + val nickname: String, + val profileImageUrl: String +) { + constructor(member: Member, cloudFrontHost: String) : this( + member.id!!, + member.nickname, + profileImageUrl = if (member.profileImage != null) { + "$cloudFrontHost/${member.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + } + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt new file mode 100644 index 0000000..80a1b59 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/CreateLiveTagRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.tag + +data class CreateLiveTagRequest(val tag: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt new file mode 100644 index 0000000..e478871 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/GetLiveTagResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.live.tag + +import com.querydsl.core.annotations.QueryProjection + +data class GetLiveTagResponse @QueryProjection constructor( + val id: Long, + val tag: String, + val image: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt new file mode 100644 index 0000000..99b0735 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTag.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.live.tag + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class LiveTag( + @Column(unique = true, nullable = false) + var tag: String, + @Column(nullable = true) + var image: String? = null, + @Column(nullable = false) + var isActive: Boolean = true, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt new file mode 100644 index 0000000..d72d1d6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.live.tag + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/live/tag") +class LiveTagController(private val service: LiveTagService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun enrollmentLiveTag( + @RequestPart("image") image: MultipartFile, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.enrollmentLiveTag(image, requestString), "등록되었습니다.") + + @GetMapping + fun getTags( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + ApiResponse.ok(service.getTags(member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt new file mode 100644 index 0000000..009768f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagRepository.kt @@ -0,0 +1,40 @@ +package kr.co.vividnext.sodalive.live.tag + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.tag.QLiveTag.liveTag +import kr.co.vividnext.sodalive.member.MemberRole +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveTagRepository : JpaRepository, LiveTagQueryRepository { + fun findByTag(it: String): LiveTag? +} + +interface LiveTagQueryRepository { + fun getTags(role: MemberRole, isAdult: Boolean, cloudFrontHost: String): List +} + +@Repository +class LiveTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveTagQueryRepository { + override fun getTags(role: MemberRole, isAdult: Boolean, cloudFrontHost: String): List { + var where = liveTag.isActive.isTrue + + if (role != MemberRole.ADMIN && !isAdult) { + where = where.and(liveTag.tag.notIn("음담패설", "EDPS")) + } + + return queryFactory + .select( + QGetLiveTagResponse( + liveTag.id, + liveTag.tag, + liveTag.image.prepend("/").prepend(cloudFrontHost) + ) + ) + .from(liveTag) + .where(where) + .orderBy(liveTag.orders.asc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt new file mode 100644 index 0000000..e168af4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt @@ -0,0 +1,52 @@ +package kr.co.vividnext.sodalive.live.tag + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.web.multipart.MultipartFile + +@Service +class LiveTagService( + private val repository: LiveTagRepository, + + private val objectMapper: ObjectMapper, + private val s3Uploader: S3Uploader, + + @Value("\${cloud.aws.s3.bucket}") + private val coverImageBucket: String, + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun enrollmentLiveTag(image: MultipartFile, requestString: String) { + val request = objectMapper.readValue(requestString, CreateLiveTagRequest::class.java) + tagExistCheck(request) + + val tag = repository.save(LiveTag(request.tag)) + + val metadata = ObjectMetadata() + metadata.contentLength = image.size + + val tagImageFileName = generateFileName(prefix = "${tag.id}-") + val tagImagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = coverImageBucket, + filePath = "live_cover/${tag.id}/$tagImageFileName", + metadata = metadata + ) + + tag.image = tagImagePath + } + + fun getTags(member: Member): List { + return repository.getTags(role = member.role, isAdult = member.auth != null, cloudFrontHost = cloudFrontHost) + } + + fun tagExistCheck(request: CreateLiveTagRequest) { + repository.findByTag(request.tag)?.let { throw SodaException("이미 등록된 태그 입니다.") } + } +} From 9545ab0789c58c5d5ee143da159f24839be8dd25 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 31 Jul 2023 02:13:05 +0900 Subject: [PATCH 21/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20-=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C,=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EC=B1=84=EB=84=90=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A3=BC=EC=86=8C=EC=97=90=20CDN=20host=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../live/recommend/LiveRecommendRepository.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 7d1ebba..7895ea5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -7,11 +7,17 @@ import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository import java.time.LocalDateTime @Repository -class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { +class LiveRecommendRepository( + private val queryFactory: JPAQueryFactory, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { fun getRecommendLive(memberId: Long, isAdult: Boolean): List { val dateNow = LocalDateTime.now() @@ -26,7 +32,7 @@ class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { .select( Projections.constructor( GetRecommendLiveResponse::class.java, - recommendLiveCreatorBanner.image, + recommendLiveCreatorBanner.image.prepend("/").prepend(cloudFrontHost), recommendLiveCreatorBanner.creator.id ) ) @@ -53,7 +59,7 @@ class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { GetRecommendChannelResponse::class.java, member.id, member.nickname, - member.profileImage, + member.profileImage.prepend("/").prepend(cloudFrontHost), Expressions.asBoolean(true) ) ) @@ -85,7 +91,7 @@ class LiveRecommendRepository(private val queryFactory: JPAQueryFactory) { GetRecommendChannelResponse::class.java, member.id, member.nickname, - member.profileImage, + member.profileImage.prepend("/").prepend(cloudFrontHost), Expressions.asBoolean(false) ) ) From 197cca1f1b3501d6bf67bc84ade994f668583278 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 31 Jul 2023 03:04:09 +0900 Subject: [PATCH 22/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A4=EA=B8=B0=20-=20Transactional=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 3115000..6a6e191 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -17,12 +17,14 @@ import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @Service +@Transactional(readOnly = true) class LiveRoomService( private val repository: LiveRoomRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository, @@ -90,6 +92,7 @@ class LiveRoomService( .toList() } + @Transactional fun createLiveRoom(coverImage: MultipartFile?, requestString: String, member: Member): CreateLiveRoomResponse { val request = objectMapper.readValue(requestString, CreateSudaRoomRequest::class.java) if (request.coverImageUrl == null && coverImage == null) { From f393c7630e37b62bb81977b04ae23070fe8e3b96 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 31 Jul 2023 17:09:45 +0900 Subject: [PATCH 23/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20-=20?= =?UTF-8?q?=EC=8B=9C=EC=9E=91,=20=EC=B7=A8=EC=86=8C,=20=EC=9E=85=EC=9E=A5,?= =?UTF-8?q?=20=EC=88=98=EC=A0=95,=20=EC=98=88=EC=95=BD=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/can/CanRepository.kt | 16 ++ .../sodalive/can/charge/ChargeRepository.kt | 73 +++++- .../sodalive/can/payment/CanPaymentService.kt | 245 ++++++++++++++++++ .../sodalive/can/use/TotalSpentCan.kt | 18 ++ .../sodalive/can/use/UseCanCalculate.kt | 2 + .../can/use/UseCanCalculateRepository.kt | 12 + .../sodalive/can/use/UseCanRepository.kt | 7 + .../reservation/LiveReservationController.kt | 24 ++ .../reservation/LiveReservationRepository.kt | 70 +++++ .../reservation/LiveReservationService.kt | 79 ++++++ .../reservation/MakeLiveReservationRequest.kt | 8 + .../MakeLiveReservationResponse.kt | 12 + .../live/room/EditLiveRoomInfoRequest.kt | 9 + .../live/room/EnterOrQuitLiveRoomRequest.kt | 7 + .../live/room/GetRecentRoomInfoResponse.kt | 9 + .../vividnext/sodalive/live/room/LiveRoom.kt | 7 + .../sodalive/live/room/LiveRoomController.kt | 62 ++++- .../sodalive/live/room/LiveRoomRepository.kt | 36 ++- .../sodalive/live/room/LiveRoomService.kt | 225 +++++++++++++++- .../sodalive/live/room/StartLiveRequest.kt | 8 + .../live/room/cancel/CancelLiveRequest.kt | 3 + .../live/room/cancel/LiveRoomCancel.kt | 21 ++ .../room/cancel/LiveRoomCancelRepository.kt | 7 + .../live/room/detail/GetRoomDetailResponse.kt | 2 +- .../visit/GetRecentVisitRoomMemberResponse.kt | 7 + .../sodalive/live/room/visit/LiveRoomVisit.kt | 24 ++ .../room/visit/LiveRoomVisitRepository.kt | 41 +++ .../live/room/visit/LiveRoomVisitService.kt | 25 ++ .../kr/co/vividnext/sodalive/member/Member.kt | 4 +- 29 files changed, 1044 insertions(+), 19 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt index 6c2f964..a404594 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -27,6 +27,7 @@ interface CanQueryRepository { fun getCanUseStatus(member: Member, pageable: Pageable): List fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? + fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? } @Repository @@ -111,4 +112,19 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue .orderBy(useCan.id.desc()) .fetchFirst() } + + override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? { + return queryFactory + .selectFrom(useCan) + .innerJoin(useCan.member, member) + .innerJoin(useCan.room, liveRoom) + .where( + member.id.eq(memberId) + .and(liveRoom.id.eq(roomId)) + .and(useCan.canUsage.eq(CanUsage.LIVE)) + .and(useCan.isRefund.isFalse) + ) + .orderBy(useCan.id.desc()) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt index d9d02d6..d8d5bf4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt @@ -1,7 +1,78 @@ package kr.co.vividnext.sodalive.can.charge +import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.charge.QCharge.charge +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.payment.QPayment.payment +import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface ChargeRepository : JpaRepository +interface ChargeRepository : JpaRepository, ChargeQueryRepository + +interface ChargeQueryRepository { + fun getOldestChargeWhereRewardCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? + fun getOldestChargeWhereChargeCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? +} + +class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ChargeQueryRepository { + override fun getOldestChargeWhereRewardCanGreaterThan0( + chargeId: Long, + memberId: Long, + container: String + ): Charge? { + return queryFactory + .selectFrom(charge) + .innerJoin(charge.member, member) + .leftJoin(charge.payment, payment) + .where( + member.id.eq(memberId) + .and(charge.rewardCan.gt(0)) + .and(charge.id.gt(chargeId)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + .and(getPaymentGatewayCondition(container)) + ) + .orderBy(charge.id.asc()) + .fetchFirst() + } + + override fun getOldestChargeWhereChargeCanGreaterThan0( + chargeId: Long, + memberId: Long, + container: String + ): Charge? { + return queryFactory + .selectFrom(charge) + .innerJoin(charge.member, member) + .leftJoin(charge.payment, payment) + .where( + member.id.eq(memberId) + .and(charge.chargeCan.gt(0)) + .and(charge.id.gt(chargeId)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + .and(getPaymentGatewayCondition(container)) + ) + .orderBy(charge.id.asc()) + .fetchFirst() + } + + private fun getPaymentGatewayCondition(container: String): BooleanExpression? { + val paymentGatewayCondition = when (container) { + "aos" -> { + payment.paymentGateway.eq(PaymentGateway.PG) + .or(payment.paymentGateway.eq(PaymentGateway.GOOGLE_IAP)) + } + + "ios" -> { + payment.paymentGateway.eq(PaymentGateway.PG) + .or(payment.paymentGateway.eq(PaymentGateway.APPLE_IAP)) + } + + else -> payment.paymentGateway.eq(PaymentGateway.PG) + } + return paymentGatewayCondition + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt new file mode 100644 index 0000000..434ced3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -0,0 +1,245 @@ +package kr.co.vividnext.sodalive.can.payment + +import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.SpentCan +import kr.co.vividnext.sodalive.can.use.TotalSpentCan +import kr.co.vividnext.sodalive.can.use.UseCan +import kr.co.vividnext.sodalive.can.use.UseCanCalculate +import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository +import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus +import kr.co.vividnext.sodalive.can.use.UseCanRepository +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CanPaymentService( + private val memberRepository: MemberRepository, + private val chargeRepository: ChargeRepository, + private val useCanRepository: UseCanRepository, + private val useCanCalculateRepository: UseCanCalculateRepository +) { + @Transactional + fun spendCan( + memberId: Long, + needCan: Int, + canUsage: CanUsage, + liveRoom: LiveRoom? = null, + container: String + ) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + val useRewardCan = spendRewardCan(memberId, needCan, container) + val useChargeCan = if (needCan - useRewardCan.total > 0) { + spendChargeCan(memberId, needCan = needCan - useRewardCan.total, container = container) + } else { + null + } + + if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) { + throw SodaException( + "${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " + + "캔이 부족합니다. 충전 후 이용해 주세요." + ) + } + + if (!useRewardCan.verify() || useChargeCan?.verify() == false) { + throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + } + + val useCan = UseCan( + canUsage = canUsage, + can = useChargeCan?.total ?: 0, + rewardCan = useRewardCan.total + ) + + var recipientId: Long? = null + if (canUsage == CanUsage.LIVE && liveRoom != null) { + recipientId = liveRoom.member!!.id!! + useCan.room = liveRoom + useCan.member = member + } else if (canUsage == CanUsage.CHANGE_NICKNAME) { + useCan.member = member + } else if (canUsage == CanUsage.DONATION && liveRoom != null) { + recipientId = liveRoom.member!!.id!! + useCan.room = liveRoom + useCan.member = member + } else { + throw SodaException("잘못된 요청입니다.") + } + + useCanRepository.save(useCan) + + setUseCoinCalculate(recipientId, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.PG) + setUseCoinCalculate( + recipientId, + useRewardCan, + useChargeCan, + useCan, + paymentGateway = PaymentGateway.GOOGLE_IAP + ) + setUseCoinCalculate( + recipientId, + useRewardCan, + useChargeCan, + useCan, + paymentGateway = PaymentGateway.APPLE_IAP + ) + } + + private fun setUseCoinCalculate( + recipientId: Long?, + useRewardCan: TotalSpentCan, + useChargeCan: TotalSpentCan?, + useCan: UseCan, + paymentGateway: PaymentGateway + ) { + val totalSpentRewardCan = useRewardCan.spentCans + .filter { it.paymentGateway == paymentGateway } + .fold(0) { sum, spentCans -> sum + spentCans.can } + + val useCanCalculate = if (useChargeCan != null) { + val totalSpentChargeCan = useChargeCan.spentCans + .filter { it.paymentGateway == paymentGateway } + .fold(0) { sum, spentCans -> sum + spentCans.can } + + UseCanCalculate( + can = totalSpentChargeCan + totalSpentRewardCan, + paymentGateway = paymentGateway, + status = UseCanCalculateStatus.RECEIVED + ) + } else { + UseCanCalculate( + can = totalSpentRewardCan, + paymentGateway = paymentGateway, + status = UseCanCalculateStatus.RECEIVED + ) + } + + if (useCanCalculate.can > 0) { + useCanCalculate.useCan = useCan + useCanCalculate.recipientCreatorId = recipientId + useCanCalculateRepository.save(useCanCalculate) + } + } + + private fun spendRewardCan(memberId: Long, needCan: Int, container: String): TotalSpentCan { + return if (needCan > 0) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + + val spentCans = mutableListOf() + var chargeId = 0L + var total = 0 + + while (needCan - total > 0) { + val remainingNeedCan = needCan - total + val charge = chargeRepository.getOldestChargeWhereRewardCanGreaterThan0(chargeId, memberId, container) + ?: break + + if (charge.rewardCan >= remainingNeedCan) { + charge.rewardCan -= remainingNeedCan + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + total += remainingNeedCan + + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = remainingNeedCan + ) + ) + } else { + total += charge.rewardCan + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = charge.rewardCan + ) + ) + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + charge.rewardCan = 0 + } + + chargeId = charge.id!! + } + + TotalSpentCan(spentCans, total) + } else { + TotalSpentCan(total = 0) + } + } + + private fun spendChargeCan(memberId: Long, needCan: Int, container: String): TotalSpentCan { + return if (needCan > 0) { + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.") + + val spentCans = mutableListOf() + var chargeId = 0L + var total = 0 + + while (needCan - total > 0) { + val remainingNeedCan = needCan - total + val charge = chargeRepository.getOldestChargeWhereChargeCanGreaterThan0(chargeId, memberId, container) + ?: break + + if (charge.rewardCan >= remainingNeedCan) { + charge.rewardCan -= remainingNeedCan + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + total += remainingNeedCan + + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = remainingNeedCan + ) + ) + } else { + total += charge.rewardCan + spentCans.add( + SpentCan( + paymentGateway = charge.payment!!.paymentGateway, + can = charge.rewardCan + ) + ) + + when (charge.payment!!.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + } + + charge.rewardCan = 0 + } + + chargeId = charge.id!! + } + + TotalSpentCan(spentCans, total) + } else { + TotalSpentCan(total = 0) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt new file mode 100644 index 0000000..7b3f283 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/TotalSpentCan.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.can.use + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway + +data class TotalSpentCan( + val spentCans: MutableList = mutableListOf(), + val total: Int +) { + fun verify(): Boolean { + val sumSpentCans = spentCans.fold(0) { sum, spentCan -> sum + spentCan.can } + return total == sumSpentCans + } +} + +data class SpentCan( + val paymentGateway: PaymentGateway, + val can: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt index 0982736..3b0efbe 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculate.kt @@ -30,6 +30,8 @@ data class UseCanCalculate( value?.useCanCalculates?.add(this) field = value } + + var recipientCreatorId: Long? = null } enum class UseCanCalculateStatus { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt new file mode 100644 index 0000000..3d47f56 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanCalculateRepository.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.can.use + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UseCanCalculateRepository : JpaRepository { + fun findByUseCanIdAndStatus( + useCanId: Long, + status: UseCanCalculateStatus = UseCanCalculateStatus.RECEIVED + ): List +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt new file mode 100644 index 0000000..f30429a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCanRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can.use + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface UseCanRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt new file mode 100644 index 0000000..a0abd2c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.live.reservation + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +@RequestMapping("/live/reservation") +class LiveReservationController(private val service: LiveReservationService) { + @PostMapping + fun makeReservation( + @RequestBody request: MakeLiveReservationRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.makeReservation(request, member.id!!)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt new file mode 100644 index 0000000..ecaf7d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt @@ -0,0 +1,70 @@ +package kr.co.vividnext.sodalive.live.reservation + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveReservationRepository : JpaRepository, LiveReservationQueryRepository + +interface LiveReservationQueryRepository { + fun getReservationList(roomId: Long): List + + fun cancelReservation(roomId: Long) + + fun getReservationBookerList(roomId: Long): List + + fun isExistsReservation(roomId: Long, memberId: Long): Boolean +} + +@Repository +class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveReservationQueryRepository { + override fun getReservationList(roomId: Long): List { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(liveReservation.isActive.isTrue) + ) + .fetch() + } + + override fun cancelReservation(roomId: Long) { + queryFactory + .update(liveReservation) + .set(liveReservation.isActive, false) + .where(liveReservation.room.id.eq(roomId)) + .execute() + } + + override fun getReservationBookerList(roomId: Long): List { + return queryFactory + .select(member) + .from(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(liveReservation.isActive.isTrue) + ) + .fetch() + } + + override fun isExistsReservation(roomId: Long, memberId: Long): Boolean { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveReservation.isActive.isTrue + .and(liveReservation.room.id.eq(roomId)) + .and(liveReservation.member.id.eq(memberId)) + ) + .fetchFirst() != null + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt new file mode 100644 index 0000000..9184f72 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt @@ -0,0 +1,79 @@ +package kr.co.vividnext.sodalive.live.reservation + +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.LiveRoomRepository +import kr.co.vividnext.sodalive.live.room.LiveRoomType +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class LiveReservationService( + private val repository: LiveReservationRepository, + private val liveRoomRepository: LiveRoomRepository, + private val memberRepository: MemberRepository, + private val canPaymentService: CanPaymentService +) { + fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse { + val room = liveRoomRepository.findByIdOrNull(id = request.roomId) + ?: throw SodaException(message = "잘못된 요청입니다.\n다시 시도해 주세요.") + + val member = memberRepository.findByIdOrNull(id = memberId) + ?: throw SodaException(message = "로그인 정보를 확인해주세요.") + + if ( + room.member!!.id!! != memberId && + room.type == LiveRoomType.PRIVATE && + (request.password == null || request.password != room.password) + ) { + throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") + } + + if (repository.isExistsReservation(roomId = request.roomId, memberId = memberId)) { + throw SodaException("이미 예약한 라이브 입니다.") + } + + val haveCan = member.getChargeCan(request.container) + member.getRewardCan(request.container) + if (haveCan < room.price) { + throw SodaException("${room.price - haveCan}캔이 부족합니다. 충전 후 이용해 주세요.") + } + + if (room.price > 0) { + canPaymentService.spendCan( + memberId = member.id!!, + needCan = room.price, + canUsage = CanUsage.LIVE, + liveRoom = room, + container = request.container + ) + } + + val reservation = LiveReservation() + reservation.room = room + reservation.member = member + repository.save(reservation) + + val beginDateTime = room.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(request.timezone)) + + return MakeLiveReservationResponse( + reservationId = reservation.id!!, + nickname = room.member!!.nickname, + title = room.title, + beginDateString = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy년 M월 d일 (E), a hh:mm")), + price = if (room.price > 0) { + "${room.price} 캔" + } else { + "무료" + }, + haveCan = haveCan, + useCan = room.price, + remainingCan = haveCan - room.price + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt new file mode 100644 index 0000000..01d00a8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class MakeLiveReservationRequest( + val roomId: Long, + val container: String, + val timezone: String = "Asia/Seoul", + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt new file mode 100644 index 0000000..3cd4ff0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/MakeLiveReservationResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class MakeLiveReservationResponse( + val reservationId: Long, + val nickname: String, + val title: String, + val beginDateString: String, + val price: String, + val haveCan: Int, + val useCan: Int, + val remainingCan: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt new file mode 100644 index 0000000..437e8f9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EditLiveRoomInfoRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.live.room + +data class EditLiveRoomInfoRequest( + val title: String?, + val notice: String?, + val numberOfPeople: Int?, + val beginDateTimeString: String?, + val timezone: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt new file mode 100644 index 0000000..223c7e8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/EnterOrQuitLiveRoomRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room + +data class EnterOrQuitLiveRoomRequest( + val roomId: Long, + val container: String, + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt new file mode 100644 index 0000000..4a076d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRecentRoomInfoResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.live.room + +data class GetRecentRoomInfoResponse( + val title: String, + val notice: String, + var coverImageUrl: String, + val coverImagePath: String, + val numberOfPeople: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt index 85035a3..3c95441 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoom.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.can.use.UseCan import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.live.reservation.LiveReservation +import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancel +import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisit import kr.co.vividnext.sodalive.member.Member import java.time.LocalDateTime import javax.persistence.CascadeType @@ -44,6 +46,11 @@ data class LiveRoom( @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) var reservations: MutableList = mutableListOf() + @OneToMany(mappedBy = "room", fetch = FetchType.LAZY) + var visits: MutableList = mutableListOf() + + @OneToOne(mappedBy = "room") + var cancel: LiveRoomCancel? = null var channelName: String? = null var isActive: Boolean = true } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index c15d880..9ff9e48 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -2,12 +2,15 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal 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 import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestPart @@ -48,10 +51,59 @@ class LiveRoomController(private val service: LiveRoomService) { @RequestParam timezone: String, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { - if (member != null) { - ApiResponse.ok(service.getRoomDetail(id, member, timezone)) - } else { - throw SodaException("로그인 정보를 확인해주세요.") - } + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRoomDetail(id, member, timezone)) + } + + @PostMapping("/enter") + fun enterLive( + @RequestBody request: EnterOrQuitLiveRoomRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.enterLive(request, member)) + } + + @PutMapping("/start") + fun startLive( + @RequestBody request: StartLiveRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.startLive(request, member)) + } + + @PutMapping("/cancel") + fun cancelLive( + @RequestBody request: CancelLiveRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.cancelLive(request, member)) + } + + @GetMapping("/recent-room-info") + fun getRecentRoomInfo( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRecentRoomInfo(member)) + } + + @PutMapping("/{id}") + fun editLiveRoomInfo( + @PathVariable("id") roomId: Long, + @RequestPart("coverImage") coverImage: MultipartFile?, + @RequestPart("request") requestString: String?, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.editLiveRoomInfo(roomId, coverImage, requestString, member)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index 169ae95..f9f0873 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -2,11 +2,10 @@ package kr.co.vividnext.sodalive.live.room import com.querydsl.core.types.OrderSpecifier import com.querydsl.core.types.Predicate +import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory -import kr.co.vividnext.sodalive.live.reservation.LiveReservation -import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.QMember @@ -33,7 +32,8 @@ interface LiveRoomQueryRepository { ): List fun getLiveRoom(id: Long): LiveRoom? - fun getReservationList(roomId: Long): List + fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom? + fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse? } class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { @@ -112,15 +112,35 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L .fetchFirst() } - override fun getReservationList(roomId: Long): List { + override fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom? { return queryFactory - .selectFrom(liveReservation) - .innerJoin(liveReservation.room, liveRoom) + .selectFrom(liveRoom) + .innerJoin(liveRoom.member, member) .where( liveRoom.id.eq(roomId) - .and(liveReservation.isActive.isTrue) + .and(liveRoom.isActive.isTrue) + .and(member.id.eq(memberId)) ) - .fetch() + .fetchFirst() + } + + override fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse? { + return queryFactory + .select( + Projections.constructor( + GetRecentRoomInfoResponse::class.java, + liveRoom.title, + liveRoom.notice, + liveRoom.coverImage.prepend("/").prepend(cloudFrontHost), + liveRoom.coverImage, + liveRoom.numberOfPeople + ) + ) + .from(liveRoom) + .where(liveRoom.member.id.eq(memberId)) + .orderBy(liveRoom.id.desc()) + .limit(1) + .fetchFirst() } private fun orderByFieldAccountId( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 6a6e191..032a5a6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -4,14 +4,31 @@ import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.can.CanRepository +import kr.co.vividnext.sodalive.can.charge.Charge +import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.payment.Payment +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository +import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository +import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest +import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancel +import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser +import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository +import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService import kr.co.vividnext.sodalive.live.tag.LiveTagRepository import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable @@ -28,7 +45,14 @@ import java.time.format.DateTimeFormatter class LiveRoomService( private val repository: LiveRoomRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository, + private val roomCancelRepository: LiveRoomCancelRepository, + private val useCanCalculateRepository: UseCanCalculateRepository, + private val reservationRepository: LiveReservationRepository, + private val roomVisitService: LiveRoomVisitService, + private val canPaymentService: CanPaymentService, + private val chargeRepository: ChargeRepository, + private val memberRepository: MemberRepository, private val tagRepository: LiveTagRepository, private val canRepository: CanRepository, private val objectMapper: ObjectMapper, @@ -194,7 +218,7 @@ class LiveRoomService( val response = GetRoomDetailResponse( roomId = roomId, title = room.title, - content = room.notice, + notice = room.notice, price = room.price, tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(), numberOfParticipantsTotal = room.numberOfPeople, @@ -254,7 +278,7 @@ class LiveRoomService( response.numberOfParticipants = users.size } } else { - val reservationList = repository.getReservationList(roomId) + val reservationList = reservationRepository.getReservationList(roomId) response.participatingUsers = reservationList .asSequence() .map { @@ -270,4 +294,201 @@ class LiveRoomService( return response } + + @Transactional + fun startLive(request: StartLiveRequest, member: Member) { + val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + val dateTime = LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(request.timezone)) + .toLocalDateTime() + + val beginDateTime = room.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(request.timezone)) + .toLocalDateTime() + + if (dateTime.plusMinutes(10).isBefore(beginDateTime)) { + val startAvailableDateTimeString = beginDateTime.minusMinutes(10).format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ) + throw SodaException("$startAvailableDateTimeString 이후에 시작할 수 있습니다.") + } + + room.channelName = "SODA_LIVE_CHANNEL_" + + "${member.id}_${dateTime.year}_${dateTime.month}_${dateTime.dayOfMonth}_" + + "${dateTime.hour}_${dateTime.minute}" + } + + fun cancelLive(request: CancelLiveRequest, member: Member) { + val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + if (request.reason.isBlank()) { + throw SodaException("취소사유를 입력해 주세요.") + } + room.isActive = false + + val roomCancel = LiveRoomCancel(request.reason) + roomCancel.room = room + roomCancelRepository.save(roomCancel) + + // 유료방인 경우 환불처리 + if (room.price > 0) { + val bookerList = reservationRepository.getReservationBookerList(roomId = room.id!!) + for (booker in bookerList) { + val useCan = canRepository.getCanUsedForLiveRoomNotRefund(memberId = booker.id!!, roomId = room.id!!) + ?: continue + useCan.isRefund = true + + val useCanCalculate = useCanCalculateRepository.findByUseCanIdAndStatus(useCanId = useCan.id!!) + useCanCalculate.forEach { + it.status = UseCanCalculateStatus.REFUND + + val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) + charge.title = "${it.can} 코인" + charge.useCan = useCan + + when (it.paymentGateway) { + PaymentGateway.PG -> booker.pgRewardCan += charge.rewardCan + PaymentGateway.GOOGLE_IAP -> booker.googleRewardCan += charge.rewardCan + PaymentGateway.APPLE_IAP -> booker.appleRewardCan += charge.rewardCan + } + charge.member = booker + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = it.paymentGateway + ) + payment.method = "환불" + charge.payment = payment + + chargeRepository.save(charge) + } + } + } + + reservationRepository.cancelReservation(roomId = room.id!!) + } + + fun enterLive(request: EnterOrQuitLiveRoomRequest, member: Member) { + val room = repository.getLiveRoom(id = request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + if ( + room.member!!.id!! != member.id!! && + room.type == LiveRoomType.PRIVATE && + (request.password == null || request.password != room.password) + ) { + throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") + } + + var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + if (roomInfo == null) { + roomInfo = roomInfoRepository.save(LiveRoomInfo(roomId = request.roomId)) + } + + if (roomInfo.speakerCount + roomInfo.listenerCount + roomInfo.managerCount >= room.numberOfPeople) { + throw SodaException("방이 가득찼습니다.") + } + + if ( + room.price > 0 && + room.member!!.id!! != member.id!! && + canRepository.isExistPaidLiveRoom(memberId = member.id!!, roomId = request.roomId) == null + ) { + val findMember = memberRepository.findByIdOrNull(id = member.id!!) + ?: throw SodaException("로그인 정보를 확인해 주세요.") + + val totalCan = findMember.getChargeCan(request.container) + findMember.getRewardCan(request.container) + if (totalCan < room.price) { + throw SodaException("${room.price - totalCan}캔이 부족합니다. 충전 후 이용해 주세요.") + } + + canPaymentService.spendCan( + memberId = member.id!!, + needCan = room.price, + canUsage = CanUsage.LIVE, + liveRoom = room, + container = request.container + ) + } + + roomInfo.removeListener(member) + roomInfo.removeSpeaker(member) + roomInfo.removeManager(member) + + if (room.member!!.id == member.id) { + roomInfo.addSpeaker(member) + } else { + roomInfo.addListener(member) + } + + roomInfoRepository.save(roomInfo) + roomVisitService.roomVisit(room, member) + } + + fun getRecentRoomInfo(member: Member): GetRecentRoomInfoResponse { + return repository.getRecentRoomInfo(memberId = member.id!!, cloudFrontHost = cloudFrontHost) + ?: throw SodaException("최근 데이터가 없습니다.") + } + + @Transactional + fun editLiveRoomInfo(roomId: Long, coverImage: MultipartFile?, requestString: String?, member: Member) { + val room = repository.getLiveRoom(roomId) + if (member.id == null || room?.member?.id != member.id!!) { + throw SodaException("잘못된 요청입니다.") + } + + if (coverImage == null && requestString == null) { + throw SodaException("변경사항이 없습니다.") + } + + if (requestString != null) { + val request = objectMapper.readValue(requestString, EditLiveRoomInfoRequest::class.java) + + if (request.title != null) { + room.title = request.title + } + + if (request.notice != null) { + room.notice = request.notice + } + + if (request.numberOfPeople != null) { + room.numberOfPeople = request.numberOfPeople + } + + if (request.beginDateTimeString != null && request.timezone != null) { + room.beginDateTime = request.beginDateTimeString.convertLocalDateTime("yyyy-MM-dd HH:mm") + .atZone(ZoneId.of(request.timezone)) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } + } + + if (coverImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${room.id}-cover") + + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "suda_room_cover/${room.id}/$coverImageFileName", + metadata = metadata + ) + + room.bgImage = coverImagePath + + if (room.channelName == null) { + room.coverImage = coverImagePath + } + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt new file mode 100644 index 0000000..b59570f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/StartLiveRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.room + +import java.util.TimeZone + +data class StartLiveRequest( + val roomId: Long, + val timezone: String = TimeZone.getDefault().id +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt new file mode 100644 index 0000000..d6add40 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/CancelLiveRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.room.cancel + +data class CancelLiveRequest(val roomId: Long, val reason: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt new file mode 100644 index 0000000..e4bc12c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancel.kt @@ -0,0 +1,21 @@ +package kr.co.vividnext.sodalive.live.room.cancel + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.room.LiveRoom +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class LiveRoomCancel( + val reason: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + var room: LiveRoom? = null + set(value) { + value?.cancel = this + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt new file mode 100644 index 0000000..959a77d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/cancel/LiveRoomCancelRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room.cancel + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveRoomCancelRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt index 8a8a0d8..be95f68 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/detail/GetRoomDetailResponse.kt @@ -7,7 +7,7 @@ data class GetRoomDetailResponse( val roomId: Long, val price: Int, val title: String, - val content: String, + val notice: String, var isPaid: Boolean, val isPrivateRoom: Boolean, val password: String?, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt new file mode 100644 index 0000000..a5dc76c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/GetRecentVisitRoomMemberResponse.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.room.visit + +data class GetRecentVisitRoomMemberResponse( + val userId: Long, + val nickname: String, + val profileImageUrl: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt new file mode 100644 index 0000000..622a585 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisit.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.live.room.visit + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +class LiveRoomVisit : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "room_id", nullable = false) + var room: LiveRoom? = null + set(value) { + value?.visits?.add(this) + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt new file mode 100644 index 0000000..b90ce96 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.live.room.visit + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveRoomVisitRepository : JpaRepository, LiveRoomVisitQueryRepository + +interface LiveRoomVisitQueryRepository { + fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? + fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? +} + +@Repository +class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomVisitQueryRepository { + override fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? { + return queryFactory + .selectFrom(liveRoomVisit) + .innerJoin(liveRoomVisit.room, liveRoom) + .innerJoin(liveRoomVisit.member, member) + .where( + liveRoom.id.eq(roomId) + .and(member.id.eq(memberId)) + ) + .orderBy(liveRoomVisit.id.desc()) + .fetchFirst() + } + + override fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? { + return queryFactory + .selectFrom(liveRoomVisit) + .innerJoin(liveRoomVisit.room, liveRoom) + .where(member.id.eq(memberId)) + .orderBy(liveRoomVisit.updatedAt.desc()) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt new file mode 100644 index 0000000..df18ba1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.live.room.visit + +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.member.Member +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional(readOnly = true) +class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { + @Transactional + fun roomVisit(room: LiveRoom, member: Member) { + var roomVisit = repository.findByRoomIdAndMemberId(room.id!!, member.id!!) + if (roomVisit == null) { + roomVisit = LiveRoomVisit() + roomVisit.member = member + roomVisit.room = room + } else { + roomVisit.updatedAt = LocalDateTime.now() + } + + repository.save(roomVisit) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index a500c93..b8badb2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -53,9 +53,9 @@ data class Member( // 화폐 private var pgChargeCan: Int = 0 - private var pgRewardCan: Int = 0 + var pgRewardCan: Int = 0 private var googleChargeCan: Int = 0 - private var googleRewardCan: Int = 0 + var googleRewardCan: Int = 0 var appleChargeCan: Int = 0 var appleRewardCan: Int = 0 From 58a7f87ffd2925d5c269e08f1e7faf0f12510781 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 04:56:47 +0900 Subject: [PATCH 24/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20-=20=EC=95=84=EA=B3=A0=EB=9D=BC=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=B0=8F=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/agora/AccessToken.kt | 171 ++++++++ .../co/vividnext/sodalive/agora/AgoraUtils.kt | 72 ++++ .../kr/co/vividnext/sodalive/agora/ByteBuf.kt | 111 ++++++ .../vividnext/sodalive/agora/DynamicKey5.kt | 256 ++++++++++++ .../sodalive/agora/DynamicKeyUtil.kt | 33 ++ .../co/vividnext/sodalive/agora/Packable.kt | 5 + .../co/vividnext/sodalive/agora/PackableEx.kt | 5 + .../sodalive/agora/RtcTokenBuilder.kt | 96 +++++ .../sodalive/agora/RtmTokenBuilder.kt | 31 ++ .../vividnext/sodalive/can/CanRepository.kt | 6 +- .../explorer/ExplorerQueryRepository.kt | 71 ++++ .../explorer/MemberDonationRankingResponse.kt | 8 + .../room/GetLiveRoomUserProfileResponse.kt | 18 + .../sodalive/live/room/LiveRoomController.kt | 123 ++++++ .../sodalive/live/room/LiveRoomRepository.kt | 46 +++ .../sodalive/live/room/LiveRoomService.kt | 372 +++++++++++++++++- .../SetManagerOrSpeakerOrAudienceRequest.kt | 6 + .../donation/DeleteLiveRoomDonationMessage.kt | 6 + .../GetLiveRoomDonationStatusResponse.kt | 16 + .../GetLiveRoomDonationTotalResponse.kt | 3 + .../room/donation/LiveRoomDonationRequest.kt | 8 + .../live/room/info/GetRoomInfoResponse.kt | 25 ++ .../live/room/kickout/LiveRoomKickOut.kt | 32 ++ .../room/kickout/LiveRoomKickOutController.kt | 25 ++ .../kickout/LiveRoomKickOutRedisRepository.kt | 5 + .../room/kickout/LiveRoomKickOutRequest.kt | 6 + .../room/kickout/LiveRoomKickOutService.kt | 61 +++ .../sodalive/member/MemberController.kt | 42 ++ .../sodalive/member/MemberService.kt | 64 +++ .../sodalive/member/block/BlockMember.kt | 12 + .../member/block/BlockMemberRepository.kt | 27 ++ .../member/block/MemberBlockRequest.kt | 3 + .../member/following/CreatorFollowRequest.kt | 3 + .../member/following/CreatorFollowing.kt | 23 ++ .../following/CreatorFollowingRepository.kt | 28 ++ src/main/resources/application.yml | 4 + src/test/resources/application.yml | 6 +- 37 files changed, 1823 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/AccessToken.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/AgoraUtils.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/ByteBuf.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKey5.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKeyUtil.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/Packable.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/PackableEx.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/RtcTokenBuilder.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/agora/RtmTokenBuilder.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLiveRoomUserProfileResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOut.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRedisRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMember.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/block/MemberBlockRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowing.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/AccessToken.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/AccessToken.kt new file mode 100644 index 0000000..ebc5648 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/AccessToken.kt @@ -0,0 +1,171 @@ +package kr.co.vividnext.sodalive.agora + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.TreeMap + +class AccessToken( + var appId: String, + private val appCertificate: String, + val channelName: String, + private val uid: String, + var crcChannelName: Int = 0, + private var crcUid: Int = 0, + val message: PrivilegeMessage = PrivilegeMessage() +) { + + private lateinit var signature: ByteArray + private lateinit var messageRawContent: ByteArray + + enum class Privileges(value: Int) { + JoinChannel(1), + PublishAudioStream(2), + PublishVideoStream(3), + PublishDataStream(4), // For RTM only + RtmLogin(1000); + + var intValue: Short + + init { + intValue = value.toShort() + } + } + + @Throws(Exception::class) + fun build(): String { + if (!AgoraUtils.isUUID(appId)) { + return "" + } + if (!AgoraUtils.isUUID(appCertificate)) { + return "" + } + + messageRawContent = AgoraUtils.pack(message) + signature = generateSignature( + appCertificate, + appId, + channelName, + uid, + messageRawContent + ) + crcChannelName = AgoraUtils.crc32(channelName) + crcUid = AgoraUtils.crc32(uid) + val packContent = PackContent(signature, crcChannelName, crcUid, messageRawContent) + val content: ByteArray = AgoraUtils.pack(packContent) + return getVersion() + appId + AgoraUtils.base64Encode(content) + } + + fun addPrivilege(privilege: Privileges, expireTimestamp: Int) { + message.messages[privilege.intValue] = expireTimestamp + } + + private fun getVersion(): String { + return VER + } + + @Throws(java.lang.Exception::class) + fun generateSignature( + appCertificate: String, + appID: String, + channelName: String, + uid: String, + message: ByteArray + ): ByteArray { + val baos = ByteArrayOutputStream() + + try { + baos.write(appID.toByteArray()) + baos.write(channelName.toByteArray()) + baos.write(uid.toByteArray()) + baos.write(message) + } catch (e: IOException) { + e.printStackTrace() + } + + return AgoraUtils.hmacSign(appCertificate, baos.toByteArray()) + } + + fun fromString(token: String): Boolean { + if (getVersion() != token.substring(0, AgoraUtils.VERSION_LENGTH)) { + return false + } + + try { + appId = token.substring(AgoraUtils.VERSION_LENGTH, AgoraUtils.VERSION_LENGTH + AgoraUtils.APP_ID_LENGTH) + val packContent = PackContent() + AgoraUtils.unpack( + AgoraUtils.base64Decode( + token.substring( + AgoraUtils.VERSION_LENGTH + AgoraUtils.APP_ID_LENGTH, + token.length + ) + ), + packContent + ) + signature = packContent.signature + crcChannelName = packContent.crcChannelName + crcUid = packContent.crcUid + messageRawContent = packContent.rawMessage + AgoraUtils.unpack(messageRawContent, message) + } catch (e: java.lang.Exception) { + e.printStackTrace() + return false + } + + return true + } + + class PrivilegeMessage : PackableEx { + var salt: Int + var ts: Int + var messages: TreeMap + + override fun marshal(out: ByteBuf): ByteBuf { + return out.put(salt).put(ts).putIntMap(messages) + } + + override fun unmarshal(input: ByteBuf) { + salt = input.readInt() + ts = input.readInt() + messages = input.readIntMap() + } + + init { + salt = AgoraUtils.randomInt() + ts = AgoraUtils.getTimestamp() + 24 * 3600 + messages = TreeMap() + } + } + + class PackContent() : PackableEx { + var signature: ByteArray = byteArrayOf() + var crcChannelName = 0 + var crcUid = 0 + var rawMessage: ByteArray = byteArrayOf() + + constructor(signature: ByteArray, crcChannelName: Int, crcUid: Int, rawMessage: ByteArray) : this() { + this.signature = signature + this.crcChannelName = crcChannelName + this.crcUid = crcUid + this.rawMessage = rawMessage + } + + override fun marshal(out: ByteBuf): ByteBuf { + return out + .put(signature) + .put(crcChannelName) + .put(crcUid).put(rawMessage) + } + + override fun unmarshal(input: ByteBuf) { + signature = input.readBytes() + crcChannelName = input.readInt() + crcUid = input.readInt() + rawMessage = input.readBytes() + } + } + + companion object { + const val VER = "006" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/AgoraUtils.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/AgoraUtils.kt new file mode 100644 index 0000000..2af6158 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/AgoraUtils.kt @@ -0,0 +1,72 @@ +package kr.co.vividnext.sodalive.agora + +import org.apache.commons.codec.binary.Base64 +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.security.SecureRandom +import java.util.Date +import java.util.zip.CRC32 +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +object AgoraUtils { + const val HMAC_SHA256_LENGTH: Long = 32 + const val VERSION_LENGTH = 3 + const val APP_ID_LENGTH = 32 + + @Throws(InvalidKeyException::class, NoSuchAlgorithmException::class) + fun hmacSign(keyString: String, msg: ByteArray?): ByteArray { + val keySpec = SecretKeySpec(keyString.toByteArray(), "HmacSHA256") + val mac = Mac.getInstance("HmacSHA256") + mac.init(keySpec) + return mac.doFinal(msg) + } + + fun pack(packableEx: PackableEx): ByteArray { + val buffer = ByteBuf() + packableEx.marshal(buffer) + return buffer.asBytes() + } + + fun unpack(data: ByteArray?, packableEx: PackableEx) { + val buffer = ByteBuf(data!!) + packableEx.unmarshal(buffer) + } + + fun base64Encode(data: ByteArray?): String { + val encodedBytes: ByteArray = Base64.encodeBase64(data) + return String(encodedBytes) + } + + fun base64Decode(data: String): ByteArray { + return Base64.decodeBase64(data.toByteArray()) + } + + fun crc32(data: String): Int { + // get bytes from string + val bytes = data.toByteArray() + return crc32(bytes) + } + + fun crc32(bytes: ByteArray?): Int { + val checksum = CRC32() + checksum.update(bytes) + return checksum.value.toInt() + } + + fun getTimestamp(): Int { + return (Date().time / 1000).toInt() + } + + fun randomInt(): Int { + return SecureRandom().nextInt() + } + + fun isUUID(uuid: String): Boolean { + return if (uuid.length != 32) { + false + } else { + uuid.matches("\\p{XDigit}+".toRegex()) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/ByteBuf.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/ByteBuf.kt new file mode 100644 index 0000000..9254628 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/ByteBuf.kt @@ -0,0 +1,111 @@ +package kr.co.vividnext.sodalive.agora + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.TreeMap + +class ByteBuf() { + private var buffer = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN) + + constructor(bytes: ByteArray) : this() { + this.buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) + } + + fun asBytes(): ByteArray { + val out = ByteArray(buffer.position()) + buffer.rewind() + buffer[out, 0, out.size] + return out + } + + // packUint16 + fun put(v: Short): ByteBuf { + buffer.putShort(v) + return this + } + + fun put(v: ByteArray): ByteBuf { + put(v.size.toShort()) + buffer.put(v) + return this + } + + // packUint32 + fun put(v: Int): ByteBuf { + buffer.putInt(v) + return this + } + + fun put(v: Long): ByteBuf { + buffer.putLong(v) + return this + } + + fun put(v: String): ByteBuf { + return put(v.toByteArray()) + } + + fun put(extra: TreeMap): ByteBuf { + put(extra.size.toShort()) + for ((key, value) in extra.entries) { + put(key) + put(value) + } + return this + } + + fun putIntMap(extra: TreeMap): ByteBuf { + put(extra.size.toShort()) + for ((key, value) in extra.entries) { + put(key) + put(value) + } + return this + } + + fun readShort(): Short { + return buffer.short + } + + fun readInt(): Int { + return buffer.int + } + + fun readBytes(): ByteArray { + val length = readShort() + val bytes = ByteArray(length.toInt()) + buffer[bytes] + return bytes + } + + fun readString(): String { + val bytes = readBytes() + return String(bytes) + } + + fun readMap(): TreeMap { + val map = TreeMap() + val length = readShort() + + for (i in 0 until length) { + val k = readShort() + val v = readString() + map[k] = v + } + + return map + } + + fun readIntMap(): TreeMap { + val map = TreeMap() + val length = readShort() + + for (i in 0 until length) { + val k = readShort() + val v = readInt() + map[k] = v + } + + return map + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKey5.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKey5.kt new file mode 100644 index 0000000..9c35d0a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKey5.kt @@ -0,0 +1,256 @@ +package kr.co.vividnext.sodalive.agora + +import org.apache.commons.codec.binary.Base64 +import org.apache.commons.codec.binary.Hex +import java.util.TreeMap + +class DynamicKey5 { + lateinit var content: DynamicKey5Content + + fun fromString(key: String): Boolean { + if (key.substring(0, 3) != version) { + return false + } + val rawContent: ByteArray = Base64().decode(key.substring(3)) + if (rawContent.isEmpty()) { + return false + } + content = DynamicKey5Content() + val buffer = ByteBuf(rawContent) + content.unmarshall(buffer) + return true + } + + companion object { + const val version = "005" + const val noUpload = "0" + const val audioVideoUpload = "3" + + // ServiceType + const val MEDIA_CHANNEL_SERVICE: Short = 1 + const val RECORDING_SERVICE: Short = 2 + const val PUBLIC_SHARING_SERVICE: Short = 3 + const val IN_CHANNEL_PERMISSION: Short = 4 + + // InChannelPermissionKey + const val ALLOW_UPLOAD_IN_CHANNEL: Short = 1 + + @Throws(Exception::class) + fun generateSignature( + appCertificate: String, + service: Short, + appID: String, + unixTs: Int, + salt: Int, + channelName: String, + uid: Long, + expiredTs: Int, + extra: TreeMap + ): String { + // decode hex to avoid case problem + val hex = Hex() + val rawAppID: ByteArray = hex.decode(appID.toByteArray()) + val rawAppCertificate: ByteArray = hex.decode(appCertificate.toByteArray()) + val m = Message( + service, + rawAppID, + unixTs, + salt, + channelName, + (uid and 0xFFFFFFFFL).toInt(), + expiredTs, + extra + ) + val toSign: ByteArray = pack(m) + return String(Hex.encodeHex(DynamicKeyUtil.encodeHMAC(rawAppCertificate, toSign), false)) + } + + @Throws(java.lang.Exception::class) + fun generateDynamicKey( + appID: String, + appCertificate: String, + channel: String, + ts: Int, + salt: Int, + uid: Long, + expiredTs: Int, + extra: TreeMap, + service: Short + ): String { + val signature = generateSignature(appCertificate, service, appID, ts, salt, channel, uid, expiredTs, extra) + val content = + DynamicKey5Content(service, signature, Hex().decode(appID.toByteArray()), ts, salt, expiredTs, extra) + val bytes: ByteArray = pack(content) + val encoded = Base64().encode(bytes) + val base64 = String(encoded) + return version + base64 + } + + private fun pack(content: Packable): ByteArray { + val buffer = ByteBuf() + content.marshal(buffer) + return buffer.asBytes() + } + + @Throws(Exception::class) + fun generatePublicSharingKey( + appID: String, + appCertificate: String, + channel: String, + ts: Int, + salt: Int, + uid: Long, + expiredTs: Int + ) = generateDynamicKey( + appID, + appCertificate, + channel, + ts, + salt, + uid, + expiredTs, + TreeMap(), + PUBLIC_SHARING_SERVICE + ) + + @Throws(Exception::class) + fun generateRecordingKey( + appID: String, + appCertificate: String, + channel: String, + ts: Int, + salt: Int, + uid: Long, + expiredTs: Int + ) = generateDynamicKey( + appID, + appCertificate, + channel, + ts, + salt, + uid, + expiredTs, + TreeMap(), + RECORDING_SERVICE + ) + + @Throws(Exception::class) + fun generateMediaChannelKey( + appID: String, + appCertificate: String, + channel: String, + ts: Int, + salt: Int, + uid: Long, + expiredTs: Int + ) = generateDynamicKey( + appID, + appCertificate, + channel, + ts, + salt, + uid, + expiredTs, + TreeMap(), + MEDIA_CHANNEL_SERVICE + ) + + @Throws(Exception::class) + fun generateInChannelPermissionKey( + appID: String, + appCertificate: String, + channel: String, + ts: Int, + salt: Int, + uid: Long, + expiredTs: Int, + permission: String + ): String { + val extra = TreeMap() + extra[ALLOW_UPLOAD_IN_CHANNEL] = permission + return generateDynamicKey( + appID, + appCertificate, + channel, + ts, + salt, + uid, + expiredTs, + extra, + IN_CHANNEL_PERMISSION + ) + } + + internal class Message( + var serviceType: Short, + var appID: ByteArray, + var unixTs: Int, + var salt: Int, + var channelName: String, + var uid: Int, + var expiredTs: Int, + var extra: TreeMap + ) : Packable { + override fun marshal(out: ByteBuf): ByteBuf { + return out + .put(serviceType) + .put(appID) + .put(unixTs) + .put(salt) + .put(channelName) + .put(uid) + .put(expiredTs) + .put(extra) + } + } + + class DynamicKey5Content() : Packable { + var serviceType: Short = 0 + var signature: String? = null + var appID: ByteArray = byteArrayOf() + var unixTs = 0 + var salt = 0 + var expiredTs = 0 + var extra: TreeMap? = null + + constructor( + serviceType: Short, + signature: String?, + appID: ByteArray, + unixTs: Int, + salt: Int, + expiredTs: Int, + extra: TreeMap + ) : this() { + this.serviceType = serviceType + this.signature = signature + this.appID = appID + this.unixTs = unixTs + this.salt = salt + this.expiredTs = expiredTs + this.extra = extra + } + + override fun marshal(out: ByteBuf): ByteBuf { + return out + .put(serviceType) + .put(signature!!) + .put(appID) + .put(unixTs) + .put(salt) + .put(expiredTs) + .put(extra!!) + } + + fun unmarshall(input: ByteBuf) { + serviceType = input.readShort() + signature = input.readString() + appID = input.readBytes() + unixTs = input.readInt() + salt = input.readInt() + expiredTs = input.readInt() + extra = input.readMap() + } + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKeyUtil.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKeyUtil.kt new file mode 100644 index 0000000..e98118a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/DynamicKeyUtil.kt @@ -0,0 +1,33 @@ +package kr.co.vividnext.sodalive.agora + +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +/** + * Created by hefeng on 15/8/10. + * Util to generate Agora media dynamic key. + */ +object DynamicKeyUtil { + @Throws(NoSuchAlgorithmException::class, InvalidKeyException::class) + fun encodeHMAC(key: String, message: ByteArray?): ByteArray? { + return encodeHMAC(key.toByteArray(), message) + } + + @Throws(NoSuchAlgorithmException::class, InvalidKeyException::class) + fun encodeHMAC(key: ByteArray?, message: ByteArray?): ByteArray? { + val keySpec = SecretKeySpec(key, "HmacSHA1") + val mac = Mac.getInstance("HmacSHA1") + mac.init(keySpec) + return mac.doFinal(message) + } + + fun bytesToHex(`in`: ByteArray): String { + val builder = StringBuilder() + for (b in `in`) { + builder.append(String.format("%02x", b)) + } + return builder.toString() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/Packable.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/Packable.kt new file mode 100644 index 0000000..4447e33 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/Packable.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.agora + +interface Packable { + fun marshal(out: ByteBuf): ByteBuf +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/PackableEx.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/PackableEx.kt new file mode 100644 index 0000000..e84605e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/PackableEx.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.agora + +interface PackableEx : Packable { + fun unmarshal(input: ByteBuf) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/RtcTokenBuilder.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/RtcTokenBuilder.kt new file mode 100644 index 0000000..c5b1677 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/RtcTokenBuilder.kt @@ -0,0 +1,96 @@ +package kr.co.vividnext.sodalive.agora + +import org.springframework.stereotype.Component + +@Component +class RtcTokenBuilder { + /** + * Builds an RTC token using an int uid. + * + * @param appId The App ID issued to you by Agora. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. + * @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are: + * + * * The 26 lowercase English letters: a to z. + * * The 26 uppercase English letters: A to Z. + * * The 10 digits: 0 to 9. + * * The space. + * * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * + * @param uid User ID. A 32-bit unsigned integer with a value ranging from + * 1 to (2^32-1). + * @param role The user role. + * + * * Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast. + * * Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher. + * + * @param privilegeTs Represented by the number of seconds elapsed since 1/1/1970. + * If, for example, you want to access the Agora Service within 10 minutes + * after the token is generated, set expireTimestamp as the current time stamp + * + 600 (seconds). + */ + fun buildTokenWithUid( + appId: String, + appCertificate: String, + channelName: String, + uid: Int, + privilegeTs: Int + ): String { + val account = if (uid == 0) "" else uid.toString() + return buildTokenWithUserAccount( + appId, + appCertificate, + channelName, + account, + privilegeTs + ) + } + + /** + * Builds an RTC token using a string userAccount. + * + * @param appId The App ID issued to you by Agora. + * @param appCertificate Certificate of the application that you registered in + * the Agora Dashboard. + * @param channelName The unique channel name for the AgoraRTC session in the string format. The string length must be less than 64 bytes. Supported character scopes are: + * + * * The 26 lowercase English letters: a to z. + * * The 26 uppercase English letters: A to Z. + * * The 10 digits: 0 to 9. + * * The space. + * * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", "_", " {", "}", "|", "~", ",". + * + * @param account The user account. + * @param role The user role. + * + * * Role_Publisher = 1: RECOMMENDED. Use this role for a voice/video call or a live broadcast. + * * Role_Subscriber = 2: ONLY use this role if your live-broadcast scenario requires authentication for [Hosting-in](https://docs.agora.io/en/Agora%20Platform/terms?platform=All%20Platforms#hosting-in). In order for this role to take effect, please contact our support team to enable authentication for Hosting-in for you. Otherwise, Role_Subscriber still has the same privileges as Role_Publisher. + * + * @param privilegeTs represented by the number of seconds elapsed since 1/1/1970. + * If, for example, you want to access the Agora Service within 10 minutes + * after the token is generated, set expireTimestamp as the current time stamp + * + 600 (seconds). + */ + fun buildTokenWithUserAccount( + appId: String, + appCertificate: String, + channelName: String, + account: String, + privilegeTs: Int + ): String { + // Assign appropriate access privileges to each role. + val builder = AccessToken(appId, appCertificate, channelName, account) + builder.addPrivilege(AccessToken.Privileges.JoinChannel, privilegeTs) + builder.addPrivilege(AccessToken.Privileges.PublishAudioStream, privilegeTs) + builder.addPrivilege(AccessToken.Privileges.PublishVideoStream, privilegeTs) + builder.addPrivilege(AccessToken.Privileges.PublishDataStream, privilegeTs) + + return try { + builder.build() + } catch (e: Exception) { + e.printStackTrace() + "" + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/agora/RtmTokenBuilder.kt b/src/main/kotlin/kr/co/vividnext/sodalive/agora/RtmTokenBuilder.kt new file mode 100644 index 0000000..d00999f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/agora/RtmTokenBuilder.kt @@ -0,0 +1,31 @@ +package kr.co.vividnext.sodalive.agora + +import kr.co.vividnext.sodalive.agora.AccessToken.Privileges +import org.springframework.stereotype.Component + +@Component +class RtmTokenBuilder { + + lateinit var mTokenCreator: AccessToken + + @Throws(Exception::class) + fun buildToken( + appId: String, + appCertificate: String, + uid: String, + privilegeTs: Int + ): String { + mTokenCreator = AccessToken(appId, appCertificate, uid, "") + mTokenCreator.addPrivilege(Privileges.RtmLogin, privilegeTs) + return mTokenCreator.build() + } + + fun setPrivilege(privilege: Privileges?, expireTs: Int) { + mTokenCreator.addPrivilege(privilege!!, expireTs) + } + + fun initTokenBuilder(originToken: String?): Boolean { + mTokenCreator.fromString(originToken!!) + return true + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt index a404594..fd4bc7d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/CanRepository.kt @@ -27,7 +27,7 @@ interface CanQueryRepository { fun getCanUseStatus(member: Member, pageable: Pageable): List fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan? - fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? + fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage = CanUsage.LIVE): UseCan? } @Repository @@ -113,7 +113,7 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue .fetchFirst() } - override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long): UseCan? { + override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage): UseCan? { return queryFactory .selectFrom(useCan) .innerJoin(useCan.member, member) @@ -121,7 +121,7 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue .where( member.id.eq(memberId) .and(liveRoom.id.eq(roomId)) - .and(useCan.canUsage.eq(CanUsage.LIVE)) + .and(useCan.canUsage.eq(canUsage)) .and(useCan.isRefund.isFalse) ) .orderBy(useCan.id.desc()) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt new file mode 100644 index 0000000..19a869f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -0,0 +1,71 @@ +package kr.co.vividnext.sodalive.explorer + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.QUseCan.useCan +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.QMember +import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Repository + +@Repository +class ExplorerQueryRepository( + private val queryFactory: JPAQueryFactory, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun getNotificationUserIds(creatorId: Long): List { + return queryFactory + .select(creatorFollowing.member.id) + .from(creatorFollowing) + .where( + creatorFollowing.isActive.isTrue + .and(creatorFollowing.creator.id.eq(creatorId)) + ) + .fetch() + } + + fun getMemberDonationRanking( + creatorId: Long, + limit: Long, + offset: Long = 0, + withDonationCoin: Boolean + ): List { + val creator = QMember("creator") + val member = QMember("user") + + val donation = useCan.rewardCan.add(useCan.can).sum() + return queryFactory + .select(member, donation) + .from(useCan) + .join(useCan.room, liveRoom) + .join(liveRoom.member, creator) + .join(useCan.member, member) + .offset(offset) + .limit(limit) + .where( + useCan.canUsage.eq(CanUsage.DONATION) + .and(useCan.isRefund.isFalse) + .and(creator.id.eq(creatorId)) + ) + .groupBy(useCan.member.id) + .orderBy(donation.desc(), member.id.desc()) + .fetch() + .map { + val account = it.get(member)!! + val donationCoin = it.get(donation)!! + MemberDonationRankingResponse( + account.id!!, + account.nickname, + if (account.profileImage != null) { + "$cloudFrontHost/${account.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + if (withDonationCoin) donationCoin else 0 + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt new file mode 100644 index 0000000..3e6e3f9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer + +data class MemberDonationRankingResponse( + val userId: Long, + val nickname: String, + val profileImage: String, + val donationCoin: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLiveRoomUserProfileResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLiveRoomUserProfileResponse.kt new file mode 100644 index 0000000..42226ab --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetLiveRoomUserProfileResponse.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.live.room + +data class GetLiveRoomUserProfileResponse( + val userId: Long, + val nickname: String, + val profileUrl: String, + val gender: String, + val instagramUrl: String, + val youtubeUrl: String, + val websiteUrl: String, + val blogUrl: String, + val introduce: String, + val tags: String, + val isSpeaker: Boolean?, + val isManager: Boolean?, + val isFollowing: Boolean?, + val isBlock: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index 9ff9e48..4f7fd58 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -3,9 +3,12 @@ package kr.co.vividnext.sodalive.live.room import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest +import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal +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 @@ -106,4 +109,124 @@ class LiveRoomController(private val service: LiveRoomService) { ApiResponse.ok(service.editLiveRoomInfo(roomId, coverImage, requestString, member)) } + + @GetMapping("/info/{id}") + fun getRoomInfo( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getRoomInfo(roomId = id, member)) + } + + @GetMapping("/donation-message") + fun getDonationMessageList( + @RequestParam roomId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getDonationMessageList(roomId, member)) + } + + @DeleteMapping("/donation-message") + fun removeDonationMessage( + @RequestBody request: DeleteLiveRoomDonationMessage, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.deleteDonationMessage(request, member)) + } + + @GetMapping("/{room_id}/profile/{user_id}") + fun getUserProfile( + @PathVariable("room_id") roomId: Long, + @PathVariable("user_id") userId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getUserProfile(roomId, userId, member)) + } + + @GetMapping("/{id}/donation-total") + fun donationTotal( + @PathVariable("id") roomId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getDonationTotal(roomId)) + } + + @PutMapping("/info/set/speaker") + fun setSpeaker( + @RequestBody request: SetManagerOrSpeakerOrAudienceRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.setSpeaker(request)) + } + + @PutMapping("/info/set/listener") + fun setListener( + @RequestBody request: SetManagerOrSpeakerOrAudienceRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.setListener(request)) + } + + @PutMapping("/info/set/manager") + fun setManager( + @RequestBody request: SetManagerOrSpeakerOrAudienceRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.setManager(request, member)) + } + + @PostMapping("/donation") + fun donation( + @RequestBody request: LiveRoomDonationRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.donation(request, member)) + } + + @PostMapping("/donation/refund/{id}") + fun refundDonation( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.refundDonation(id, member)) + } + + @GetMapping("/{id}/donation-list") + fun donationList( + @PathVariable("id") roomId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getDonationStatus(roomId, member)) + } + + @PostMapping("/quit") + fun quitRoom( + @RequestParam("id") roomId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.quitRoom(roomId, member)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index f9f0873..e2c12c4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -6,7 +6,12 @@ import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.CaseBuilder import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.QUseCan.useCan +import kr.co.vividnext.sodalive.can.use.QUseCanCalculate.useCanCalculate import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationItem +import kr.co.vividnext.sodalive.live.room.donation.QGetLiveRoomDonationItem import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member @@ -34,6 +39,8 @@ interface LiveRoomQueryRepository { fun getLiveRoom(id: Long): LiveRoom? fun getLiveRoomAndAccountId(roomId: Long, memberId: Long): LiveRoom? fun getRecentRoomInfo(memberId: Long, cloudFrontHost: String): GetRecentRoomInfoResponse? + fun getDonationTotal(roomId: Long): Int? + fun getDonationList(roomId: Long, cloudFrontHost: String): List } class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : LiveRoomQueryRepository { @@ -143,6 +150,45 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L .fetchFirst() } + override fun getDonationTotal(roomId: Long): Int? { + return queryFactory + .select(useCanCalculate.can.sum()) + .from(useCanCalculate) + .innerJoin(useCanCalculate.useCan, useCan) + .innerJoin(useCan.room, liveRoom) + .where( + liveRoom.id.eq(roomId) + .and(useCan.canUsage.eq(CanUsage.DONATION)) + .and(useCan.isRefund.isFalse) + ) + .fetchOne() + } + + override fun getDonationList(roomId: Long, cloudFrontHost: String): List { + return queryFactory + .select( + QGetLiveRoomDonationItem( + member.profileImage + .coalesce("profile/default-profile.png") + .prepend("/") + .prepend(cloudFrontHost), + member.nickname, + member.id.coalesce(0), + useCan.can.sum().add(useCan.rewardCan.sum()) + ) + ) + .from(useCan) + .join(useCan.member, member) + .groupBy(useCan.member) + .where( + useCan.room.id.eq(roomId) + .and(useCan.canUsage.eq(CanUsage.DONATION)) + .and(useCan.isRefund.isFalse) + ) + .orderBy(useCan.can.sum().add(useCan.rewardCan.sum()).desc()) + .fetch() + } + private fun orderByFieldAccountId( memberId: Long, status: LiveRoomStatus, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 032a5a6..6d8d20a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.live.room import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.agora.RtcTokenBuilder +import kr.co.vividnext.sodalive.agora.RtmTokenBuilder import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.can.CanRepository import kr.co.vividnext.sodalive.can.charge.Charge @@ -15,6 +17,7 @@ import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.extensions.convertLocalDateTime import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest @@ -23,12 +26,22 @@ import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailManager import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser +import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationStatusResponse +import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationTotalResponse +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationMessage +import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest +import kr.co.vividnext.sodalive.live.room.info.GetRoomInfoResponse import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository +import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember +import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService import kr.co.vividnext.sodalive.live.tag.LiveTagRepository +import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable @@ -39,6 +52,7 @@ import org.springframework.web.multipart.MultipartFile import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.util.Date @Service @Transactional(readOnly = true) @@ -46,9 +60,11 @@ class LiveRoomService( private val repository: LiveRoomRepository, private val roomInfoRepository: LiveRoomInfoRedisRepository, private val roomCancelRepository: LiveRoomCancelRepository, + private val kickOutService: LiveRoomKickOutService, private val useCanCalculateRepository: UseCanCalculateRepository, private val reservationRepository: LiveReservationRepository, + private val explorerQueryRepository: ExplorerQueryRepository, private val roomVisitService: LiveRoomVisitService, private val canPaymentService: CanPaymentService, private val chargeRepository: ChargeRepository, @@ -58,6 +74,15 @@ class LiveRoomService( private val objectMapper: ObjectMapper, private val s3Uploader: S3Uploader, + private val rtcTokenBuilder: RtcTokenBuilder, + private val rtmTokenBuilder: RtmTokenBuilder, + + @Value("\${agora.app-id}") + private val agoraAppId: String, + + @Value("\${agora.app-certificate}") + private val agoraAppCertificate: String, + @Value("\${cloud.aws.s3.bucket}") private val coverImageBucket: String, @Value("\${cloud.aws.cloud-front.host}") @@ -322,6 +347,7 @@ class LiveRoomService( "${dateTime.hour}_${dateTime.minute}" } + @Transactional fun cancelLive(request: CancelLiveRequest, member: Member) { val room = repository.getLiveRoomAndAccountId(request.roomId, member.id!!) ?: throw SodaException("해당하는 라이브가 없습니다.") @@ -339,8 +365,11 @@ class LiveRoomService( if (room.price > 0) { val bookerList = reservationRepository.getReservationBookerList(roomId = room.id!!) for (booker in bookerList) { - val useCan = canRepository.getCanUsedForLiveRoomNotRefund(memberId = booker.id!!, roomId = room.id!!) - ?: continue + val useCan = canRepository.getCanUsedForLiveRoomNotRefund( + memberId = booker.id!!, + roomId = room.id!!, + canUsage = CanUsage.LIVE + ) ?: continue useCan.isRefund = true val useCanCalculate = useCanCalculateRepository.findByUseCanIdAndStatus(useCanId = useCan.id!!) @@ -373,6 +402,7 @@ class LiveRoomService( reservationRepository.cancelReservation(roomId = room.id!!) } + @Transactional fun enterLive(request: EnterOrQuitLiveRoomRequest, member: Member) { val room = repository.getLiveRoom(id = request.roomId) ?: throw SodaException("해당하는 라이브가 없습니다.") @@ -491,4 +521,342 @@ class LiveRoomService( } } } + + fun getRoomInfo(roomId: Long, member: Member): GetRoomInfoResponse { + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val room = repository.findByIdOrNull(roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val currentTimeStamp = Date().time + val expireTimestamp = (currentTimeStamp + (60 * 60 * 24 * 1000)) / 1000 + + val rtcToken = rtcTokenBuilder.buildTokenWithUid( + agoraAppId, + agoraAppCertificate, + room.channelName!!, + member.id!!.toInt(), + expireTimestamp.toInt() + ) + + val rtmToken = rtmTokenBuilder.buildToken( + agoraAppId, + agoraAppCertificate, + member.id!!.toString(), + expireTimestamp.toInt() + ) + + val tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList() + val isRadioMode = tags.contains("라디오") or tags.contains("콘서트") + val isAvailableDonation = room.member!!.id!! != member.id!! && + room.member!!.role == MemberRole.CREATOR + val isFollowingManager = explorerQueryRepository + .getNotificationUserIds(room.member!!.id!!) + .contains(member.id) + + val donationRankingTop3UserIds = explorerQueryRepository + .getMemberDonationRanking( + room.member!!.id!!, + 3, + withDonationCoin = false + ) + .asSequence() + .map { it.userId } + .toList() + + return GetRoomInfoResponse( + roomId = roomId, + title = room.title, + notice = room.notice, + coverImageUrl = if (room.bgImage != null) { + if (room.bgImage!!.startsWith("https://")) { + room.bgImage!! + } else { + "$cloudFrontHost/${room.bgImage!!}" + } + } else { + if (room.coverImage!!.startsWith("https://")) { + room.coverImage!! + } else { + "$cloudFrontHost/${room.coverImage!!}" + } + }, + channelName = room.channelName!!, + rtcToken = rtcToken, + rtmToken = rtmToken, + managerId = room.member!!.id!!, + managerNickname = room.member!!.nickname, + managerProfileUrl = if (room.member!!.profileImage != null) { + "$cloudFrontHost/${room.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + isFollowingManager = isFollowingManager, + participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount, + totalAvailableParticipantsCount = room.numberOfPeople, + speakerList = roomInfo.speakerList, + listenerList = roomInfo.listenerList, + managerList = roomInfo.managerList, + donationRankingTop3UserIds = donationRankingTop3UserIds, + isRadioMode = isRadioMode, + isAvailableDonation = isAvailableDonation, + isPrivateRoom = room.type == LiveRoomType.PRIVATE, + password = room.password + ) + } + + fun getDonationMessageList(roomId: Long, member: Member): List { + val room = repository.findByIdOrNull(roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + if (member.id!! != room.member!!.id!!) { + throw SodaException("잘못된 요청입니다.") + } + + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + return roomInfo.donationMessageList + } + + fun deleteDonationMessage(request: DeleteLiveRoomDonationMessage, member: Member) { + val room = repository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + if (member.id!! != room.member!!.id!!) { + throw SodaException("잘못된 요청입니다.") + } + + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + roomInfo.removeDonationMessage(request.messageUUID) + roomInfoRepository.save(roomInfo) + } + + fun getUserProfile(roomId: Long, userId: Long, member: Member): GetLiveRoomUserProfileResponse { + val room = repository.getLiveRoom(roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val user = memberRepository.findByIdOrNull(userId) + ?: throw SodaException("잘못된 요청입니다.") + + val isFollowing = if (user.role == MemberRole.CREATOR) { + explorerQueryRepository + .getNotificationUserIds(userId) + .contains(member.id!!) + } else { + null + } + + // 조회 하는 유저 + val memberResponse = LiveRoomMember(member) + // 조회 당하는 유저 + val userResponse = LiveRoomMember(user) + + val isSpeaker = if ( + room.member!!.id!! != userId && + (room.member!!.id!! == member.id!! || roomInfo.managerList.contains(memberResponse)) + ) { + roomInfo.speakerList.contains(userResponse) + } else { + null + } + + val isManager = if (room.member!!.id!! != userId && room.member!!.id!! == member.id!!) { + roomInfo.managerList.contains(userResponse) + } else { + null + } + + return GetLiveRoomUserProfileResponse( + userId = user.id!!, + nickname = user.nickname, + profileUrl = if (user.profileImage != null) { + "$cloudFrontHost/${user.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + gender = if (user.gender == Gender.FEMALE) "여" else if (user.gender == Gender.MALE) "남" else "미", + instagramUrl = user.instagramUrl, + youtubeUrl = user.youtubeUrl, + websiteUrl = user.websiteUrl, + blogUrl = user.blogUrl, + introduce = user.introduce, + tags = "", + isSpeaker = isSpeaker, + isManager = isManager, + isFollowing = isFollowing, + isBlock = false + ) + } + + fun getDonationTotal(roomId: Long): GetLiveRoomDonationTotalResponse { + return GetLiveRoomDonationTotalResponse( + totalDonationCoin = repository.getDonationTotal(roomId = roomId) ?: 0 + ) + } + + fun setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest) { + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val account = memberRepository.findByIdOrNull(request.accountId) + ?: throw SodaException("로그인 정보를 확인해 주세요.") + + if (roomInfo.speakerCount > 9) { + throw SodaException("스피커 정원이 초과하였습니다.") + } + + roomInfo.removeListener(account) + roomInfo.removeManager(account) + roomInfo.addSpeaker(account) + + roomInfoRepository.save(roomInfo) + } + + fun setListener(request: SetManagerOrSpeakerOrAudienceRequest) { + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val member = memberRepository.findByIdOrNull(request.accountId) + ?: throw SodaException("로그인 정보를 확인해 주세요.") + + roomInfo.removeSpeaker(member) + roomInfo.removeManager(member) + roomInfo.addListener(member) + + roomInfoRepository.save(roomInfo) + } + + fun setManager(request: SetManagerOrSpeakerOrAudienceRequest, member: Member) { + val room = repository.getLiveRoom(request.roomId) ?: throw SodaException("잘못된 요청입니다.") + if (room.member!!.id!! != member.id!!) { + throw SodaException("권한이 없습니다.") + } + + val user = memberRepository.findByIdOrNull(request.accountId) ?: throw SodaException("해당하는 유저가 없습니다.") + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val roomAccountResponse = LiveRoomMember(member = user) + if (roomInfo.managerList.contains(roomAccountResponse)) { + throw SodaException("이미 매니저 입니다.") + } + + if ( + !roomInfo.speakerList.contains(roomAccountResponse) && + !roomInfo.listenerList.contains(roomAccountResponse) + ) { + throw SodaException("해당하는 유저가 없습니다.") + } + + roomInfo.removeListener(user) + roomInfo.removeSpeaker(user) + roomInfo.addManager(user) + + roomInfoRepository.save(roomInfo) + } + + @Transactional + fun donation(request: LiveRoomDonationRequest, member: Member) { + val room = repository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + val host = room.member ?: throw SodaException("잘못된 요청입니다.") + + if (host.role != MemberRole.CREATOR) { + throw SodaException("비비드넥스트와 계약한\n요즘친구에게만 후원을 하실 수 있습니다.") + } + + canPaymentService.spendCan( + memberId = member.id!!, + needCan = request.can, + canUsage = CanUsage.DONATION, + liveRoom = room, + container = request.container + ) + + if (request.message.isNotBlank()) { + val roomInfo = roomInfoRepository.findByIdOrNull(room.id!!) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + roomInfo.addDonationMessage( + nickname = member.nickname, + can = request.can, + donationMessage = request.message + ) + + roomInfoRepository.save(roomInfo) + } + } + + @Transactional + fun refundDonation(roomId: Long, member: Member) { + val donator = memberRepository.findByIdOrNull(member.id) + ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") + + val useCan = canRepository.getCanUsedForLiveRoomNotRefund( + memberId = member.id!!, + roomId = roomId, + canUsage = CanUsage.DONATION + ) ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") + useCan.isRefund = true + + val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) + useCoinCalculates.forEach { + it.status = UseCanCalculateStatus.REFUND + val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) + charge.title = "${it.can} 코인" + charge.useCan = useCan + + when (it.paymentGateway) { + PaymentGateway.PG -> donator.pgRewardCan += charge.rewardCan + PaymentGateway.GOOGLE_IAP -> donator.googleRewardCan += charge.rewardCan + PaymentGateway.APPLE_IAP -> donator.appleRewardCan += charge.rewardCan + } + charge.member = donator + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = it.paymentGateway + ) + payment.method = "환불" + charge.payment = payment + + chargeRepository.save(charge) + } + } + + fun getDonationStatus(roomId: Long, member: Member): GetLiveRoomDonationStatusResponse { + val room = repository.getLiveRoom(roomId) ?: throw SodaException("잘못된 요청입니다.") + val donationList = repository.getDonationList(roomId = room.id!!, cloudFrontHost = cloudFrontHost) + val totalCan = donationList.sumOf { it.can } + + return GetLiveRoomDonationStatusResponse( + donationList = donationList, + totalCount = donationList.size, + totalCan = totalCan + ) + } + + fun quitRoom(roomId: Long, member: Member) { + val room = repository.getLiveRoom(roomId) + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + if (roomInfo != null) { + if (room?.member != null && room.member!! == member) { + room.isActive = false + kickOutService.deleteKickOutData(roomId = room.id!!) + roomInfoRepository.deleteById(roomInfo.roomId) + } else { + roomInfo.removeSpeaker(member) + roomInfo.removeListener(member) + roomInfo.removeManager(member) + roomInfoRepository.save(roomInfo) + } + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt new file mode 100644 index 0000000..e5afd3f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.room + +data class SetManagerOrSpeakerOrAudienceRequest( + val roomId: Long, + val accountId: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt new file mode 100644 index 0000000..4677041 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/DeleteLiveRoomDonationMessage.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.room.donation + +data class DeleteLiveRoomDonationMessage( + val roomId: Long, + val messageUUID: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt new file mode 100644 index 0000000..2cc8a12 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationStatusResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.live.room.donation + +import com.querydsl.core.annotations.QueryProjection + +data class GetLiveRoomDonationStatusResponse( + val donationList: List, + val totalCount: Int, + val totalCan: Int +) + +data class GetLiveRoomDonationItem @QueryProjection constructor( + val profileImage: String, + val nickname: String, + val userId: Long, + val can: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt new file mode 100644 index 0000000..8dc9556 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.room.donation + +data class GetLiveRoomDonationTotalResponse(val totalDonationCoin: Int) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt new file mode 100644 index 0000000..732bd84 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/LiveRoomDonationRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.live.room.donation + +data class LiveRoomDonationRequest( + val roomId: Long, + val can: Int, + val container: String, + val message: String = "" +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt new file mode 100644 index 0000000..0bede8c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.live.room.info + +data class GetRoomInfoResponse( + val roomId: Long, + val title: String, + val notice: String, + val coverImageUrl: String, + val channelName: String, + val rtcToken: String, + val rtmToken: String, + val managerId: Long, + val managerNickname: String, + val managerProfileUrl: String, + val isFollowingManager: Boolean, + val participantsCount: Int, + val totalAvailableParticipantsCount: Int, + val speakerList: List, + val listenerList: List, + val managerList: List, + val donationRankingTop3UserIds: List, + val isRadioMode: Boolean = false, + val isAvailableDonation: Boolean = false, + val isPrivateRoom: Boolean = false, + val password: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOut.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOut.kt new file mode 100644 index 0000000..aa4bc54 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOut.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.live.room.kickout + +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash + +@RedisHash("live_room_kick_out") +data class LiveRoomKickOut( + @Id + val roomId: Long, + var userList: MutableList = mutableListOf() +) { + fun kickOut(userId: Long) { + var liveRoomKickOutUser = userList.find { it.userId == userId } + if (liveRoomKickOutUser == null) { + liveRoomKickOutUser = LiveRoomKickOutUser(userId) + } else { + liveRoomKickOutUser.plusCount() + } + + userList.removeIf { it.userId == userId } + userList.add(liveRoomKickOutUser) + } +} + +data class LiveRoomKickOutUser( + val userId: Long, + var count: Int = 1 +) { + fun plusCount() { + count += 1 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutController.kt new file mode 100644 index 0000000..fa77973 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutController.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.live.room.kickout + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +@RequestMapping("/live/room/kick-out") +class LiveRoomKickOutController(private val service: LiveRoomKickOutService) { + + @PostMapping + fun liveRoomKickOut( + @RequestBody request: LiveRoomKickOutRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.kickOut(request = request, member = member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRedisRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRedisRepository.kt new file mode 100644 index 0000000..fb01cd8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRedisRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.live.room.kickout + +import org.springframework.data.repository.CrudRepository + +interface LiveRoomKickOutRedisRepository : CrudRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRequest.kt new file mode 100644 index 0000000..612d5d2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.live.room.kickout + +data class LiveRoomKickOutRequest( + val roomId: Long, + val userId: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt new file mode 100644 index 0000000..133edd0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt @@ -0,0 +1,61 @@ +package kr.co.vividnext.sodalive.live.room.kickout + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.LiveRoomRepository +import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository +import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service + +@Service +class LiveRoomKickOutService( + private val roomInfoRepository: LiveRoomInfoRedisRepository, + private val repository: LiveRoomKickOutRedisRepository, + private val accountRepository: MemberRepository, + private val roomRepository: LiveRoomRepository +) { + fun kickOut(request: LiveRoomKickOutRequest, member: Member) { + val room = roomRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") + + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + if (room.member == null || room.member!!.id == null) { + throw SodaException("해당하는 라이브가 없습니다.") + } + + if (!roomInfo.managerList.contains(LiveRoomMember(member)) && room.member!!.id != member.id) { + throw SodaException("권한이 없습니다.") + } + + var liveRoomKickOut = repository.findByIdOrNull(request.roomId) + if (liveRoomKickOut == null) { + liveRoomKickOut = repository.save(LiveRoomKickOut(roomId = request.roomId)) + } + + liveRoomKickOut.kickOut(request.userId) + repository.save(liveRoomKickOut) + + val kickOutUser = accountRepository.findByIdOrNull(request.userId) + if (kickOutUser != null) { + roomInfo.removeSpeaker(kickOutUser) + roomInfo.removeListener(kickOutUser) + roomInfo.removeManager(kickOutUser) + roomInfoRepository.save(roomInfo) + } + } + + fun getKickOutCount(roomId: Long, userId: Long): Int { + val liveRoomKickOut = repository.findByIdOrNull(roomId) ?: return 0 + + val findUser = liveRoomKickOut.userList.find { it.userId == userId } ?: return 0 + return findUser.count + } + + fun deleteKickOutData(roomId: Long) { + repository.deleteById(roomId) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 30cfcb9..89d996d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -2,6 +2,8 @@ package kr.co.vividnext.sodalive.member import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.block.MemberBlockRequest +import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -72,4 +74,44 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.getMyPage(member, container)) } + + @PostMapping("/creator/follow") + fun creatorFollow( + @RequestBody request: CreatorFollowRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.creatorFollow(creatorId = request.creatorId, memberId = member.id!!)) + } + + @PostMapping("/creator/unfollow") + fun creatorUnFollow( + @RequestBody request: CreatorFollowRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.creatorUnFollow(creatorId = request.creatorId, memberId = member.id!!)) + } + + @PostMapping("/block") + fun memberBlock( + @RequestBody request: MemberBlockRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.memberBlock(request = request, memberId = member.id!!)) + } + + @PostMapping("/unblock") + fun memberUnBlock( + @RequestBody request: MemberBlockRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.memberUnBlock(request = request, memberId = member.id!!)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 1f5d324..e70aaf0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -6,6 +6,11 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.jwt.TokenProvider +import kr.co.vividnext.sodalive.member.block.BlockMember +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.member.block.MemberBlockRequest +import kr.co.vividnext.sodalive.member.following.CreatorFollowing +import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.login.LoginResponse @@ -39,6 +44,8 @@ class MemberService( private val repository: MemberRepository, private val stipulationRepository: StipulationRepository, private val stipulationAgreeRepository: StipulationAgreeRepository, + private val creatorFollowingRepository: CreatorFollowingRepository, + private val blockMemberRepository: BlockMemberRepository, private val memberNotificationService: MemberNotificationService, @@ -264,4 +271,61 @@ class MemberService( return MemberAdapter(member) } + + @Transactional + fun creatorFollow(creatorId: Long, memberId: Long) { + val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId( + creatorId = creatorId, + memberId = memberId + ) + + if (creatorFollowing == null) { + val creator = repository.findByIdOrNull(creatorId) ?: throw SodaException("크리에이터 정보를 확인해주세요.") + val member = repository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") + creatorFollowingRepository.save(CreatorFollowing(creator = creator, member = member)) + } else { + creatorFollowing.isActive = true + } + } + + @Transactional + fun creatorUnFollow(creatorId: Long, memberId: Long) { + val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId( + creatorId = creatorId, + memberId = memberId + ) + + if (creatorFollowing != null) { + creatorFollowing.isActive = false + } + } + + fun memberBlock(request: MemberBlockRequest, memberId: Long) { + var blockMember = blockMemberRepository.getBlockAccount( + blockedMemberId = request.blockMemberId, + memberId = memberId + ) + + if (blockMember == null) { + blockMember = BlockMember( + blockedMemberId = request.blockMemberId, + memberId = memberId + ) + + blockMemberRepository.save(blockMember) + } else { + blockMember.isActive = true + } + } + + fun memberUnBlock(request: MemberBlockRequest, memberId: Long) { + val blockMember = blockMemberRepository.getBlockAccount( + blockedMemberId = request.blockMemberId, + memberId = memberId + ) + + if (blockMember != null) { + blockMember.isActive = true + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMember.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMember.kt new file mode 100644 index 0000000..57811cd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMember.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.member.block + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity + +@Entity +data class BlockMember( + val blockedMemberId: Long, + val memberId: Long +) : BaseEntity() { + var isActive: Boolean = true +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt new file mode 100644 index 0000000..c9a42d9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt @@ -0,0 +1,27 @@ +package kr.co.vividnext.sodalive.member.block + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface BlockMemberRepository : JpaRepository, BlockMemberQueryRepository + +interface BlockMemberQueryRepository { + fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember? +} + +@Repository +class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : BlockMemberQueryRepository { + override fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember? { + return queryFactory + .selectFrom(blockMember) + .where( + blockMember.blockedMemberId.eq(blockedMemberId) + .and(blockMember.memberId.eq(memberId)) + ) + .orderBy(blockMember.id.desc()) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/MemberBlockRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/MemberBlockRequest.kt new file mode 100644 index 0000000..1fddf16 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/MemberBlockRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.member.block + +data class MemberBlockRequest(val blockMemberId: Long) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowRequest.kt new file mode 100644 index 0000000..453004b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.member.following + +data class CreatorFollowRequest(val creatorId: Long) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowing.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowing.kt new file mode 100644 index 0000000..c0a3044 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowing.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.member.following + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +class CreatorFollowing( + // 유저가 알림받기 한 크리에이터 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id", nullable = false) + var creator: Member, + + // 크리에이터를 알림받기 한 유저 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member, + + var isActive: Boolean = true +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt new file mode 100644 index 0000000..d74b551 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/following/CreatorFollowingRepository.kt @@ -0,0 +1,28 @@ +package kr.co.vividnext.sodalive.member.following + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface CreatorFollowingRepository : JpaRepository, CreatorFollowingQueryRepository + +interface CreatorFollowingQueryRepository { + fun findByCreatorIdAndMemberId(creatorId: Long, memberId: Long): CreatorFollowing? +} + +@Repository +class CreatorFollowingQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : CreatorFollowingQueryRepository { + override fun findByCreatorIdAndMemberId(creatorId: Long, memberId: Long): CreatorFollowing? { + return queryFactory + .selectFrom(creatorFollowing) + .where( + creatorFollowing.creator.id.eq(creatorId) + .and(creatorFollowing.member.id.eq(memberId)) + ) + .fetchFirst() + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 75ee46f..4dfbeac 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,6 +16,10 @@ apple: iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt +agora: + appId: ${AGORA_APP_ID} + appCertificate: ${AGORA_APP_CERTIFICATE} + cloud: aws: credentials: diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 4cbecc3..3fc8d6c 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -8,7 +8,11 @@ logging: apple: iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt - + +agora: + appId: ${AGORA_APP_ID} + appCertificate: ${AGORA_APP_CERTIFICATE} + cloud: aws: credentials: From fb1386be05d82406f4f80289ba7d635f41366709 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 05:18:21 +0900 Subject: [PATCH 25/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20-=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=B0=B8=EC=97=AC?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EC=97=90=20cloudfront=20host=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/live/room/info/LiveRoomMember.kt | 8 ++++++-- .../kr/co/vividnext/sodalive/member/MemberService.kt | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt index cdca980..0663059 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomMember.kt @@ -9,10 +9,14 @@ data class LiveRoomMember( ) { var role = LiveRoomMemberRole.LISTENER - constructor(member: Member) : this( + constructor(member: Member, cloudFrontHost: String) : this( id = member.id!!, nickname = member.nickname, - profileImage = member.profileImage ?: "profile/default-profile.png" + profileImage = if (member.profileImage != null) { + "$cloudFrontHost/${member.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + } ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index e70aaf0..42f34b5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -184,7 +184,11 @@ class MemberService( token = jwt, nickname = member.nickname, email = member.email, - profileImage = member.profileImage ?: "" + profileImage = if (member.profileImage != null) { + "$cloudFrontHost/${member.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + } ) } From 3cac42b5b999e0496cdb5f870c4d446be789b798 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 05:24:35 +0900 Subject: [PATCH 26/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20-=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=B0=B8=EC=97=AC?= =?UTF-8?q?=EC=9E=90=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EC=97=90=20cloudfront=20host=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomService.kt | 16 ++++++++-------- .../sodalive/live/room/info/LiveRoomInfo.kt | 12 ++++++------ .../live/room/kickout/LiveRoomKickOutService.kt | 8 ++++++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 6d8d20a..62e6967 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -451,9 +451,9 @@ class LiveRoomService( roomInfo.removeManager(member) if (room.member!!.id == member.id) { - roomInfo.addSpeaker(member) + roomInfo.addSpeaker(member, cloudFrontHost) } else { - roomInfo.addListener(member) + roomInfo.addListener(member, cloudFrontHost) } roomInfoRepository.save(roomInfo) @@ -653,9 +653,9 @@ class LiveRoomService( } // 조회 하는 유저 - val memberResponse = LiveRoomMember(member) + val memberResponse = LiveRoomMember(member, cloudFrontHost) // 조회 당하는 유저 - val userResponse = LiveRoomMember(user) + val userResponse = LiveRoomMember(user, cloudFrontHost) val isSpeaker = if ( room.member!!.id!! != userId && @@ -713,7 +713,7 @@ class LiveRoomService( roomInfo.removeListener(account) roomInfo.removeManager(account) - roomInfo.addSpeaker(account) + roomInfo.addSpeaker(account, cloudFrontHost) roomInfoRepository.save(roomInfo) } @@ -727,7 +727,7 @@ class LiveRoomService( roomInfo.removeSpeaker(member) roomInfo.removeManager(member) - roomInfo.addListener(member) + roomInfo.addListener(member, cloudFrontHost) roomInfoRepository.save(roomInfo) } @@ -742,7 +742,7 @@ class LiveRoomService( val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") - val roomAccountResponse = LiveRoomMember(member = user) + val roomAccountResponse = LiveRoomMember(member = user, cloudFrontHost) if (roomInfo.managerList.contains(roomAccountResponse)) { throw SodaException("이미 매니저 입니다.") } @@ -756,7 +756,7 @@ class LiveRoomService( roomInfo.removeListener(user) roomInfo.removeSpeaker(user) - roomInfo.addManager(user) + roomInfo.addManager(user, cloudFrontHost) roomInfoRepository.save(roomInfo) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt index 3946fbf..7bb3ca9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/LiveRoomInfo.kt @@ -23,8 +23,8 @@ data class LiveRoomInfo( var managerCount = 0 private set - fun addSpeaker(member: Member) { - val liveRoomMember = LiveRoomMember(member) + fun addSpeaker(member: Member, cloudFrontHost: String) { + val liveRoomMember = LiveRoomMember(member, cloudFrontHost) liveRoomMember.role = LiveRoomMemberRole.SPEAKER val speakerSet = speakerList.toMutableSet() @@ -43,8 +43,8 @@ data class LiveRoomInfo( speakerCount = speakerList.size } - fun addListener(member: Member) { - val liveRoomMember = LiveRoomMember(member) + fun addListener(member: Member, cloudFrontHost: String) { + val liveRoomMember = LiveRoomMember(member, cloudFrontHost) liveRoomMember.role = LiveRoomMemberRole.LISTENER val listenerSet = listenerList.toMutableSet() @@ -63,8 +63,8 @@ data class LiveRoomInfo( listenerCount = listenerList.size } - fun addManager(member: Member) { - val liveRoomMember = LiveRoomMember(member) + fun addManager(member: Member, cloudFrontHost: String) { + val liveRoomMember = LiveRoomMember(member, cloudFrontHost) liveRoomMember.role = LiveRoomMemberRole.MANAGER val managerSet = managerList.toMutableSet() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt index 133edd0..0ee28c6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt @@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository import kr.co.vividnext.sodalive.live.room.info.LiveRoomMember import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -14,7 +15,10 @@ class LiveRoomKickOutService( private val roomInfoRepository: LiveRoomInfoRedisRepository, private val repository: LiveRoomKickOutRedisRepository, private val accountRepository: MemberRepository, - private val roomRepository: LiveRoomRepository + private val roomRepository: LiveRoomRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String ) { fun kickOut(request: LiveRoomKickOutRequest, member: Member) { val room = roomRepository.findByIdOrNull(request.roomId) @@ -27,7 +31,7 @@ class LiveRoomKickOutService( throw SodaException("해당하는 라이브가 없습니다.") } - if (!roomInfo.managerList.contains(LiveRoomMember(member)) && room.member!!.id != member.id) { + if (!roomInfo.managerList.contains(LiveRoomMember(member, cloudFrontHost)) && room.member!!.id != member.id) { throw SodaException("권한이 없습니다.") } From 7671e24470e404ba107a02d6e4d8dfc6f6d7f7a9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 05:39:14 +0900 Subject: [PATCH 27/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20-=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomController.kt | 3 +- .../sodalive/live/room/LiveRoomService.kt | 31 +++++++++++-------- .../room/visit/LiveRoomVisitRepository.kt | 10 ++++++ .../live/room/visit/LiveRoomVisitService.kt | 4 +++ 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index 4f7fd58..994a7cb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -223,10 +223,9 @@ class LiveRoomController(private val service: LiveRoomService) { @PostMapping("/quit") fun quitRoom( - @RequestParam("id") roomId: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - ApiResponse.ok(service.quitRoom(roomId, member)) + ApiResponse.ok(service.quitRoom(member)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 62e6967..9761c77 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -843,19 +843,24 @@ class LiveRoomService( ) } - fun quitRoom(roomId: Long, member: Member) { - val room = repository.getLiveRoom(roomId) - val roomInfo = roomInfoRepository.findByIdOrNull(roomId) - if (roomInfo != null) { - if (room?.member != null && room.member!! == member) { - room.isActive = false - kickOutService.deleteKickOutData(roomId = room.id!!) - roomInfoRepository.deleteById(roomInfo.roomId) - } else { - roomInfo.removeSpeaker(member) - roomInfo.removeListener(member) - roomInfo.removeManager(member) - roomInfoRepository.save(roomInfo) + fun quitRoom(member: Member) { + val roomVisit = roomVisitService.getLatestRoomVisit(member.id!!) + + val roomId = roomVisit?.room?.id + if (roomId != null) { + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + if (roomInfo != null) { + val room = repository.getLiveRoom(roomId) ?: return + if (room.member != null && room.member!! == member) { + room.isActive = false + kickOutService.deleteKickOutData(roomId = room.id!!) + roomInfoRepository.deleteById(roomInfo.roomId) + } else { + roomInfo.removeSpeaker(member) + roomInfo.removeListener(member) + roomInfo.removeManager(member) + roomInfoRepository.save(roomInfo) + } } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt index b90ce96..2eed38f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -13,6 +13,7 @@ interface LiveRoomVisitRepository : JpaRepository, LiveRoom interface LiveRoomVisitQueryRepository { fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? + fun getLatestRoomVisit(memberId: Long): LiveRoomVisit? } @Repository @@ -38,4 +39,13 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory .orderBy(liveRoomVisit.updatedAt.desc()) .fetchFirst() } + + override fun getLatestRoomVisit(memberId: Long): LiveRoomVisit? { + return queryFactory + .selectFrom(liveRoomVisit) + .innerJoin(liveRoomVisit.member, member) + .where(member.id.eq(memberId)) + .orderBy(liveRoomVisit.updatedAt.desc()) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt index df18ba1..a0cda13 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt @@ -22,4 +22,8 @@ class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { repository.save(roomVisit) } + + fun getLatestRoomVisit(memberId: Long): LiveRoomVisit? { + return repository.getLatestRoomVisit(memberId) + } } From 5a56990d0b1ef58a1f0206a0bac748498f84404c Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 05:48:06 +0900 Subject: [PATCH 28/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20-=20ReentrantReadWriteLock=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomService.kt | 259 ++++++++++-------- .../SetManagerOrSpeakerOrAudienceRequest.kt | 2 +- 2 files changed, 145 insertions(+), 116 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 9761c77..495d919 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -53,6 +53,8 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Date +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.write @Service @Transactional(readOnly = true) @@ -88,6 +90,8 @@ class LiveRoomService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { + private val tokenLocks: MutableMap = mutableMapOf() + fun getRoomList( dateString: String?, status: LiveRoomStatus, @@ -415,49 +419,52 @@ class LiveRoomService( throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") } - var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) - if (roomInfo == null) { - roomInfo = roomInfoRepository.save(LiveRoomInfo(roomId = request.roomId)) - } - - if (roomInfo.speakerCount + roomInfo.listenerCount + roomInfo.managerCount >= room.numberOfPeople) { - throw SodaException("방이 가득찼습니다.") - } - - if ( - room.price > 0 && - room.member!!.id!! != member.id!! && - canRepository.isExistPaidLiveRoom(memberId = member.id!!, roomId = request.roomId) == null - ) { - val findMember = memberRepository.findByIdOrNull(id = member.id!!) - ?: throw SodaException("로그인 정보를 확인해 주세요.") - - val totalCan = findMember.getChargeCan(request.container) + findMember.getRewardCan(request.container) - if (totalCan < room.price) { - throw SodaException("${room.price - totalCan}캔이 부족합니다. 충전 후 이용해 주세요.") + val lock = getOrCreateLock(memberId = member.id!!) + lock.write { + var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + if (roomInfo == null) { + roomInfo = roomInfoRepository.save(LiveRoomInfo(roomId = request.roomId)) } - canPaymentService.spendCan( - memberId = member.id!!, - needCan = room.price, - canUsage = CanUsage.LIVE, - liveRoom = room, - container = request.container - ) + if (roomInfo.speakerCount + roomInfo.listenerCount + roomInfo.managerCount >= room.numberOfPeople) { + throw SodaException("방이 가득찼습니다.") + } + + if ( + room.price > 0 && + room.member!!.id!! != member.id!! && + canRepository.isExistPaidLiveRoom(memberId = member.id!!, roomId = request.roomId) == null + ) { + val findMember = memberRepository.findByIdOrNull(id = member.id!!) + ?: throw SodaException("로그인 정보를 확인해 주세요.") + + val totalCan = findMember.getChargeCan(request.container) + findMember.getRewardCan(request.container) + if (totalCan < room.price) { + throw SodaException("${room.price - totalCan}캔이 부족합니다. 충전 후 이용해 주세요.") + } + + canPaymentService.spendCan( + memberId = member.id!!, + needCan = room.price, + canUsage = CanUsage.LIVE, + liveRoom = room, + container = request.container + ) + } + + roomInfo.removeListener(member) + roomInfo.removeSpeaker(member) + roomInfo.removeManager(member) + + if (room.member!!.id == member.id) { + roomInfo.addSpeaker(member, cloudFrontHost) + } else { + roomInfo.addListener(member, cloudFrontHost) + } + + roomInfoRepository.save(roomInfo) + roomVisitService.roomVisit(room, member) } - - roomInfo.removeListener(member) - roomInfo.removeSpeaker(member) - roomInfo.removeManager(member) - - if (room.member!!.id == member.id) { - roomInfo.addSpeaker(member, cloudFrontHost) - } else { - roomInfo.addListener(member, cloudFrontHost) - } - - roomInfoRepository.save(roomInfo) - roomVisitService.roomVisit(room, member) } fun getRecentRoomInfo(member: Member): GetRecentRoomInfoResponse { @@ -621,18 +628,21 @@ class LiveRoomService( } fun deleteDonationMessage(request: DeleteLiveRoomDonationMessage, member: Member) { - val room = repository.findByIdOrNull(request.roomId) - ?: throw SodaException("해당하는 라이브가 없습니다.") + val lock = getOrCreateLock(memberId = member.id!!) + lock.write { + val room = repository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브가 없습니다.") - if (member.id!! != room.member!!.id!!) { - throw SodaException("잘못된 요청입니다.") + if (member.id!! != room.member!!.id!!) { + throw SodaException("잘못된 요청입니다.") + } + + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + roomInfo.removeDonationMessage(request.messageUUID) + roomInfoRepository.save(roomInfo) } - - val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) - ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") - - roomInfo.removeDonationMessage(request.messageUUID) - roomInfoRepository.save(roomInfo) } fun getUserProfile(roomId: Long, userId: Long, member: Member): GetLiveRoomUserProfileResponse { @@ -701,64 +711,73 @@ class LiveRoomService( } fun setSpeaker(request: SetManagerOrSpeakerOrAudienceRequest) { - val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) - ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + val lock = getOrCreateLock(memberId = request.memberId) + lock.write { + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") - val account = memberRepository.findByIdOrNull(request.accountId) - ?: throw SodaException("로그인 정보를 확인해 주세요.") + val member = memberRepository.findByIdOrNull(request.memberId) + ?: throw SodaException("로그인 정보를 확인해 주세요.") - if (roomInfo.speakerCount > 9) { - throw SodaException("스피커 정원이 초과하였습니다.") + if (roomInfo.speakerCount > 9) { + throw SodaException("스피커 정원이 초과하였습니다.") + } + + roomInfo.removeListener(member) + roomInfo.removeManager(member) + roomInfo.addSpeaker(member, cloudFrontHost) + + roomInfoRepository.save(roomInfo) } - - roomInfo.removeListener(account) - roomInfo.removeManager(account) - roomInfo.addSpeaker(account, cloudFrontHost) - - roomInfoRepository.save(roomInfo) } fun setListener(request: SetManagerOrSpeakerOrAudienceRequest) { - val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) - ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + val lock = getOrCreateLock(memberId = request.memberId) + lock.write { + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") - val member = memberRepository.findByIdOrNull(request.accountId) - ?: throw SodaException("로그인 정보를 확인해 주세요.") + val member = memberRepository.findByIdOrNull(request.memberId) + ?: throw SodaException("로그인 정보를 확인해 주세요.") - roomInfo.removeSpeaker(member) - roomInfo.removeManager(member) - roomInfo.addListener(member, cloudFrontHost) + roomInfo.removeSpeaker(member) + roomInfo.removeManager(member) + roomInfo.addListener(member, cloudFrontHost) - roomInfoRepository.save(roomInfo) + roomInfoRepository.save(roomInfo) + } } fun setManager(request: SetManagerOrSpeakerOrAudienceRequest, member: Member) { - val room = repository.getLiveRoom(request.roomId) ?: throw SodaException("잘못된 요청입니다.") - if (room.member!!.id!! != member.id!!) { - throw SodaException("권한이 없습니다.") + val lock = getOrCreateLock(memberId = member.id!!) + lock.write { + val room = repository.getLiveRoom(request.roomId) ?: throw SodaException("잘못된 요청입니다.") + if (room.member!!.id!! != member.id!!) { + throw SodaException("권한이 없습니다.") + } + + val user = memberRepository.findByIdOrNull(request.memberId) ?: throw SodaException("해당하는 유저가 없습니다.") + val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + + val roomAccountResponse = LiveRoomMember(member = user, cloudFrontHost) + if (roomInfo.managerList.contains(roomAccountResponse)) { + throw SodaException("이미 매니저 입니다.") + } + + if ( + !roomInfo.speakerList.contains(roomAccountResponse) && + !roomInfo.listenerList.contains(roomAccountResponse) + ) { + throw SodaException("해당하는 유저가 없습니다.") + } + + roomInfo.removeListener(user) + roomInfo.removeSpeaker(user) + roomInfo.addManager(user, cloudFrontHost) + + roomInfoRepository.save(roomInfo) } - - val user = memberRepository.findByIdOrNull(request.accountId) ?: throw SodaException("해당하는 유저가 없습니다.") - val roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) - ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") - - val roomAccountResponse = LiveRoomMember(member = user, cloudFrontHost) - if (roomInfo.managerList.contains(roomAccountResponse)) { - throw SodaException("이미 매니저 입니다.") - } - - if ( - !roomInfo.speakerList.contains(roomAccountResponse) && - !roomInfo.listenerList.contains(roomAccountResponse) - ) { - throw SodaException("해당하는 유저가 없습니다.") - } - - roomInfo.removeListener(user) - roomInfo.removeSpeaker(user) - roomInfo.addManager(user, cloudFrontHost) - - roomInfoRepository.save(roomInfo) } @Transactional @@ -781,16 +800,19 @@ class LiveRoomService( ) if (request.message.isNotBlank()) { - val roomInfo = roomInfoRepository.findByIdOrNull(room.id!!) - ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") + val lock = getOrCreateLock(memberId = member.id!!) + lock.write { + val roomInfo = roomInfoRepository.findByIdOrNull(room.id!!) + ?: throw SodaException("해당하는 라이브의 정보가 없습니다.") - roomInfo.addDonationMessage( - nickname = member.nickname, - can = request.can, - donationMessage = request.message - ) + roomInfo.addDonationMessage( + nickname = member.nickname, + can = request.can, + donationMessage = request.message + ) - roomInfoRepository.save(roomInfo) + roomInfoRepository.save(roomInfo) + } } } @@ -848,20 +870,27 @@ class LiveRoomService( val roomId = roomVisit?.room?.id if (roomId != null) { - val roomInfo = roomInfoRepository.findByIdOrNull(roomId) - if (roomInfo != null) { - val room = repository.getLiveRoom(roomId) ?: return - if (room.member != null && room.member!! == member) { - room.isActive = false - kickOutService.deleteKickOutData(roomId = room.id!!) - roomInfoRepository.deleteById(roomInfo.roomId) - } else { - roomInfo.removeSpeaker(member) - roomInfo.removeListener(member) - roomInfo.removeManager(member) - roomInfoRepository.save(roomInfo) + val lock = getOrCreateLock(memberId = member.id!!) + lock.write { + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + if (roomInfo != null) { + val room = repository.getLiveRoom(roomId) ?: return + if (room.member != null && room.member!! == member) { + room.isActive = false + kickOutService.deleteKickOutData(roomId = room.id!!) + roomInfoRepository.deleteById(roomInfo.roomId) + } else { + roomInfo.removeSpeaker(member) + roomInfo.removeListener(member) + roomInfo.removeManager(member) + roomInfoRepository.save(roomInfo) + } } } } } + + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { + return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt index e5afd3f..5d41045 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/SetManagerOrSpeakerOrAudienceRequest.kt @@ -2,5 +2,5 @@ package kr.co.vividnext.sodalive.live.room data class SetManagerOrSpeakerOrAudienceRequest( val roomId: Long, - val accountId: Long + val memberId: Long ) From 3c09047371e5fcbcaf374c5bfac65adee7dd41d4 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 06:08:23 +0900 Subject: [PATCH 29/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20-=20=EB=B0=A9=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 495d919..04c1271 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -875,7 +875,7 @@ class LiveRoomService( val roomInfo = roomInfoRepository.findByIdOrNull(roomId) if (roomInfo != null) { val room = repository.getLiveRoom(roomId) ?: return - if (room.member != null && room.member!! == member) { + if (room.member != null && room.member!!.id!! == member.id!!) { room.isActive = false kickOutService.deleteKickOutData(roomId = room.id!!) roomInfoRepository.deleteById(roomInfo.roomId) From 018a4a95a22e3dcbb0d6067f5d7c32b134a8302c Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 06:14:53 +0900 Subject: [PATCH 30/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20-=20=EB=B0=A9=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt index e2c12c4..df04a37 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomRepository.kt @@ -112,6 +112,7 @@ class LiveRoomQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : L override fun getLiveRoom(id: Long): LiveRoom? { return queryFactory .selectFrom(liveRoom) + .innerJoin(liveRoom.member, member) .where( liveRoom.id.eq(id) .and(liveRoom.isActive.isTrue) From 7d904067fed152d093cdf71017bd6de34f61df85 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 06:25:05 +0900 Subject: [PATCH 31/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20-=20=EB=B0=A9=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20=EB=94=94=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=B4=20print=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 04c1271..f997c90 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -875,6 +875,7 @@ class LiveRoomService( val roomInfo = roomInfoRepository.findByIdOrNull(roomId) if (roomInfo != null) { val room = repository.getLiveRoom(roomId) ?: return + println(room) if (room.member != null && room.member!!.id!! == member.id!!) { room.isActive = false kickOutService.deleteKickOutData(roomId = room.id!!) From df861bf8a1e90db75be762a5c2cc3744dd85955e Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 06:32:21 +0900 Subject: [PATCH 32/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20-=20=EB=B0=A9=20=EB=82=98=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=A1=9C=EC=A7=81=20@Transactional=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomController.kt | 3 +- .../sodalive/live/room/LiveRoomService.kt | 37 ++++++++----------- .../room/visit/LiveRoomVisitRepository.kt | 10 ----- .../live/room/visit/LiveRoomVisitService.kt | 4 -- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index 994a7cb..4f7fd58 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -223,9 +223,10 @@ class LiveRoomController(private val service: LiveRoomService) { @PostMapping("/quit") fun quitRoom( + @RequestParam("id") roomId: Long, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - ApiResponse.ok(service.quitRoom(member)) + ApiResponse.ok(service.quitRoom(roomId, member)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index f997c90..3938006 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -865,27 +865,22 @@ class LiveRoomService( ) } - fun quitRoom(member: Member) { - val roomVisit = roomVisitService.getLatestRoomVisit(member.id!!) - - val roomId = roomVisit?.room?.id - if (roomId != null) { - val lock = getOrCreateLock(memberId = member.id!!) - lock.write { - val roomInfo = roomInfoRepository.findByIdOrNull(roomId) - if (roomInfo != null) { - val room = repository.getLiveRoom(roomId) ?: return - println(room) - if (room.member != null && room.member!!.id!! == member.id!!) { - room.isActive = false - kickOutService.deleteKickOutData(roomId = room.id!!) - roomInfoRepository.deleteById(roomInfo.roomId) - } else { - roomInfo.removeSpeaker(member) - roomInfo.removeListener(member) - roomInfo.removeManager(member) - roomInfoRepository.save(roomInfo) - } + @Transactional + fun quitRoom(roomId: Long, member: Member) { + val room = repository.getLiveRoom(roomId) + val lock = getOrCreateLock(memberId = member.id!!) + lock.write { + val roomInfo = roomInfoRepository.findByIdOrNull(roomId) + if (roomInfo != null) { + if (room?.member != null && room.member!! == member) { + room.isActive = false + kickOutService.deleteKickOutData(roomId = room.id!!) + roomInfoRepository.deleteById(roomInfo.roomId) + } else { + roomInfo.removeSpeaker(member) + roomInfo.removeListener(member) + roomInfo.removeManager(member) + roomInfoRepository.save(roomInfo) } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt index 2eed38f..b90ce96 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -13,7 +13,6 @@ interface LiveRoomVisitRepository : JpaRepository, LiveRoom interface LiveRoomVisitQueryRepository { fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? - fun getLatestRoomVisit(memberId: Long): LiveRoomVisit? } @Repository @@ -39,13 +38,4 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory .orderBy(liveRoomVisit.updatedAt.desc()) .fetchFirst() } - - override fun getLatestRoomVisit(memberId: Long): LiveRoomVisit? { - return queryFactory - .selectFrom(liveRoomVisit) - .innerJoin(liveRoomVisit.member, member) - .where(member.id.eq(memberId)) - .orderBy(liveRoomVisit.updatedAt.desc()) - .fetchFirst() - } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt index a0cda13..df18ba1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt @@ -22,8 +22,4 @@ class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { repository.save(roomVisit) } - - fun getLatestRoomVisit(memberId: Long): LiveRoomVisit? { - return repository.getLatestRoomVisit(memberId) - } } From 049e1c41dee81c6c6f50d0d460eb64dcb6d2d909 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 10:23:49 +0900 Subject: [PATCH 33/94] =?UTF-8?q?=ED=83=90=EC=83=89=20=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20-=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/explorer/ExplorerController.kt | 32 ++++ .../explorer/ExplorerQueryRepository.kt | 73 +++++++++ .../sodalive/explorer/ExplorerService.kt | 145 ++++++++++++++++++ .../sodalive/explorer/GetExplorerResponse.kt | 17 ++ .../explorer/section/ExplorerSection.kt | 43 ++++++ .../sodalive/live/room/LiveRoomService.kt | 6 +- .../kr/co/vividnext/sodalive/member/Member.kt | 8 + .../sodalive/member/MemberService.kt | 2 + .../member/block/BlockMemberRepository.kt | 15 ++ .../sodalive/member/tag/CreatorTag.kt | 17 ++ .../sodalive/member/tag/MemberCreatorTag.kt | 19 +++ 11 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/section/ExplorerSection.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/tag/CreatorTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberCreatorTag.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt new file mode 100644 index 0000000..574e2ca --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.explorer + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/explorer") +class ExplorerController(private val service: ExplorerService) { + @GetMapping + fun getExplorer( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getExplorer(member)) + } + + @GetMapping("/search/channel") + fun getSearchChannel( + @RequestParam channel: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getSearchChannel(channel, member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index 19a869f..6a13f67 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -1,13 +1,22 @@ package kr.co.vividnext.sodalive.explorer +import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.QUseCan.useCan +import kr.co.vividnext.sodalive.explorer.section.ExplorerSection +import kr.co.vividnext.sodalive.explorer.section.QExplorerSection.explorerSection import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember +import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing +import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag +import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository +import java.time.LocalDateTime @Repository class ExplorerQueryRepository( @@ -68,4 +77,68 @@ class ExplorerQueryRepository( ) } } + + fun getSubscriberGrowthRankingCreators(limit: Long): List { + return queryFactory + .selectFrom(member) + .join(member.follower, creatorFollowing) + .where( + member.role.eq(MemberRole.CREATOR) + .and(creatorFollowing.createdAt.goe(LocalDateTime.now().minusMonths(1))) + .and(creatorFollowing.isActive.isTrue) + ) + .groupBy(member.id) + .orderBy(member.follower.size().desc()) + .limit(limit) + .fetch() + } + + fun getNewCreators(): List { + return queryFactory + .selectFrom(member) + .where( + member.role.eq(MemberRole.CREATOR) + .and(member.createdAt.goe(LocalDateTime.now().minusDays(30))) + ) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .limit(20) + .fetch() + } + + fun getExplorerSectionData(isAdult: Boolean): List { + var explorerSectionCondition = explorerSection.isActive.eq(true) + if (!isAdult) { + explorerSectionCondition = explorerSectionCondition.and(explorerSection.isAdult.isFalse) + } + + return queryFactory + .selectFrom(explorerSection) + .where(explorerSectionCondition) + .orderBy(explorerSection.orders.asc()) + .fetch() + } + + fun findMemberByTag(tags: List): List { + return queryFactory + .selectFrom(member) + .leftJoin(member.tags, memberCreatorTag) + .join(memberCreatorTag.tag, creatorTag) + .where( + member.role.eq(MemberRole.CREATOR) + .and(creatorTag.tag.`in`(tags)) + ) + .fetch() + .distinct() + } + + fun getSearchChannel(channel: String, accountId: Long): List { + return queryFactory.selectFrom(member) + .where( + member.nickname.containsIgnoreCase(channel) + .and(member.isActive.isTrue) + .and(member.id.ne(accountId)) + .and(member.role.eq(MemberRole.CREATOR)) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt new file mode 100644 index 0000000..b0802c8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -0,0 +1,145 @@ +package kr.co.vividnext.sodalive.explorer + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberService +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class ExplorerService( + private val memberService: MemberService, + private val queryRepository: ExplorerQueryRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun getExplorer(member: Member, growthRankingCreatorsLimit: Long = 20): GetExplorerResponse { + val sections = mutableListOf() + + // 인기 급상승중 (subscriberGrowthRankingCreators) + val growthRankingCreators = queryRepository + .getSubscriberGrowthRankingCreators(limit = growthRankingCreatorsLimit) + .asSequence() + .filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) } + .map { + GetExplorerSectionCreatorResponse( + id = it.id!!, + nickname = it.nickname, + tags = it.tags + .asSequence() + .filter { tag -> tag.tag.isActive } + .map { tag -> tag.tag.tag } + .joinToString(" ") { tag -> "#$tag" }, + profileImageUrl = if (it.profileImage != null) { + "$cloudFrontHost/${it.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + } + ) + } + .toList() + + val growthRankingSection = GetExplorerSectionResponse( + title = "인기 급상승중", + coloredTitle = "인기", + color = "FF5C49", + creators = growthRankingCreators + ) + sections.add(growthRankingSection) + + // 새로 시작 (newCreators) + val newCreators = queryRepository + .getNewCreators() + .asSequence() + .filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) } + .map { + GetExplorerSectionCreatorResponse( + id = it.id!!, + nickname = it.nickname, + tags = it.tags + .asSequence() + .filter { tag -> tag.tag.isActive } + .map { tag -> tag.tag.tag } + .joinToString(" ") { tag -> "#$tag" }, + profileImageUrl = if (it.profileImage != null) { + "$cloudFrontHost/${it.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + } + ) + } + .toList() + + val newCreatorsSection = GetExplorerSectionResponse( + title = "새로 시작", + coloredTitle = "새로", + color = "5FD28F", + creators = newCreators + ) + sections.add(newCreatorsSection) + + // 관리자에서 설정한 타이틀과 크리에이터 + sections.addAll( + queryRepository + .getExplorerSectionData(isAdult = member.auth != null) + .asSequence() + .map { + val tags = it.tags.asSequence().map { explorerSectionTag -> explorerSectionTag.tag!!.tag }.toList() + val creators = queryRepository.findMemberByTag(tags) + .asSequence() + .filter { creator -> + !memberService.isBlocked( + blockedMemberId = member.id!!, + memberId = creator.id!! + ) + } + .toList() + + GetExplorerSectionResponse( + it.title, + it.coloredTitle, + it.color, + creators = creators + .asSequence() + .map { account -> + GetExplorerSectionCreatorResponse( + id = account.id!!, + nickname = account.nickname, + tags = account.tags + .asSequence() + .filter { counselorTag -> counselorTag.tag.isActive } + .toList() + .joinToString(" ") { counselorTag -> + "#${counselorTag.tag.tag}" + }, + profileImageUrl = if (account.profileImage != null) { + "$cloudFrontHost/${account.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + } + ) + }.toList() + ) + } + .toList() + ) + + return GetExplorerResponse(sections = sections) + } + + fun getSearchChannel(channel: String, member: Member): List { + if (channel.length < 2) { + throw SodaException("두 글자 이상 입력 하셔야 합니다.") + } + + return queryRepository.getSearchChannel(channel, member.id!!) + .asSequence() + .filter { !memberService.isBlocked(blockedMemberId = member.id!!, memberId = it.id!!) } + .map { GetRoomDetailUser(it, cloudFrontHost) } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt new file mode 100644 index 0000000..2d7f4e8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetExplorerResponse.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetExplorerResponse(val sections: List) + +data class GetExplorerSectionResponse( + val title: String, + val coloredTitle: String?, + val color: String?, + val creators: List +) + +data class GetExplorerSectionCreatorResponse( + val id: Long, + val nickname: String, + val tags: String, + val profileImageUrl: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/section/ExplorerSection.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/section/ExplorerSection.kt new file mode 100644 index 0000000..503d78f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/section/ExplorerSection.kt @@ -0,0 +1,43 @@ +package kr.co.vividnext.sodalive.explorer.section + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.tag.CreatorTag +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany + +@Entity +data class ExplorerSection( + var title: String, + var isAdult: Boolean, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() { + + @Column(nullable = true) + var coloredTitle: String? = null + + @Column(nullable = true) + var color: String? = null + + @Column(nullable = false) + var isActive: Boolean = true + + @OneToMany(mappedBy = "explorerSection", cascade = [CascadeType.ALL], orphanRemoval = true) + var tags: MutableList = mutableListOf() +} + +@Entity +class ExplorerSectionCreatorTag : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "explorer_section_id", nullable = false) + var explorerSection: ExplorerSection? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_tag_id", nullable = false) + var tag: CreatorTag? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 3938006..711371b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -696,7 +696,11 @@ class LiveRoomService( websiteUrl = user.websiteUrl, blogUrl = user.blogUrl, introduce = user.introduce, - tags = "", + tags = user.tags + .asSequence() + .filter { it.tag.isActive } + .map { it.tag.tag } + .joinToString(" ") { tag -> "#$tag" }, isSpeaker = isSpeaker, isManager = isManager, isFollowing = isFollowing, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index b8badb2..9179ddf 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -2,8 +2,10 @@ package kr.co.vividnext.sodalive.member import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.member.auth.Auth +import kr.co.vividnext.sodalive.member.following.CreatorFollowing import kr.co.vividnext.sodalive.member.notification.MemberNotification import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree +import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity @@ -33,6 +35,12 @@ data class Member( @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL]) val stipulationAgrees: MutableList = mutableListOf() + @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL], orphanRemoval = true) + var tags: MutableList = mutableListOf() + + @OneToMany(mappedBy = "creator") + var follower: MutableList = mutableListOf() + @OneToOne(mappedBy = "member", fetch = FetchType.LAZY) var notification: MemberNotification? = null diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 42f34b5..d2cd91e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -332,4 +332,6 @@ class MemberService( blockMember.isActive = true } } + + fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt index c9a42d9..29eb50e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt @@ -10,6 +10,7 @@ interface BlockMemberRepository : JpaRepository, BlockMemberQ interface BlockMemberQueryRepository { fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember? + fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean } @Repository @@ -24,4 +25,18 @@ class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .orderBy(blockMember.id.desc()) .fetchFirst() } + + override fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean { + val blockedAccount = queryFactory + .select(blockMember.id) + .from(blockMember) + .where( + blockMember.memberId.eq(memberId) + .and(blockMember.blockedMemberId.eq(blockedMemberId)) + .and(blockMember.isActive.isTrue) + ) + .fetchOne() + + return blockedAccount != null + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/CreatorTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/CreatorTag.kt new file mode 100644 index 0000000..99e3dfc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/CreatorTag.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.member.tag + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class CreatorTag( + @Column(unique = true, nullable = false) + var tag: String, + @Column(nullable = true) + var image: String? = null, + @Column(nullable = false) + var isActive: Boolean = true, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberCreatorTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberCreatorTag.kt new file mode 100644 index 0000000..ea0ecd3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberCreatorTag.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.member.tag + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class MemberCreatorTag( + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_tag_id", nullable = false) + var tag: CreatorTag +) : BaseEntity() From c25b105d4d4c75b7d9bc8dfdfdaddb80cb3585c6 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 1 Aug 2023 14:40:52 +0900 Subject: [PATCH 34/94] =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=B1=84=EB=84=90=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/explorer/CreatorResponse.kt | 15 + .../sodalive/explorer/ExplorerController.kt | 57 +++ .../explorer/ExplorerQueryRepository.kt | 456 ++++++++++++++++++ .../sodalive/explorer/ExplorerService.kt | 164 +++++++ .../sodalive/explorer/GetCheersResponse.kt | 15 + .../explorer/GetCreatorProfileResponse.kt | 19 + .../sodalive/explorer/LiveRoomResponse.kt | 22 + .../explorer/SimilarCreatorResponse.kt | 8 + .../explorer/UserDonationRankingResponse.kt | 16 + .../explorer/follower/GetFollowerListDto.kt | 11 + .../follower/GetFollowerListResponse.kt | 13 + .../explorer/profile/ChannelNotice.kt | 19 + .../profile/ChannelNoticeRepository.kt | 7 + .../explorer/profile/CreatorCheers.kt | 36 ++ .../profile/CreatorCheersRepository.kt | 7 + .../profile/PostCreatorNoticeRequest.kt | 3 + .../profile/PostWriteCheersRequest.kt | 7 + .../explorer/profile/PutWriteCheersRequest.kt | 6 + .../explorer/profile/TimeDifferenceResult.kt | 8 + 19 files changed, 889 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt new file mode 100644 index 0000000..c5b8f8b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/CreatorResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.explorer + +data class CreatorResponse( + val creatorId: Long, + val profileUrl: String, + val nickname: String, + val tags: List, + val introduce: String = "", + val instagramUrl: String? = null, + val youtubeUrl: String? = null, + val websiteUrl: String? = null, + val blogUrl: String? = null, + val isNotification: Boolean, + val notificationRecipientCount: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt index 574e2ca..b5d9f7b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerController.kt @@ -2,9 +2,18 @@ package kr.co.vividnext.sodalive.explorer import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.profile.PostCreatorNoticeRequest +import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest +import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.annotation.AuthenticationPrincipal 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 import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -29,4 +38,52 @@ class ExplorerController(private val service: ExplorerService) { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") ApiResponse.ok(service.getSearchChannel(channel, member)) } + + @GetMapping("/profile/{id}") + fun getCreatorProfile( + @PathVariable("id") creatorId: Long, + @RequestParam timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getCreatorProfile(creatorId, timezone, member)) + } + + @PostMapping("/profile/cheers") + fun writeCheers( + @RequestBody request: PostWriteCheersRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.writeCheers(request, member), "등록되었습니다.") + } + + @PutMapping("/profile/cheers") + fun modifyCheers( + @RequestBody request: PutWriteCheersRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.modifyCheers(request, member), "수정되었습니다.") + } + + @PostMapping("/profile/notice") + @PreAuthorize("hasAnyRole('CREATOR')") + fun postCreatorNotice( + @RequestBody request: PostCreatorNoticeRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.saveNotice(member, request.notice), "공지사항이 저장되었습니다.") + } + + @GetMapping("/profile/{id}/follower-list") + fun getFollowerList( + @PathVariable("id") creatorId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getFollowerList(creatorId, member, pageable)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index 6a13f67..eb29c23 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -1,22 +1,40 @@ package kr.co.vividnext.sodalive.explorer +import com.querydsl.core.types.Predicate +import com.querydsl.core.types.Projections import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.QUseCan.useCan +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListDto +import kr.co.vividnext.sodalive.explorer.follower.QGetFollowerListDto +import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers +import kr.co.vividnext.sodalive.explorer.profile.QChannelNotice.channelNotice +import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers +import kr.co.vividnext.sodalive.explorer.profile.TimeDifferenceResult import kr.co.vividnext.sodalive.explorer.section.ExplorerSection import kr.co.vividnext.sodalive.explorer.section.QExplorerSection.explorerSection +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.live.room.cancel.QLiveRoomCancel.liveRoomCancel +import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.member.auth.QAuth.auth import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository +import java.time.Duration import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter @Repository class ExplorerQueryRepository( @@ -141,4 +159,442 @@ class ExplorerQueryRepository( ) .fetch() } + + fun getAccount(userId: Long): Member? { + return queryFactory + .selectFrom(member) + .where(member.id.eq(userId)) + .fetchFirst() + } + + fun getUserDonationRanking( + creatorId: Long, + limit: Long, + offset: Long = 0, + withDonationCan: Boolean + ): List { + val creatorMember = QMember("creator") + val userMember = QMember("user") + + val donation = useCan.rewardCan.add(useCan.can).sum() + return queryFactory + .select(userMember, donation) + .from(useCan) + .join(useCan.room, liveRoom) + .join(liveRoom.member, creatorMember) + .join(useCan.member, userMember) + .offset(offset) + .limit(limit) + .where( + useCan.canUsage.eq(CanUsage.DONATION) + .and(useCan.isRefund.isFalse) + .and(creatorMember.id.eq(creatorId)) + ) + .groupBy(useCan.member.id) + .orderBy(donation.desc(), userMember.id.desc()) + .fetch() + .map { + val account = it.get(userMember)!! + val donationCan = it.get(donation)!! + UserDonationRankingResponse( + account.id!!, + account.nickname, + if (account.profileImage != null) { + "$cloudFrontHost/${account.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + if (withDonationCan) donationCan else 0 + ) + } + } + + fun getSimilarCreatorList(creatorId: Long): List { + val creator = queryFactory + .selectFrom(member) + .where(member.id.eq(creatorId)) + .fetchFirst() ?: throw SodaException("없는 사용자 입니다.") + + val creatorTagCount = creator.tags + .asSequence() + .filter { it.tag.isActive } + .map { it.tag } + .toList().size + + if (creatorTagCount <= 0) { + val where = member.role.eq(MemberRole.CREATOR) + .and(member.id.ne(creatorId)) + .and(member.tags.size().gt(0)) + .and(memberCreatorTag.tag.isActive.isTrue) + + return getRandomCreatorList(where = where, limit = 3) + } + + val cnt = memberCreatorTag.member.count() + val similarCreators = queryFactory + .select(member, cnt) + .from(memberCreatorTag) + .innerJoin(memberCreatorTag.member, member) + .innerJoin(memberCreatorTag.tag, creatorTag) + .leftJoin(member.auth, auth) + .where( + member.role.eq(MemberRole.CREATOR) + .and(member.id.ne(creatorId)) + .and( + memberCreatorTag.tag.`in`( + creator.tags + .asSequence() + .filter { it.tag.isActive } + .map { it.tag } + .toList() + ) + ) + ) + .groupBy(memberCreatorTag.member.id) + .orderBy(cnt.desc()) + .limit(3) + .fetch() + .map { + val account = it.get(member)!! + SimilarCreatorResponse( + account.id!!, + account.nickname, + if (account.profileImage != null) { + "$cloudFrontHost/${account.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + account.tags + .asSequence() + .filter { accountCounselorTag -> accountCounselorTag.tag.isActive } + .map { accountCounselorTag -> accountCounselorTag.tag.tag } + .toList() + ) + } + + if (similarCreators.size < 3) { + val userIds = similarCreators.map { it.userId } + var where = member.role.eq(MemberRole.CREATOR) + .and(member.id.ne(creatorId)) + .and(member.tags.size().gt(0)) + .and(memberCreatorTag.tag.isActive.isTrue) + + for (userId in userIds) { + where = where.and(member.id.ne(userId)) + } + + val additionalCreator = getRandomCreatorList(where = where, limit = (3 - similarCreators.size).toLong()) + return similarCreators + additionalCreator + } else { + return similarCreators + } + } + + private fun getRandomCreatorList(where: Predicate, limit: Long): List { + val result = queryFactory + .select(member) + .from(memberCreatorTag) + .join(memberCreatorTag.member, member) + .innerJoin(memberCreatorTag.tag, creatorTag) + .leftJoin(member.auth, auth) + .groupBy(memberCreatorTag.member.id) + .where(where) + .limit(limit) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .fetch() + .map { + SimilarCreatorResponse( + it.id!!, + it.nickname, + if (it.profileImage != null) { + "$cloudFrontHost/${it.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + it.tags + .asSequence() + .filter { accountCounselorTag -> accountCounselorTag.tag.isActive } + .map { accountCounselorTag -> accountCounselorTag.tag.tag } + .toList() + ) + } + return result + } + + fun getLiveRoomList( + creatorId: Long, + userMember: Member, + timezone: String, + limit: Int, + offset: Long = 0 + ): List { + var where = liveRoom.member.id.eq(creatorId) + .and(liveRoom.cancel.id.isNull) + .and(liveRoom.isActive.isTrue) + + if (userMember.auth == null) { + where = where.and(liveRoom.isAdult.isFalse) + } + + val result = mutableListOf() + + if (offset == 0L) { + result.addAll( + queryFactory + .selectFrom(liveRoom) + .innerJoin(liveRoom.member, member) + .leftJoin(liveRoom.cancel, liveRoomCancel) + .where(where) + .orderBy(liveRoom.beginDateTime.asc()) + .fetch() + ) + } + + return result + .map { + val reservations = it.reservations + .filter { reservation -> + reservation.member!!.id!! == userMember.id!! && reservation.isActive + } + + val beginDateTime = it.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + val isPaid = if (it.channelName != null) { + val useCan = queryFactory + .selectFrom(useCan) + .where( + useCan.member.id.eq(member.id) + .and(useCan.room.id.eq(it.id!!)) + .and(useCan.canUsage.eq(CanUsage.LIVE)) + ) + .orderBy(useCan.id.desc()) + .fetchFirst() + + useCan != null + } else { + false + } + + LiveRoomResponse( + roomId = it.id!!, + title = it.title, + content = it.notice, + isPaid = isPaid, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + isAdult = it.isAdult, + price = it.price, + channelName = it.channelName, + managerNickname = it.member!!.nickname, + coverImageUrl = if (it.coverImage!!.startsWith("https://")) { + it.coverImage!! + } else { + "$cloudFrontHost/${it.coverImage!!}" + }, + isReservation = reservations.isNotEmpty(), + isActive = it.isActive, + isPrivateRoom = it.type == LiveRoomType.PRIVATE + ) + } + } + + fun getNoticeString(creatorId: Long): String { + return queryFactory + .select(channelNotice.notice) + .from(channelNotice) + .where(channelNotice.member.id.eq(creatorId)) + .fetchFirst() ?: "" + } + + fun getCheersList(creatorId: Long, timezone: String, offset: Long, limit: Long): GetCheersResponse { + val totalCount = queryFactory + .selectFrom(creatorCheers) + .where( + creatorCheers.creator.id.eq(creatorId) + .and(creatorCheers.isActive.isTrue) + .and(creatorCheers.parent.isNull) + ) + .fetch() + .count() + + val cheers = queryFactory + .selectFrom(creatorCheers) + .where( + creatorCheers.creator.id.eq(creatorId) + .and(creatorCheers.isActive.isTrue) + .and(creatorCheers.parent.isNull) + ) + .offset(offset) + .limit(limit) + .orderBy(creatorCheers.id.desc()) + .fetch() + .asSequence() + .map { + val date = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetCheersResponseItem( + cheersId = it.id!!, + nickname = it.member!!.nickname, + profileUrl = if (it.member!!.profileImage != null) { + "$cloudFrontHost/${it.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + content = it.cheers, + date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + replyList = it.children.asSequence() + .map { cheers -> + val replyDate = cheers.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetCheersResponseItem( + cheersId = cheers.id!!, + nickname = cheers.member!!.nickname, + profileUrl = if (cheers.member!!.profileImage != null) { + "$cloudFrontHost/${cheers.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + content = cheers.cheers, + date = replyDate.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + replyList = listOf() + ) + } + .toList() + ) + } + .toList() + + return GetCheersResponse( + totalCount = totalCount, + cheers = cheers + ) + } + + fun getLiveCount(creatorId: Long): Long? { + return queryFactory + .select(liveRoom.id.count()) + .from(liveRoom) + .where( + liveRoom.member.id.eq(creatorId) + .and(liveRoom.channelName.isNotNull) + ) + .fetchFirst() + } + + fun getLiveTime(creatorId: Long): Long { + val diffs = queryFactory + .select( + Projections.constructor( + TimeDifferenceResult::class.java, + liveRoom.beginDateTime, + liveRoom.updatedAt + ) + ) + .from(liveRoom) + .where( + liveRoom.member.id.eq(creatorId) + .and(liveRoom.channelName.isNotNull) + ) + .fetch() + + return diffs + .asSequence() + .map { Duration.between(it.beginDateTime, it.updatedAt).toSeconds() } + .sum() / 3600 + } + + fun getLiveContributorCount(creatorId: Long): Long? { + return queryFactory + .select(liveRoomVisit.member.count()) + .from(liveRoomVisit) + .innerJoin(liveRoomVisit.room, liveRoom) + .where( + liveRoom.member.id.eq(creatorId) + .and(liveRoom.channelName.isNotNull) + ) + .fetchFirst() + } + + fun getFollowerListTotalCount(creatorId: Long): Int { + return queryFactory.select(creatorFollowing.id) + .from(creatorFollowing) + .innerJoin(creatorFollowing.member, member) + .where( + member.isActive.isTrue + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.creator.id.eq(creatorId)) + .and(creatorFollowing.member.id.ne(creatorId)) + ) + .fetch() + .size + } + + fun getFollowerList( + creatorId: Long, + offset: Long, + limit: Long + ): List { + return queryFactory + .select( + QGetFollowerListDto( + member.id, + member.profileImage.prepend("/").prepend(cloudFrontHost), + member.nickname, + member.role + ) + ) + .from(creatorFollowing) + .innerJoin(creatorFollowing.member, member) + .where( + member.isActive.isTrue + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.creator.id.eq(creatorId)) + .and(creatorFollowing.member.id.ne(creatorId)) + ) + .offset(offset) + .limit(limit) + .fetch() + } + + fun isFollow(creatorId: Long, memberId: Long): Boolean { + return queryFactory + .select(creatorFollowing.isActive) + .from(creatorFollowing) + .where( + creatorFollowing.creator.id.eq(creatorId) + .and(creatorFollowing.member.id.eq(memberId)) + ) + .fetchOne() ?: false + } + + fun getCreatorCheers(cheersId: Long): CreatorCheers? { + return queryFactory + .selectFrom(creatorCheers) + .where(creatorCheers.id.eq(cheersId)) + .fetchFirst() + } + + fun getCheers(cheersId: Long, memberId: Long): CreatorCheers? { + return queryFactory + .selectFrom(creatorCheers) + .where( + creatorCheers.id.eq(cheersId) + .and(creatorCheers.member.id.eq(memberId)) + ) + .fetchFirst() + } + + fun getNotice(creatorId: Long): ChannelNotice? { + return queryFactory + .selectFrom(channelNotice) + .where(channelNotice.member.id.eq(creatorId)) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index b0802c8..a517775 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -1,10 +1,20 @@ package kr.co.vividnext.sodalive.explorer import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponse +import kr.co.vividnext.sodalive.explorer.follower.GetFollowerListResponseItem +import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice +import kr.co.vividnext.sodalive.explorer.profile.ChannelNoticeRepository +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository +import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest +import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberService import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -13,6 +23,8 @@ import org.springframework.transaction.annotation.Transactional class ExplorerService( private val memberService: MemberService, private val queryRepository: ExplorerQueryRepository, + private val cheersRepository: CreatorCheersRepository, + private val noticeRepository: ChannelNoticeRepository, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String @@ -142,4 +154,156 @@ class ExplorerService( .map { GetRoomDetailUser(it, cloudFrontHost) } .toList() } + + fun getCreatorProfile(creatorId: Long, timezone: String, member: Member): GetCreatorProfileResponse { + // 크리에이터(유저) 정보 + val creatorAccount = queryRepository.getAccount(creatorId) ?: throw SodaException("없는 사용자 입니다.") + + // 차단된 사용자 체크 + val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = creatorId) + if (isBlocked) throw SodaException("${creatorAccount.nickname}님의 요청으로 채널 접근이 제한됩니다.") + + val notificationUserIds = queryRepository.getNotificationUserIds(creatorId) + val isNotification = notificationUserIds.contains(member.id) + val notificationRecipientCount = notificationUserIds.size + + // 후원랭킹 + val userDonationRanking = queryRepository.getUserDonationRanking( + creatorId, + 10, + withDonationCan = creatorId == member.id!! + ) + + // 추천 크리에이터 + val similarCreatorList = queryRepository.getSimilarCreatorList(creatorId) + + // 라이브 + val liveRoomList = queryRepository.getLiveRoomList( + creatorId, + userMember = member, + timezone = timezone, + limit = 4 + ) + + // 공지사항 + val notice = queryRepository.getNoticeString(creatorId) + + // 응원 + val cheers = queryRepository.getCheersList(creatorId, timezone = timezone, offset = 0, limit = 4) + + // 차단한 크리에이터 인지 체크 + val isBlock = memberService.isBlocked(blockedMemberId = creatorId, memberId = member.id!!) + + // 활동요약 (라이브 횟수, 라이브 시간, 라이브 참여자, 콘텐츠 수) + val liveCount = queryRepository.getLiveCount(creatorId) ?: 0 + val liveTime = queryRepository.getLiveTime(creatorId) + val liveContributorCount = queryRepository.getLiveContributorCount(creatorId) ?: 0 + val contentCount = 0L + + return GetCreatorProfileResponse( + creator = CreatorResponse( + creatorId = creatorAccount.id!!, + profileUrl = if (creatorAccount.profileImage != null) { + "$cloudFrontHost/${creatorAccount.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + nickname = creatorAccount.nickname, + tags = creatorAccount.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(), + introduce = creatorAccount.introduce, + instagramUrl = creatorAccount.instagramUrl, + youtubeUrl = creatorAccount.youtubeUrl, + websiteUrl = creatorAccount.websiteUrl, + blogUrl = creatorAccount.blogUrl, + isNotification = isNotification, + notificationRecipientCount = notificationRecipientCount + ), + userDonationRanking = userDonationRanking, + similarCreatorList = similarCreatorList, + liveRoomList = liveRoomList, + notice = notice, + cheers = cheers, + activitySummary = GetCreatorActivitySummary( + liveCount = liveCount, + liveTime = liveTime, + liveContributorCount = liveContributorCount, + contentCount = contentCount + ), + isBlock = isBlock + ) + } + + fun getFollowerList( + creatorId: Long, + member: Member, + pageable: Pageable + ): GetFollowerListResponse { + val totalCount = queryRepository + .getFollowerListTotalCount(creatorId) + + val followerList = queryRepository.getFollowerList(creatorId, pageable.offset, pageable.pageSize.toLong()) + .asSequence() + .map { + val isFollow = if (it.role == MemberRole.CREATOR) { + queryRepository.isFollow(creatorId = it.userId, memberId = member.id!!) + } else { + null + } + + GetFollowerListResponseItem( + userId = it.userId, + profileImage = it.profileImage, + nickname = it.nickname, + isFollow = isFollow + ) + } + .toList() + + return GetFollowerListResponse(totalCount = totalCount, items = followerList) + } + + @Transactional + fun writeCheers(request: PostWriteCheersRequest, member: Member) { + val creator = queryRepository.getAccount(request.creatorId) ?: throw SodaException("없는 사용자 입니다.") + + val isBlocked = memberService.isBlocked(blockedMemberId = member.id!!, memberId = request.creatorId) + if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 팬토크 작성이 제한됩니다.") + + val cheers = CreatorCheers(cheers = request.content) + cheers.member = member + cheers.creator = creator + + val parent = if (request.parentId != null) { + queryRepository.getCreatorCheers(request.parentId) + } else { + null + } + + if (parent != null) { + cheers.parent = parent + } + + cheersRepository.save(cheers) + } + + @Transactional + fun modifyCheers(request: PutWriteCheersRequest, member: Member) { + val cheers = queryRepository.getCheers(request.cheersId, member.id!!) + ?: throw SodaException("잘못된 요청입니다.") + + cheers.cheers = request.content + } + + @Transactional + fun saveNotice(member: Member, notice: String) { + var channelNotice = queryRepository.getNotice(creatorId = member.id!!) + + if (channelNotice == null) { + channelNotice = ChannelNotice(notice) + channelNotice.member = member + noticeRepository.save(channelNotice) + } else { + channelNotice.notice = notice + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt new file mode 100644 index 0000000..142f1b2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCheersResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetCheersResponse( + val totalCount: Int, + val cheers: List +) + +data class GetCheersResponseItem( + val cheersId: Long, + val nickname: String, + val profileUrl: String, + val content: String, + val date: String, + val replyList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt new file mode 100644 index 0000000..3a1b5f6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/GetCreatorProfileResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetCreatorProfileResponse( + val creator: CreatorResponse, + val userDonationRanking: List, + val similarCreatorList: List, + val liveRoomList: List, + val notice: String, + val cheers: GetCheersResponse, + val activitySummary: GetCreatorActivitySummary, + val isBlock: Boolean +) + +data class GetCreatorActivitySummary( + val liveCount: Long, + val liveTime: Long, + val liveContributorCount: Long, + val contentCount: Long +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt new file mode 100644 index 0000000..d21bd6b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/LiveRoomResponse.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetLiveRoomAllResponse( + val totalCount: Int, + val liveRoomList: List +) + +data class LiveRoomResponse( + val roomId: Long, + val title: String, + val content: String, + val isPaid: Boolean, + val beginDateTime: String, + val coverImageUrl: String, + val isAdult: Boolean, + val price: Int, + val channelName: String?, + val managerNickname: String, + val isReservation: Boolean, + val isActive: Boolean, + val isPrivateRoom: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt new file mode 100644 index 0000000..710baf7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/SimilarCreatorResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer + +data class SimilarCreatorResponse( + val userId: Long, + val nickname: String, + val profileImage: String, + val tags: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt new file mode 100644 index 0000000..d68947d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/UserDonationRankingResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.explorer + +data class GetDonationAllResponse( + val accumulatedCansToday: Int, + val accumulatedCansLastWeek: Int, + val accumulatedCansThisMonth: Int, + val totalCount: Int, + val userDonationRanking: List +) + +data class UserDonationRankingResponse( + val userId: Long, + val nickname: String, + val profileImage: String, + val donationCan: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt new file mode 100644 index 0000000..9f16c0e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListDto.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.explorer.follower + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.member.MemberRole + +data class GetFollowerListDto @QueryProjection constructor( + val userId: Long, + val profileImage: String, + val nickname: String, + val role: MemberRole +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt new file mode 100644 index 0000000..d7ef0ec --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/follower/GetFollowerListResponse.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.explorer.follower + +data class GetFollowerListResponse( + val totalCount: Int, + val items: List +) + +data class GetFollowerListResponseItem( + val userId: Long, + val profileImage: String, + val nickname: String, + val isFollow: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt new file mode 100644 index 0000000..33993b9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNotice.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class ChannelNotice( + @Column(columnDefinition = "TEXT") + var notice: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt new file mode 100644 index 0000000..d506bb4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/ChannelNoticeRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ChannelNoticeRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt new file mode 100644 index 0000000..56aee2e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheers.kt @@ -0,0 +1,36 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany + +@Entity +data class CreatorCheers( + @Column(columnDefinition = "TEXT", nullable = false) + var cheers: String, + val isActive: Boolean = true +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id", nullable = true) + var parent: CreatorCheers? = null + set(value) { + value?.children?.add(this) + field = value + } + + @OneToMany(mappedBy = "parent") + var children: MutableList = mutableListOf() + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id", nullable = false) + var creator: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt new file mode 100644 index 0000000..831264a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorCheersRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface CreatorCheersRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt new file mode 100644 index 0000000..c937b9b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostCreatorNoticeRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.explorer.profile + +data class PostCreatorNoticeRequest(val notice: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt new file mode 100644 index 0000000..9d75c7f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PostWriteCheersRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.explorer.profile + +data class PostWriteCheersRequest( + val parentId: Long? = null, + val creatorId: Long, + val content: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt new file mode 100644 index 0000000..ebb7209 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/PutWriteCheersRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.explorer.profile + +data class PutWriteCheersRequest( + val cheersId: Long, + val content: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt new file mode 100644 index 0000000..c1e62f9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/TimeDifferenceResult.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import java.time.LocalDateTime + +data class TimeDifferenceResult( + val beginDateTime: LocalDateTime, + val updatedAt: LocalDateTime +) From b3d72ead1f0c53a9439e56758b3b1581ea04d633 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 14:04:31 +0900 Subject: [PATCH 35/94] =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomController.kt | 15 +- .../room/visit/LiveRoomVisitRepository.kt | 16 ++ .../live/room/visit/LiveRoomVisitService.kt | 19 +- .../sodalive/member/MemberController.kt | 10 + .../sodalive/member/MemberRepository.kt | 13 + .../sodalive/member/MemberService.kt | 15 ++ .../sodalive/message/GetMessageResponse.kt | 35 +++ .../co/vividnext/sodalive/message/Message.kt | 37 +++ .../sodalive/message/MessageController.kt | 136 ++++++++++ .../sodalive/message/MessageRepository.kt | 146 +++++++++++ .../sodalive/message/MessageService.kt | 240 ++++++++++++++++++ .../sodalive/message/SendMessageRequest.kt | 5 + 12 files changed, 685 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index 4f7fd58..6d8daac 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest +import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -22,7 +23,10 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/live/room") -class LiveRoomController(private val service: LiveRoomService) { +class LiveRoomController( + private val service: LiveRoomService, + private val visitService: LiveRoomVisitService +) { @GetMapping fun getRoomList( @@ -229,4 +233,13 @@ class LiveRoomController(private val service: LiveRoomService) { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") ApiResponse.ok(service.quitRoom(roomId, member)) } + + @GetMapping("/recent_visit_room/users") + fun recentVisitRoomUsers( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(visitService.getRecentVisitRoomUsers(member.id!!)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt index b90ce96..bd0f32b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.live.room.visit import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -13,6 +15,7 @@ interface LiveRoomVisitRepository : JpaRepository, LiveRoom interface LiveRoomVisitQueryRepository { fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? + fun getRecentVisitRoomUsers(roomId: Long, memberId: Long): List } @Repository @@ -38,4 +41,17 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory .orderBy(liveRoomVisit.updatedAt.desc()) .fetchFirst() } + + override fun getRecentVisitRoomUsers(roomId: Long, memberId: Long): List { + return queryFactory + .selectFrom(member) + .where( + liveRoomVisit.room.id.eq(roomId) + .and(liveRoomVisit.member.isActive.isTrue) + .and(liveRoomVisit.member.id.ne(memberId)) + .and(liveRoomVisit.member.role.ne(MemberRole.ADMIN)) + .and(liveRoomVisit.member.role.ne(MemberRole.AGENT)) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt index df18ba1..0e89fd0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt @@ -1,14 +1,21 @@ package kr.co.vividnext.sodalive.live.room.visit import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.member.Member +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @Service @Transactional(readOnly = true) -class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { +class LiveRoomVisitService( + private val repository: LiveRoomVisitRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { @Transactional fun roomVisit(room: LiveRoom, member: Member) { var roomVisit = repository.findByRoomIdAndMemberId(room.id!!, member.id!!) @@ -22,4 +29,14 @@ class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { repository.save(roomVisit) } + + fun getRecentVisitRoomUsers(memberId: Long): List { + val roomVisit = repository.findFirstByMemberIdOrderByUpdatedAtDesc(memberId) + ?: return emptyList() + + return repository.getRecentVisitRoomUsers(roomVisit.room!!.id!!, memberId) + .asSequence() + .map { GetRoomDetailUser(it, cloudFrontHost) } + .toList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 89d996d..5979817 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -114,4 +114,14 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.memberUnBlock(request = request, memberId = member.id!!)) } + + @GetMapping("/search") + fun searchMember( + @RequestParam nickname: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.searchMember(nickname = nickname, memberId = member.id!!)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 011d73a..7edb17f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -13,6 +13,7 @@ interface MemberRepository : JpaRepository, MemberQueryRepository interface MemberQueryRepository { fun findByPushToken(pushToken: String): List + fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List } @Repository @@ -23,4 +24,16 @@ class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Mem .where(member.pushToken.eq(pushToken)) .fetch() } + + override fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List { + return queryFactory + .selectFrom(member) + .where( + member.nickname.containsIgnoreCase(nickname) + .and(member.id.ne(memberId)) + .and(member.role.ne(MemberRole.ADMIN)) + .and(member.role.ne(MemberRole.AGENT)) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index d2cd91e..b8d7bfd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.jwt.TokenProvider +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.member.block.BlockMember import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.block.MemberBlockRequest @@ -334,4 +335,18 @@ class MemberService( } fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId) + + fun searchMember(nickname: String, memberId: Long): List { + if (nickname.length < 2) { + throw SodaException("두 글자 이상 입력 하셔야 합니다.") + } + + return repository.findByNicknameAndOtherCondition(nickname, memberId) + .asSequence() + .filter { blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.id!!) } + .map { + GetRoomDetailUser(it, cloudFrontHost) + } + .toList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt new file mode 100644 index 0000000..dfa3efb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.message + +data class GetVoiceMessageResponse( + val totalCount: Int, + val items: List +) { + data class VoiceMessageItem( + val messageId: Long, + val senderId: Long, + val senderNickname: String, + val senderProfileImageUrl: String, + val recipientNickname: String, + val recipientProfileImageUrl: String, + val voiceMessageUrl: String, + val date: String, + val isKept: Boolean + ) +} + +data class GetTextMessageResponse( + val totalCount: Int, + val items: List +) { + data class TextMessageItem( + val messageId: Long, + val senderId: Long, + val senderNickname: String, + val senderProfileImageUrl: String, + val recipientNickname: String, + val recipientProfileImageUrl: String, + val textMessage: String, + val date: String, + val isKept: Boolean + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt new file mode 100644 index 0000000..35fed97 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.message + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class Message( + @Column(columnDefinition = "TEXT") + val textMessage: String? = null, + var voiceMessage: String? = null, + + @Enumerated(EnumType.STRING) + val messageType: MessageType = MessageType.TEXT, + + var isRecipientKeep: Boolean = false, + var isSenderDelete: Boolean = false, + var isRecipientDelete: Boolean = false +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id", nullable = false) + var sender: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipient_id", nullable = false) + var recipient: Member? = null +} + +enum class MessageType { + TEXT, VOICE +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt new file mode 100644 index 0000000..1cb13b3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt @@ -0,0 +1,136 @@ +package kr.co.vividnext.sodalive.message + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/message") +class MessageController(private val service: MessageService) { + @PostMapping("/send/text") + fun sendTextMessage( + @RequestBody request: SendTextMessageRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.sendTextMessage(request, member)) + } + + @GetMapping("/sent/text") + fun getSentTextMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getSentTextMessages(member, pageable, timezone)) + } + + @GetMapping("/received/text") + fun getReceivedTextMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getReceivedTextMessages(member, pageable, timezone)) + } + + @GetMapping("/keep/text") + fun getKeepTextMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getKeepTextMessages(member, pageable, timezone)) + } + + @PutMapping("/keep/text/{id}") + fun keepTextMessage( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.keepTextMessage(id, member)) + } + + @PostMapping("/send/voice") + fun sendVoiceMessage( + @RequestPart("voiceMessageFile") voiceMessageFile: MultipartFile, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.sendVoiceMessage(voiceMessageFile, requestString, member)) + } + + @GetMapping("/sent/voice") + fun getSentVoiceMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getSentVoiceMessages(member, pageable, timezone)) + } + + @GetMapping("/received/voice") + fun getReceivedVoiceMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getReceivedVoiceMessages(member, pageable, timezone)) + } + + @GetMapping("/keep/voice") + fun getKeepVoiceMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getKeepVoiceMessages(member, pageable, timezone)) + } + + @PutMapping("/keep/voice/{id}") + fun keepVoiceMessage( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.keepVoiceMessage(id, member)) + } + + @DeleteMapping("/{messageId}") + fun deleteMessage( + @PathVariable messageId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.deleteMessage(messageId, member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt new file mode 100644 index 0000000..8e08125 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt @@ -0,0 +1,146 @@ +package kr.co.vividnext.sodalive.message + +import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.message.QMessage.message +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MessageRepository : JpaRepository, MessageQueryRepository + +interface MessageQueryRepository { + fun getSentTextMessageCount(memberId: Long): Int + fun getSentTextMessageList(pageable: Pageable, memberId: Long): List + fun getReceivedTextMessageCount(memberId: Long): Int + fun getReceivedTextMessageList(pageable: Pageable, memberId: Long): List + fun getKeepTextMessageCount(memberId: Long): Int + fun getKeepTextMessageList(pageable: Pageable, memberId: Long): List + fun getSentVoiceMessageCount(memberId: Long): Int + fun getSentVoiceMessageList(pageable: Pageable, memberId: Long): List + fun getReceivedVoiceMessageCount(memberId: Long): Int + fun getReceivedVoiceMessageList(pageable: Pageable, memberId: Long): List + fun getKeepVoiceMessageCount(memberId: Long): Int + fun getKeepVoiceMessageList(pageable: Pageable, memberId: Long): List +} + +@Repository +class MessageQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MessageQueryRepository { + override fun getSentTextMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return totalCount(where) + } + + override fun getSentTextMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getReceivedTextMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getReceivedTextMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getKeepTextMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getKeepTextMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getSentVoiceMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return totalCount(where) + } + + override fun getSentVoiceMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getReceivedVoiceMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getReceivedVoiceMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getKeepVoiceMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getKeepVoiceMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + private fun messageList(pageable: Pageable, where: BooleanExpression): List { + return queryFactory.selectFrom(message) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .where(where) + .orderBy(message.id.desc()) + .fetch() + } + + private fun totalCount(where: BooleanExpression): Int { + return queryFactory.select(message.id) + .from(message) + .where(where) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt new file mode 100644 index 0000000..a6e3ec5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt @@ -0,0 +1,240 @@ +package kr.co.vividnext.sodalive.message + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +@Transactional(readOnly = true) +class MessageService( + private val repository: MessageRepository, + private val memberRepository: MemberRepository, + private val blockMemberRepository: BlockMemberRepository, + + private val objectMapper: ObjectMapper, + private val s3Uploader: S3Uploader, + + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String, + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + @Transactional + fun sendTextMessage(request: SendTextMessageRequest, member: Member) { + val recipient = memberRepository.findByIdOrNull(request.recipientId) + ?: throw SodaException("받는 사람이 없습니다.") + + if (!recipient.isActive) { + throw SodaException("탈퇴한 유저에게는 메시지를 보내실 수 없습니다.") + } + + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = request.recipientId) + if (isBlocked) throw SodaException("${recipient.nickname}님의 요청으로 메시지를 보낼 수 없습니다.") + + val sender = memberRepository.findByIdOrNull(member.id!!) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + val message = Message( + textMessage = request.textMessage, + messageType = MessageType.TEXT + ) + + message.sender = sender + message.recipient = recipient + + repository.save(message) + } + + fun getSentTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getSentTextMessageCount(memberId = member.id!!) + val messageList = repository.getSentTextMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + fun getReceivedTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getReceivedTextMessageCount(memberId = member.id!!) + val messageList = repository.getReceivedTextMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + fun getKeepTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getKeepTextMessageCount(memberId = member.id!!) + val messageList = repository.getKeepTextMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + @Transactional + fun keepTextMessage(messageId: Long, member: Member) { + keepMessage(messageId, member) + } + + @Transactional + fun sendVoiceMessage(voiceMessageFile: MultipartFile, requestString: String, member: Member) { + val request = objectMapper.readValue(requestString, SendVoiceMessageRequest::class.java) + + val recipient = memberRepository.findByIdOrNull(request.recipientId) + ?: throw SodaException("받는 사람이 없습니다.") + + if (!recipient.isActive) { + throw SodaException("탈퇴한 유저에게는 메시지를 보내실 수 없습니다.") + } + + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = request.recipientId) + if (isBlocked) throw SodaException("${recipient.nickname}님의 요청으로 메시지를 보낼 수 없습니다.") + + val sender = memberRepository.findByIdOrNull(member.id!!) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + val message = Message(messageType = MessageType.VOICE) + message.sender = sender + message.recipient = recipient + + repository.save(message) + + val metadata = ObjectMetadata() + metadata.contentLength = voiceMessageFile.size + + val messagePath = s3Uploader.upload( + inputStream = voiceMessageFile.inputStream, + bucket = bucket, + filePath = "voice_message/${message.id}/${generateFileName(prefix = "${message.id}-message-")}", + metadata = metadata + ) + + message.voiceMessage = messagePath + } + + fun getSentVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetVoiceMessageResponse { + val totalCount = repository.getSentVoiceMessageCount(memberId = member.id!!) + val messageList = repository.getSentVoiceMessageList(pageable, memberId = member.id!!) + + return getVoiceMessageResponse(totalCount, messageList, timezone) + } + + fun getReceivedVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getReceivedVoiceMessageCount(memberId = member.id!!) + val messageList = repository.getReceivedVoiceMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + fun getKeepVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getKeepVoiceMessageCount(memberId = member.id!!) + val messageList = repository.getKeepVoiceMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + @Transactional + fun keepVoiceMessage(messageId: Long, member: Member) { + keepMessage(messageId, member) + } + + @Transactional + fun deleteMessage(messageId: Long, member: Member) { + val message = repository.findByIdOrNull(messageId) + ?: throw SodaException("해당하는 메시지가 없습니다.\n다시 확인해 주시기 바랍니다.") + + if (message.sender!!.id!! == member.id!!) { + message.isSenderDelete = true + } else if (message.recipient!!.id!! == member.id!!) { + message.isRecipientDelete = true + } + } + + private fun getTextMessageResponse( + totalCount: Int, + messageList: List, + timezone: String + ) = GetTextMessageResponse( + totalCount = totalCount, + items = messageList.asSequence() + .map { + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetTextMessageResponse.TextMessageItem( + messageId = it.id!!, + senderId = it.sender!!.id!!, + senderNickname = it.sender!!.nickname, + senderProfileImageUrl = if (it.sender!!.profileImage != null) { + "$cloudFrontHost/${it.sender!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + recipientNickname = it.recipient!!.nickname, + recipientProfileImageUrl = if (it.recipient!!.profileImage != null) { + "$cloudFrontHost/${it.recipient!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + textMessage = it.textMessage!!, + date = createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + isKept = false + ) + } + .toList() + ) + + private fun getVoiceMessageResponse( + totalCount: Int, + messageList: List, + timezone: String + ) = GetVoiceMessageResponse( + totalCount = totalCount, + items = messageList.asSequence() + .map { + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetVoiceMessageResponse.VoiceMessageItem( + messageId = it.id!!, + senderId = it.sender!!.id!!, + senderNickname = it.sender!!.nickname, + senderProfileImageUrl = if (it.sender!!.profileImage != null) { + "$cloudFrontHost/${it.sender!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + recipientNickname = it.recipient!!.nickname, + recipientProfileImageUrl = if (it.recipient!!.profileImage != null) { + "$cloudFrontHost/${it.recipient!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + voiceMessageUrl = "$cloudFrontHost/${it.voiceMessage!!}", + date = createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + isKept = it.isRecipientKeep + ) + } + .toList() + ) + + private fun keepMessage(messageId: Long, member: Member) { + val message = repository.findByIdOrNull(messageId) + ?: throw SodaException("잘못된 요청입니다.") + + if (message.recipient != member) { + throw SodaException("잘못된 요청입니다.") + } + + if (message.isRecipientKeep) { + throw SodaException("이미 보관된 메시지 입니다.") + } + + message.isRecipientKeep = true + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt new file mode 100644 index 0000000..2afe5f5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.message + +data class SendVoiceMessageRequest(val recipientId: Long, val container: String) + +data class SendTextMessageRequest(val recipientId: Long, val textMessage: String, val container: String) From bc0baeffe90f41d6d98dd72a3f8c720fa8909682 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 14:14:45 +0900 Subject: [PATCH 36/94] =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=20-=20=EC=B0=A8=EB=8B=A8=EB=90=9C=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=EB=A7=8C=20=EA=B2=80=EC=83=89=EC=9D=B4=20=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index b8d7bfd..b7feb09 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -343,7 +343,7 @@ class MemberService( return repository.findByNicknameAndOtherCondition(nickname, memberId) .asSequence() - .filter { blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.id!!) } + .filter { !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.id!!) } .map { GetRoomDetailUser(it, cloudFrontHost) } From 4a7e6846061d331b8e28ec9476ea4c7dbbc52a0f Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 14:20:57 +0900 Subject: [PATCH 37/94] =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EB=B0=A9=EB=AC=B8?= =?UTF-8?q?=ED=95=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=9D=B4=20=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/visit/LiveRoomVisitRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt index bd0f32b..0a41a98 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -37,6 +37,7 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory return queryFactory .selectFrom(liveRoomVisit) .innerJoin(liveRoomVisit.room, liveRoom) + .innerJoin(liveRoomVisit.member, member) .where(member.id.eq(memberId)) .orderBy(liveRoomVisit.updatedAt.desc()) .fetchFirst() From fff80372772005c604fa7d29e88ee9e679aca8e6 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 14:28:24 +0900 Subject: [PATCH 38/94] =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EB=B0=A9=EB=AC=B8?= =?UTF-8?q?=ED=95=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EC=9D=B4=20=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8A=94=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/visit/LiveRoomVisitRepository.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt index 0a41a98..bbf9f90 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -45,7 +45,10 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory override fun getRecentVisitRoomUsers(roomId: Long, memberId: Long): List { return queryFactory - .selectFrom(member) + .select(member) + .from(liveRoomVisit) + .innerJoin(liveRoomVisit.member, member) + .innerJoin(liveRoomVisit.room, liveRoom) .where( liveRoomVisit.room.id.eq(roomId) .and(liveRoomVisit.member.isActive.isTrue) From 16c5c5f6b66c99d815d716aa87d7c35764a0660d Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 15:46:02 +0900 Subject: [PATCH 39/94] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=84=9C=EB=B8=94=EB=A6=BF=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=97=90=EC=84=9C=20Exception=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=EC=8B=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/common/ExceptionHandlerFilter.kt | 26 ++++++++++++++++ .../sodalive/configs/SecurityConfig.kt | 4 +++ .../sodalive/member/MemberController.kt | 11 +++++++ .../sodalive/member/MemberService.kt | 30 +++++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt new file mode 100644 index 0000000..a8ab7ee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.common + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.web.filter.OncePerRequestFilter +import javax.servlet.FilterChain +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class ExceptionHandlerFilter(private val objectMapper: ObjectMapper) : OncePerRequestFilter() { + override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + try { + filterChain.doFilter(request, response) + } catch (e: Exception) { + response.status = 401 + response.contentType = "application/json" + response.characterEncoding = "UTF-8" + + val json = objectMapper.writeValueAsString(ApiResponse.error("로그인 정보를 확인해주세요.")) + response.writer.write(json) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt index aae4472..172a24a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -1,5 +1,7 @@ package kr.co.vividnext.sodalive.configs +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.common.ExceptionHandlerFilter import kr.co.vividnext.sodalive.jwt.JwtAccessDeniedHandler import kr.co.vividnext.sodalive.jwt.JwtAuthenticationEntryPoint import kr.co.vividnext.sodalive.jwt.JwtFilter @@ -21,6 +23,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) class SecurityConfig( + private val objectMapper: ObjectMapper, private val tokenProvider: TokenProvider, private val accessDeniedHandler: JwtAccessDeniedHandler, private val authenticationEntryPoint: JwtAuthenticationEntryPoint @@ -69,6 +72,7 @@ class SecurityConfig( .anyRequest().authenticated() .and() .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) + .addFilterBefore(ExceptionHandlerFilter(objectMapper), JwtFilter::class.java) .build() } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 5979817..6d12515 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping 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.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestPart @@ -29,6 +30,16 @@ class MemberController(private val service: MemberService) { @PostMapping("/login") fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest) + @PostMapping("/logout") + fun logout( + @RequestHeader("Authorization") token: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.logout(token.removePrefix("Bearer "), member.id!!)) + } + @GetMapping("/info") fun getMemberInfo( @RequestParam container: String?, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index b7feb09..1b59996 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository import kr.co.vividnext.sodalive.member.stipulation.StipulationIds import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository +import kr.co.vividnext.sodalive.member.token.MemberTokenRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull @@ -38,11 +39,14 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile +import java.util.concurrent.locks.ReentrantReadWriteLock +import kotlin.concurrent.write @Service @Transactional(readOnly = true) class MemberService( private val repository: MemberRepository, + private val tokenRepository: MemberTokenRepository, private val stipulationRepository: StipulationRepository, private val stipulationAgreeRepository: StipulationAgreeRepository, private val creatorFollowingRepository: CreatorFollowingRepository, @@ -64,6 +68,9 @@ class MemberService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) : UserDetailsService { + + private val tokenLocks: MutableMap = mutableMapOf() + @Transactional fun signUp( profileImage: MultipartFile?, @@ -349,4 +356,27 @@ class MemberService( } .toList() } + + @Transactional + fun logout(token: String, memberId: Long) { + val member = repository.findByIdOrNull(memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + member.pushToken = null + + val lock = getOrCreateLock(memberId = memberId) + lock.write { + val memberToken = tokenRepository.findByIdOrNull(memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + val memberTokenSet = memberToken.tokenList.toMutableSet() + memberTokenSet.remove(token) + memberToken.tokenList = memberTokenSet.toList() + tokenRepository.save(memberToken) + } + } + + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { + return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } + } } From 0c106540cdc105c821bf5f39ca43effcb5f76d5c Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 15:54:25 +0900 Subject: [PATCH 40/94] =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=EA=B8=B0=EA=B8=B0?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../co/vividnext/sodalive/member/MemberController.kt | 9 +++++++++ .../kr/co/vividnext/sodalive/member/MemberService.kt | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 6d12515..850f27e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -40,6 +40,15 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.logout(token.removePrefix("Bearer "), member.id!!)) } + @PostMapping("/logout/all") + fun logoutAll( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.logoutAll(member.id!!)) + } + @GetMapping("/info") fun getMemberInfo( @RequestParam container: String?, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 1b59996..b4f11eb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -376,6 +376,17 @@ class MemberService( } } + @Transactional + fun logoutAll(memberId: Long) { + val member = repository.findByIdOrNull(memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + member.pushToken = null + + val lock = getOrCreateLock(memberId = memberId) + lock.write { tokenRepository.deleteById(memberId) } + } + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } } From baad5653e885f222cb3733f468ee79461f07c09e Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 16:46:56 +0900 Subject: [PATCH 41/94] =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B3=B5?= =?UTF-8?q?=EC=A7=80=EC=82=AC=ED=95=AD=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/notice/CreateNoticeRequest.kt | 10 +++ .../sodalive/notice/GetNoticeResponse.kt | 13 ++++ .../sodalive/notice/ServiceNotice.kt | 15 +++++ .../notice/ServiceNoticeController.kt | 38 +++++++++++ .../sodalive/notice/ServiceNoticeService.kt | 66 +++++++++++++++++++ .../notice/ServiceServiceNoticeRepository.kt | 37 +++++++++++ .../sodalive/notice/UpdateNoticeRequest.kt | 7 ++ 7 files changed, 186 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/CreateNoticeRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNotice.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/notice/UpdateNoticeRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/CreateNoticeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/CreateNoticeRequest.kt new file mode 100644 index 0000000..5c48a40 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/CreateNoticeRequest.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.notice + +data class CreateNoticeRequest( + val title: String, + val content: String +) { + fun toEntity(): ServiceNotice { + return ServiceNotice(title, content) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt new file mode 100644 index 0000000..aa35bbf --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.notice + +data class GetNoticeResponse( + val totalCount: Int, + val noticeList: List +) + +data class NoticeItem( + val id: Long, + val title: String, + val content: String, + val date: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNotice.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNotice.kt new file mode 100644 index 0000000..6800fff --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNotice.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.notice + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class ServiceNotice( + @Column(nullable = false) + var title: String, + @Column(columnDefinition = "TEXT", nullable = false) + var content: String, + @Column(nullable = false) + var isActive: Boolean = true +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt new file mode 100644 index 0000000..f1f7921 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt @@ -0,0 +1,38 @@ +package kr.co.vividnext.sodalive.notice + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize +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 +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/notice") +class ServiceNoticeController(private val service: ServiceNoticeService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun createNotice(@RequestBody request: CreateNoticeRequest) = ApiResponse.ok( + service.save(request), + "등록되었습니다." + ) + + @PutMapping + @PreAuthorize("hasRole('ADMIN')") + fun updateNotice(@RequestBody request: UpdateNoticeRequest) = ApiResponse.ok( + service.update(request), + "수정되었습니다." + ) + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun deleteCoin(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") + + @GetMapping + fun getNoticeList(pageable: Pageable, timezone: String) = ApiResponse.ok(service.getNoticeList(pageable, timezone)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt new file mode 100644 index 0000000..10d28c2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt @@ -0,0 +1,66 @@ +package kr.co.vividnext.sodalive.notice + +import kr.co.vividnext.sodalive.common.SodaException +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +@Transactional(readOnly = true) +class ServiceNoticeService(private val repository: ServiceServiceNoticeRepository) { + @Transactional + fun save(request: CreateNoticeRequest): Long { + if (request.title.isBlank()) throw SodaException("제목을 입력하세요.") + if (request.content.isBlank()) throw SodaException("내용을 입력하세요.") + + val notice = request.toEntity() + return repository.save(notice).id!! + } + + @Transactional + fun update(request: UpdateNoticeRequest) { + if (request.id <= 0) throw SodaException("잘못된 요청입니다.") + if (request.title.isNullOrBlank() && request.content.isNullOrBlank()) { + throw SodaException("수정할 내용을 입력하세요.") + } + + val notice = repository.findByIdOrNull(request.id) + ?: throw SodaException("잘못된 요청입니다.") + + if (!request.title.isNullOrBlank()) notice.title = request.title + if (!request.content.isNullOrBlank()) notice.content = request.content + } + + @Transactional + fun delete(id: Long) { + if (id <= 0) throw SodaException("잘못된 요청입니다.") + val notice = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + notice.isActive = false + } + + fun getNoticeList(pageable: Pageable, timezone: String): GetNoticeResponse { + val totalCount = repository.getNoticeTotalCount() + val noticeList = repository.getNoticeList(pageable) + .asSequence() + .map { + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + NoticeItem( + it.id!!, + it.title, + it.content, + createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")) + ) + } + .toList() + + return GetNoticeResponse(totalCount, noticeList) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt new file mode 100644 index 0000000..90d5a47 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.notice + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.notice.QServiceNotice.serviceNotice +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ServiceServiceNoticeRepository : JpaRepository, ServiceNoticeQueryRepository + +interface ServiceNoticeQueryRepository { + fun getNoticeTotalCount(): Int + fun getNoticeList(pageable: Pageable): List +} + +@Repository +class ServiceNoticeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ServiceNoticeQueryRepository { + override fun getNoticeTotalCount(): Int { + return queryFactory + .select(serviceNotice.id) + .from(serviceNotice) + .where(serviceNotice.isActive.isTrue) + .fetch() + .size + } + + override fun getNoticeList(pageable: Pageable): List { + return queryFactory + .selectFrom(serviceNotice) + .where(serviceNotice.isActive.isTrue) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .orderBy(serviceNotice.id.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/UpdateNoticeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/UpdateNoticeRequest.kt new file mode 100644 index 0000000..c3da07d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/UpdateNoticeRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.notice + +data class UpdateNoticeRequest( + val id: Long, + val title: String? = null, + val content: String? = null +) From d9f6ac01f45cd4ba8f827ef73800c5486de8f6f3 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 16:57:26 +0900 Subject: [PATCH 42/94] =?UTF-8?q?=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20AP?= =?UTF-8?q?I=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/member/Member.kt | 3 +++ .../sodalive/member/MemberController.kt | 7 ++++++ .../sodalive/member/MemberService.kt | 20 +++++++++++++++++ .../co/vividnext/sodalive/member/SignOut.kt | 22 +++++++++++++++++++ .../sodalive/member/SignOutRepository.kt | 7 ++++++ .../sodalive/member/SignOutRequest.kt | 6 +++++ 6 files changed, 65 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/SignOut.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index 9179ddf..785517d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -41,6 +41,9 @@ data class Member( @OneToMany(mappedBy = "creator") var follower: MutableList = mutableListOf() + @OneToMany(mappedBy = "member", cascade = [CascadeType.ALL]) + val signOutReasons: MutableList = mutableListOf() + @OneToOne(mappedBy = "member", fetch = FetchType.LAZY) var notification: MemberNotification? = null diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 850f27e..a515312 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.User import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping @@ -144,4 +145,10 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.searchMember(nickname = nickname, memberId = member.id!!)) } + + @PostMapping("/sign_out") + fun signOut( + @RequestBody signOutRequest: SignOutRequest, + @AuthenticationPrincipal user: User + ) = ApiResponse.ok(service.signOut(signOutRequest, user), "정상적으로 탈퇴 처리되었습니다.") } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index b4f11eb..4ed6090 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -32,6 +32,7 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.core.userdetails.UsernameNotFoundException @@ -51,6 +52,7 @@ class MemberService( private val stipulationAgreeRepository: StipulationAgreeRepository, private val creatorFollowingRepository: CreatorFollowingRepository, private val blockMemberRepository: BlockMemberRepository, + private val signOutRepository: SignOutRepository, private val memberNotificationService: MemberNotificationService, @@ -390,4 +392,22 @@ class MemberService( private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } } + + @Transactional + fun signOut(signOutRequest: SignOutRequest, user: User) { + val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + if (!passwordEncoder.matches(signOutRequest.password, member.password)) { + throw SodaException("비밀번호가 일치하지 않습니다.") + } + + if (signOutRequest.reason.isBlank()) { + throw SodaException("탈퇴하려는 이유를 입력해 주세요.") + } + + member.isActive = false + + val signOut = SignOut(reason = signOutRequest.reason) + signOut.member = member + signOutRepository.save(signOut) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOut.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOut.kt new file mode 100644 index 0000000..5447060 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOut.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.member + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class SignOut( + @Column(columnDefinition = "TEXT", nullable = false) + val reason: String +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + set(value) { + value?.signOutReasons?.add(this) + field = value + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRepository.kt new file mode 100644 index 0000000..c81dc61 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.member + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface SignOutRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRequest.kt new file mode 100644 index 0000000..208323d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/SignOutRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.member + +data class SignOutRequest( + val reason: String, + val password: String +) From 234e02dca4b9a1d82c70cf270eb1d51fe702cff7 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 17:04:51 +0900 Subject: [PATCH 43/94] =?UTF-8?q?=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20AP?= =?UTF-8?q?I=20-=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/member/MemberService.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 4ed6090..f972dbd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -389,12 +389,8 @@ class MemberService( lock.write { tokenRepository.deleteById(memberId) } } - private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { - return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } - } - @Transactional - fun signOut(signOutRequest: SignOutRequest, user: User) { + fun signOut(signOutRequest: SignOutRequest, token: String, user: User) { val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") if (!passwordEncoder.matches(signOutRequest.password, member.password)) { throw SodaException("비밀번호가 일치하지 않습니다.") @@ -404,10 +400,15 @@ class MemberService( throw SodaException("탈퇴하려는 이유를 입력해 주세요.") } + logoutAll(memberId = member.id!!) member.isActive = false val signOut = SignOut(reason = signOutRequest.reason) signOut.member = member signOutRepository.save(signOut) } + + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { + return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } + } } From 980faae943691f78c201e7813141ee13fd41dc3f Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 17:09:08 +0900 Subject: [PATCH 44/94] =?UTF-8?q?=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20AP?= =?UTF-8?q?I=20-=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index f972dbd..32468db 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -390,7 +390,7 @@ class MemberService( } @Transactional - fun signOut(signOutRequest: SignOutRequest, token: String, user: User) { + fun signOut(signOutRequest: SignOutRequest, user: User) { val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") if (!passwordEncoder.matches(signOutRequest.password, member.password)) { throw SodaException("비밀번호가 일치하지 않습니다.") From 25b3bcb534c33a87d7ae46c8245d8e0c985c8ffe Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 19:11:25 +0900 Subject: [PATCH 45/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=98=88?= =?UTF-8?q?=EC=95=BD=20=EC=B7=A8=EC=86=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/can/payment/CanPaymentService.kt | 41 +++++++ .../CancelLiveReservationRequest.kt | 3 + .../reservation/GetLiveReservationResponse.kt | 12 ++ .../live/reservation/LiveReservationCancel.kt | 16 +++ .../LiveReservationCancelRepository.kt | 7 ++ .../reservation/LiveReservationController.kt | 33 ++++++ .../reservation/LiveReservationRepository.kt | 30 +++++ .../reservation/LiveReservationService.kt | 104 +++++++++++++++++- .../sodalive/live/room/LiveRoomService.kt | 8 +- 9 files changed, 249 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index 434ced3..82885d9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -1,6 +1,9 @@ package kr.co.vividnext.sodalive.can.payment +import kr.co.vividnext.sodalive.can.CanRepository +import kr.co.vividnext.sodalive.can.charge.Charge import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.charge.ChargeStatus import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.SpentCan import kr.co.vividnext.sodalive.can.use.TotalSpentCan @@ -18,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional @Service class CanPaymentService( + private val repository: CanRepository, private val memberRepository: MemberRepository, private val chargeRepository: ChargeRepository, private val useCanRepository: UseCanRepository, @@ -242,4 +246,41 @@ class CanPaymentService( TotalSpentCan(total = 0) } } + + @Transactional + fun refund(memberId: Long, roomId: Long) { + val member = memberRepository.findByIdOrNull(memberId) + ?: throw SodaException("잘못된 예약정보 입니다.") + + val useCan = repository.getCanUsedForLiveRoomNotRefund( + memberId = memberId, + roomId = roomId, + canUsage = CanUsage.LIVE + ) ?: throw SodaException("잘못된 예약정보 입니다.") + useCan.isRefund = true + + val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) + useCoinCalculates.forEach { + it.status = UseCanCalculateStatus.REFUND + val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) + charge.title = "${it.can} 캔" + charge.useCan = useCan + + when (it.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan += charge.rewardCan + PaymentGateway.GOOGLE_IAP -> member.googleRewardCan += charge.rewardCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan += charge.rewardCan + } + charge.member = member + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = it.paymentGateway + ) + payment.method = "환불" + charge.payment = payment + + chargeRepository.save(charge) + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt new file mode 100644 index 0000000..edcf504 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class CancelLiveReservationRequest(val reservationId: Long, val reason: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt new file mode 100644 index 0000000..65627d1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class GetLiveReservationResponse( + val reservationId: Long, + val roomId: Long, + val title: String, + val coverImageUrl: String, + val price: Int, + val masterNickname: String, + val beginDateTime: String, + val cancelable: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt new file mode 100644 index 0000000..1154988 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.live.reservation + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class LiveReservationCancel( + val reason: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reservation_id", nullable = false) + var reservation: LiveReservation? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt new file mode 100644 index 0000000..e5eebe3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.reservation + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveReservationCancelRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt index a0abd2c..ead7b72 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt @@ -4,9 +4,13 @@ import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -21,4 +25,33 @@ class LiveReservationController(private val service: LiveReservationService) { ApiResponse.ok(service.makeReservation(request, member.id!!)) } + + @GetMapping + fun getReservationList( + @RequestParam isActive: Boolean, + @RequestParam(value = "timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getReservationList(member.id!!, isActive, timezone)) + } + + @GetMapping("/{id}") + fun getReservation( + @PathVariable id: Long, + @RequestParam(value = "timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getReservation(id, member.id!!, timezone)) + } + + @PutMapping("/cancel") + fun cancelReservation( + @RequestBody request: CancelLiveReservationRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.cancelReservation(request, member.id!!)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt index ecaf7d2..07a396f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt @@ -19,6 +19,12 @@ interface LiveReservationQueryRepository { fun getReservationBookerList(roomId: Long): List fun isExistsReservation(roomId: Long, memberId: Long): Boolean + fun getReservationListByMemberId(memberId: Long, active: Boolean): List + + fun getReservationByReservationAndMemberId( + reservationId: Long, + memberId: Long + ): LiveReservation? } @Repository @@ -67,4 +73,28 @@ class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFacto ) .fetchFirst() != null } + + override fun getReservationListByMemberId(memberId: Long, active: Boolean): List { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveReservation.isActive.eq(active) + .and(member.id.eq(memberId)) + ) + .fetch() + } + + override fun getReservationByReservationAndMemberId(reservationId: Long, memberId: Long): LiveReservation? { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveReservation.id.eq(reservationId) + .and(member.id.eq(memberId)) + ) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt index 9184f72..df2a75d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt @@ -6,8 +6,11 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.live.room.LiveRoomRepository import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -16,7 +19,11 @@ class LiveReservationService( private val repository: LiveReservationRepository, private val liveRoomRepository: LiveRoomRepository, private val memberRepository: MemberRepository, - private val canPaymentService: CanPaymentService + private val canPaymentService: CanPaymentService, + private val liveReservationCancelRepository: LiveReservationCancelRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String ) { fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse { val room = liveRoomRepository.findByIdOrNull(id = request.roomId) @@ -76,4 +83,99 @@ class LiveReservationService( remainingCan = haveCan - room.price ) } + + fun getReservationList(memberId: Long, active: Boolean, timezone: String): List { + return repository + .getReservationListByMemberId(memberId, active) + .asSequence() + .map { + val beginDateTime = it.room!!.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetLiveReservationResponse( + reservationId = it.id!!, + roomId = it.room!!.id!!, + title = it.room!!.title, + coverImageUrl = if (it.room!!.coverImage != null) { + "$cloudFrontHost/${it.room!!.coverImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + price = it.room!!.price, + masterNickname = it.room!!.member!!.nickname, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + cancelable = beginDateTime.minusHours(4).isAfter( + LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + ) + ) + } + .toList() + } + + fun getReservation(reservationId: Long, memberId: Long, timezone: String): GetLiveReservationResponse { + val reservation = repository.getReservationByReservationAndMemberId(reservationId, memberId) + ?: throw SodaException("잘못된 예약정보 입니다.") + + val beginDateTime = reservation.room!!.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + return GetLiveReservationResponse( + reservationId = reservation.id!!, + roomId = reservation.room!!.id!!, + title = reservation.room!!.title, + coverImageUrl = if (reservation.room!!.coverImage != null) { + "$cloudFrontHost/${reservation.room!!.coverImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + price = reservation.room!!.price, + masterNickname = reservation.room!!.member!!.nickname, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + cancelable = beginDateTime.minusHours(4).isAfter( + LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + ) + ) + } + + @Transactional + fun cancelReservation(request: CancelLiveReservationRequest, memberId: Long) { + if (request.reason.isBlank()) { + throw SodaException("취소사유를 입력하세요.") + } + + val reservation = repository.findByIdOrNull(request.reservationId) + ?: throw SodaException("잘못된 예약정보 입니다.") + + if (reservation.member == null || reservation.member!!.id!! != memberId) { + throw SodaException("잘못된 예약정보 입니다.") + } + + if (reservation.room == null || reservation.room?.id == null) { + throw SodaException("잘못된 예약정보 입니다.") + } + + if (reservation.room!!.beginDateTime.isBefore(LocalDateTime.now().plusHours(4))) { + throw SodaException("라이브 시작 4시간 이내에는 예약취소가 불가능 합니다.") + } + + if (reservation.room!!.price > 0) { + canPaymentService.refund(memberId, roomId = reservation.room!!.id!!) + } + + reservation.isActive = false + + val reservationCancel = LiveReservationCancel(request.reason) + reservationCancel.reservation = reservation + liveReservationCancelRepository.save(reservationCancel) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 711371b..f060fe9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -381,7 +381,7 @@ class LiveRoomService( it.status = UseCanCalculateStatus.REFUND val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) - charge.title = "${it.can} 코인" + charge.title = "${it.can} 캔" charge.useCan = useCan when (it.paymentGateway) { @@ -823,20 +823,20 @@ class LiveRoomService( @Transactional fun refundDonation(roomId: Long, member: Member) { val donator = memberRepository.findByIdOrNull(member.id) - ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") + ?: throw SodaException("후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") val useCan = canRepository.getCanUsedForLiveRoomNotRefund( memberId = member.id!!, roomId = roomId, canUsage = CanUsage.DONATION - ) ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") + ) ?: throw SodaException("후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") useCan.isRefund = true val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) useCoinCalculates.forEach { it.status = UseCanCalculateStatus.REFUND val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) - charge.title = "${it.can} 코인" + charge.title = "${it.can} 캔" charge.useCan = useCan when (it.paymentGateway) { From 472b8d36f51a9c401f871de78d61bc9d6c83a9c2 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 19:30:53 +0900 Subject: [PATCH 46/94] =?UTF-8?q?=EC=BD=94=EC=9D=B8=20=EC=86=8C=EB=B9=84?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/can/payment/CanPaymentService.kt | 26 +++++++++---------- .../kr/co/vividnext/sodalive/member/Member.kt | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index 82885d9..e181567 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -151,7 +151,7 @@ class CanPaymentService( when (charge.payment!!.paymentGateway) { PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan - PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.googleRewardCan -= remainingNeedCan } total += remainingNeedCan @@ -174,7 +174,7 @@ class CanPaymentService( when (charge.payment!!.paymentGateway) { PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan - PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.googleRewardCan -= remainingNeedCan } charge.rewardCan = 0 @@ -203,13 +203,13 @@ class CanPaymentService( val charge = chargeRepository.getOldestChargeWhereChargeCanGreaterThan0(chargeId, memberId, container) ?: break - if (charge.rewardCan >= remainingNeedCan) { - charge.rewardCan -= remainingNeedCan + if (charge.chargeCan >= remainingNeedCan) { + charge.chargeCan -= remainingNeedCan when (charge.payment!!.paymentGateway) { - PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan - PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan - PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.PG -> member.pgChargeCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleChargeCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.googleChargeCan -= remainingNeedCan } total += remainingNeedCan @@ -221,21 +221,21 @@ class CanPaymentService( ) ) } else { - total += charge.rewardCan + total += charge.chargeCan spentCans.add( SpentCan( paymentGateway = charge.payment!!.paymentGateway, - can = charge.rewardCan + can = charge.chargeCan ) ) when (charge.payment!!.paymentGateway) { - PaymentGateway.PG -> member.pgRewardCan -= remainingNeedCan - PaymentGateway.APPLE_IAP -> member.appleRewardCan -= remainingNeedCan - PaymentGateway.GOOGLE_IAP -> member.pgRewardCan -= remainingNeedCan + PaymentGateway.PG -> member.pgChargeCan -= remainingNeedCan + PaymentGateway.APPLE_IAP -> member.appleChargeCan -= remainingNeedCan + PaymentGateway.GOOGLE_IAP -> member.pgChargeCan -= remainingNeedCan } - charge.rewardCan = 0 + charge.chargeCan = 0 } chargeId = charge.id!! diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index 785517d..a6b5c49 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -63,9 +63,9 @@ data class Member( var pushToken: String? = null // 화폐 - private var pgChargeCan: Int = 0 + var pgChargeCan: Int = 0 var pgRewardCan: Int = 0 - private var googleChargeCan: Int = 0 + var googleChargeCan: Int = 0 var googleRewardCan: Int = 0 var appleChargeCan: Int = 0 var appleRewardCan: Int = 0 From 5d6eb5da4fcea80d0b9d3cdf263aa273beee85a1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 3 Aug 2023 00:24:17 +0900 Subject: [PATCH 47/94] =?UTF-8?q?=EC=8B=A0=EA=B3=A0=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/report/Report.kt | 34 ++++++++++++ .../sodalive/report/ReportController.kt | 23 ++++++++ .../sodalive/report/ReportRepository.kt | 7 +++ .../sodalive/report/ReportRequest.kt | 9 ++++ .../sodalive/report/ReportService.kt | 52 +++++++++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/report/ReportController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/report/ReportService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt b/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt new file mode 100644 index 0000000..89a46d6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.report + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class Report( + @Enumerated(value = EnumType.STRING) + val type: ReportType, + val reason: String +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reported_member_id", nullable = true) + var reportedAccount: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cheers_id", nullable = true) + var cheers: CreatorCheers? = null +} + +enum class ReportType { + REVIEW, PROFILE, USER, CHEERS, AUDIO_CONTENT +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportController.kt new file mode 100644 index 0000000..10a2903 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportController.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.report + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +@RequestMapping("/report") +class ReportController(private val service: ReportService) { + @PostMapping + fun report( + @RequestBody request: ReportRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.save(member, request), "신고가 접수되었습니다.") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRepository.kt new file mode 100644 index 0000000..e6368c9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.report + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface ReportRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRequest.kt new file mode 100644 index 0000000..0c52797 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.report + +data class ReportRequest( + val type: ReportType, + val reason: String, + val reportedMemberId: Long? = null, + val cheersId: Long? = null, + val audioContentId: Long? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportService.kt new file mode 100644 index 0000000..2926dda --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/report/ReportService.kt @@ -0,0 +1,52 @@ +package kr.co.vividnext.sodalive.report + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class ReportService( + private val repository: ReportRepository, + private val memberRepository: MemberRepository, + private val cheersRepository: CreatorCheersRepository +) { + @Transactional + fun save(member: Member, request: ReportRequest) { + if (conditionAllIsNull(request, isNull = true) || conditionAllIsNull(request, isNull = false)) { + throw SodaException("신고가 접수되었습니다.") + } + + val reportedAccount = if (request.reportedMemberId != null) { + memberRepository.findByIdOrNull(request.reportedMemberId) + ?: throw SodaException("신고가 접수되었습니다.") + } else { + null + } + + val cheers = if (request.cheersId != null) { + cheersRepository.findByIdOrNull(request.cheersId) + ?: throw SodaException("신고가 접수되었습니다.") + } else { + null + } + + val report = Report(type = request.type, reason = request.reason) + report.member = member + report.reportedAccount = reportedAccount + report.cheers = cheers + repository.save(report) + } + + private fun conditionAllIsNull(request: ReportRequest, isNull: Boolean): Boolean { + return if (isNull) { + request.reportedMemberId == null && request.cheersId == null && request.audioContentId == null + } else { + request.reportedMemberId != null && request.cheersId != null && request.audioContentId != null + } + } +} From 1fe5309fdc2e8192ec55617044b83efb70acf923 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 3 Aug 2023 20:36:37 +0900 Subject: [PATCH 48/94] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aws/cloudfront/AudioContentCloudFront.kt | 48 ++ .../sodalive/can/payment/CanPaymentService.kt | 12 + .../co/vividnext/sodalive/can/use/UseCan.kt | 11 + .../content/AddAllPlaybackTrackingRequest.kt | 12 + .../sodalive/content/AudioContent.kt | 64 +++ .../content/AudioContentController.kt | 151 ++++++ .../content/AudioContentRepository.kt | 341 +++++++++++++ .../sodalive/content/AudioContentService.kt | 469 ++++++++++++++++++ .../sodalive/content/BundleAudioContent.kt | 22 + .../content/CreateAudioContentRequest.kt | 14 + .../content/CreateAudioContentResponse.kt | 3 + .../content/GetAudioContentDetailResponse.kt | 43 ++ .../content/GetAudioContentListResponse.kt | 18 + .../content/ModifyAudioContentRequest.kt | 9 + .../sodalive/content/PlaybackTracking.kt | 26 + .../content/PlaybackTrackingRepository.kt | 7 + .../sodalive/content/UploadCompleteRequest.kt | 3 + .../content/comment/AudioContentComment.kt | 41 ++ .../comment/AudioContentCommentController.kt | 80 +++ .../comment/AudioContentCommentRepository.kt | 141 ++++++ .../comment/AudioContentCommentService.kt | 89 ++++ .../GetAudioContentCommentListResponse.kt | 17 + .../content/comment/ModifyCommentRequest.kt | 3 + .../content/comment/RegisterCommentRequest.kt | 3 + .../AudioContentDonationController.kt | 25 + .../donation/AudioContentDonationRequest.kt | 8 + .../donation/AudioContentDonationService.kt | 43 ++ .../content/hashtag/AudioContentHashTag.kt | 36 ++ .../sodalive/content/hashtag/HashTag.kt | 23 + .../content/hashtag/HashTagRepository.kt | 23 + .../sodalive/content/like/AudioContentLike.kt | 37 ++ .../like/AudioContentLikeRepository.kt | 39 ++ .../like/PutAudioContentLikeRequest.kt | 3 + .../like/PutAudioContentLikeResponse.kt | 3 + .../main/AudioContentMainController.kt | 34 ++ .../content/main/AudioContentMainService.kt | 147 ++++++ .../content/main/GetAudioContentMainItem.kt | 13 + .../main/GetAudioContentMainResponse.kt | 13 + .../main/GetNewContentUploadCreator.kt | 9 + .../content/main/banner/AudioContentBanner.kt | 43 ++ .../banner/GetAudioContentBannerResponse.kt | 11 + .../main/curation/AudioContentCuration.kt | 26 + .../GetAudioContentCurationResponse.kt | 9 + .../order/GetAudioContentOrderListResponse.kt | 20 + .../vividnext/sodalive/content/order/Order.kt | 58 +++ .../sodalive/content/order/OrderController.kt | 49 ++ .../sodalive/content/order/OrderRepository.kt | 215 ++++++++ .../sodalive/content/order/OrderRequest.kt | 3 + .../sodalive/content/order/OrderService.kt | 99 ++++ .../content/theme/AudioContentTheme.kt | 19 + .../theme/AudioContentThemeController.kt | 24 + .../theme/AudioContentThemeQueryRepository.kt | 57 +++ .../content/theme/AudioContentThemeService.kt | 10 + .../theme/GetAudioContentThemeResponse.kt | 9 + src/main/resources/application.yml | 5 + 55 files changed, 2740 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/AddAllPlaybackTrackingRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/BundleAudioContent.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/ModifyAudioContentRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTracking.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTrackingRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/UploadCompleteRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentComment.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/ModifyCommentRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/comment/RegisterCommentRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/AudioContentHashTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTag.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTagRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLike.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLikeRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainItem.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBanner.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/GetAudioContentBannerResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCuration.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/GetAudioContentCurationResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/order/GetAudioContentOrderListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/order/Order.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentTheme.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeQueryRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/theme/GetAudioContentThemeResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt b/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt new file mode 100644 index 0000000..a43f938 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt @@ -0,0 +1,48 @@ +package kr.co.vividnext.sodalive.aws.cloudfront + +import com.amazonaws.services.cloudfront.CloudFrontUrlSigner +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.nio.file.Files +import java.nio.file.Paths +import java.security.KeyFactory +import java.security.PrivateKey +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Date + +@Component +class AudioContentCloudFront( + @Value("\${cloud.aws.content-cloud-front.host}") + private val cloudfrontDomain: String, + + @Value("\${cloud.aws.content-cloud-front.private-key-file-path}") + private val privateKeyFilePath: String, + + @Value("\${cloud.aws.content-cloud-front.key-pair-id}") + private val keyPairId: String +) { + fun generateSignedURL( + resourcePath: String, + expirationTime: Long + ): String { + // Load private key from file + val privateKey = loadPrivateKey(privateKeyFilePath) + + // Generate signed URL for resource with custom policy and expiration time + + return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( + "https://$cloudfrontDomain/$resourcePath", // Resource URL + keyPairId, // CloudFront key pair ID + privateKey, // CloudFront private key + Date(System.currentTimeMillis() + expirationTime) // Expiration date + ) + } + + private fun loadPrivateKey(resourceName: String): PrivateKey { + val path = Paths.get(resourceName) + val bytes = Files.readAllBytes(path) + val keySpec = PKCS8EncodedKeySpec(bytes) + val keyFactory = KeyFactory.getInstance("RSA") + return keyFactory.generatePrivate(keySpec) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index e181567..d9dbbe0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -13,6 +13,8 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.can.use.UseCanRepository import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.content.order.Order import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.MemberRepository import org.springframework.data.repository.findByIdOrNull @@ -33,6 +35,8 @@ class CanPaymentService( needCan: Int, canUsage: CanUsage, liveRoom: LiveRoom? = null, + order: Order? = null, + audioContent: AudioContent? = null, container: String ) { val member = memberRepository.findByIdOrNull(id = memberId) @@ -72,6 +76,14 @@ class CanPaymentService( recipientId = liveRoom.member!!.id!! useCan.room = liveRoom useCan.member = member + } else if (canUsage == CanUsage.ORDER_CONTENT && order != null) { + recipientId = order.creator!!.id!! + useCan.order = order + useCan.member = member + } else if (canUsage == CanUsage.DONATION && audioContent != null) { + recipientId = audioContent.member!!.id!! + useCan.audioContent = audioContent + useCan.member = member } else { throw SodaException("잘못된 요청입니다.") } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt index 9232846..683492a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/use/UseCan.kt @@ -1,6 +1,8 @@ package kr.co.vividnext.sodalive.can.use import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.content.order.Order import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.member.Member import javax.persistence.CascadeType @@ -11,6 +13,7 @@ import javax.persistence.FetchType import javax.persistence.JoinColumn import javax.persistence.ManyToOne import javax.persistence.OneToMany +import javax.persistence.OneToOne @Entity data class UseCan( @@ -35,6 +38,14 @@ data class UseCan( field = value } + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id", nullable = true) + var order: Order? = null + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = true) + var audioContent: AudioContent? = null + @OneToMany(mappedBy = "useCan", cascade = [CascadeType.ALL]) val useCanCalculates: MutableList = mutableListOf() } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AddAllPlaybackTrackingRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AddAllPlaybackTrackingRequest.kt new file mode 100644 index 0000000..b31f736 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AddAllPlaybackTrackingRequest.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.content + +data class AddAllPlaybackTrackingRequest( + val timezone: String, + val trackingDataList: List +) + +data class PlaybackTrackingData( + val contentId: Long, + val playDateTime: String, + val isPreview: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt new file mode 100644 index 0000000..116d8b4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt @@ -0,0 +1,64 @@ +package kr.co.vividnext.sodalive.content + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag +import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration +import kr.co.vividnext.sodalive.content.theme.AudioContentTheme +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany +import javax.persistence.OneToOne +import javax.persistence.Table + +enum class AudioContentType { + INDIVIDUAL, BUNDLE +} + +enum class SortType { + NEWEST, PRICE_HIGH, PRICE_LOW +} + +@Entity +@Table(name = "content") +data class AudioContent( + var title: String, + @Column(columnDefinition = "TEXT", nullable = false) + var detail: String, + val price: Int = 0, + @Enumerated(value = EnumType.STRING) + val type: AudioContentType = AudioContentType.INDIVIDUAL, + val isGeneratePreview: Boolean = true, + var isAdult: Boolean = false, + var isCommentAvailable: Boolean = true +) : BaseEntity() { + var isActive: Boolean = false + var content: String? = null + var coverImage: String? = null + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id", nullable = false) + var theme: AudioContentTheme? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "curation_id", nullable = true) + var curation: AudioContentCuration? = null + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + var duration: String? = null + + @OneToMany(mappedBy = "audioContent", cascade = [CascadeType.ALL]) + val audioContentHashTags: MutableList = mutableListOf() + + @OneToMany(mappedBy = "child", cascade = [CascadeType.ALL]) + var children: MutableList = mutableListOf() +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt new file mode 100644 index 0000000..0d32d5e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -0,0 +1,151 @@ +package kr.co.vividnext.sodalive.content + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeRequest +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.lang.Nullable +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/audio-content") +class AudioContentController(private val service: AudioContentService) { + @PostMapping + @PreAuthorize("hasRole('CREATOR')") + fun createAudioContent( + @Nullable + @RequestPart("contentFile") + contentFile: MultipartFile?, + @RequestPart("coverImage") coverImage: MultipartFile?, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.createAudioContent( + contentFile = contentFile, + coverImage = coverImage, + requestString = requestString, + member = member + ) + ) + } + + @PutMapping + @PreAuthorize("hasRole('CREATOR')") + fun modifyAudioContent( + @Nullable + @RequestPart("coverImage") + coverImage: MultipartFile?, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.modifyAudioContent( + coverImage = coverImage, + requestString = requestString, + member = member + ) + ) + } + + @PutMapping("/upload-complete") + @PreAuthorize("hasRole('ADMIN')") + fun uploadComplete( + @RequestBody request: UploadCompleteRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.uploadComplete( + contentId = request.contentId, + content = request.contentPath, + duration = request.duration + ) + ) + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('CREATOR')") + fun deleteAudioContent( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.deleteAudioContent( + audioContentId = id, + member = member + ) + ) + } + + @GetMapping + fun getAudioContentList( + @RequestParam("creator-id") creatorId: Long, + @RequestParam("sort-type", required = false) sortType: SortType = SortType.NEWEST, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.getAudioContentList( + creatorId = creatorId, + sortType = sortType, + member = member, + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + } + + @GetMapping("/{id}") + fun getDetail( + @PathVariable id: Long, + @RequestParam timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getDetail(id = id, member = member, timezone = timezone)) + } + + @PostMapping("/playback-tracking") + fun addAllPlaybackTracking( + @RequestBody request: AddAllPlaybackTrackingRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.addAllPlaybackTracking(request, member)) + } + + @PutMapping("/like") + fun audioContentLike( + @RequestBody request: PutAudioContentLikeRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.audioContentLike(request, member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt new file mode 100644 index 0000000..72ac833 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -0,0 +1,341 @@ +package kr.co.vividnext.sodalive.content + +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.content.QBundleAudioContent.bundleAudioContent +import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.content.main.GetNewContentUploadCreator +import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem +import kr.co.vividnext.sodalive.content.main.QGetNewContentUploadCreator +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner +import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner +import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration +import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration +import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme +import kr.co.vividnext.sodalive.event.QEvent.event +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +interface AudioContentRepository : JpaRepository, AudioContentQueryRepository + +interface AudioContentQueryRepository { + fun findByIdAndActive(contentId: Long): AudioContent? + fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent? + fun findBundleByContentId(contentId: Long): List + fun findByCreatorId( + creatorId: Long, + isAdult: Boolean = false, + sortType: SortType = SortType.NEWEST, + offset: Long = 0, + limit: Long = 10 + ): List + + fun findTotalCountByCreatorId(creatorId: Long, isAdult: Boolean = false): Int + fun getCreatorOtherContentList( + cloudfrontHost: String, + contentId: Long, + creatorId: Long, + isAdult: Boolean + ): List + + fun getSameThemeOtherContentList( + cloudfrontHost: String, + contentId: Long, + themeId: Long, + isAdult: Boolean + ): List + + fun findByTheme( + cloudfrontHost: String, + theme: String = "", + isAdult: Boolean = false, + limit: Long = 20 + ): List + + fun getNewContentUploadCreatorList( + cloudfrontHost: String, + isAdult: Boolean = false + ): List + + fun getAudioContentMainBannerList(isAdult: Boolean): List + fun getAudioContentCurations(isAdult: Boolean): List + fun findAudioContentByCurationId( + curationId: Long, + cloudfrontHost: String, + isAdult: Boolean + ): List +} + +@Repository +class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AudioContentQueryRepository { + override fun findByIdAndActive(contentId: Long): AudioContent? { + return queryFactory + .selectFrom(audioContent) + .where( + audioContent.isActive.isTrue + .and(audioContent.id.eq(contentId)) + ) + .fetchOne() + } + + override fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent? { + return queryFactory + .selectFrom(audioContent) + .where( + audioContent.isActive.isTrue + .and(audioContent.id.eq(contentId)) + .and(audioContent.member.id.eq(creatorId)) + ) + .fetchOne() + } + + // 해당 컨텐츠가 속한 묶음(번들) 상품 리스트 검색 + override fun findBundleByContentId(contentId: Long): List { + return queryFactory + .select(bundleAudioContent.parent) + .from(bundleAudioContent) + .where( + bundleAudioContent.child.id.eq(contentId) + .and(bundleAudioContent.child.isActive.isTrue) + ) + .fetch() + } + + override fun findByCreatorId( + creatorId: Long, + isAdult: Boolean, + sortType: SortType, + offset: Long, + limit: Long + ): List { + val orderBy = when (sortType) { + SortType.NEWEST -> audioContent.createdAt.desc() + SortType.PRICE_HIGH -> audioContent.price.desc() + SortType.PRICE_LOW -> audioContent.price.asc() + } + + var where = audioContent.isActive.isTrue + .and(audioContent.member.id.eq(creatorId)) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .selectFrom(audioContent) + .where(where) + .offset(offset) + .limit(limit) + .orderBy(orderBy) + .fetch() + } + + override fun findTotalCountByCreatorId( + creatorId: Long, + isAdult: Boolean + ): Int { + var where = audioContent.isActive.isTrue + .and(audioContent.member.id.eq(creatorId)) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .selectFrom(audioContent) + .where(where) + .fetch() + .size + } + + override fun getCreatorOtherContentList( + cloudfrontHost: String, + contentId: Long, + creatorId: Long, + isAdult: Boolean + ): List { + var where = audioContent.id.ne(contentId) + .and(audioContent.member.id.eq(creatorId)) + .and(audioContent.isActive.isTrue) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .select( + QOtherContentResponse( + audioContent.id, + audioContent.title, + audioContent.coverImage.prepend("$cloudfrontHost/") + ) + ) + .from(audioContent) + .where(where) + .offset(0) + .limit(10) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .fetch() + } + + override fun getSameThemeOtherContentList( + cloudfrontHost: String, + contentId: Long, + themeId: Long, + isAdult: Boolean + ): List { + var where = audioContent.id.ne(contentId) + .and(audioContent.theme.id.eq(themeId)) + .and(audioContent.isActive.isTrue) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .select( + QOtherContentResponse( + audioContent.id, + audioContent.title, + audioContent.coverImage.prepend("$cloudfrontHost/") + ) + ) + .from(audioContent) + .where(where) + .offset(0) + .limit(10) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .fetch() + } + + override fun findByTheme( + cloudfrontHost: String, + theme: String, + isAdult: Boolean, + limit: Long + ): List { + var where = audioContent.isActive.isTrue + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + if (theme.isNotBlank()) { + where = where.and(audioContentTheme.theme.eq(theme)) + } + + return queryFactory + .select( + QGetAudioContentMainItem( + audioContent.id, + audioContent.coverImage.prepend("/").prepend(cloudfrontHost), + audioContent.title, + audioContent.isAdult, + member.id, + member.profileImage.prepend("/").prepend(cloudfrontHost), + member.nickname + ) + ) + .from(audioContent) + .innerJoin(audioContent.member, member) + .innerJoin(audioContent.theme, audioContentTheme) + .where(where) + .limit(limit) + .orderBy(audioContent.createdAt.desc()) + .fetch() + } + + override fun getNewContentUploadCreatorList( + cloudfrontHost: String, + isAdult: Boolean + ): List { + var where = audioContent.createdAt.after(LocalDateTime.now().minusWeeks(2)) + .and(audioContent.isActive.isTrue) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .select( + QGetNewContentUploadCreator( + member.id, + member.nickname, + member.profileImage.nullif("profile/default-profile.png").prepend("$cloudfrontHost/") + ) + ) + .from(audioContent) + .innerJoin(audioContent.member, member) + .where(where) + .groupBy(member.id) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .limit(20) + .fetch() + } + + override fun getAudioContentMainBannerList(isAdult: Boolean): List { + var where = audioContentBanner.isActive.isTrue + + if (!isAdult) { + where = where.and(audioContentBanner.isAdult.isFalse) + } + + return queryFactory + .selectFrom(audioContentBanner) + .leftJoin(audioContentBanner.event, event) + .leftJoin(audioContentBanner.creator, member) + .where(where) + .orderBy(audioContentBanner.orders.asc()) + .fetch() + } + + override fun getAudioContentCurations(isAdult: Boolean): List { + var where = audioContentCuration.isActive.isTrue + + if (!isAdult) { + where = where.and(audioContentCuration.isAdult.isFalse) + } + + return queryFactory + .selectFrom(audioContentCuration) + .where(where) + .orderBy(audioContentCuration.orders.asc()) + .fetch() + } + + override fun findAudioContentByCurationId( + curationId: Long, + cloudfrontHost: String, + isAdult: Boolean + ): List { + var where = audioContent.isActive.isTrue + .and(audioContent.curation.id.eq(curationId)) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .select( + QGetAudioContentMainItem( + audioContent.id, + audioContent.coverImage.prepend("/").prepend(cloudfrontHost), + audioContent.title, + audioContent.isAdult, + member.id, + member.profileImage.nullif("profile/default-profile.png") + .prepend("/") + .prepend(cloudfrontHost), + member.nickname + ) + ) + .from(audioContent) + .where(where) + .orderBy(audioContent.id.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt new file mode 100644 index 0000000..1bceec0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -0,0 +1,469 @@ +package kr.co.vividnext.sodalive.content + +import com.amazonaws.services.s3.model.ObjectMetadata +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository +import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag +import kr.co.vividnext.sodalive.content.hashtag.HashTag +import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository +import kr.co.vividnext.sodalive.content.like.AudioContentLike +import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository +import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeRequest +import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeResponse +import kr.co.vividnext.sodalive.content.order.OrderRepository +import kr.co.vividnext.sodalive.content.order.OrderType +import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository +import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +@Transactional(readOnly = true) +class AudioContentService( + private val repository: AudioContentRepository, + private val explorerQueryRepository: ExplorerQueryRepository, + private val blockMemberRepository: BlockMemberRepository, + private val hashTagRepository: HashTagRepository, + private val orderRepository: OrderRepository, + private val themeQueryRepository: AudioContentThemeQueryRepository, + private val playbackTrackingRepository: PlaybackTrackingRepository, + private val commentRepository: AudioContentCommentRepository, + private val audioContentLikeRepository: AudioContentLikeRepository, + + private val s3Uploader: S3Uploader, + private val objectMapper: ObjectMapper, + private val audioContentCloudFront: AudioContentCloudFront, + + @Value("\${cloud.aws.s3.content-bucket}") + private val audioContentBucket: String, + + @Value("\${cloud.aws.s3.bucket}") + private val coverImageBucket: String, + + @Value("\${cloud.aws.cloud-front.host}") + private val coverImageHost: String +) { + @Transactional + fun audioContentLike(request: PutAudioContentLikeRequest, member: Member): PutAudioContentLikeResponse { + var audioContentLike = audioContentLikeRepository.findByMemberIdAndContentId( + memberId = member.id!!, + contentId = request.contentId + ) + + if (audioContentLike == null) { + audioContentLike = AudioContentLike( + memberId = member.id!!, + contentId = request.contentId + ) + + audioContentLikeRepository.save(audioContentLike) + } else { + audioContentLike.isActive = !audioContentLike.isActive + } + + return PutAudioContentLikeResponse(like = audioContentLike.isActive) + } + + @Transactional + fun modifyAudioContent( + coverImage: MultipartFile?, + requestString: String, + member: Member + ) { + // request 내용 파싱 + val request = objectMapper.readValue(requestString, ModifyAudioContentRequest::class.java) + + val audioContent = repository.findByIdAndCreatorId(request.contentId, member.id!!) + ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + + if (request.title != null) audioContent.title = request.title + if (request.detail != null) audioContent.detail = request.detail + audioContent.isCommentAvailable = request.isCommentAvailable + audioContent.isAdult = request.isAdult + + if (coverImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${audioContent.id}-cover") + + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "audio_content_cover/${audioContent.id}/$coverImageFileName", + metadata = metadata + ) + + audioContent.coverImage = coverImagePath + } + } + + @Transactional + fun deleteAudioContent(audioContentId: Long, member: Member) { + val audioContent = repository.findByIdAndCreatorId(audioContentId, member.id!!) + ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + + audioContent.isActive = false + } + + @Transactional + fun createAudioContent( + contentFile: MultipartFile?, + coverImage: MultipartFile?, + requestString: String, + member: Member + ): CreateAudioContentResponse { + // coverImage 체크 + if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.") + + // request 내용 파싱 + val request = objectMapper.readValue(requestString, CreateAudioContentRequest::class.java) + + // contentFile 체크 + if (contentFile == null && request.type == AudioContentType.INDIVIDUAL) { + throw SodaException("콘텐츠를 선택해 주세요.") + } + + if (request.type == AudioContentType.BUNDLE && request.childIds == null) { + throw SodaException("묶음상품의 하위상품을 선택해 주세요.") + } + + // 테마 체크 + val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId) + ?: throw SodaException("잘못된 테마입니다. 다시 선택해 주세요.") + + if (request.price in 1..9) throw SodaException("콘텐츠의 최소금액은 10코인 입니다.") + + // DB에 값 추가 + val audioContent = AudioContent( + title = request.title, + detail = request.detail, + type = request.type, + price = if (request.price < 0) { + 0 + } else { + request.price + }, + isAdult = request.isAdult, + isGeneratePreview = if (request.type == AudioContentType.INDIVIDUAL) { + request.isGeneratePreview + } else { + false + }, + isCommentAvailable = request.isCommentAvailable + ) + audioContent.theme = theme + audioContent.member = member + audioContent.isActive = request.type == AudioContentType.BUNDLE + + repository.save(audioContent) + + // 태그 분리, #추가, 등록 + if (request.tags.isNotBlank()) { + val tags = request.tags + .split(" ") + .asSequence() + .map { it.trim() } + .filter { it.isNotEmpty() } + .map { + val tag = if (!it.startsWith("#")) { + "#$it" + } else { + it + } + + val hashTag = hashTagRepository.findByTag(tag) + ?: hashTagRepository.save(HashTag(tag)) + + val audioContentHashTag = AudioContentHashTag() + audioContentHashTag.audioContent = audioContent + audioContentHashTag.hashTag = hashTag + + audioContentHashTag + }.toList() + + audioContent.audioContentHashTags.addAll(tags) + } + + var metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${audioContent.id}-cover") + + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "audio_content_cover/${audioContent.id}/$coverImageFileName", + metadata = metadata + ) + + audioContent.coverImage = coverImagePath + + if (contentFile != null && request.type == AudioContentType.INDIVIDUAL) { + // 콘텐츠 파일명 생성 + val contentFileName = generateFileName(prefix = "${audioContent.id}-content") + + // 콘텐츠 파일 업로드 + metadata = ObjectMetadata() + metadata.contentLength = contentFile.size + metadata.addUserMetadata("generate_preview", "true") + + val contentPath = s3Uploader.upload( + inputStream = contentFile.inputStream, + bucket = audioContentBucket, + filePath = "input/${audioContent.id}/$contentFileName", + metadata = metadata + ) + + audioContent.content = contentPath + } + + if (request.childIds != null && request.type == AudioContentType.BUNDLE) { + for (childId in request.childIds) { + val childContent = repository.findByIdAndActive(childId) + ?: continue + + val bundleAudioContent = BundleAudioContent() + bundleAudioContent.parent = audioContent + bundleAudioContent.child = childContent + audioContent.children.add(bundleAudioContent) + } + } + + return CreateAudioContentResponse(contentId = audioContent.id!!) + } + + @Transactional + fun uploadComplete(contentId: Long, content: String, duration: String) { + val keyFileName = content.split("/").last() + if (!keyFileName.startsWith(contentId.toString())) throw SodaException("잘못된 요청입니다.") + + val audioContent = repository.findByIdOrNull(contentId) + ?: throw SodaException("잘못된 요청입니다.") + + audioContent.isActive = true + audioContent.content = content + audioContent.duration = duration + } + + fun getDetail(id: Long, member: Member, timezone: String): GetAudioContentDetailResponse { + // 묶음 콘텐츠 조회 + val bundleAudioContentList = repository.findBundleByContentId(contentId = id) + + // 오디오 콘텐츠 조회 (content_id, 제목, 내용, 테마, 태그, 19여부, 이미지, 콘텐츠 PATH) + val audioContent = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + + // 크리에이터(유저) 정보 + val creatorId = audioContent.member!!.id!! + val creator = explorerQueryRepository.getAccount(creatorId) + ?: throw SodaException("없는 사용자 입니다.") + + val notificationUserIds = explorerQueryRepository.getNotificationUserIds(creatorId) + val isFollowing = notificationUserIds.contains(member.id) + + // 차단된 사용자 체크 + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId) + if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 콘텐츠 접근이 제한됩니다.") + + // 구매 여부 확인 + val isExistsBundleAudioContent = bundleAudioContentList + .map { orderRepository.isExistOrdered(memberId = member.id!!, contentId = it.id!!) } + .contains(true) + + val (isExistsAudioContent, orderType) = orderRepository.isExistOrderedAndOrderType( + memberId = member.id!!, + contentId = audioContent.id!! + ) + + if (!isExistsAudioContent && !isExistsBundleAudioContent && !audioContent.isActive) { + throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + } + + // 댓글 + val commentList = if (audioContent.isCommentAvailable) { + commentRepository.findByContentId( + cloudFrontHost = coverImageHost, + contentId = audioContent.id!!, + timezone = timezone, + offset = 0, + limit = 1 + ) + } else { + listOf() + } + + // 댓글 수 + val commentCount = if (audioContent.isCommentAvailable) { + commentRepository.totalCountCommentByContentId(contentId = audioContent.id!!) + } else { + 0 + } + + val audioContentUrl = audioContentCloudFront.generateSignedURL( + resourcePath = if ( + isExistsAudioContent || + isExistsBundleAudioContent || + audioContent.member!!.id!! == member.id!! || + audioContent.price <= 0 + ) { + audioContent.content!! + } else { + audioContent.content!!.replace("output/", "preview/") + }, + expirationTime = 1000 * 60 * 60 * (audioContent.duration!!.split(":")[0].toLong() + 2) + ) + + val tag = audioContent.audioContentHashTags + .map { it.hashTag!!.tag } + .joinToString(" ") { it } + + val creatorOtherContentList = repository.getCreatorOtherContentList( + cloudfrontHost = coverImageHost, + contentId = audioContent.id!!, + creatorId = creatorId, + isAdult = member.auth != null + ) + + val sameThemeOtherContentList = repository.getSameThemeOtherContentList( + cloudfrontHost = coverImageHost, + contentId = audioContent.id!!, + themeId = audioContent.theme!!.id!!, + isAdult = member.auth != null + ) + + val likeCount = audioContentLikeRepository.totalCountAudioContentLike(contentId = id) + val isLike = audioContentLikeRepository.findByMemberIdAndContentId( + memberId = member.id!!, + contentId = id + )?.isActive ?: false + + val remainingTime = if (orderType == OrderType.RENTAL) { + orderRepository.getAudioContentRemainingTime( + memberId = member.id!!, + contentId = audioContent.id!!, + timezone = timezone + ) + } else { + null + } + + return GetAudioContentDetailResponse( + contentId = audioContent.id!!, + title = audioContent.title, + detail = audioContent.detail, + coverImageUrl = "$coverImageHost/${audioContent.coverImage!!}", + contentUrl = audioContentUrl, + themeStr = audioContent.theme!!.theme, + tag = tag, + price = audioContent.price, + duration = audioContent.duration ?: "", + isAdult = audioContent.isAdult, + isMosaic = audioContent.isAdult && member.auth == null, + existOrdered = isExistsBundleAudioContent || isExistsAudioContent, + orderType = orderType, + remainingTime = remainingTime, + creatorOtherContentList = creatorOtherContentList, + sameThemeOtherContentList = sameThemeOtherContentList, + isCommentAvailable = audioContent.isCommentAvailable, + isLike = isLike, + likeCount = likeCount, + commentList = commentList, + commentCount = commentCount, + creator = AudioContentCreator( + creatorId = creatorId, + nickname = creator.nickname, + profileImageUrl = if (creator.profileImage != null) { + "$coverImageHost/${creator.profileImage}" + } else { + "$coverImageHost/profile/default-profile.png" + }, + isFollowing = isFollowing + ) + ) + } + + fun getAudioContentList( + creatorId: Long, + sortType: SortType, + member: Member, + offset: Long, + limit: Long + ): GetAudioContentListResponse { + val totalCount = repository.findTotalCountByCreatorId( + creatorId = creatorId, + isAdult = member.auth != null + ) + + val audioContentList = repository.findByCreatorId( + creatorId = creatorId, + isAdult = member.auth != null, + sortType = sortType, + offset = offset, + limit = limit + ) + + val items = audioContentList + .map { + val commentCount = commentRepository + .totalCountCommentByContentId(it.id!!) + + val likeCount = audioContentLikeRepository + .totalCountAudioContentLike(it.id!!) + + GetAudioContentListItem( + contentId = it.id!!, + coverImageUrl = "$coverImageHost/${it.coverImage!!}", + title = it.title, + price = it.price, + themeStr = it.theme!!.theme, + duration = it.duration, + likeCount = likeCount, + commentCount = commentCount, + isAdult = it.isAdult + ) + } + + return GetAudioContentListResponse( + totalCount = totalCount, + items = items + ) + } + + @Transactional + fun addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest, member: Member) { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + + for (trackingData in request.trackingDataList) { + val playDate = LocalDateTime.parse(trackingData.playDateTime, dateTimeFormatter) + .atZone(ZoneId.of(request.timezone)) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + playbackTrackingRepository.save( + PlaybackTracking( + memberId = member.id!!, + contentId = trackingData.contentId, + playDate = playDate, + isPreview = trackingData.isPreview + ) + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/BundleAudioContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/BundleAudioContent.kt new file mode 100644 index 0000000..375e819 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/BundleAudioContent.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.content + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.Table + +@Entity +@Table(name = "bundle_content") +data class BundleAudioContent( + var isActive: Boolean = true +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_content_id", nullable = false) + var parent: AudioContent? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "child_content_id", nullable = false) + var child: AudioContent? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt new file mode 100644 index 0000000..8952c84 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.content + +data class CreateAudioContentRequest( + val title: String, + val detail: String, + val tags: String, + val price: Int, + val themeId: Long = 0, + val isAdult: Boolean = false, + val isGeneratePreview: Boolean = true, + val isCommentAvailable: Boolean = false, + val type: AudioContentType = AudioContentType.INDIVIDUAL, + val childIds: List? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentResponse.kt new file mode 100644 index 0000000..3bffecf --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentResponse.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content + +data class CreateAudioContentResponse(val contentId: Long) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt new file mode 100644 index 0000000..0c5f3e9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt @@ -0,0 +1,43 @@ +package kr.co.vividnext.sodalive.content + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem +import kr.co.vividnext.sodalive.content.order.OrderType + +data class GetAudioContentDetailResponse( + val contentId: Long, + val title: String, + val detail: String, + val coverImageUrl: String, + val contentUrl: String, + val themeStr: String, + val tag: String, + val price: Int, + val duration: String, + val isAdult: Boolean, + val isMosaic: Boolean, + val existOrdered: Boolean, + val orderType: OrderType?, + val remainingTime: String?, + val creatorOtherContentList: List, + val sameThemeOtherContentList: List, + val isCommentAvailable: Boolean, + val isLike: Boolean, + val likeCount: Int, + val commentList: List, + val commentCount: Int, + val creator: AudioContentCreator +) + +data class OtherContentResponse @QueryProjection constructor( + val contentId: Long, + val title: String, + val coverUrl: String +) + +data class AudioContentCreator( + val creatorId: Long, + val nickname: String, + val profileImageUrl: String, + val isFollowing: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt new file mode 100644 index 0000000..bfef003 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.content + +data class GetAudioContentListResponse( + val totalCount: Int, + val items: List +) + +data class GetAudioContentListItem( + val contentId: Long, + val coverImageUrl: String, + val title: String, + val price: Int, + val themeStr: String, + val duration: String?, + val likeCount: Int, + val commentCount: Int, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/ModifyAudioContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/ModifyAudioContentRequest.kt new file mode 100644 index 0000000..fc104f7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/ModifyAudioContentRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.content + +data class ModifyAudioContentRequest( + val contentId: Long, + val title: String?, + val detail: String?, + val isAdult: Boolean, + val isCommentAvailable: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTracking.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTracking.kt new file mode 100644 index 0000000..2639f37 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTracking.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.content + +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.PrePersist + +@Entity +data class PlaybackTracking( + val memberId: Long, + val contentId: Long, + val isPreview: Boolean, + val playDate: LocalDateTime +) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + var createdAt: LocalDateTime? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTrackingRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTrackingRepository.kt new file mode 100644 index 0000000..f768729 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/PlaybackTrackingRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.content + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface PlaybackTrackingRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/UploadCompleteRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/UploadCompleteRequest.kt new file mode 100644 index 0000000..75a4898 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/UploadCompleteRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content + +data class UploadCompleteRequest(val contentId: Long, val contentPath: String, val duration: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentComment.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentComment.kt new file mode 100644 index 0000000..0f345e3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentComment.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.content.comment + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany +import javax.persistence.Table + +@Entity +@Table(name = "content_comment") +data class AudioContentComment( + @Column(columnDefinition = "TEXT", nullable = false) + var comment: String, + @Column(nullable = true) + var donationCan: Int? = null, + var isActive: Boolean = true +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id", nullable = true) + var parent: AudioContentComment? = null + set(value) { + value?.children?.add(this) + field = value + } + + @OneToMany(mappedBy = "parent") + var children: MutableList = mutableListOf() + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var audioContent: AudioContent? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt new file mode 100644 index 0000000..dc153e2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentController.kt @@ -0,0 +1,80 @@ +package kr.co.vividnext.sodalive.content.comment + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.core.annotation.AuthenticationPrincipal +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.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +class AudioContentCommentController(private val service: AudioContentCommentService) { + @PostMapping("/audio-content/comment") + fun registerComment( + @RequestBody request: RegisterCommentRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.registerComment( + comment = request.comment, + audioContentId = request.contentId, + parentId = request.parentId, + member = member + ) + ) + } + + @PutMapping("/audio-content/comment") + fun modifyComment( + @RequestBody request: ModifyCommentRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.modifyComment(request = request, member = member)) + } + + @GetMapping("/audio-content/{id}/comment") + fun getCommentList( + @PathVariable("id") audioContentId: Long, + @RequestParam timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.getCommentList( + audioContentId = audioContentId, + timezone = timezone, + pageable = pageable + ) + ) + } + + @GetMapping("/audio-content/comment/{id}") + fun getCommentReplyList( + @PathVariable("id") commentId: Long, + @RequestParam timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ): ApiResponse { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + return ApiResponse.ok( + service.getCommentReplyList( + commentId = commentId, + timezone = timezone, + pageable = pageable + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt new file mode 100644 index 0000000..d09397c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt @@ -0,0 +1,141 @@ +package kr.co.vividnext.sodalive.content.comment + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Repository +interface AudioContentCommentRepository : JpaRepository, AudioContentCommentQueryRepository + +interface AudioContentCommentQueryRepository { + fun findByContentId( + cloudFrontHost: String, + contentId: Long, + timezone: String, + offset: Long, + limit: Int + ): List + + fun totalCountCommentByContentId(contentId: Long): Int + fun commentReplyCountByAudioContentCommentId(commentId: Long): Int + fun getAudioContentCommentReplyList( + cloudFrontHost: String, + commentId: Long, + timezone: String, + offset: Long, + limit: Int + ): List +} + +@Repository +class AudioContentCommentQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AudioContentCommentQueryRepository { + override fun findByContentId( + cloudFrontHost: String, + contentId: Long, + timezone: String, + offset: Long, + limit: Int + ): List { + return queryFactory.selectFrom(audioContentComment) + .where( + audioContentComment.audioContent.id.eq(contentId) + .and(audioContentComment.isActive.isTrue) + .and(audioContentComment.parent.isNull) + ) + .offset(offset) + .limit(limit.toLong()) + .orderBy(audioContentComment.createdAt.desc()) + .fetch() + .asSequence() + .map { + val date = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetAudioContentCommentListItem( + id = it.id!!, + writerId = it.member!!.id!!, + nickname = it.member!!.nickname, + profileUrl = if (it.member!!.profileImage != null) { + "$cloudFrontHost/${it.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + comment = it.comment, + donationCoin = it.donationCan ?: 0, + date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + replyCount = commentReplyCountByAudioContentCommentId(it.id!!) + ) + } + .toList() + } + + override fun totalCountCommentByContentId(contentId: Long): Int { + return queryFactory.select(audioContentComment.id) + .from(audioContentComment) + .where( + audioContentComment.audioContent.id.eq(contentId) + .and(audioContentComment.isActive.isTrue) + ) + .fetch() + .size + } + + override fun commentReplyCountByAudioContentCommentId(commentId: Long): Int { + return queryFactory.select(audioContentComment.id) + .from(audioContentComment) + .where( + audioContentComment.parent.isNotNull + .and(audioContentComment.parent.id.eq(commentId)) + .and(audioContentComment.isActive.isTrue) + ) + .fetch() + .size + } + + override fun getAudioContentCommentReplyList( + cloudFrontHost: String, + commentId: Long, + timezone: String, + offset: Long, + limit: Int + ): List { + return queryFactory.selectFrom(audioContentComment) + .where( + audioContentComment.parent.isNotNull + .and(audioContentComment.parent.id.eq(commentId)) + .and(audioContentComment.isActive.isTrue) + ) + .offset(offset) + .limit(limit.toLong()) + .orderBy(audioContentComment.createdAt.desc()) + .fetch() + .asSequence() + .map { + val date = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetAudioContentCommentListItem( + id = it.id!!, + writerId = it.member!!.id!!, + nickname = it.member!!.nickname, + profileUrl = if (it.member!!.profileImage != null) { + "$cloudFrontHost/${it.member!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + comment = it.comment, + donationCoin = it.donationCan ?: 0, + date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + replyCount = 0 + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt new file mode 100644 index 0000000..dd91014 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt @@ -0,0 +1,89 @@ +package kr.co.vividnext.sodalive.content.comment + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.member.Member +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class AudioContentCommentService( + private val repository: AudioContentCommentRepository, + private val audioContentRepository: AudioContentRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + @Transactional + fun registerComment(member: Member, comment: String, audioContentId: Long, parentId: Long? = null) { + val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId) + ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + + val audioContentComment = AudioContentComment(comment = comment) + audioContentComment.audioContent = audioContent + audioContentComment.member = member + + val parent = if (parentId != null) { + repository.findByIdOrNull(id = parentId) + } else { + null + } + + if (parent != null) { + audioContentComment.parent = parent + } + + repository.save(audioContentComment) + } + + @Transactional + fun modifyComment(request: ModifyCommentRequest, member: Member) { + val audioContentComment = repository.findByIdOrNull(request.commentId) + ?: throw SodaException("잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.") + + if (audioContentComment.audioContent!!.member!!.id!! != member.id!!) { + if (audioContentComment.member == null || audioContentComment.member!!.id!! != member.id!!) { + throw SodaException("잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.") + } + + if (request.comment != null) { + audioContentComment.comment = request.comment + } + } + + if (request.isActive != null) { + audioContentComment.isActive = request.isActive + } + } + + fun getCommentList(audioContentId: Long, timezone: String, pageable: Pageable): GetAudioContentCommentListResponse { + val commentList = + repository.findByContentId( + cloudFrontHost = cloudFrontHost, + contentId = audioContentId, + timezone = timezone, + offset = pageable.offset, + limit = pageable.pageSize + ) + val totalCount = repository.totalCountCommentByContentId(audioContentId) + + return GetAudioContentCommentListResponse(totalCount, commentList) + } + + fun getCommentReplyList(commentId: Long, timezone: String, pageable: Pageable): GetAudioContentCommentListResponse { + val commentList = repository.getAudioContentCommentReplyList( + cloudFrontHost = cloudFrontHost, + commentId = commentId, + timezone = timezone, + offset = pageable.offset, + limit = pageable.pageSize + ) + val totalCount = repository.commentReplyCountByAudioContentCommentId(commentId) + + return GetAudioContentCommentListResponse(totalCount, commentList) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt new file mode 100644 index 0000000..08be9e4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.content.comment + +data class GetAudioContentCommentListResponse( + val totalCount: Int, + val items: List +) + +data class GetAudioContentCommentListItem( + val id: Long, + val writerId: Long, + val nickname: String, + val profileUrl: String, + val comment: String, + val donationCoin: Int, + val date: String, + val replyCount: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/ModifyCommentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/ModifyCommentRequest.kt new file mode 100644 index 0000000..574baee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/ModifyCommentRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content.comment + +data class ModifyCommentRequest(val commentId: Long, val comment: String? = null, val isActive: Boolean? = null) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/RegisterCommentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/RegisterCommentRequest.kt new file mode 100644 index 0000000..df29747 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/RegisterCommentRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content.comment + +data class RegisterCommentRequest(val comment: String, val contentId: Long, val parentId: Long?) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt new file mode 100644 index 0000000..ebd097a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationController.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.content.donation + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +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 +@RequestMapping("/audio-content/donation") +class AudioContentDonationController(private val service: AudioContentDonationService) { + + @PostMapping + fun donation( + @RequestBody request: AudioContentDonationRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.donation(request = request, member = member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationRequest.kt new file mode 100644 index 0000000..a8ed8a9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.content.donation + +data class AudioContentDonationRequest( + val contentId: Long, + val donationCan: Int, + val comment: String, + val container: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt new file mode 100644 index 0000000..043e6ca --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt @@ -0,0 +1,43 @@ +package kr.co.vividnext.sodalive.content.donation + +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.content.comment.AudioContentComment +import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository +import kr.co.vividnext.sodalive.member.Member +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AudioContentDonationService( + private val coinPaymentService: CanPaymentService, + private val queryRepository: AudioContentRepository, + private val commentRepository: AudioContentCommentRepository +) { + @Transactional + fun donation(request: AudioContentDonationRequest, member: Member) { + if (request.donationCan < 1) throw SodaException("1캔 이상 후원하실 수 있습니다.") + if (request.comment.isBlank()) throw SodaException("함께 보낼 메시지를 입력하세요.") + + val audioContent = queryRepository.findByIdAndActive(request.contentId) + ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") + + coinPaymentService.spendCan( + memberId = member.id!!, + needCan = request.donationCan, + canUsage = CanUsage.DONATION, + audioContent = audioContent, + container = request.container + ) + + val audioContentComment = AudioContentComment( + comment = request.comment, + donationCan = request.donationCan + ) + audioContentComment.audioContent = audioContent + audioContentComment.member = member + commentRepository.save(audioContentComment) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/AudioContentHashTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/AudioContentHashTag.kt new file mode 100644 index 0000000..0ff2b6d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/AudioContentHashTag.kt @@ -0,0 +1,36 @@ +package kr.co.vividnext.sodalive.content.hashtag + +import kr.co.vividnext.sodalive.content.AudioContent +import java.time.LocalDateTime +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.PrePersist +import javax.persistence.Table + +@Entity +@Table(name = "content_hash_tag") +class AudioContentHashTag { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var audioContent: AudioContent? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "hash_tag_id", nullable = false) + var hashTag: HashTag? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTag.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTag.kt new file mode 100644 index 0000000..118e081 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTag.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.content.hashtag + +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.PrePersist + +@Entity +class HashTag( + val tag: String +) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Int? = null + var createdAt: LocalDateTime? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTagRepository.kt new file mode 100644 index 0000000..93edd35 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/hashtag/HashTagRepository.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.content.hashtag + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface HashTagRepository : JpaRepository, HashTagQueryRepository + +interface HashTagQueryRepository { + fun findByTag(tag: String): HashTag? +} + +@Repository +class HashTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : HashTagQueryRepository { + override fun findByTag(tag: String): HashTag? { + return queryFactory + .selectFrom(hashTag) + .where(hashTag.tag.eq(tag)) + .fetchOne() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLike.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLike.kt new file mode 100644 index 0000000..5b9b16a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLike.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.content.like + +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.PrePersist +import javax.persistence.PreUpdate +import javax.persistence.Table + +@Entity +@Table(name = "content_like") +data class AudioContentLike( + val memberId: Long, + val contentId: Long +) { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + var updatedAt: LocalDateTime? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + updatedAt = LocalDateTime.now() + } + + @PreUpdate + fun preUpdate() { + updatedAt = LocalDateTime.now() + } + + var isActive = true +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLikeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLikeRepository.kt new file mode 100644 index 0000000..db8b1ad --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/AudioContentLikeRepository.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.content.like + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AudioContentLikeRepository : JpaRepository, AudioContentLikeQueryRepository + +interface AudioContentLikeQueryRepository { + fun findByMemberIdAndContentId(memberId: Long, contentId: Long): AudioContentLike? + fun totalCountAudioContentLike(contentId: Long): Int +} + +@Repository +class AudioContentLikeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AudioContentLikeQueryRepository { + override fun findByMemberIdAndContentId(memberId: Long, contentId: Long): AudioContentLike? { + return queryFactory + .selectFrom(audioContentLike) + .where( + audioContentLike.memberId.eq(memberId) + .and(audioContentLike.contentId.eq(contentId)) + ) + .fetchFirst() + } + + override fun totalCountAudioContentLike(contentId: Long): Int { + return queryFactory + .select(audioContentLike.id) + .from(audioContentLike) + .where( + audioContentLike.contentId.eq(contentId) + .and(audioContentLike.isActive.isTrue) + ) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeRequest.kt new file mode 100644 index 0000000..5bc9df2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content.like + +data class PutAudioContentLikeRequest(val contentId: Long) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeResponse.kt new file mode 100644 index 0000000..55698e2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/like/PutAudioContentLikeResponse.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content.like + +data class PutAudioContentLikeResponse(val like: Boolean) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt new file mode 100644 index 0000000..d021553 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.content.main + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/audio-content/main") +class AudioContentMainController(private val service: AudioContentMainService) { + + @GetMapping + fun getMain( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getMain(member = member)) + } + + @GetMapping("/new") + fun getNewContentByTheme( + @RequestParam("theme") theme: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getNewContentByTheme(theme, member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt new file mode 100644 index 0000000..8c01c2a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt @@ -0,0 +1,147 @@ +package kr.co.vividnext.sodalive.content.main + +import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType +import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse +import kr.co.vividnext.sodalive.content.main.curation.GetAudioContentCurationResponse +import kr.co.vividnext.sodalive.content.order.OrderService +import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository +import kr.co.vividnext.sodalive.event.EventItem +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class AudioContentMainService( + private val repository: AudioContentRepository, + private val blockMemberRepository: BlockMemberRepository, + private val orderService: OrderService, + private val audioContentThemeRepository: AudioContentThemeQueryRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val imageHost: String +) { + fun getMain(member: Member): GetAudioContentMainResponse { + val isAdult = member.auth != null + + // 2주일 이내에 콘텐츠를 올린 크리에이터 20명 조회 + val newContentUploadCreatorList = repository.getNewContentUploadCreatorList( + cloudfrontHost = imageHost, + isAdult = isAdult + ) + .asSequence() + .filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) } + .toList() + + val bannerList = repository.getAudioContentMainBannerList(isAdult = isAdult) + .asSequence() + .filter { + if (it.type == AudioContentBannerType.CREATOR && it.creator != null) { + !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creator!!.id!!) + } else { + true + } + } + .map { + GetAudioContentBannerResponse( + type = it.type, + thumbnailImageUrl = "$imageHost/${it.thumbnailImage}", + eventItem = if (it.type == AudioContentBannerType.EVENT && it.event != null) { + EventItem( + id = it.event!!.id!!, + thumbnailImageUrl = if (!it.event!!.thumbnailImage.startsWith("https://")) { + "$imageHost/${it.event!!.thumbnailImage}" + } else { + it.event!!.thumbnailImage + }, + detailImageUrl = if ( + it.event!!.detailImage != null && + !it.event!!.detailImage!!.startsWith("https://") + ) { + "$imageHost/${it.event!!.detailImage}" + } else { + it.event!!.detailImage + }, + popupImageUrl = null, + link = it.event!!.link, + title = it.event!!.title, + isPopup = false + ) + } else { + null + }, + creatorId = if (it.type == AudioContentBannerType.CREATOR && it.creator != null) { + it.creator!!.id + } else { + null + }, + link = it.link + ) + } + .toList() + + // 구매목록 20개 + val orderList = orderService.getAudioContentMainOrderList( + member = member, + limit = 20 + ) + + // 콘텐츠 테마 + val themeList = audioContentThemeRepository.getActiveThemeOfContent(isAdult = isAdult) + + // 새 콘텐츠 20개 - 시간 내림차순 정렬 + val newContentList = repository.findByTheme( + cloudfrontHost = imageHost, + isAdult = isAdult + ) + .asSequence() + .filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) } + .toList() + + val curationList = repository + .getAudioContentCurations(isAdult = isAdult) + .asSequence() + .map { + GetAudioContentCurationResponse( + title = it.title, + description = it.description, + contents = repository.findAudioContentByCurationId( + curationId = it.id!!, + cloudfrontHost = imageHost, + isAdult = isAdult + ) + .asSequence() + .filter { content -> + !blockMemberRepository.isBlocked( + blockedMemberId = member.id!!, + memberId = content.creatorId + ) + } + .toList() + ) + } + .filter { it.contents.isNotEmpty() } + .toList() + + return GetAudioContentMainResponse( + newContentUploadCreatorList = newContentUploadCreatorList, + bannerList = bannerList, + orderList = orderList, + themeList = themeList, + newContentList = newContentList, + curationList = curationList + ) + } + + fun getNewContentByTheme(theme: String, member: Member): List { + return repository.findByTheme( + cloudfrontHost = imageHost, + theme = theme, + isAdult = member.auth != null + ) + .asSequence() + .filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainItem.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainItem.kt new file mode 100644 index 0000000..1ab49eb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainItem.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.content.main + +import com.querydsl.core.annotations.QueryProjection + +data class GetAudioContentMainItem @QueryProjection constructor( + val contentId: Long, + val coverImageUrl: String, + val title: String, + val isAdult: Boolean, + val creatorId: Long, + val creatorProfileImageUrl: String, + val creatorNickname: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainResponse.kt new file mode 100644 index 0000000..60b708a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetAudioContentMainResponse.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.content.main + +import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse +import kr.co.vividnext.sodalive.content.main.curation.GetAudioContentCurationResponse + +data class GetAudioContentMainResponse( + val newContentUploadCreatorList: List, + val bannerList: List, + val orderList: List, + val themeList: List, + val newContentList: List, + val curationList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt new file mode 100644 index 0000000..55a50a7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.content.main + +import com.querydsl.core.annotations.QueryProjection + +data class GetNewContentUploadCreator @QueryProjection constructor( + val creatorId: Long, + val creatorNickname: String, + val creatorProfileImageUrl: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBanner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBanner.kt new file mode 100644 index 0000000..dd71d3b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBanner.kt @@ -0,0 +1,43 @@ +package kr.co.vividnext.sodalive.content.main.banner + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.event.Event +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne +import javax.persistence.Table + +@Entity +@Table(name = "content_banner") +data class AudioContentBanner( + @Column(nullable = false) + var thumbnailImage: String = "", + @Enumerated(value = EnumType.STRING) + var type: AudioContentBannerType, + @Column(nullable = false) + var isAdult: Boolean = false, + @Column(nullable = false) + var isActive: Boolean = true, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "event_id", nullable = true) + var event: Event? = null + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id", nullable = true) + var creator: Member? = null + + @Column(nullable = true) + var link: String? = null +} + +enum class AudioContentBannerType { + EVENT, CREATOR, LINK +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/GetAudioContentBannerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/GetAudioContentBannerResponse.kt new file mode 100644 index 0000000..31cc797 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/GetAudioContentBannerResponse.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.content.main.banner + +import kr.co.vividnext.sodalive.event.EventItem + +data class GetAudioContentBannerResponse( + val type: AudioContentBannerType, + val thumbnailImageUrl: String, + val eventItem: EventItem?, + val creatorId: Long?, + val link: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCuration.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCuration.kt new file mode 100644 index 0000000..83ec820 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCuration.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.content.main.curation + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.AudioContent +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.OneToMany +import javax.persistence.Table + +@Entity +@Table(name = "content_curation") +data class AudioContentCuration( + @Column(nullable = false) + var title: String, + @Column(nullable = false) + var description: String, + @Column(nullable = false) + var isAdult: Boolean = false, + @Column(nullable = false) + var isActive: Boolean = true, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() { + @OneToMany(mappedBy = "curation") + val audioContents: MutableList = mutableListOf() +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/GetAudioContentCurationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/GetAudioContentCurationResponse.kt new file mode 100644 index 0000000..802e9e4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/GetAudioContentCurationResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.content.main.curation + +import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem + +data class GetAudioContentCurationResponse( + val title: String, + val description: String, + val contents: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/GetAudioContentOrderListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/GetAudioContentOrderListResponse.kt new file mode 100644 index 0000000..c762fae --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/GetAudioContentOrderListResponse.kt @@ -0,0 +1,20 @@ +package kr.co.vividnext.sodalive.content.order + +import com.querydsl.core.annotations.QueryProjection + +data class GetAudioContentOrderListResponse( + val totalCount: Int, + val items: List +) + +data class GetAudioContentOrderListItem @QueryProjection constructor( + val contentId: Long, + val coverImageUrl: String, + val title: String, + val themeStr: String, + val duration: String?, + val isAdult: Boolean, + val orderType: OrderType, + var likeCount: Int = 0, + var commentCount: Int = 0 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/Order.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/Order.kt new file mode 100644 index 0000000..7b778cd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/Order.kt @@ -0,0 +1,58 @@ +package kr.co.vividnext.sodalive.content.order + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.member.Member +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.Table +import kotlin.math.ceil + +enum class OrderType { + RENTAL, KEEP +} + +@Entity +@Table(name = "orders") +data class Order( + @Enumerated(value = EnumType.STRING) + val type: OrderType, + var isActive: Boolean = true +) : BaseEntity() { + var can: Int = 0 + + val startDate: LocalDateTime = LocalDateTime.now() + var endDate: LocalDateTime? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "creator_id", nullable = false) + var creator: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var audioContent: AudioContent? = null + set(value) { + can = if (type == OrderType.RENTAL) { + ceil(value?.price!! * 0.7).toInt() + } else { + value?.price!! + } + field = value + } + + override fun prePersist() { + super.prePersist() + if (type == OrderType.RENTAL) { + endDate = startDate.plusDays(7) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderController.kt new file mode 100644 index 0000000..89c1e08 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderController.kt @@ -0,0 +1,49 @@ +package kr.co.vividnext.sodalive.content.order + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +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 +@RequestMapping("/order") +class OrderController(private val service: OrderService) { + @PostMapping("/audio-content") + fun order( + @RequestBody request: OrderRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.order( + request.contentId, + request.orderType, + request.container, + member + ) + ) + } + + @GetMapping("/audio-content") + fun getAudioContentOrderList( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.getAudioContentOrderList( + member = member, + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt new file mode 100644 index 0000000..b76ef6f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt @@ -0,0 +1,215 @@ +package kr.co.vividnext.sodalive.content.order + +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem +import kr.co.vividnext.sodalive.content.order.QOrder.order +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.Duration +import java.time.LocalDateTime + +@Repository +interface OrderRepository : JpaRepository, OrderQueryRepository + +interface OrderQueryRepository { + fun isExistOrdered(memberId: Long, contentId: Long): Boolean + fun isExistOrderedAndOrderType(memberId: Long, contentId: Long): Pair + fun getAudioContentRemainingTime(memberId: Long, contentId: Long, timezone: String): String + fun getAudioContentOrderList( + dateTime: LocalDateTime, + coverImageHost: String, + memberId: Long, + offset: Long = 0, + limit: Long = 10 + ): List + + fun totalAudioContentOrderListCount(memberId: Long, dateTime: LocalDateTime): Int + fun getAudioContentMainOrderList( + dateTime: LocalDateTime, + coverImageHost: String, + memberId: Long, + offset: Long = 0, + limit: Long = 20 + ): List +} + +@Repository +class OrderQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : OrderQueryRepository { + override fun isExistOrdered(memberId: Long, contentId: Long): Boolean { + return queryFactory + .select(order.id) + .from(order) + .where( + order.member.id.eq(memberId) + .and(order.audioContent.id.eq(contentId)) + .and(order.isActive.isTrue) + .and( + order.type.eq(OrderType.KEEP) + .or( + order.type.eq(OrderType.RENTAL) + .and(order.endDate.after(LocalDateTime.now())) + ) + ) + ) + .fetch() + .isNotEmpty() + } + + override fun isExistOrderedAndOrderType(memberId: Long, contentId: Long): Pair { + val result = queryFactory + .select(order.type) + .from(order) + .where( + order.member.id.eq(memberId) + .and(order.audioContent.id.eq(contentId)) + .and(order.isActive.isTrue) + .and( + order.type.eq(OrderType.KEEP) + .or( + order.type.eq(OrderType.RENTAL) + .and(order.endDate.after(LocalDateTime.now())) + ) + ) + ) + .fetch() + + val isExist = result.isNotEmpty() + + return if (!isExist) { + Pair(false, null) + } else { + if (result.contains(OrderType.KEEP)) { + Pair(true, OrderType.KEEP) + } else { + Pair(true, OrderType.RENTAL) + } + } + } + + override fun getAudioContentRemainingTime(memberId: Long, contentId: Long, timezone: String): String { + val result = queryFactory + .select(order.endDate) + .from(order) + .where( + order.member.id.eq(memberId) + .and(order.audioContent.id.eq(contentId)) + .and(order.isActive.isTrue) + .and( + order.type.eq(OrderType.KEEP) + .or( + order.type.eq(OrderType.RENTAL) + .and(order.endDate.after(LocalDateTime.now())) + ) + ) + ) + .fetchFirst() ?: return "" + + val duration = Duration.between(LocalDateTime.now(), result) + + return duration.toHours().toString().padStart(2, '0') + "시간 " + + duration.toMinutesPart().toString().padStart(2, '0') + "분" + } + + override fun getAudioContentOrderList( + dateTime: LocalDateTime, + coverImageHost: String, + memberId: Long, + offset: Long, + limit: Long + ): List { + return queryFactory + .select( + QGetAudioContentOrderListItem( + audioContent.id, + audioContent.coverImage.prepend("/").prepend(coverImageHost), + audioContent.title, + audioContent.theme.theme, + audioContent.duration, + audioContent.isAdult, + order.type, + Expressions.ZERO, + Expressions.ZERO + ) + ) + .from(order) + .innerJoin(order.audioContent, audioContent) + .where( + order.member.id.eq(memberId) + .and( + order.type.eq(OrderType.KEEP) + .or( + order.type.eq(OrderType.RENTAL) + .and(order.startDate.before(dateTime)) + .and(order.endDate.after(dateTime)) + ) + ) + ) + .offset(offset) + .limit(limit) + .orderBy(order.createdAt.desc()) + .fetch() + } + + override fun totalAudioContentOrderListCount(memberId: Long, dateTime: LocalDateTime): Int { + return queryFactory.select(order.id) + .from(order) + .where( + order.member.id.eq(memberId) + .and( + order.type.eq(OrderType.KEEP) + .or( + order.type.eq(OrderType.RENTAL) + .and(order.startDate.before(dateTime)) + .and(order.endDate.after(dateTime)) + ) + ) + ) + .fetch() + .size + } + + override fun getAudioContentMainOrderList( + dateTime: LocalDateTime, + coverImageHost: String, + memberId: Long, + offset: Long, + limit: Long + ): List { + return queryFactory + .select( + QGetAudioContentMainItem( + audioContent.id, + audioContent.coverImage.prepend("/").prepend(coverImageHost), + audioContent.title, + audioContent.isAdult, + member.id, + member.profileImage.nullif("profile/default-profile.png") + .prepend("/") + .prepend(coverImageHost), + member.nickname + ) + ) + .from(order) + .innerJoin(order.audioContent, audioContent) + .innerJoin(audioContent.member, member) + .where( + order.member.id.eq(memberId) + .and( + order.type.eq(OrderType.KEEP) + .or( + order.type.eq(OrderType.RENTAL) + .and(order.startDate.before(dateTime)) + .and(order.endDate.after(dateTime)) + ) + ) + ) + .offset(offset) + .limit(limit) + .orderBy(order.createdAt.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRequest.kt new file mode 100644 index 0000000..9fbc616 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.content.order + +data class OrderRequest(val contentId: Long, val orderType: OrderType, val container: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt new file mode 100644 index 0000000..1112fcd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt @@ -0,0 +1,99 @@ +package kr.co.vividnext.sodalive.content.order + +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository +import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository +import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem +import kr.co.vividnext.sodalive.member.Member +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional(readOnly = true) +class OrderService( + private val repository: OrderRepository, + private val coinPaymentService: CanPaymentService, + private val audioContentRepository: AudioContentRepository, + private val audioContentCommentQueryRepository: AudioContentCommentRepository, + private val audioContentLikeQueryRepository: AudioContentLikeRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val audioContentCoverImageHost: String +) { + @Transactional + fun order(contentId: Long, orderType: OrderType, container: String, member: Member) { + val order = Order(type = orderType) + val content = audioContentRepository.findByIdAndActive(contentId) + ?: throw SodaException("잘못된 콘텐츠 입니다\n다시 시도해 주세요.") + + if (member.id!! == content.member!!.id!!) throw SodaException("자신이 올린 콘텐츠는 구매할 수 없습니다.") + if (repository.isExistOrdered(memberId = member.id!!, contentId = contentId)) { + throw SodaException("이미 구매한 콘텐츠 입니다.") + } + + order.member = member + order.creator = content.member + order.audioContent = content + + repository.save(order) + + coinPaymentService.spendCan( + memberId = member.id!!, + needCan = order.can, + canUsage = CanUsage.ORDER_CONTENT, + order = order, + container = container + ) + } + + fun getAudioContentOrderList( + member: Member, + offset: Long, + limit: Long + ): GetAudioContentOrderListResponse { + val totalCount = repository.totalAudioContentOrderListCount( + memberId = member.id!!, + dateTime = LocalDateTime.now() + ) + val orderItems = repository.getAudioContentOrderList( + dateTime = LocalDateTime.now(), + coverImageHost = audioContentCoverImageHost, + memberId = member.id!!, + offset = offset, + limit = limit + ) + .asSequence() + .map { + val commentCount = audioContentCommentQueryRepository + .totalCountCommentByContentId(it.contentId) + + val likeCount = audioContentLikeQueryRepository + .totalCountAudioContentLike(it.contentId) + + it.commentCount = commentCount + it.likeCount = likeCount + it + } + .toList() + + return GetAudioContentOrderListResponse( + totalCount = totalCount, + items = orderItems + ) + } + + fun getAudioContentMainOrderList(member: Member, limit: Int): List { + return repository.getAudioContentMainOrderList( + dateTime = LocalDateTime.now(), + coverImageHost = audioContentCoverImageHost, + memberId = member.id!!, + offset = 0, + limit = 20 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentTheme.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentTheme.kt new file mode 100644 index 0000000..621b444 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentTheme.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.content.theme + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.Table + +@Entity +@Table(name = "content_theme") +data class AudioContentTheme( + @Column(nullable = false) + var theme: String, + @Column(nullable = false) + var image: String, + @Column(nullable = false) + var isActive: Boolean = true, + @Column(nullable = false) + var orders: Int = 1 +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeController.kt new file mode 100644 index 0000000..7b13530 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeController.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.content.theme + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/audio-content/theme") +@PreAuthorize("hasRole('CREATOR')") +class AudioContentThemeController(private val service: AudioContentThemeService) { + @GetMapping + fun getThemes( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getThemes()) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeQueryRepository.kt new file mode 100644 index 0000000..e5cba07 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeQueryRepository.kt @@ -0,0 +1,57 @@ +package kr.co.vividnext.sodalive.content.theme + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Repository + +@Repository +class AudioContentThemeQueryRepository( + private val queryFactory: JPAQueryFactory, + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun getActiveThemes(): List { + return queryFactory + .select( + QGetAudioContentThemeResponse( + audioContentTheme.id, + audioContentTheme.theme, + audioContentTheme.image.prepend("/").prepend(cloudFrontHost) + ) + ) + .from(audioContentTheme) + .where(audioContentTheme.isActive.isTrue) + .orderBy(audioContentTheme.orders.asc()) + .fetch() + } + + fun getActiveThemeOfContent(isAdult: Boolean = false): List { + var where = audioContent.isActive.isTrue + .and(audioContentTheme.isActive.isTrue) + + if (!isAdult) { + where = where.and(audioContent.isAdult.isFalse) + } + + return queryFactory + .select(audioContentTheme.theme) + .from(audioContent) + .innerJoin(audioContent.theme, audioContentTheme) + .where(where) + .groupBy(audioContentTheme.id) + .orderBy(audioContentTheme.orders.asc()) + .fetch() + } + + fun findThemeByIdAndActive(id: Long): AudioContentTheme? { + return queryFactory + .selectFrom(audioContentTheme) + .where( + audioContentTheme.id.eq(id) + .and(audioContentTheme.isActive.isTrue) + ) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt new file mode 100644 index 0000000..f8b2fb0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.content.theme + +import org.springframework.stereotype.Service + +@Service +class AudioContentThemeService(private val queryRepository: AudioContentThemeQueryRepository) { + fun getThemes(): List { + return queryRepository.getActiveThemes() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/GetAudioContentThemeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/GetAudioContentThemeResponse.kt new file mode 100644 index 0000000..e9e07c5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/GetAudioContentThemeResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.content.theme + +import com.querydsl.core.annotations.QueryProjection + +data class GetAudioContentThemeResponse @QueryProjection constructor( + val id: Long, + val theme: String, + val image: String +) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4dfbeac..c33f8dc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,7 +26,12 @@ cloud: accessKey: ${APP_AWS_ACCESS_KEY} secretKey: ${APP_AWS_SECRET_KEY} s3: + contentBucket: ${S3_CONTENT_BUCKET} bucket: ${S3_BUCKET} + contentCloudFront: + host: ${CONTENT_CLOUD_FRONT_HOST} + privateKeyFilePath: ${CONTENT_CLOUD_FRONT_PRIVATE_KEY_FILE_PATH} + keyPairId: ${CONTENT_CLOUD_FRONT_KEY_PAIR_ID} cloudFront: host: ${CLOUD_FRONT_HOST} region: From b12fba992c39110c7fd476ed3d56467601d7bb8e Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 4 Aug 2023 01:45:56 +0900 Subject: [PATCH 49/94] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=20=EC=99=84=EB=A3=8C=20-=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/content/AudioContentController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt index 0d32d5e..5a9deda 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -66,7 +66,7 @@ class AudioContentController(private val service: AudioContentService) { } @PutMapping("/upload-complete") - @PreAuthorize("hasRole('ADMIN')") + @PreAuthorize("hasAnyRole('ADMIN', 'BOT')") fun uploadComplete( @RequestBody request: UploadCompleteRequest, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? From a009a728a87919109b3b282cdb579aeca60f9a6f Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 4 Aug 2023 01:51:00 +0900 Subject: [PATCH 50/94] =?UTF-8?q?jsr305=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 78ca8b7..dfb9490 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.9.3") implementation("org.json:json:20230227") + implementation("com.google.code.findbugs:jsr305:3.0.2") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") From 03d782850c0cc22955ba762d6223a4ff770fe0d0 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 4 Aug 2023 21:51:46 +0900 Subject: [PATCH 51/94] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20-=20MemberT?= =?UTF-8?q?oken=20init=20=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt | 4 ++-- src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt index 172a24a..986869f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -61,6 +61,8 @@ class SecurityConfig( .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) + .addFilterBefore(ExceptionHandlerFilter(objectMapper), JwtFilter::class.java) .authorizeRequests() .antMatchers("/member/check/email").permitAll() .antMatchers("/member/check/nickname").permitAll() @@ -71,8 +73,6 @@ class SecurityConfig( .antMatchers("/stplat/privacy_policy").permitAll() .anyRequest().authenticated() .and() - .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java) - .addFilterBefore(ExceptionHandlerFilter(objectMapper), JwtFilter::class.java) .build() } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt index 0c12ad9..24499f1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt @@ -66,7 +66,7 @@ class TokenProvider( val lock = getOrCreateLock(memberId = memberId) lock.write { val memberToken = tokenRepository.findByIdOrNull(memberId) - ?: MemberToken(id = memberId, listOf()) + ?: MemberToken(id = memberId, tokenList = mutableListOf()) val memberTokenSet = memberToken.tokenList.toMutableSet() memberTokenSet.add(token) From 88df83fdc00b3ed80f36dff681936e1ab4fb920a Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 4 Aug 2023 22:32:34 +0900 Subject: [PATCH 52/94] =?UTF-8?q?=EB=A9=94=EB=89=B4=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/menu/GetMenuResponse.kt | 12 +++++ .../kr/co/vividnext/sodalive/menu/Menu.kt | 32 +++++++++++++ .../vividnext/sodalive/menu/MenuController.kt | 17 +++++++ .../vividnext/sodalive/menu/MenuRepository.kt | 47 +++++++++++++++++++ .../co/vividnext/sodalive/menu/MenuService.kt | 18 +++++++ 5 files changed, 126 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/menu/GetMenuResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/menu/Menu.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/menu/GetMenuResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/menu/GetMenuResponse.kt new file mode 100644 index 0000000..254ac78 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/menu/GetMenuResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.menu + +import com.fasterxml.jackson.annotation.JsonInclude +import com.querydsl.core.annotations.QueryProjection + +data class GetMenuResponse @QueryProjection constructor( + val title: String, + @JsonInclude(JsonInclude.Include.NON_NULL) + val route: String? = null, + @JsonInclude(JsonInclude.Include.NON_NULL) + val items: List? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/menu/Menu.kt b/src/main/kotlin/kr/co/vividnext/sodalive/menu/Menu.kt new file mode 100644 index 0000000..d790707 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/menu/Menu.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.menu + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.MemberRole +import javax.persistence.CascadeType +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany + +@Entity +data class Menu( + @Column(nullable = false) + val title: String, + @Column(nullable = false) + val route: String, + @Enumerated(value = EnumType.STRING) + val roles: MemberRole, + val orders: Int, + val isActive: Boolean +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinColumn(name = "parent_id", nullable = true) + var parent: Menu? = null + + @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) + var children: MutableList = mutableListOf() +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuController.kt new file mode 100644 index 0000000..5146119 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuController.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.menu + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.security.core.userdetails.User +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/menu") +class MenuController(private val service: MenuService) { + @GetMapping + @PreAuthorize("hasAnyRole('AGENT', 'ADMIN')") + fun getMenus(@AuthenticationPrincipal user: User) = ApiResponse.ok(service.getMenus(user)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuRepository.kt new file mode 100644 index 0000000..c5eb43f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuRepository.kt @@ -0,0 +1,47 @@ +package kr.co.vividnext.sodalive.menu + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.menu.QMenu.menu +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MenuRepository : JpaRepository, MenuQueryRepository + +interface MenuQueryRepository { + fun getMenu(role: MemberRole): List +} + +class MenuQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MenuQueryRepository { + override fun getMenu(role: MemberRole): List { + return queryFactory + .selectFrom(menu) + .where( + menu.isActive.isTrue + .and(menu.roles.eq(role)) + .and( + (menu.route.isNotNull.and(menu.parent.isNull)) + .or(menu.route.isNull.and(menu.children.isNotEmpty)) + ) + ) + .orderBy(menu.orders.asc()) + .fetch() + .asSequence() + .map { + GetMenuResponse( + title = it.title, + route = it.route, + items = if (it.children.isNotEmpty()) { + it.children + .asSequence() + .map { menu -> GetMenuResponse(menu.title, menu.route) } + .toList() + } else { + null + } + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuService.kt new file mode 100644 index 0000000..bb7dc6e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/menu/MenuService.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.menu + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.security.core.userdetails.User +import org.springframework.stereotype.Service + +@Service +class MenuService( + private val repository: MenuRepository, + private val memberRepository: MemberRepository +) { + fun getMenus(user: User): List { + val member = memberRepository.findByEmail(user.username) + ?: throw SodaException("로그인 정보를 확인해주세요.") + return repository.getMenu(member.role) + } +} From fcd435f4706ac52d1692221f3897078d397c1ae9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 4 Aug 2023 22:42:14 +0900 Subject: [PATCH 53/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=ED=85=8C=EB=A7=88=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../theme/AdminContentThemeController.kt | 36 ++++++++++ .../theme/AdminContentThemeRepository.kt | 48 +++++++++++++ .../content/theme/AdminContentThemeService.kt | 70 +++++++++++++++++++ .../theme/CreateContentThemeRequest.kt | 3 + .../content/theme/UpdateThemeOrdersRequest.kt | 5 ++ 5 files changed, 162 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/CreateContentThemeRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/UpdateThemeOrdersRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeController.kt new file mode 100644 index 0000000..354aabb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeController.kt @@ -0,0 +1,36 @@ +package kr.co.vividnext.sodalive.admin.content.theme + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +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 +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/admin/audio-content/theme") +@PreAuthorize("hasRole('ADMIN')") +class AdminContentThemeController(private val service: AdminContentThemeService) { + @PostMapping + fun enrollmentTheme( + @RequestPart("image") image: MultipartFile, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.uploadThemeImage(image, requestString), "등록되었습니다.") + + @DeleteMapping("/{id}") + fun deleteTheme(@PathVariable id: Long) = ApiResponse.ok(service.deleteTheme(id), "삭제되었습니다.") + + @PutMapping("/orders") + fun updateTagOrders( + @RequestBody request: UpdateThemeOrdersRequest + ) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.") + + @GetMapping + fun getThemes() = ApiResponse.ok(service.getThemes()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeRepository.kt new file mode 100644 index 0000000..77f2af2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeRepository.kt @@ -0,0 +1,48 @@ +package kr.co.vividnext.sodalive.admin.content.theme + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.theme.AudioContentTheme +import kr.co.vividnext.sodalive.content.theme.GetAudioContentThemeResponse +import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme +import kr.co.vividnext.sodalive.content.theme.QGetAudioContentThemeResponse +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AdminContentThemeRepository : JpaRepository, AdminContentThemeQueryRepository + +interface AdminContentThemeQueryRepository { + fun findIdByTheme(theme: String): Long? + fun getActiveThemes(): List +} + +class AdminContentThemeQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) : AdminContentThemeQueryRepository { + override fun findIdByTheme(theme: String): Long? { + return queryFactory + .select(audioContentTheme.id) + .from(audioContentTheme) + .where(audioContentTheme.theme.eq(theme)) + .fetchOne() + } + + override fun getActiveThemes(): List { + return queryFactory + .select( + QGetAudioContentThemeResponse( + audioContentTheme.id, + audioContentTheme.theme, + audioContentTheme.image.prepend("/").prepend(cloudFrontHost) + ) + ) + .from(audioContentTheme) + .where(audioContentTheme.isActive.isTrue) + .orderBy(audioContentTheme.orders.asc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeService.kt new file mode 100644 index 0000000..00e6237 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/AdminContentThemeService.kt @@ -0,0 +1,70 @@ +package kr.co.vividnext.sodalive.admin.content.theme + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.theme.AudioContentTheme +import kr.co.vividnext.sodalive.content.theme.GetAudioContentThemeResponse +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +class AdminContentThemeService( + private val s3Uploader: S3Uploader, + private val objectMapper: ObjectMapper, + private val repository: AdminContentThemeRepository, + + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String +) { + @Transactional + fun uploadThemeImage(image: MultipartFile, requestString: String) { + val request = objectMapper.readValue(requestString, CreateContentThemeRequest::class.java) + themeExistCheck(request) + + val fileName = generateFileName() + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "audio_content_theme/$fileName" + ) + + return createTheme(request.theme, imagePath) + } + + fun createTheme(theme: String, imagePath: String) { + repository.save(AudioContentTheme(theme = theme, image = imagePath)) + } + + fun themeExistCheck(request: CreateContentThemeRequest) { + repository.findIdByTheme(request.theme)?.let { throw SodaException("이미 등록된 테마 입니다.") } + } + + @Transactional + fun deleteTheme(id: Long) { + val theme = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + theme.theme = "${theme.theme}_deleted" + theme.isActive = false + } + + @Transactional + fun updateTagOrders(ids: List) { + for (index in ids.indices) { + val theme = repository.findByIdOrNull(ids[index]) + + if (theme != null) { + theme.orders = index + 1 + } + } + } + + fun getThemes(): List { + return repository.getActiveThemes() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/CreateContentThemeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/CreateContentThemeRequest.kt new file mode 100644 index 0000000..9df9b30 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/CreateContentThemeRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.admin.content.theme + +data class CreateContentThemeRequest(val theme: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/UpdateThemeOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/UpdateThemeOrdersRequest.kt new file mode 100644 index 0000000..dacdc47 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/theme/UpdateThemeOrdersRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.admin.content.theme + +data class UpdateThemeOrdersRequest( + val ids: List +) From c9a9c8c310e3a9e317217e17ea18b1dac87642ee Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 5 Aug 2023 00:15:05 +0900 Subject: [PATCH 54/94] =?UTF-8?q?=EC=83=88=EB=A1=9C=EC=9A=B4=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=EB=A5=BC=20=EC=98=AC=EB=A6=B0=20=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=20-=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=EA=B0=80=20null?= =?UTF-8?q?=EC=9D=B8=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/AudioContentRepository.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 72ac833..d29aae7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -7,7 +7,6 @@ import kr.co.vividnext.sodalive.content.QBundleAudioContent.bundleAudioContent import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem import kr.co.vividnext.sodalive.content.main.GetNewContentUploadCreator import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem -import kr.co.vividnext.sodalive.content.main.QGetNewContentUploadCreator import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration @@ -261,13 +260,7 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) } return queryFactory - .select( - QGetNewContentUploadCreator( - member.id, - member.nickname, - member.profileImage.nullif("profile/default-profile.png").prepend("$cloudfrontHost/") - ) - ) + .select(member) .from(audioContent) .innerJoin(audioContent.member, member) .where(where) @@ -275,6 +268,19 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) .limit(20) .fetch() + .asSequence() + .map { + GetNewContentUploadCreator( + it.id!!, + it.nickname, + creatorProfileImageUrl = if (it.profileImage != null) { + "$cloudfrontHost/${it.profileImage}" + } else { + "$cloudfrontHost/profile/default-profile.png" + } + ) + } + .toList() } override fun getAudioContentMainBannerList(isAdult: Boolean): List { From 9b4e2fd192362a64ee21c1c20ed78f4082bec38f Mon Sep 17 00:00:00 2001 From: Klaus Date: Sat, 5 Aug 2023 00:58:45 +0900 Subject: [PATCH 55/94] =?UTF-8?q?cloudfront=20-=20https=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt b/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt index a43f938..0f50e56 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt @@ -31,7 +31,7 @@ class AudioContentCloudFront( // Generate signed URL for resource with custom policy and expiration time return CloudFrontUrlSigner.getSignedURLWithCannedPolicy( - "https://$cloudfrontDomain/$resourcePath", // Resource URL + "$cloudfrontDomain/$resourcePath", // Resource URL keyPairId, // CloudFront key pair ID privateKey, // CloudFront private key Date(System.currentTimeMillis() + expirationTime) // Expiration date From cbcc63dc71bc88045a1045062176051caec9109a Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 11:13:27 +0900 Subject: [PATCH 56/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/member/AdminMemberController.kt | 23 +++++ .../admin/member/AdminMemberRepository.kt | 95 +++++++++++++++++++ .../admin/member/AdminMemberService.kt | 92 ++++++++++++++++++ .../member/GetAdminMemberListResponse.kt | 19 ++++ 4 files changed, 229 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt new file mode 100644 index 0000000..78389da --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -0,0 +1,23 @@ +package kr.co.vividnext.sodalive.admin.member + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/admin/member") +class AdminMemberController(private val service: AdminMemberService) { + @GetMapping("/list") + @PreAuthorize("hasRole('ADMIN')") + fun getMemberList(pageable: Pageable) = ApiResponse.ok(service.getMemberList(pageable)) + + @GetMapping("/search") + fun searchMember( + @RequestParam(value = "search_word") searchWord: String, + pageable: Pageable + ) = ApiResponse.ok(service.searchMember(searchWord, pageable)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt new file mode 100644 index 0000000..9d20fe9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt @@ -0,0 +1,95 @@ +package kr.co.vividnext.sodalive.admin.member + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository + +interface AdminMemberRepository : JpaRepository, AdminMemberQueryRepository + +interface AdminMemberQueryRepository { + fun getMemberTotalCount(role: MemberRole? = null): Int + fun getMemberList(offset: Long, limit: Long, role: MemberRole? = null): List + fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole? = null): List + + fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int +} + +class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository { + override fun getMemberList(offset: Long, limit: Long, role: MemberRole?): List { + return queryFactory + .selectFrom(member) + .where( + member.role.ne(MemberRole.ADMIN) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .offset(offset) + .limit(limit) + .orderBy(member.id.desc()) + .fetch() + } + + override fun getMemberTotalCount(role: MemberRole?): Int { + return queryFactory + .select(member.id) + .from(member) + .where( + member.id.gt(1) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .fetch() + .size + } + + override fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole?): List { + return queryFactory + .selectFrom(member) + .where( + member.nickname.contains(searchWord) + .or(member.email.contains(searchWord)) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .offset(offset) + .limit(limit) + .orderBy(member.id.desc()) + .fetch() + } + + override fun searchMemberTotalCount(searchWord: String, role: MemberRole?): Int { + return queryFactory + .select(member.id) + .from(member) + .where( + member.nickname.contains(searchWord) + .or(member.email.contains(searchWord)) + .and( + if (role != null) { + member.role.eq(role) + } else { + null + } + ) + ) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt new file mode 100644 index 0000000..49f1d46 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -0,0 +1,92 @@ +package kr.co.vividnext.sodalive.admin.member + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class AdminMemberService( + private val repository: AdminMemberRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + fun getMemberList(pageable: Pageable): GetAdminMemberListResponse { + val totalCount = repository.getMemberTotalCount() + val memberList = processMemberListToGetAdminMemberListResponseItemList( + memberList = repository.getMemberList( + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + + return GetAdminMemberListResponse(totalCount, memberList) + } + + fun searchMember(searchWord: String, pageable: Pageable): GetAdminMemberListResponse { + if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") + val totalCount = repository.searchMemberTotalCount(searchWord = searchWord) + val memberList = processMemberListToGetAdminMemberListResponseItemList( + memberList = repository.searchMember( + searchWord = searchWord, + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) + + return GetAdminMemberListResponse(totalCount, memberList) + } + + private fun processMemberListToGetAdminMemberListResponseItemList( + memberList: List + ): List { + return memberList + .asSequence() + .map { + val userType = when (it.role) { + MemberRole.ADMIN -> "관리자" + MemberRole.USER -> "일반회원" + MemberRole.CREATOR -> "요즘친구" + MemberRole.AGENT -> "에이전트" + MemberRole.BOT -> "봇" + } + + val signUpDate = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + + val signOutDate = if (it.signOutReasons.isNotEmpty()) { + it.signOutReasons.last().createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + } else { + "" + } + + GetAdminMemberListResponseItem( + id = it.id!!, + email = it.email, + nickname = it.nickname, + profileUrl = if (it.profileImage != null) { + "$cloudFrontHost/${it.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + userType = userType, + container = it.container, + auth = it.auth != null, + signUpDate = signUpDate, + signOutDate = signOutDate, + isActive = it.isActive + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt new file mode 100644 index 0000000..1fd1c47 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.admin.member + +data class GetAdminMemberListResponse( + val totalCount: Int, + val items: List +) + +data class GetAdminMemberListResponseItem( + val id: Long, + val email: String, + val nickname: String, + val profileUrl: String, + val userType: String, + val container: String, + val auth: Boolean, + val signUpDate: String, + val signOutDate: String, + val isActive: Boolean +) From 841e32a50b09b4c46a8a14af42c7a37cbed794d8 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 14:01:09 +0900 Subject: [PATCH 57/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=ED=83=9C=EA=B7=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/tag/AdminMemberTagController.kt | 42 ++++++++++ .../member/tag/AdminMemberTagRepository.kt | 22 +++++ .../admin/member/tag/AdminMemberTagService.kt | 83 +++++++++++++++++++ .../member/tag/CreateMemberTagRequest.kt | 3 + .../member/tag/UpdateTagOrdersRequest.kt | 3 + .../member/tag/GetMemberTagResponse.kt | 9 ++ .../member/tag/MemberTagController.kt | 13 +++ .../member/tag/MemberTagRepository.kt | 34 ++++++++ .../sodalive/member/tag/MemberTagService.kt | 10 +++ 9 files changed, 219 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/CreateMemberTagRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/UpdateTagOrdersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/tag/GetMemberTagResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt new file mode 100644 index 0000000..d78139e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt @@ -0,0 +1,42 @@ +package kr.co.vividnext.sodalive.admin.member.tag + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.DeleteMapping +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 +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/admin/member/tag") +class AdminMemberTagController(private val service: AdminMemberTagService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun enrollmentCreatorTag( + @RequestPart("image") image: MultipartFile, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.uploadTagImage(image, requestString), "등록되었습니다.") + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun deleteCreatorTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.") + + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun modifyCreatorTag( + @PathVariable id: Long, + @RequestPart("image") image: MultipartFile?, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.") + + @PutMapping("/orders") + @PreAuthorize("hasRole('ADMIN')") + fun updateTagOrders( + @RequestBody request: UpdateTagOrdersRequest + ) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.") +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagRepository.kt new file mode 100644 index 0000000..76061c7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagRepository.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.admin.member.tag + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.tag.CreatorTag +import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag +import org.springframework.data.jpa.repository.JpaRepository + +interface AdminMemberTagRepository : JpaRepository, AdminMemberTagQueryRepository + +interface AdminMemberTagQueryRepository { + fun findByTag(tag: String): Long? +} + +class AdminMemberTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberTagQueryRepository { + override fun findByTag(tag: String): Long? { + return queryFactory + .select(creatorTag.id) + .from(creatorTag) + .where(creatorTag.tag.eq(tag)) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagService.kt new file mode 100644 index 0000000..0e60a59 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagService.kt @@ -0,0 +1,83 @@ +package kr.co.vividnext.sodalive.admin.member.tag + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.tag.CreatorTag +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +class AdminMemberTagService( + private val repository: AdminMemberTagRepository, + private val s3Uploader: S3Uploader, + private val objectMapper: ObjectMapper, + + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String +) { + @Transactional + fun uploadTagImage(image: MultipartFile, requestString: String) { + val request = objectMapper.readValue(requestString, CreateMemberTagRequest::class.java) + tagExistCheck(request) + + val fileName = generateFileName() + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "creator_tag/$fileName" + ) + return createTag(request.tag, imagePath) + } + + private fun tagExistCheck(request: CreateMemberTagRequest) { + repository.findByTag(request.tag)?.let { throw SodaException("이미 등록된 태그 입니다.") } + } + + private fun createTag(tag: String, imagePath: String) { + repository.save(CreatorTag(tag, imagePath)) + } + + @Transactional + fun deleteTag(id: Long) { + val creatorTag = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + creatorTag.tag = "${creatorTag.tag}_deleted" + creatorTag.isActive = false + } + + @Transactional + fun modifyTag(id: Long, image: MultipartFile?, requestString: String) { + val creatorTag = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + val request = objectMapper.readValue(requestString, CreateMemberTagRequest::class.java) + creatorTag.tag = request.tag + + if (image != null) { + val fileName = generateFileName() + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "creator_tag/$fileName" + ) + creatorTag.image = imagePath + } + } + + @Transactional + fun updateTagOrders(ids: List) { + for (index in ids.indices) { + val tag = repository.findByIdOrNull(ids[index]) + + if (tag != null) { + tag.orders = index + 1 + } + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/CreateMemberTagRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/CreateMemberTagRequest.kt new file mode 100644 index 0000000..fa70355 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/CreateMemberTagRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.admin.member.tag + +data class CreateMemberTagRequest(val tag: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/UpdateTagOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/UpdateTagOrdersRequest.kt new file mode 100644 index 0000000..4da263a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/UpdateTagOrdersRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.admin.member.tag + +data class UpdateTagOrdersRequest(val ids: List) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/GetMemberTagResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/GetMemberTagResponse.kt new file mode 100644 index 0000000..a98882b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/GetMemberTagResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.member.tag + +import com.querydsl.core.annotations.QueryProjection + +data class GetMemberTagResponse @QueryProjection constructor( + val id: Long, + val tag: String, + val image: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt new file mode 100644 index 0000000..ff4c7f5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagController.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.member.tag + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/member/tag") +class MemberTagController(private val service: MemberTagService) { + @GetMapping + fun getTags() = ApiResponse.ok(service.getTags()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt new file mode 100644 index 0000000..84a2e62 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.member.tag + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.jpa.repository.JpaRepository + +interface MemberTagRepository : JpaRepository, MemberTagQueryRepository + +interface MemberTagQueryRepository { + fun getTags(): List +} + +class MemberTagQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) : MemberTagQueryRepository { + override fun getTags(): List { + return queryFactory + .select( + QGetMemberTagResponse( + creatorTag.id, + creatorTag.tag, + creatorTag.image.prepend("/").prepend(cloudFrontHost) + ) + ) + .from(creatorTag) + .where(creatorTag.isActive.eq(true)) + .orderBy(creatorTag.orders.asc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagService.kt new file mode 100644 index 0000000..72a51b3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagService.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.member.tag + +import org.springframework.stereotype.Service + +@Service +class MemberTagService(private val repository: MemberTagRepository) { + fun getTags(): List { + return repository.getTags() + } +} From 94551b05ffc20dff557555722be5d60644be261d Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 14:29:36 +0900 Subject: [PATCH 58/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EC=BA=94,=20=EC=B6=A9=EC=A0=84=ED=98=84=ED=99=A9=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/can/AdminCanChargeRequest.kt | 7 ++ .../sodalive/admin/can/AdminCanController.kt | 24 +++++ .../sodalive/admin/can/AdminCanRepository.kt | 6 ++ .../sodalive/admin/can/AdminCanRequest.kt | 26 +++++ .../sodalive/admin/can/AdminCanService.kt | 56 ++++++++++ .../charge/AdminChargeStatusController.kt | 26 +++++ .../AdminChargeStatusQueryRepository.kt | 90 ++++++++++++++++ .../admin/charge/AdminChargeStatusService.kt | 101 ++++++++++++++++++ .../charge/GetChargeStatusDetailQueryDto.kt | 12 +++ .../charge/GetChargeStatusDetailResponse.kt | 9 ++ .../admin/charge/GetChargeStatusQueryDto.kt | 12 +++ .../admin/charge/GetChargeStatusResponse.kt | 8 ++ .../admin/member/AdminMemberController.kt | 2 +- .../member/tag/AdminMemberTagController.kt | 5 +- .../sodalive/extensions/NumberExtensions.kt | 5 + 15 files changed, 384 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt new file mode 100644 index 0000000..bbb8fd0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanChargeRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.admin.can + +data class AdminCanChargeRequest( + val memberId: Long, + val method: String, + val can: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt new file mode 100644 index 0000000..042eb29 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanController.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.DeleteMapping +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 +@RequestMapping("/admin/can") +@PreAuthorize("hasRole('ADMIN')") +class AdminCanController(private val service: AdminCanService) { + @PostMapping + fun insertCan(@RequestBody request: AdminCanRequest) = ApiResponse.ok(service.saveCan(request)) + + @DeleteMapping("/{id}") + fun deleteCan(@PathVariable id: Long) = ApiResponse.ok(service.deleteCan(id)) + + @PostMapping("/charge") + fun charge(@RequestBody request: AdminCanChargeRequest) = ApiResponse.ok(service.charge(request)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt new file mode 100644 index 0000000..e784d13 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRepository.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.can.Can +import org.springframework.data.jpa.repository.JpaRepository + +interface AdminCanRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt new file mode 100644 index 0000000..92258ff --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanRequest.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.can.Can +import kr.co.vividnext.sodalive.can.CanStatus +import kr.co.vividnext.sodalive.extensions.moneyFormat + +data class AdminCanRequest( + val can: Int, + val rewardCan: Int, + val price: Int +) { + fun toEntity(): Can { + var title = "${can.moneyFormat()} 캔" + if (rewardCan > 0) { + title = "$title + ${rewardCan.moneyFormat()} 캔" + } + + return Can( + title = title, + can = can, + rewardCan = rewardCan, + price = price, + status = CanStatus.SALE + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt new file mode 100644 index 0000000..3cdd6e1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt @@ -0,0 +1,56 @@ +package kr.co.vividnext.sodalive.admin.can + +import kr.co.vividnext.sodalive.admin.member.AdminMemberRepository +import kr.co.vividnext.sodalive.can.CanStatus +import kr.co.vividnext.sodalive.can.charge.Charge +import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.payment.Payment +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.extensions.moneyFormat +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AdminCanService( + private val repository: AdminCanRepository, + private val chargeRepository: ChargeRepository, + private val memberRepository: AdminMemberRepository +) { + @Transactional + fun saveCan(request: AdminCanRequest) { + repository.save(request.toEntity()) + } + + @Transactional + fun deleteCan(id: Long) { + val can = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + can.status = CanStatus.END_OF_SALE + } + + @Transactional + fun charge(request: AdminCanChargeRequest) { + val member = memberRepository.findByIdOrNull(request.memberId) + ?: throw SodaException("잘못된 회원번호 입니다.") + + if (request.can <= 0) throw SodaException("0 코인 이상 입력하세요.") + if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.") + + val charge = Charge(0, request.can, status = ChargeStatus.ADMIN) + charge.title = "${request.can.moneyFormat()} 캔" + charge.member = member + + val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG) + payment.method = request.method + charge.payment = payment + + chargeRepository.save(charge) + + member.pgRewardCan += charge.rewardCan + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt new file mode 100644 index 0000000..be859be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusController.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.admin.charge + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@PreAuthorize("hasRole('ADMIN')") +@RequestMapping("/admin/charge/status") +class AdminChargeStatusController(private val service: AdminChargeStatusService) { + @GetMapping + fun getChargeStatus( + @RequestParam startDateStr: String, + @RequestParam endDateStr: String + ) = ApiResponse.ok(service.getChargeStatus(startDateStr, endDateStr)) + + @GetMapping("/detail") + fun getChargeStatusDetail( + @RequestParam startDateStr: String, + @RequestParam paymentGateway: PaymentGateway + ) = ApiResponse.ok(service.getChargeStatusDetail(startDateStr, paymentGateway)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt new file mode 100644 index 0000000..cf2f704 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusQueryRepository.kt @@ -0,0 +1,90 @@ +package kr.co.vividnext.sodalive.admin.charge + +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.QCan.can1 +import kr.co.vividnext.sodalive.can.charge.ChargeStatus +import kr.co.vividnext.sodalive.can.charge.QCharge.charge +import kr.co.vividnext.sodalive.can.payment.PaymentStatus +import kr.co.vividnext.sodalive.can.payment.QPayment.payment +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class AdminChargeStatusQueryRepository(private val queryFactory: JPAQueryFactory) { + fun getChargeStatus(startDate: LocalDateTime, endDate: LocalDateTime): List { + val formattedDate = Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + charge.createdAt, + "UTC", + "Asia/Seoul" + ), + "%Y-%m-%d" + ) + + return queryFactory + .select( + QGetChargeStatusQueryDto( + formattedDate, + payment.price.sum(), + can1.price.sum(), + payment.id.count(), + payment.paymentGateway + ) + ) + .from(payment) + .innerJoin(payment.charge, charge) + .leftJoin(charge.can, can1) + .where( + charge.createdAt.goe(startDate) + .and(charge.createdAt.loe(endDate)) + .and(charge.status.eq(ChargeStatus.CHARGE)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + ) + .groupBy(formattedDate, payment.paymentGateway) + .orderBy(formattedDate.desc()) + .fetch() + } + + fun getChargeStatusDetail(startDate: LocalDateTime, endDate: LocalDateTime): List { + val formattedDate = Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + charge.createdAt, + "UTC", + "Asia/Seoul" + ), + "%Y-%m-%d %H:%i:%s" + ) + + return queryFactory + .select( + QGetChargeStatusDetailQueryDto( + member.id, + member.nickname, + payment.method.coalesce(""), + payment.price, + can1.price, + formattedDate + ) + ) + .from(charge) + .innerJoin(charge.member, member) + .innerJoin(charge.payment, payment) + .leftJoin(charge.can, can1) + .where( + charge.createdAt.goe(startDate) + .and(charge.createdAt.loe(endDate)) + .and(charge.status.eq(ChargeStatus.CHARGE)) + .and(payment.status.eq(PaymentStatus.COMPLETE)) + ) + .orderBy(formattedDate.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt new file mode 100644 index 0000000..1cb97c9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt @@ -0,0 +1,101 @@ +package kr.co.vividnext.sodalive.admin.charge + +import kr.co.vividnext.sodalive.can.payment.PaymentGateway +import org.springframework.stereotype.Service +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository) { + fun getChargeStatus(startDateStr: String, endDateStr: String): List { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val endDate = LocalDate.parse(endDateStr, dateTimeFormatter).atTime(23, 59, 59) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + var totalChargeAmount = 0 + var totalChargeCount = 0L + + val chargeStatusList = repository.getChargeStatus(startDate, endDate) + .asSequence() + .map { + val chargeAmount = if (it.paymentGateWay == PaymentGateway.APPLE_IAP) { + it.appleChargeAmount.toInt() + } else { + it.pgChargeAmount + } + + val chargeCount = it.chargeCount + + totalChargeAmount += chargeAmount + totalChargeCount += chargeCount + + GetChargeStatusResponse( + date = it.date, + chargeAmount = chargeAmount, + chargeCount = chargeCount, + pg = it.paymentGateWay.name + ) + } + .toMutableList() + + chargeStatusList.add( + 0, + GetChargeStatusResponse( + date = "합계", + chargeAmount = totalChargeAmount, + chargeCount = totalChargeCount, + pg = "" + ) + ) + + return chargeStatusList.toList() + } + + fun getChargeStatusDetail( + startDateStr: String, + paymentGateway: PaymentGateway + ): List { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val endDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(23, 59, 59) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + return repository.getChargeStatusDetail(startDate, endDate) + .asSequence() + .filter { + if (paymentGateway == PaymentGateway.APPLE_IAP) { + it.appleChargeAmount > 0 + } else { + it.pgChargeAmount > 0 + } + } + .map { + GetChargeStatusDetailResponse( + accountId = it.accountId, + nickname = it.nickname, + method = it.method, + amount = if (paymentGateway == PaymentGateway.APPLE_IAP) { + it.appleChargeAmount.toInt() + } else { + it.pgChargeAmount + }, + datetime = it.datetime + ) + } + .toList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt new file mode 100644 index 0000000..2d94030 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.charge + +import com.querydsl.core.annotations.QueryProjection + +data class GetChargeStatusDetailQueryDto @QueryProjection constructor( + val accountId: Long, + val nickname: String, + val method: String, + val appleChargeAmount: Double, + val pgChargeAmount: Int, + val datetime: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt new file mode 100644 index 0000000..0d8ae2f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.admin.charge + +data class GetChargeStatusDetailResponse( + val accountId: Long, + val nickname: String, + val method: String, + val amount: Int, + val datetime: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt new file mode 100644 index 0000000..c5c16d0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusQueryDto.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.charge + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.can.payment.PaymentGateway + +data class GetChargeStatusQueryDto @QueryProjection constructor( + val date: String, + val appleChargeAmount: Double, + val pgChargeAmount: Int, + val chargeCount: Long, + val paymentGateWay: PaymentGateway +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt new file mode 100644 index 0000000..efbe3aa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.admin.charge + +data class GetChargeStatusResponse( + val date: String, + val chargeAmount: Int, + val chargeCount: Long, + val pg: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt index 78389da..d3de5c0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -10,9 +10,9 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/admin/member") +@PreAuthorize("hasRole('ADMIN')") class AdminMemberController(private val service: AdminMemberService) { @GetMapping("/list") - @PreAuthorize("hasRole('ADMIN')") fun getMemberList(pageable: Pageable) = ApiResponse.ok(service.getMemberList(pageable)) @GetMapping("/search") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt index d78139e..befa1e4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/tag/AdminMemberTagController.kt @@ -14,20 +14,18 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/admin/member/tag") +@PreAuthorize("hasRole('ADMIN')") class AdminMemberTagController(private val service: AdminMemberTagService) { @PostMapping - @PreAuthorize("hasRole('ADMIN')") fun enrollmentCreatorTag( @RequestPart("image") image: MultipartFile, @RequestPart("request") requestString: String ) = ApiResponse.ok(service.uploadTagImage(image, requestString), "등록되었습니다.") @DeleteMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") fun deleteCreatorTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.") @PutMapping("/{id}") - @PreAuthorize("hasRole('ADMIN')") fun modifyCreatorTag( @PathVariable id: Long, @RequestPart("image") image: MultipartFile?, @@ -35,7 +33,6 @@ class AdminMemberTagController(private val service: AdminMemberTagService) { ) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.") @PutMapping("/orders") - @PreAuthorize("hasRole('ADMIN')") fun updateTagOrders( @RequestBody request: UpdateTagOrdersRequest ) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt new file mode 100644 index 0000000..3665094 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/extensions/NumberExtensions.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.extensions + +import java.text.DecimalFormat + +fun Int.moneyFormat(): String = DecimalFormat("###,###").format(this) From 3d514e8ad4fe96189397676e6f903e7e74328a0a Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 14:40:44 +0900 Subject: [PATCH 59/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=ED=81=AC=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/member/AdminMemberController.kt | 9 ++++++ .../admin/member/AdminMemberRepository.kt | 2 +- .../admin/member/AdminMemberService.kt | 30 ++++++++++++++++++- .../sodalive/live/room/LiveRoomService.kt | 2 +- 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt index d3de5c0..ad6d0d1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -20,4 +20,13 @@ class AdminMemberController(private val service: AdminMemberService) { @RequestParam(value = "search_word") searchWord: String, pageable: Pageable ) = ApiResponse.ok(service.searchMember(searchWord, pageable)) + + @GetMapping("/creator/list") + fun getCreatorList(pageable: Pageable) = ApiResponse.ok(service.getCreatorList(pageable)) + + @GetMapping("/creator/search") + fun searchCreator( + @RequestParam(value = "search_word") searchWord: String, + pageable: Pageable + ) = ApiResponse.ok(service.searchCreator(searchWord, pageable)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt index 9d20fe9..3dd53b0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt @@ -41,7 +41,7 @@ class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .select(member.id) .from(member) .where( - member.id.gt(1) + member.role.ne(MemberRole.ADMIN) .and( if (role != null) { member.role.eq(role) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt index 49f1d46..15256d1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -42,6 +42,34 @@ class AdminMemberService( return GetAdminMemberListResponse(totalCount, memberList) } + fun getCreatorList(pageable: Pageable): GetAdminMemberListResponse { + val totalCount = repository.getMemberTotalCount(role = MemberRole.CREATOR) + val creatorList = processMemberListToGetAdminMemberListResponseItemList( + memberList = repository.getMemberList( + offset = pageable.offset, + limit = pageable.pageSize.toLong(), + role = MemberRole.CREATOR + ) + ) + + return GetAdminMemberListResponse(totalCount, creatorList) + } + + fun searchCreator(searchWord: String, pageable: Pageable): GetAdminMemberListResponse { + if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") + val totalCount = repository.searchMemberTotalCount(searchWord = searchWord, role = MemberRole.CREATOR) + val creatorList = processMemberListToGetAdminMemberListResponseItemList( + memberList = repository.searchMember( + searchWord = searchWord, + offset = pageable.offset, + limit = pageable.pageSize.toLong(), + role = MemberRole.CREATOR + ) + ) + + return GetAdminMemberListResponse(totalCount, creatorList) + } + private fun processMemberListToGetAdminMemberListResponseItemList( memberList: List ): List { @@ -51,7 +79,7 @@ class AdminMemberService( val userType = when (it.role) { MemberRole.ADMIN -> "관리자" MemberRole.USER -> "일반회원" - MemberRole.CREATOR -> "요즘친구" + MemberRole.CREATOR -> "크리에이터" MemberRole.AGENT -> "에이전트" MemberRole.BOT -> "봇" } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index f060fe9..3244c86 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -792,7 +792,7 @@ class LiveRoomService( val host = room.member ?: throw SodaException("잘못된 요청입니다.") if (host.role != MemberRole.CREATOR) { - throw SodaException("비비드넥스트와 계약한\n요즘친구에게만 후원을 하실 수 있습니다.") + throw SodaException("비비드넥스트와 계약한\n크리에이터에게만 후원을 하실 수 있습니다.") } canPaymentService.spendCan( From 87a5ceee9c2ad2eda6423501e9a0353edfee5127 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 22:03:25 +0900 Subject: [PATCH 60/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EA=B0=9C=EC=9D=B8=EC=A0=95=EB=B3=B4=EC=B2=98=EB=A6=AC=EB=B0=A9?= =?UTF-8?q?=EC=B9=A8,=20=EC=9D=B4=EC=9A=A9=EC=95=BD=EA=B4=80=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stipulation/StipulationController.kt | 33 ++++++++++++++++ .../member/stipulation/StipulationDto.kt | 11 ++++++ .../stipulation/StipulationModifyRequest.kt | 6 +++ .../member/stipulation/StipulationService.kt | 38 +++++++++++++++++++ 4 files changed, 88 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationDto.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationModifyRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationController.kt new file mode 100644 index 0000000..89163e7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationController.kt @@ -0,0 +1,33 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +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 +@RequestMapping("/stplat") +class StipulationController(private val service: StipulationService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun enrollment(@RequestBody request: StipulationDto) = ApiResponse.ok( + service.enrollment(request), + "등록되었습니다." + ) + + @PostMapping("/modify") + @PreAuthorize("hasRole('ADMIN')") + fun modify(@RequestBody request: StipulationModifyRequest) = ApiResponse.ok( + service.modify(request), + "수정되었습니다." + ) + + @GetMapping("/terms_of_service") + fun getTermsOfService() = ApiResponse.ok(service.getTermsOfService()) + + @GetMapping("/privacy_policy") + fun getPrivacyPolicy() = ApiResponse.ok(service.getPrivacyPolicy()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationDto.kt new file mode 100644 index 0000000..effec70 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationDto.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.member.stipulation + +data class StipulationDto( + val id: Long = 0, + val title: String, + val description: String +) { + constructor(stipulation: Stipulation) : this(stipulation.id!!, stipulation.title, stipulation.description) + + fun toEntity() = Stipulation(title, description) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationModifyRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationModifyRequest.kt new file mode 100644 index 0000000..9e49c05 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationModifyRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.member.stipulation + +data class StipulationModifyRequest( + val id: Long, + val description: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt new file mode 100644 index 0000000..617b071 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/stipulation/StipulationService.kt @@ -0,0 +1,38 @@ +package kr.co.vividnext.sodalive.member.stipulation + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.stipulation.StipulationIds.PRIVACY_POLICY_ID +import kr.co.vividnext.sodalive.member.stipulation.StipulationIds.TERMS_OF_SERVICE_ID +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class StipulationService(private val repository: StipulationRepository) { + fun enrollment(request: StipulationDto) { + repository.save(request.toEntity()) + } + + fun getTermsOfService(): StipulationDto { + val stipulation = repository.findByIdOrNull(TERMS_OF_SERVICE_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + return StipulationDto(stipulation) + } + + fun getPrivacyPolicy(): StipulationDto { + val stipulation = repository.findByIdOrNull(PRIVACY_POLICY_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + return StipulationDto(stipulation) + } + + @Transactional + fun modify(request: StipulationModifyRequest) { + val stipulation = repository.findByIdOrNull(request.id) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + stipulation.description = request.description + } +} From a983595bad8b0ecebe6da598a504eecbf0219f4e Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 22:10:33 +0900 Subject: [PATCH 61/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20FAQ=20A?= =?UTF-8?q?PI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/faq/CreateFaqRequest.kt | 7 ++ .../kr/co/vividnext/sodalive/faq/Faq.kt | 22 +++++++ .../co/vividnext/sodalive/faq/FaqCategory.kt | 13 ++++ .../sodalive/faq/FaqCategoryRepository.kt | 7 ++ .../vividnext/sodalive/faq/FaqController.kt | 41 ++++++++++++ .../vividnext/sodalive/faq/FaqRepository.kt | 64 +++++++++++++++++++ .../co/vividnext/sodalive/faq/FaqService.kt | 62 ++++++++++++++++++ .../sodalive/faq/GetFaqResponseItem.kt | 10 +++ .../sodalive/faq/ModifyFaqRequest.kt | 8 +++ 9 files changed, 234 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/CreateFaqRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/Faq.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategory.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategoryRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/GetFaqResponseItem.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/faq/ModifyFaqRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/CreateFaqRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/CreateFaqRequest.kt new file mode 100644 index 0000000..1a820c4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/CreateFaqRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.faq + +data class CreateFaqRequest( + val question: String, + val answer: String, + val category: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/Faq.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/Faq.kt new file mode 100644 index 0000000..26ce603 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/Faq.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.faq + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class Faq( + @Column(nullable = false) + var question: String, + @Column(columnDefinition = "TEXT", nullable = false) + var answer: String, + var isActive: Boolean = true, + val orders: Int = 1 +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id", nullable = false) + var category: FaqCategory? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategory.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategory.kt new file mode 100644 index 0000000..b3deac5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategory.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.faq + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Column +import javax.persistence.Entity + +@Entity +data class FaqCategory( + @Column(nullable = false) + val category: String, + val isActive: Boolean = true, + val orders: Int = 1 +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategoryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategoryRepository.kt new file mode 100644 index 0000000..07e4ee0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqCategoryRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.faq + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface FaqCategoryRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt new file mode 100644 index 0000000..0e835a1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.faq + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +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 +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/faq") +class FaqController(private val service: FaqService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun createFaq(@RequestBody request: CreateFaqRequest) = ApiResponse.ok( + service.save(request), + "등록되었습니다." + ) + + @PutMapping + @PreAuthorize("hasRole('ADMIN')") + fun modifyFaq(@RequestBody request: ModifyFaqRequest) = ApiResponse.ok( + service.modify(request), + "수정되었습니다." + ) + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun deleteCoin(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") + + @GetMapping + fun getFaqList(@RequestParam("category") category: String) = ApiResponse.ok(service.getFaqList(category)) + + @GetMapping("/category") + fun getFaqCategoryList() = ApiResponse.ok(service.getCategoryList()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqRepository.kt new file mode 100644 index 0000000..9ed977d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqRepository.kt @@ -0,0 +1,64 @@ +package kr.co.vividnext.sodalive.faq + +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.faq.QFaq.faq +import kr.co.vividnext.sodalive.faq.QFaqCategory.faqCategory +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface FaqRepository : JpaRepository + +@Repository +class FaqQueryRepository(private val queryFactory: JPAQueryFactory) { + fun getCategory(category: String): FaqCategory? { + return queryFactory + .selectFrom(faqCategory) + .where( + faqCategory.isActive.isTrue + .and(faqCategory.category.eq(category)) + ) + .fetchFirst() + } + + fun getCategoryList(): List { + return queryFactory + .select(faqCategory.category) + .from(faqCategory) + .where(faqCategory.isActive.isTrue) + .orderBy(faqCategory.orders.desc()) + .fetch() + } + + fun getFaqList(category: String): List { + return queryFactory + .select( + QGetFaqResponseItem( + faq.id, + Expressions.asString(category), + faq.question, + faq.answer + ) + ) + .from(faq) + .innerJoin(faq.category, faqCategory) + .where( + faq.isActive.isTrue + .and(faqCategory.isActive.isTrue) + .and(faqCategory.category.eq(category)) + ) + .orderBy(faq.orders.desc()) + .fetch() + } + + fun getFaq(id: Long): Faq? { + return queryFactory + .selectFrom(faq) + .where( + faq.isActive.isTrue + .and(faq.id.eq(id)) + ) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqService.kt new file mode 100644 index 0000000..5912d4a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqService.kt @@ -0,0 +1,62 @@ +package kr.co.vividnext.sodalive.faq + +import kr.co.vividnext.sodalive.common.SodaException +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class FaqService( + private val repository: FaqRepository, + private val queryRepository: FaqQueryRepository +) { + @Transactional + fun save(request: CreateFaqRequest): Long { + if (request.question.isBlank()) throw SodaException("질문을 입력하세요.") + if (request.answer.isBlank()) throw SodaException("답변을 입력하세요.") + if (request.category.isBlank()) throw SodaException("카테고리를 선택하세요.") + + val category = queryRepository.getCategory(request.category) + ?: throw SodaException("잘못된 카테고리 입니다.") + + val faq = Faq(request.question, request.answer) + faq.category = category + + return repository.save(faq).id!! + } + + @Transactional + fun modify(request: ModifyFaqRequest) { + val faq = queryRepository.getFaq(request.id) + ?: throw SodaException("잘못된 요청입니다.") + + if (request.question != null) { + if (request.question.isBlank()) throw SodaException("질문을 입력하세요.") + faq.question = request.question + } + + if (request.answer != null) { + if (request.answer.isBlank()) throw SodaException("답변을 입력하세요.") + faq.answer = request.answer + } + + if (request.category != null) { + if (request.category.isBlank()) throw SodaException("카테고리를 선택하세요.") + val category = queryRepository.getCategory(request.category) ?: throw SodaException("잘못된 카테고리 입니다.") + faq.category = category + } + } + + @Transactional + fun delete(id: Long) { + if (id <= 0) throw SodaException("잘못된 요청입니다.") + val faq = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + faq.isActive = false + } + + fun getFaqList(category: String) = queryRepository.getFaqList(category) + + fun getCategoryList() = queryRepository.getCategoryList() +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/GetFaqResponseItem.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/GetFaqResponseItem.kt new file mode 100644 index 0000000..3227079 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/GetFaqResponseItem.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.faq + +import com.querydsl.core.annotations.QueryProjection + +data class GetFaqResponseItem @QueryProjection constructor( + val id: Long, + val category: String, + val question: String, + val answer: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/ModifyFaqRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/ModifyFaqRequest.kt new file mode 100644 index 0000000..f071507 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/ModifyFaqRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.faq + +data class ModifyFaqRequest( + val id: Long, + val question: String? = null, + val answer: String? = null, + val category: String? = null +) From dc299f77272881a39489afcc89e3c2bd0ab8d82c Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 22:18:45 +0900 Subject: [PATCH 62/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/event/EventController.kt | 43 +++++ .../sodalive/event/EventRepository.kt | 24 +++ .../vividnext/sodalive/event/EventService.kt | 170 ++++++++++++++++++ 3 files changed, 237 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt index fdbe64b..17e8bab 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventController.kt @@ -1,13 +1,56 @@ package kr.co.vividnext.sodalive.event import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +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.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/event") class EventController(private val service: EventService) { @GetMapping fun getEventList() = ApiResponse.ok(service.getEventList()) + + @GetMapping("/popup") + fun getEventPopup() = ApiResponse.ok(service.getEventPopup()) + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun createEvent( + @RequestParam("thumbnail") thumbnail: MultipartFile, + @RequestParam(value = "detail", required = false) detail: MultipartFile? = null, + @RequestParam(value = "popup", required = false) popup: MultipartFile? = null, + @RequestParam(value = "link", required = false) link: String? = null, + @RequestParam(value = "title", required = false) title: String? = null, + @RequestParam(value = "isPopup") isPopup: Boolean + ) = ApiResponse.ok( + service.save(thumbnail, detail, popup, link, title, isPopup), + "등록되었습니다." + ) + + @PutMapping + @PreAuthorize("hasRole('ADMIN')") + fun updateEvent( + @RequestParam(value = "id") id: Long, + @RequestParam(value = "thumbnail", required = false) thumbnail: MultipartFile? = null, + @RequestParam(value = "detail", required = false) detail: MultipartFile? = null, + @RequestParam(value = "popup", required = false) popup: MultipartFile? = null, + @RequestParam(value = "link", required = false) link: String? = null, + @RequestParam(value = "title", required = false) title: String? = null, + @RequestParam(value = "isPopup", required = false) isPopup: Boolean? = null + ) = ApiResponse.ok( + service.update(id, thumbnail, detail, popup, link, title, isPopup), + "수정되었습니다." + ) + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun deleteEvent(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt index 9b45e2f..29624eb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventRepository.kt @@ -10,6 +10,7 @@ interface EventRepository : JpaRepository, EventQueryRepository interface EventQueryRepository { fun getEventList(): List + fun getMainEventPopup(): EventItem? } @Repository @@ -32,4 +33,27 @@ class EventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Even .orderBy(event.id.desc()) .fetch() } + + override fun getMainEventPopup(): EventItem? { + return queryFactory + .select( + QEventItem( + event.id, + event.title, + event.thumbnailImage, + event.detailImage, + event.popupImage, + event.link, + event.isPopup + ) + ) + .from(event) + .where( + event.isActive.isTrue + .and(event.isPopup.isTrue) + .and(event.popupImage.isNotNull) + ) + .orderBy(event.id.desc()) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt index 0469524..e6ddd74 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/event/EventService.kt @@ -1,12 +1,22 @@ package kr.co.vividnext.sodalive.event +import com.amazonaws.services.s3.model.ObjectMetadata +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile @Service class EventService( private val repository: EventRepository, + private val s3Uploader: S3Uploader, + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String, @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { @@ -32,4 +42,164 @@ class EventService( return GetEventResponse(0, eventList) } + + fun getEventPopup(): EventItem? { + val eventPopup = repository.getMainEventPopup() + + if (eventPopup != null) { + if (!eventPopup.thumbnailImageUrl.startsWith("https://")) { + eventPopup.thumbnailImageUrl = "$cloudFrontHost/${eventPopup.thumbnailImageUrl}" + } + + if (eventPopup.detailImageUrl != null && !eventPopup.detailImageUrl!!.startsWith("https://")) { + eventPopup.detailImageUrl = "$cloudFrontHost/${eventPopup.detailImageUrl}" + } + + if (eventPopup.popupImageUrl != null && !eventPopup.popupImageUrl!!.startsWith("https://")) { + eventPopup.popupImageUrl = "$cloudFrontHost/${eventPopup.popupImageUrl}" + } + } + + return eventPopup + } + + @Transactional + fun save( + thumbnail: MultipartFile, + detail: MultipartFile? = null, + popup: MultipartFile? = null, + link: String? = null, + title: String? = null, + isPopup: Boolean + ): Long { + if (detail == null && link.isNullOrBlank()) throw SodaException("상세이미지 혹은 링크를 등록하세요") + + val event = repository.save( + Event( + thumbnailImage = "", + detailImage = null, + popupImage = null, + link = link, + title = title, + isPopup = isPopup + ) + ) + + var metadata = ObjectMetadata() + metadata.contentLength = thumbnail.size + val thumbnailImagePath = s3Uploader.upload( + inputStream = thumbnail.inputStream, + bucket = bucket, + filePath = "event/${event.id}/${generateFileName()}", + metadata = metadata + ) + + val detailImagePath = if (detail != null) { + metadata = ObjectMetadata() + metadata.contentLength = detail.size + + s3Uploader.upload( + inputStream = detail.inputStream, + bucket = bucket, + filePath = "event/${event.id}/${generateFileName()}", + metadata = metadata + ) + } else { + null + } + + val popupImagePath = if (popup != null) { + metadata = ObjectMetadata() + metadata.contentLength = popup.size + + s3Uploader.upload( + inputStream = popup.inputStream, + bucket = bucket, + filePath = "event/${event.id}/${generateFileName()}", + metadata = metadata + ) + } else { + null + } + + event.thumbnailImage = thumbnailImagePath + event.detailImage = detailImagePath + event.popupImage = popupImagePath + + return event.id ?: throw SodaException("이벤트 등록을 하지 못했습니다.") + } + + @Transactional + fun update( + id: Long, + thumbnail: MultipartFile? = null, + detail: MultipartFile? = null, + popup: MultipartFile? = null, + link: String? = null, + title: String? = null, + isPopup: Boolean? = null + ) { + if (id <= 0) throw SodaException("잘못된 요청입니다.") + + if (thumbnail == null && detail == null && link.isNullOrBlank() && title.isNullOrBlank()) { + throw SodaException("수정할 내용을 입력하세요.") + } + + val event = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + if (thumbnail != null) { + val metadata = ObjectMetadata() + metadata.contentLength = thumbnail.size + + event.thumbnailImage = s3Uploader.upload( + inputStream = thumbnail.inputStream, + bucket = bucket, + filePath = "event/${event.id}/${generateFileName()}" + ) + } + + if (detail != null) { + val metadata = ObjectMetadata() + metadata.contentLength = detail.size + + event.detailImage = s3Uploader.upload( + inputStream = detail.inputStream, + bucket = bucket, + filePath = "event/${event.id}/${generateFileName()}" + ) + } + + if (popup != null) { + val metadata = ObjectMetadata() + metadata.contentLength = popup.size + + event.popupImage = s3Uploader.upload( + inputStream = popup.inputStream, + bucket = bucket, + filePath = "event/${event.id}/${generateFileName()}" + ) + } + + if (!link.isNullOrBlank() && event.link != link) { + event.link = link + } + + if (!title.isNullOrBlank() && event.title != title) { + event.title = title + } + + if (isPopup != null) { + event.isPopup = isPopup + } + } + + @Transactional + fun delete(id: Long) { + if (id <= 0) throw SodaException("잘못된 요청입니다.") + val event = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + event.isActive = false + } } From 12c9b141685dd1fcafcb90dafd4e359846ee0bf6 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 22:30:46 +0900 Subject: [PATCH 63/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EC=B6=A9=EC=A0=84=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/event/AdminChargeEventController.kt | 32 ++++++ .../admin/event/AdminChargeEventRepository.kt | 22 ++++ .../admin/event/AdminChargeEventService.kt | 100 ++++++++++++++++++ .../sodalive/admin/event/ChargeEvent.kt | 15 +++ .../admin/event/CreateChargeEventRequest.kt | 9 ++ .../admin/event/GetChargeEventListResponse.kt | 11 ++ .../admin/event/ModifyChargeEventRequest.kt | 11 ++ 7 files changed, 200 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ChargeEvent.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/CreateChargeEventRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/GetChargeEventListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ModifyChargeEventRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventController.kt new file mode 100644 index 0000000..0502923 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventController.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.admin.event + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +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 +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/event/charge") +class AdminChargeEventController(private val service: AdminChargeEventService) { + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + fun createChargeEvent(@RequestBody request: CreateChargeEventRequest): ApiResponse { + service.createChargeEvent(request) + return ApiResponse.ok(null, "등록되었습니다.") + } + + @PutMapping + @PreAuthorize("hasRole('ADMIN')") + fun modifyChargeEvent(@RequestBody request: ModifyChargeEventRequest) = ApiResponse.ok( + service.modifyChargeEvent(request), + "수정되었습니다." + ) + + @GetMapping("/list") + @PreAuthorize("hasRole('ADMIN')") + fun getChargeEventList() = ApiResponse.ok(service.getChargeEventList()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventRepository.kt new file mode 100644 index 0000000..4cd02e4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventRepository.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.admin.event + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.admin.event.QChargeEvent.chargeEvent +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AdminChargeEventRepository : JpaRepository, AdminChargeEventQueryRepository + +interface AdminChargeEventQueryRepository { + fun getChargeEventList(): List +} + +class AdminChargeEventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminChargeEventQueryRepository { + override fun getChargeEventList(): List { + return queryFactory + .selectFrom(chargeEvent) + .orderBy(chargeEvent.createdAt.desc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventService.kt new file mode 100644 index 0000000..fac0957 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/AdminChargeEventService.kt @@ -0,0 +1,100 @@ +package kr.co.vividnext.sodalive.admin.event + +import kr.co.vividnext.sodalive.common.SodaException +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +@Transactional(readOnly = true) +class AdminChargeEventService(private val repository: AdminChargeEventRepository) { + @Transactional + fun createChargeEvent(request: CreateChargeEventRequest): Long { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = LocalDate.parse(request.startDateString, dateTimeFormatter).atTime(0, 0) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val endDate = LocalDate.parse(request.endDateString, dateTimeFormatter).atTime(23, 59, 59) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val chargeEvent = ChargeEvent( + title = request.title, + startDate = startDate, + endDate = endDate, + availableCount = request.availableCount, + addPercent = request.addPercent / 100f + ) + + return repository.save(chargeEvent).id!! + } + + @Transactional + fun modifyChargeEvent(request: ModifyChargeEventRequest) { + val chargeEvent = repository.findByIdOrNull(request.id) + ?: throw SodaException("해당하는 충전이벤트가 없습니다\n다시 시도해 주세요.") + + if (request.title != null) { + chargeEvent.title = request.title + } + + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + if (request.startDateString != null) { + chargeEvent.startDate = LocalDate.parse(request.startDateString, dateTimeFormatter).atTime(0, 0) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } + + if (request.endDateString != null) { + chargeEvent.endDate = LocalDate.parse(request.endDateString, dateTimeFormatter).atTime(23, 59, 59) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } + + if (request.availableCount != null) { + chargeEvent.availableCount = request.availableCount + } + + if (request.addPercent != null) { + chargeEvent.addPercent = request.addPercent / 100f + } + + if (request.isActive != null) { + chargeEvent.isActive = request.isActive + } + } + + fun getChargeEventList(): List { + return repository.getChargeEventList() + .map { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val startDate = it.startDate + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(dateTimeFormatter) + + val endDate = it.endDate + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(dateTimeFormatter) + + GetChargeEventListResponse( + id = it.id!!, + title = it.title, + startDate = startDate, + endDate = endDate, + availableCount = it.availableCount, + addPercent = (it.addPercent * 100).toInt(), + isActive = it.isActive + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ChargeEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ChargeEvent.kt new file mode 100644 index 0000000..2d93162 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ChargeEvent.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.admin.event + +import kr.co.vividnext.sodalive.common.BaseEntity +import java.time.LocalDateTime +import javax.persistence.Entity + +@Entity +data class ChargeEvent( + var title: String, + var startDate: LocalDateTime, + var endDate: LocalDateTime, + var availableCount: Int, + var addPercent: Float, + var isActive: Boolean = true +) : BaseEntity() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/CreateChargeEventRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/CreateChargeEventRequest.kt new file mode 100644 index 0000000..7e81f4e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/CreateChargeEventRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.admin.event + +data class CreateChargeEventRequest( + val title: String, + val startDateString: String, + val endDateString: String, + val availableCount: Int, + val addPercent: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/GetChargeEventListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/GetChargeEventListResponse.kt new file mode 100644 index 0000000..fbac78b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/GetChargeEventListResponse.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.admin.event + +data class GetChargeEventListResponse( + val id: Long, + val title: String, + val startDate: String, + val endDate: String, + val availableCount: Int, + val addPercent: Int, + val isActive: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ModifyChargeEventRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ModifyChargeEventRequest.kt new file mode 100644 index 0000000..1086ae2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/event/ModifyChargeEventRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.admin.event + +data class ModifyChargeEventRequest( + val id: Long, + val title: String? = null, + val startDateString: String? = null, + val endDateString: String? = null, + val availableCount: Int? = null, + val addPercent: Int? = null, + val isActive: Boolean? = null +) From 38f6e8d870cf7b7319e7dd1861e79f131e624c83 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 22:42:50 +0900 Subject: [PATCH 64/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EA=B4=80=EC=8B=AC=EC=82=AC=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D/=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/tag/LiveTagController.kt | 23 +++++++++ .../sodalive/live/tag/LiveTagService.kt | 47 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt index d72d1d6..0e29647 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagController.kt @@ -1,12 +1,17 @@ package kr.co.vividnext.sodalive.live.tag +import kr.co.vividnext.sodalive.admin.member.tag.UpdateTagOrdersRequest import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.core.annotation.AuthenticationPrincipal +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 import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController @@ -22,6 +27,24 @@ class LiveTagController(private val service: LiveTagService) { @RequestPart("request") requestString: String ) = ApiResponse.ok(service.enrollmentLiveTag(image, requestString), "등록되었습니다.") + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun deleteSudaTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.") + + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + fun modifySudaTag( + @PathVariable id: Long, + @RequestPart("image") image: MultipartFile?, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.") + + @PutMapping("/orders") + @PreAuthorize("hasRole('ADMIN')") + fun updateTagOrders( + @RequestBody request: UpdateTagOrdersRequest + ) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.") + @GetMapping fun getTags( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt index e168af4..6237a01 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/tag/LiveTagService.kt @@ -7,7 +7,9 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile @Service @@ -22,6 +24,7 @@ class LiveTagService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { + @Transactional fun enrollmentLiveTag(image: MultipartFile, requestString: String) { val request = objectMapper.readValue(requestString, CreateLiveTagRequest::class.java) tagExistCheck(request) @@ -42,6 +45,50 @@ class LiveTagService( tag.image = tagImagePath } + @Transactional + fun deleteTag(id: Long) { + val tag = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + tag.tag = "${tag.tag}_deleted" + tag.isActive = false + } + + @Transactional + fun modifyTag(id: Long, image: MultipartFile?, requestString: String) { + val tag = repository.findByIdOrNull(id) + ?: throw SodaException("잘못된 요청입니다.") + + val request = objectMapper.readValue(requestString, CreateLiveTagRequest::class.java) + tag.tag = request.tag + + if (image != null) { + val metadata = ObjectMetadata() + metadata.contentLength = image.size + + val tagImageFileName = generateFileName(prefix = "${tag.id}-") + val tagImagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = coverImageBucket, + filePath = "live_cover/${tag.id}/$tagImageFileName", + metadata = metadata + ) + + tag.image = tagImagePath + } + } + + @Transactional + fun updateTagOrders(ids: List) { + for (index in ids.indices) { + val tag = repository.findByIdOrNull(ids[index]) + + if (tag != null) { + tag.orders = index + 1 + } + } + } + fun getTags(member: Member): List { return repository.getTags(role = member.role, isAdult = member.auth != null, cloudFrontHost = cloudFrontHost) } From 7696f06fbd60277a9efb5ad704ada810067aad7c Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 6 Aug 2023 22:50:57 +0900 Subject: [PATCH 65/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/live/AdminLiveController.kt | 15 ++++++++ .../live/AdminLiveRoomQueryRepository.kt | 19 ++++++++++ .../sodalive/admin/live/AdminLiveService.kt | 37 +++++++++++++++++++ .../sodalive/admin/live/GetLiveResponse.kt | 19 ++++++++++ 4 files changed, 90 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetLiveResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt new file mode 100644 index 0000000..a4f7224 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.admin.live + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@PreAuthorize("hasRole('ADMIN')") +@RequestMapping("/admin/live") +class AdminLiveController(private val service: AdminLiveService) { + @GetMapping + fun getOnAirLive() = ApiResponse.ok(data = service.getLiveList()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt new file mode 100644 index 0000000..a7f081b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.admin.live + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.stereotype.Repository + +@Repository +class AdminLiveRoomQueryRepository(private val queryFactory: JPAQueryFactory) { + fun getLiveRoomList(): List { + return queryFactory + .selectFrom(liveRoom) + .innerJoin(liveRoom.member, member) + .where(liveRoom.isActive.isTrue) + .orderBy(liveRoom.channelName.desc(), liveRoom.beginDateTime.asc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt new file mode 100644 index 0000000..bec8b06 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.admin.live + +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class AdminLiveService( + private val repository: AdminLiveRoomQueryRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val coverImageHost: String +) { + fun getLiveList(): GetLiveResponse { + return GetLiveResponse( + liveList = repository.getLiveRoomList() + .asSequence() + .map { + GetLiveResponseItem( + id = it.id!!, + title = it.title, + content = it.notice, + managerNickname = it.member!!.nickname, + coverImageUrl = if (it.coverImage!!.startsWith("https://")) { + it.coverImage!! + } else { + "$coverImageHost/${it.coverImage!!}" + }, + channelName = it.channelName ?: "", + type = it.type, + password = it.password, + isAdult = it.isAdult + ) + } + .toList() + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetLiveResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetLiveResponse.kt new file mode 100644 index 0000000..b0f76fc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetLiveResponse.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.admin.live + +import kr.co.vividnext.sodalive.live.room.LiveRoomType + +data class GetLiveResponse( + val liveList: List +) + +data class GetLiveResponseItem( + val id: Long, + val title: String, + val content: String, + val managerNickname: String, + val coverImageUrl: String, + val channelName: String, + val type: LiveRoomType, + val password: String?, + val isAdult: Boolean +) From 14b25bdfc306713d5452411848197cdcd05d86dd Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 01:23:42 +0900 Subject: [PATCH 66/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8,=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=B0=B0=EB=84=88=EA=B4=80?= =?UTF-8?q?=EB=A6=AC,=20=EC=BD=98=ED=85=90=EC=B8=A0=20=ED=81=90=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EA=B4=80=EB=A6=AC=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/content/AdminContentController.kt | 30 ++++ .../admin/content/AdminContentRepository.kt | 119 +++++++++++++++ .../admin/content/AdminContentService.kt | 121 +++++++++++++++ .../content/GetAdminContentListResponse.kt | 26 ++++ .../content/UpdateAdminContentRequest.kt | 12 ++ .../banner/AdminContentBannerController.kt | 37 +++++ .../banner/AdminContentBannerRepository.kt | 46 ++++++ .../banner/AdminContentBannerService.kt | 144 ++++++++++++++++++ .../banner/CreateContentBannerRequest.kt | 11 ++ .../banner/GetAdminContentBannerResponse.kt | 16 ++ .../banner/UpdateBannerOrdersRequest.kt | 5 + .../banner/UpdateContentBannerRequest.kt | 13 ++ .../AdminContentCurationController.kt | 33 ++++ .../AdminContentCurationRepository.kt | 48 ++++++ .../curation/AdminContentCurationService.kt | 60 ++++++++ .../curation/AudioContentCurationRequest.kt | 19 +++ .../GetAdminContentCurationResponse.kt | 10 ++ .../admin/member/AdminMemberController.kt | 3 + .../admin/member/AdminMemberRepository.kt | 17 +++ .../admin/member/AdminMemberService.kt | 4 + .../member/GetAdminCreatorAllListResponse.kt | 8 + 21 files changed, 782 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt new file mode 100644 index 0000000..2b951e6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.admin.content + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@PreAuthorize("hasRole('ADMIN')") +@RequestMapping("/admin/audio-content") +class AdminContentController(private val service: AdminContentService) { + @GetMapping("/list") + fun getAudioContentList(pageable: Pageable) = ApiResponse.ok(service.getAudioContentList(pageable)) + + @GetMapping("/search") + fun searchAudioContent( + @RequestParam(value = "search_word") searchWord: String, + pageable: Pageable + ) = ApiResponse.ok(service.searchAudioContent(searchWord, pageable)) + + @PutMapping + fun modifyAudioContent( + @RequestBody request: UpdateAdminContentRequest + ) = ApiResponse.ok(service.updateAudioContent(request)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt new file mode 100644 index 0000000..1db3529 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt @@ -0,0 +1,119 @@ +package kr.co.vividnext.sodalive.admin.content + +import com.querydsl.core.types.dsl.DateTimePath +import com.querydsl.core.types.dsl.Expressions +import com.querydsl.core.types.dsl.StringTemplate +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.content.hashtag.QAudioContentHashTag.audioContentHashTag +import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag +import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration +import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +interface AdminContentRepository : JpaRepository, AdminAudioContentQueryRepository + +interface AdminAudioContentQueryRepository { + fun getAudioContentTotalCount(searchWord: String = ""): Int + fun getAudioContentList(offset: Long, limit: Long, searchWord: String = ""): List + fun getHashTagList(audioContentId: Long): List +} + +class AdminAudioContentQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AdminAudioContentQueryRepository { + override fun getAudioContentTotalCount(searchWord: String): Int { + var where = audioContent.duration.isNotNull + .and(audioContent.member.isNotNull) + .and(audioContent.isActive.isTrue) + + if (searchWord.trim().length > 1) { + where = where.and( + audioContent.title.contains(searchWord) + .or(audioContent.member.nickname.contains(searchWord)) + ) + } + + return queryFactory + .select(audioContent.id) + .from(audioContent) + .where(where) + .fetch() + .size + } + + override fun getAudioContentList(offset: Long, limit: Long, searchWord: String): List { + var where = audioContent.duration.isNotNull + .and(audioContent.member.isNotNull) + .and(audioContent.isActive.isTrue) + + if (searchWord.trim().length > 1) { + where = where.and( + audioContent.title.contains(searchWord) + .or(audioContent.member.nickname.contains(searchWord)) + ) + } + + return queryFactory + .select( + QGetAdminContentListItem( + audioContent.id, + audioContent.title, + audioContent.detail, + audioContentCuration.title, + audioContentCuration.id.nullif(0), + audioContent.coverImage, + audioContent.member!!.nickname, + audioContentTheme.theme, + audioContent.price, + audioContent.isAdult, + audioContent.duration, + audioContent.content, + formattedDateExpression(audioContent.createdAt) + ) + ) + .from(audioContent) + .leftJoin(audioContent.curation, audioContentCuration) + .innerJoin(audioContent.theme, audioContentTheme) + .where(where) + .offset(offset) + .limit(limit) + .orderBy(audioContent.id.desc()) + .fetch() + } + + override fun getHashTagList(audioContentId: Long): List { + return queryFactory + .select(hashTag.tag) + .from(audioContentHashTag) + .innerJoin(audioContentHashTag.hashTag, hashTag) + .innerJoin(audioContentHashTag.audioContent, audioContent) + .where( + audioContent.duration.isNotNull + .and(audioContent.member.isNotNull) + .and(audioContentHashTag.audioContent.id.eq(audioContentId)) + ) + .fetch() + } + + private fun formattedDateExpression( + dateTime: DateTimePath, + format: String = "%Y-%m-%d" + ): StringTemplate { + return Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + dateTime, + "UTC", + "Asia/Seoul" + ), + format + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt new file mode 100644 index 0000000..677e3b9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt @@ -0,0 +1,121 @@ +package kr.co.vividnext.sodalive.admin.content + +import kr.co.vividnext.sodalive.admin.content.curation.AdminContentCurationRepository +import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront +import kr.co.vividnext.sodalive.common.SodaException +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AdminContentService( + private val repository: AdminContentRepository, + private val audioContentCloudFront: AudioContentCloudFront, + private val curationRepository: AdminContentCurationRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val coverImageHost: String +) { + fun getAudioContentList(pageable: Pageable): GetAdminContentListResponse { + val totalCount = repository.getAudioContentTotalCount() + val audioContentAndThemeList = repository.getAudioContentList( + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + + val audioContentList = audioContentAndThemeList + .asSequence() + .map { + val tags = repository + .getHashTagList(audioContentId = it.audioContentId) + .joinToString(" ") { tag -> tag } + it.tags = tags + it + } + .map { + it.contentUrl = audioContentCloudFront.generateSignedURL( + resourcePath = it.contentUrl, + expirationTime = 1000 * 60 * 60 * (it.remainingTime.split(":")[0].toLong() + 2) + ) + it + } + .map { + it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}" + it + } + .toList() + + return GetAdminContentListResponse(totalCount, audioContentList) + } + + fun searchAudioContent(searchWord: String, pageable: Pageable): GetAdminContentListResponse { + if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") + val totalCount = repository.getAudioContentTotalCount(searchWord) + val audioContentAndThemeList = repository.getAudioContentList( + offset = pageable.offset, + limit = pageable.pageSize.toLong(), + searchWord = searchWord + ) + + val audioContentList = audioContentAndThemeList + .asSequence() + .map { + val tags = repository + .getHashTagList(audioContentId = it.audioContentId) + .joinToString(" ") { tag -> tag } + it.tags = tags + it + } + .map { + it.contentUrl = audioContentCloudFront.generateSignedURL( + resourcePath = it.contentUrl, + expirationTime = 1000 * 60 * 60 * (it.remainingTime.split(":")[0].toLong() + 2) + ) + it + } + .map { + it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}" + it + } + .toList() + + return GetAdminContentListResponse(totalCount, audioContentList) + } + + @Transactional + fun updateAudioContent(request: UpdateAdminContentRequest) { + val audioContent = repository.findByIdOrNull(id = request.id) + ?: throw SodaException("없는 콘텐츠 입니다.") + + if (request.isDefaultCoverImage) { + audioContent.coverImage = "profile/default_profile.png" + } + + if (request.isActive != null) { + audioContent.isActive = request.isActive + } + + if (request.isAdult != null) { + audioContent.isAdult = request.isAdult + } + + if (request.isCommentAvailable != null) { + audioContent.isCommentAvailable = request.isCommentAvailable + } + + if (request.title != null) { + audioContent.title = request.title + } + + if (request.detail != null) { + audioContent.detail = request.detail + } + + if (request.curationId != null) { + val curation = curationRepository.findByIdAndActive(id = request.curationId) + audioContent.curation = curation + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt new file mode 100644 index 0000000..81b88b8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.admin.content + +import com.querydsl.core.annotations.QueryProjection + +data class GetAdminContentListResponse( + val totalCount: Int, + val items: List +) + +data class GetAdminContentListItem @QueryProjection constructor( + val audioContentId: Long, + val title: String, + val detail: String, + val curationTitle: String?, + val curationId: Long, + var coverImageUrl: String, + val creatorNickname: String, + val theme: String, + val price: Int, + val isAdult: Boolean, + val remainingTime: String, + var contentUrl: String, + val date: String +) { + var tags: String = "" +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt new file mode 100644 index 0000000..376485b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.content + +data class UpdateAdminContentRequest( + val id: Long, + val isDefaultCoverImage: Boolean, + val title: String?, + val detail: String?, + val curationId: Long?, + val isAdult: Boolean?, + val isActive: Boolean?, + val isCommentAvailable: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt new file mode 100644 index 0000000..1324585 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +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 +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/admin/audio-content/banner") +@PreAuthorize("hasRole('ADMIN')") +class AdminContentBannerController(private val service: AdminContentBannerService) { + @PostMapping + fun createAudioContentMainBanner( + @RequestPart("image") image: MultipartFile, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.createAudioContentMainBanner(image, requestString)) + + @PutMapping + fun modifyAudioContentMainBanner( + @RequestPart("image", required = false) image: MultipartFile? = null, + @RequestPart("request") requestString: String + ) = ApiResponse.ok(service.updateAudioContentMainBanner(image, requestString)) + + @PutMapping("/orders") + fun updateBannerOrders( + @RequestBody request: UpdateBannerOrdersRequest + ) = ApiResponse.ok(service.updateBannerOrders(request.ids), "수정되었습니다.") + + @GetMapping + fun getAudioContentMainBannerList() = ApiResponse.ok(service.getAudioContentMainBannerList()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt new file mode 100644 index 0000000..aa654dd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt @@ -0,0 +1,46 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner +import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner +import kr.co.vividnext.sodalive.event.QEvent.event +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AdminContentBannerRepository : JpaRepository, AdminContentBannerQueryRepository + +interface AdminContentBannerQueryRepository { + fun getAudioContentMainBannerList(): List +} + +class AdminContentBannerQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) : AdminContentBannerQueryRepository { + override fun getAudioContentMainBannerList(): List { + return queryFactory + .select( + QGetAdminContentBannerResponse( + audioContentBanner.id, + audioContentBanner.type, + audioContentBanner.thumbnailImage.prepend("/").prepend(cloudFrontHost), + audioContentBanner.event.id, + audioContentBanner.event.thumbnailImage, + audioContentBanner.creator.id, + audioContentBanner.creator.nickname, + audioContentBanner.link, + audioContentBanner.isAdult + ) + ) + .from(audioContentBanner) + .leftJoin(audioContentBanner.event, event) + .leftJoin(audioContentBanner.creator, member) + .where(audioContentBanner.isActive.isTrue) + .orderBy(audioContentBanner.orders.asc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt new file mode 100644 index 0000000..b7e4837 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt @@ -0,0 +1,144 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType +import kr.co.vividnext.sodalive.event.EventRepository +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +class AdminContentBannerService( + private val s3Uploader: S3Uploader, + private val repository: AdminContentBannerRepository, + private val memberRepository: MemberRepository, + private val eventRepository: EventRepository, + private val objectMapper: ObjectMapper, + + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String +) { + @Transactional + fun createAudioContentMainBanner(image: MultipartFile, requestString: String) { + val request = objectMapper.readValue(requestString, CreateContentBannerRequest::class.java) + if (request.type == AudioContentBannerType.CREATOR && request.creatorId == null) { + throw SodaException("크리에이터를 선택하세요.") + } + + if (request.type == AudioContentBannerType.LINK && request.link == null) { + throw SodaException("링크 url을 입력하세요.") + } + + if (request.type == AudioContentBannerType.EVENT && request.eventId == null) { + throw SodaException("이벤트를 선택하세요.") + } + + val event = if (request.eventId != null && request.eventId > 0) { + eventRepository.findByIdOrNull(request.eventId) + } else { + null + } + + val creator = if (request.creatorId != null && request.creatorId > 0) { + memberRepository.findByIdOrNull(request.creatorId) + } else { + null + } + + val audioContentBanner = AudioContentBanner(type = request.type) + audioContentBanner.link = request.link + audioContentBanner.isAdult = request.isAdult + audioContentBanner.event = event + audioContentBanner.creator = creator + repository.save(audioContentBanner) + + val fileName = generateFileName() + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "audio_content_banner/${audioContentBanner.id}/$fileName" + ) + audioContentBanner.thumbnailImage = imagePath + } + + @Transactional + fun updateAudioContentMainBanner(image: MultipartFile?, requestString: String) { + val request = objectMapper.readValue(requestString, UpdateContentBannerRequest::class.java) + val audioContentBanner = repository.findByIdOrNull(request.id) + ?: throw SodaException("잘못된 요청입니다.") + + if (image != null) { + val fileName = generateFileName() + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "audio_content_banner/${audioContentBanner.id}/$fileName" + ) + audioContentBanner.thumbnailImage = imagePath + } + + if (request.isAdult != null) { + audioContentBanner.isAdult = request.isAdult + } + + if (request.isActive != null) { + audioContentBanner.isActive = request.isActive + } + + if (request.type != null) { + audioContentBanner.creator = null + audioContentBanner.event = null + audioContentBanner.link = null + + if (request.type == AudioContentBannerType.CREATOR) { + if (request.creatorId != null) { + val creator = memberRepository.findByIdOrNull(request.creatorId) + ?: throw SodaException("크리에이터를 선택하세요.") + + audioContentBanner.creator = creator + } else { + throw SodaException("크리에이터를 선택하세요.") + } + } else if (request.type == AudioContentBannerType.LINK) { + if (request.link != null) { + audioContentBanner.link = request.link + } else { + throw SodaException("링크 url을 입력하세요.") + } + } else if (request.type == AudioContentBannerType.EVENT) { + if (request.eventId != null) { + val event = eventRepository.findByIdOrNull(request.eventId) + ?: throw SodaException("이벤트를 선택하세요.") + + audioContentBanner.event = event + } else { + throw SodaException("이벤트를 선택하세요.") + } + } + + audioContentBanner.type = request.type + } + } + + @Transactional + fun updateBannerOrders(ids: List) { + for (index in ids.indices) { + val tag = repository.findByIdOrNull(ids[index]) + + if (tag != null) { + tag.orders = index + 1 + } + } + } + + fun getAudioContentMainBannerList(): List { + return repository.getAudioContentMainBannerList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt new file mode 100644 index 0000000..3203366 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType + +data class CreateContentBannerRequest( + val type: AudioContentBannerType, + val eventId: Long?, + val creatorId: Long?, + val link: String?, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt new file mode 100644 index 0000000..6618f2b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType + +data class GetAdminContentBannerResponse @QueryProjection constructor( + val id: Long, + val type: AudioContentBannerType, + val thumbnailImageUrl: String, + val eventId: Long?, + val eventThumbnailImage: String?, + val creatorId: Long?, + val creatorNickname: String?, + val link: String?, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt new file mode 100644 index 0000000..2dba5aa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +data class UpdateBannerOrdersRequest( + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt new file mode 100644 index 0000000..fe97928 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType + +data class UpdateContentBannerRequest( + val id: Long, + val type: AudioContentBannerType?, + val eventId: Long?, + val creatorId: Long?, + val link: String?, + val isAdult: Boolean?, + val isActive: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt new file mode 100644 index 0000000..849697a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt @@ -0,0 +1,33 @@ +package kr.co.vividnext.sodalive.admin.content.curation + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +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 +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/admin/audio-content/curation") +@PreAuthorize("hasRole('ADMIN')") +class AdminContentCurationController(private val service: AdminContentCurationService) { + @PostMapping + fun createContentCuration( + @RequestBody request: CreateContentCurationRequest + ) = ApiResponse.ok(service.createContentCuration(request)) + + @PutMapping + fun updateContentCuration( + @RequestBody request: UpdateContentCurationRequest + ) = ApiResponse.ok(service.updateContentCuration(request)) + + @PutMapping("/orders") + fun updateContentCurationOrders( + @RequestBody request: UpdateContentCurationOrdersRequest + ) = ApiResponse.ok(service.updateContentCurationOrders(request.ids), "수정되었습니다.") + + @GetMapping + fun getContentCurationList() = ApiResponse.ok(service.getContentCurationList()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt new file mode 100644 index 0000000..a24fa77 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt @@ -0,0 +1,48 @@ +package kr.co.vividnext.sodalive.admin.content.curation + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration +import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AdminContentCurationRepository : + JpaRepository, + AdminContentCurationQueryRepository + +interface AdminContentCurationQueryRepository { + fun getAudioContentCurationList(): List + fun findByIdAndActive(id: Long): AudioContentCuration? +} + +@Repository +class AdminContentCurationQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AdminContentCurationQueryRepository { + override fun getAudioContentCurationList(): List { + return queryFactory + .select( + QGetAdminContentCurationResponse( + audioContentCuration.id, + audioContentCuration.title, + audioContentCuration.description, + audioContentCuration.isAdult + ) + ) + .from(audioContentCuration) + .where(audioContentCuration.isActive.isTrue) + .orderBy(audioContentCuration.orders.asc()) + .fetch() + } + + override fun findByIdAndActive(id: Long): AudioContentCuration? { + return queryFactory + .selectFrom(audioContentCuration) + .where( + audioContentCuration.id.eq(id) + .and(audioContentCuration.isActive.isTrue) + ) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt new file mode 100644 index 0000000..b6d0c14 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt @@ -0,0 +1,60 @@ +package kr.co.vividnext.sodalive.admin.content.curation + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class AdminContentCurationService( + private val repository: AdminContentCurationRepository +) { + @Transactional + fun createContentCuration(request: CreateContentCurationRequest) { + repository.save( + AudioContentCuration( + title = request.title, + description = request.description, + isAdult = request.isAdult + ) + ) + } + + @Transactional + fun updateContentCuration(request: UpdateContentCurationRequest) { + val audioContentCuration = repository.findByIdOrNull(id = request.id) + ?: throw SodaException("잘못된 요청입니다.") + + if (request.title != null) { + audioContentCuration.title = request.title + } + + if (request.description != null) { + audioContentCuration.description = request.description + } + + if (request.isAdult != null) { + audioContentCuration.isAdult = request.isAdult + } + + if (request.isActive != null) { + audioContentCuration.isActive = request.isActive + } + } + + @Transactional + fun updateContentCurationOrders(ids: List) { + for (index in ids.indices) { + val audioContentCuration = repository.findByIdOrNull(ids[index]) + + if (audioContentCuration != null) { + audioContentCuration.orders = index + 1 + } + } + } + + fun getContentCurationList(): List { + return repository.getAudioContentCurationList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt new file mode 100644 index 0000000..49b46d7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.admin.content.curation + +data class CreateContentCurationRequest( + val title: String, + val description: String, + val isAdult: Boolean +) + +data class UpdateContentCurationRequest( + val id: Long, + val title: String?, + val description: String?, + val isAdult: Boolean?, + val isActive: Boolean? +) + +data class UpdateContentCurationOrdersRequest( + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt new file mode 100644 index 0000000..2938cfd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt @@ -0,0 +1,10 @@ +package kr.co.vividnext.sodalive.admin.content.curation + +import com.querydsl.core.annotations.QueryProjection + +data class GetAdminContentCurationResponse @QueryProjection constructor( + val id: Long, + val title: String, + val description: String, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt index ad6d0d1..43675a7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -21,6 +21,9 @@ class AdminMemberController(private val service: AdminMemberService) { pageable: Pageable ) = ApiResponse.ok(service.searchMember(searchWord, pageable)) + @GetMapping("/creator/all/list") + fun getCreatorAllList() = ApiResponse.ok(service.getCreatorAllList()) + @GetMapping("/creator/list") fun getCreatorList(pageable: Pageable) = ApiResponse.ok(service.getCreatorList(pageable)) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt index 3dd53b0..4b1a2ed 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt @@ -14,6 +14,7 @@ interface AdminMemberQueryRepository { fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole? = null): List fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int + fun getCreatorAllList(): List } class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository { @@ -92,4 +93,20 @@ class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .fetch() .size } + + override fun getCreatorAllList(): List { + return queryFactory + .select( + QGetAdminCreatorAllListResponse( + member.id, + member.nickname + ) + ) + .from(member) + .where( + member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt index 15256d1..0550575 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -117,4 +117,8 @@ class AdminMemberService( } .toList() } + + fun getCreatorAllList(): List { + return repository.getCreatorAllList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt new file mode 100644 index 0000000..30c006a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.admin.member + +import com.querydsl.core.annotations.QueryProjection + +data class GetAdminCreatorAllListResponse @QueryProjection constructor( + val id: Long, + val nickname: String +) From 34590347a6673b6dfded26e2adba5241fc0697fd Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 03:09:44 +0900 Subject: [PATCH 67/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=ED=81=AC?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EC=9D=B4=ED=84=B0=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/live/AdminLiveController.kt | 45 +++- .../live/AdminLiveRoomQueryRepository.kt | 20 ++ .../sodalive/admin/live/AdminLiveService.kt | 203 ++++++++++++++++++ .../live/GetAdminRecommendCreatorResponse.kt | 16 ++ ...dminRecommendCreatorBannerOrdersRequest.kt | 6 + .../recommend/RecommendLiveCreatorBanner.kt | 6 +- .../RecommendLiveCreatorBannerRepository.kt | 7 + .../sodalive/member/MemberRepository.kt | 11 + 8 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetAdminRecommendCreatorResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/live/UpdateAdminRecommendCreatorBannerOrdersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBannerRepository.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt index a4f7224..869dd19 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveController.kt @@ -1,15 +1,58 @@ package kr.co.vividnext.sodalive.admin.live import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping +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 +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @RestController -@PreAuthorize("hasRole('ADMIN')") @RequestMapping("/admin/live") class AdminLiveController(private val service: AdminLiveService) { @GetMapping + @PreAuthorize("hasRole('ADMIN')") fun getOnAirLive() = ApiResponse.ok(data = service.getLiveList()) + + @GetMapping("/recommend-creator") + @PreAuthorize("hasRole('ADMIN')") + fun getRecommendCreatorBanner(pageable: Pageable) = ApiResponse.ok(service.getRecommendCreator(pageable)) + + @PostMapping("/recommend-creator") + fun createRecommendCreatorBanner( + @RequestParam("image") image: MultipartFile, + @RequestParam("creator_id") creatorId: Long, + @RequestParam("start_date") startDate: String, + @RequestParam("end_date") endDate: String, + @RequestParam("is_adult") isAdult: Boolean + ) = ApiResponse.ok( + service.createRecommendCreatorBanner(image, creatorId, startDate, endDate, isAdult), + "등록되었습니다." + ) + + @PutMapping("/recommend-creator") + fun updateRecommendCreatorBanner( + @RequestParam("recommend_creator_banner_id") recommendCreatorBannerId: Long, + @RequestParam("image", required = false) image: MultipartFile?, + @RequestParam("creator_id", required = false) creatorId: Long?, + @RequestParam("start_date", required = false) startDate: String?, + @RequestParam("end_date", required = false) endDate: String?, + @RequestParam("is_adult", required = false) isAdult: Boolean? + ) = ApiResponse.ok( + service.updateRecommendCreatorBanner(recommendCreatorBannerId, image, creatorId, startDate, endDate, isAdult), + "수정되었습니다." + ) + + @PutMapping("/recommend-creator/orders") + fun updateRecommendCreatorBannerOrders( + @RequestBody request: UpdateAdminRecommendCreatorBannerOrdersRequest + ) = ApiResponse.ok( + service.updateRecommendCreatorBannerOrders(request.firstOrders, request.ids), + "수정되었습니다." + ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt index a7f081b..dbc2890 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveRoomQueryRepository.kt @@ -1,9 +1,12 @@ package kr.co.vividnext.sodalive.admin.live import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner +import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Repository @Repository @@ -16,4 +19,21 @@ class AdminLiveRoomQueryRepository(private val queryFactory: JPAQueryFactory) { .orderBy(liveRoom.channelName.desc(), liveRoom.beginDateTime.asc()) .fetch() } + + fun getRecommendCreatorTotalCount(): Int { + return queryFactory + .select(recommendLiveCreatorBanner.id) + .from(recommendLiveCreatorBanner) + .fetch() + .size + } + + fun getRecommendCreatorList(pageable: Pageable): List { + return queryFactory + .selectFrom(recommendLiveCreatorBanner) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .orderBy(recommendLiveCreatorBanner.orders.asc(), recommendLiveCreatorBanner.id.desc()) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt index bec8b06..0865230 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt @@ -1,12 +1,31 @@ package kr.co.vividnext.sodalive.admin.live +import com.amazonaws.services.s3.model.ObjectMetadata +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner +import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBannerRepository +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter @Service class AdminLiveService( + private val recommendCreatorBannerRepository: RecommendLiveCreatorBannerRepository, private val repository: AdminLiveRoomQueryRepository, + private val memberRepository: MemberRepository, + private val s3Uploader: S3Uploader, + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String, @Value("\${cloud.aws.cloud-front.host}") private val coverImageHost: String ) { @@ -34,4 +53,188 @@ class AdminLiveService( .toList() ) } + + fun getRecommendCreator(pageable: Pageable): GetAdminRecommendCreatorResponse { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + + val totalCount = repository.getRecommendCreatorTotalCount() + + val recommendCreatorList = repository + .getRecommendCreatorList(pageable) + .asSequence() + .map { + GetAdminRecommendCreatorResponseItem( + it.id!!, + "$coverImageHost/${it.image}", + it.creator!!.id!!, + it.creator!!.nickname, + it.startDate + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .toLocalDateTime() + .format(dateTimeFormatter), + it.endDate + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .toLocalDateTime() + .format(dateTimeFormatter), + it.isAdult + ) + } + .toList() + + return GetAdminRecommendCreatorResponse( + totalCount = totalCount, + recommendCreatorList = recommendCreatorList + ) + } + + @Transactional + fun createRecommendCreatorBanner( + image: MultipartFile, + creatorId: Long, + startDateString: String, + endDateString: String, + isAdult: Boolean + ): Long { + if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.") + + val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId) + ?: throw SodaException("올바른 크리에이터를 선택해 주세요.") + + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val nowDate = LocalDateTime.now() + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + if (startDate < nowDate) throw SodaException("노출 시작일은 현재시간 이후로 설정하셔야 합니다.") + + val endDate = LocalDateTime.parse(endDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + if (endDate < nowDate) throw SodaException("노출 종료일은 현재시간 이후로 설정하셔야 합니다.") + if (endDate <= startDate) throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.") + + val recommendCreatorBanner = RecommendLiveCreatorBanner( + startDate = startDate, + endDate = endDate, + isAdult = isAdult + ) + recommendCreatorBanner.creator = creator + recommendCreatorBannerRepository.save(recommendCreatorBanner) + + val metadata = ObjectMetadata() + metadata.contentLength = image.size + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "recommend_creator_banner/${recommendCreatorBanner.id}/${generateFileName()}", + metadata = metadata + ) + + recommendCreatorBanner.image = imagePath + + return recommendCreatorBanner.id!! + } + + @Transactional + fun updateRecommendCreatorBanner( + recommendCreatorBannerId: Long, + image: MultipartFile?, + creatorId: Long?, + startDateString: String?, + endDateString: String?, + isAdult: Boolean? + ) { + val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(recommendCreatorBannerId) + ?: throw SodaException("해당하는 추천라이브가 없습니다. 다시 확인해 주세요.") + + if (creatorId != null) { + if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.") + + val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId) + ?: throw SodaException("올바른 크리에이터를 선택해 주세요.") + + recommendCreatorBanner.creator = creator + } + + if (image != null) { + val metadata = ObjectMetadata() + metadata.contentLength = image.size + val imagePath = s3Uploader.upload( + inputStream = image.inputStream, + bucket = bucket, + filePath = "recommend_creator_banner/${recommendCreatorBanner.id}/${generateFileName()}", + metadata = metadata + ) + + recommendCreatorBanner.image = imagePath + } + + if (startDateString != null) { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + val endDate = if (endDateString != null) { + LocalDateTime.parse(endDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + } else { + null + } + + if (endDate != null) { + if (endDate <= startDate) { + throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.") + } + + recommendCreatorBanner.endDate = endDate + } else { + if (recommendCreatorBanner.endDate <= startDate) { + throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.") + } + } + + recommendCreatorBanner.startDate = startDate + } else if (endDateString != null) { + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") + val endDate = LocalDateTime.parse(endDateString, dateTimeFormatter) + .atZone(ZoneId.of("Asia/Seoul")) + .withZoneSameInstant(ZoneId.of("UTC")) + .toLocalDateTime() + + if (endDate <= recommendCreatorBanner.startDate) { + throw SodaException("노출 종료일은 노출 시작일 이후로 설정하셔야 합니다.") + } + + recommendCreatorBanner.endDate = endDate + } + + if (isAdult != null) { + recommendCreatorBanner.isAdult = isAdult + } + } + + @Transactional + fun updateRecommendCreatorBannerOrders(firstOrders: Int, ids: List) { + for (index in ids.indices) { + val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(id = ids[index]) + + if (recommendCreatorBanner != null) { + recommendCreatorBanner.orders = firstOrders + index + } + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetAdminRecommendCreatorResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetAdminRecommendCreatorResponse.kt new file mode 100644 index 0000000..f527cfe --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/GetAdminRecommendCreatorResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.admin.live + +data class GetAdminRecommendCreatorResponse( + val totalCount: Int, + val recommendCreatorList: List +) + +data class GetAdminRecommendCreatorResponseItem( + val id: Long, + val image: String, + val creatorId: Long, + val creatorNickname: String, + val startDate: String, + val endDate: String, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/UpdateAdminRecommendCreatorBannerOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/UpdateAdminRecommendCreatorBannerOrdersRequest.kt new file mode 100644 index 0000000..93bd453 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/live/UpdateAdminRecommendCreatorBannerOrdersRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.admin.live + +data class UpdateAdminRecommendCreatorBannerOrdersRequest( + val firstOrders: Int, + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt index 78e823e..b56d030 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBanner.kt @@ -11,8 +11,6 @@ import javax.persistence.ManyToOne @Entity data class RecommendLiveCreatorBanner( - @Column(nullable = false) - var image: String, @Column(nullable = false) var startDate: LocalDateTime, @Column(nullable = false) @@ -20,7 +18,9 @@ data class RecommendLiveCreatorBanner( @Column(nullable = false) var isAdult: Boolean = false, @Column(nullable = false) - var orders: Int = 1 + var orders: Int = 1, + @Column(nullable = true) + var image: String? = null ) : BaseEntity() { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "creator_id", nullable = false) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBannerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBannerRepository.kt new file mode 100644 index 0000000..7c5c605 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/RecommendLiveCreatorBannerRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.recommend + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface RecommendLiveCreatorBannerRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 7edb17f..6eaa3fd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -14,6 +14,7 @@ interface MemberRepository : JpaRepository, MemberQueryRepository interface MemberQueryRepository { fun findByPushToken(pushToken: String): List fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List + fun findCreatorByIdOrNull(memberId: Long): Member? } @Repository @@ -36,4 +37,14 @@ class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Mem ) .fetch() } + + override fun findCreatorByIdOrNull(memberId: Long): Member? { + return queryFactory + .selectFrom(member) + .where( + member.id.eq(memberId) + .and(member.role.eq(MemberRole.CREATOR)) + ) + .fetchFirst() + } } From b99ab9103dd3a67776ba0b00ba7e1ec5f5eebcf6 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 14:18:52 +0900 Subject: [PATCH 68/94] =?UTF-8?q?ReportType=20-=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20Review=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt b/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt index 89a46d6..3133dcc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/report/Report.kt @@ -30,5 +30,5 @@ data class Report( } enum class ReportType { - REVIEW, PROFILE, USER, CHEERS, AUDIO_CONTENT + PROFILE, USER, CHEERS, AUDIO_CONTENT } From 581b6975a3cc67902bc12b01f7ce5a13a4641e80 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 14:30:49 +0900 Subject: [PATCH 69/94] =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20->=20mutableList=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt | 2 +- .../kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt index 24499f1..2393dff 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt @@ -70,7 +70,7 @@ class TokenProvider( val memberTokenSet = memberToken.tokenList.toMutableSet() memberTokenSet.add(token) - memberToken.tokenList = memberTokenSet.toList() + memberToken.tokenList = memberTokenSet.toMutableList() tokenRepository.save(memberToken) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt index 514e0e8..1027928 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt @@ -7,5 +7,5 @@ import org.springframework.data.redis.core.RedisHash data class MemberToken( @Id val id: Long, - var tokenList: List + var tokenList: MutableList ) From 872e84baf12d3cf8b5f6b87209a8adcf847246ee Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 14:36:50 +0900 Subject: [PATCH 70/94] =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=20->=20mutableList=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 32468db..3dc8967 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -373,7 +373,7 @@ class MemberService( val memberTokenSet = memberToken.tokenList.toMutableSet() memberTokenSet.remove(token) - memberToken.tokenList = memberTokenSet.toList() + memberToken.tokenList = memberTokenSet.toMutableList() tokenRepository.save(memberToken) } } From 6ff38decab17dcb94fa462120a73240e49efa33a Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 14:46:31 +0900 Subject: [PATCH 71/94] account -> member --- .../admin/charge/AdminChargeStatusService.kt | 2 +- .../admin/charge/GetChargeStatusDetailQueryDto.kt | 2 +- .../admin/charge/GetChargeStatusDetailResponse.kt | 2 +- .../sodalive/explorer/ExplorerQueryRepository.kt | 12 ++++++------ .../live/room/kickout/LiveRoomKickOutService.kt | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt index 1cb97c9..811e4bd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/AdminChargeStatusService.kt @@ -85,7 +85,7 @@ class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository) } .map { GetChargeStatusDetailResponse( - accountId = it.accountId, + memberId = it.memberId, nickname = it.nickname, method = it.method, amount = if (paymentGateway == PaymentGateway.APPLE_IAP) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt index 2d94030..fcfa0d9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailQueryDto.kt @@ -3,7 +3,7 @@ package kr.co.vividnext.sodalive.admin.charge import com.querydsl.core.annotations.QueryProjection data class GetChargeStatusDetailQueryDto @QueryProjection constructor( - val accountId: Long, + val memberId: Long, val nickname: String, val method: String, val appleChargeAmount: Double, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt index 0d8ae2f..430b6f8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/charge/GetChargeStatusDetailResponse.kt @@ -1,7 +1,7 @@ package kr.co.vividnext.sodalive.admin.charge data class GetChargeStatusDetailResponse( - val accountId: Long, + val memberId: Long, val nickname: String, val method: String, val amount: Int, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index eb29c23..33fb311 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -149,12 +149,12 @@ class ExplorerQueryRepository( .distinct() } - fun getSearchChannel(channel: String, accountId: Long): List { + fun getSearchChannel(channel: String, memberId: Long): List { return queryFactory.selectFrom(member) .where( member.nickname.containsIgnoreCase(channel) .and(member.isActive.isTrue) - .and(member.id.ne(accountId)) + .and(member.id.ne(memberId)) .and(member.role.eq(MemberRole.CREATOR)) ) .fetch() @@ -266,8 +266,8 @@ class ExplorerQueryRepository( }, account.tags .asSequence() - .filter { accountCounselorTag -> accountCounselorTag.tag.isActive } - .map { accountCounselorTag -> accountCounselorTag.tag.tag } + .filter { tag -> tag.tag.isActive } + .map { tag -> tag.tag.tag } .toList() ) } @@ -313,8 +313,8 @@ class ExplorerQueryRepository( }, it.tags .asSequence() - .filter { accountCounselorTag -> accountCounselorTag.tag.isActive } - .map { accountCounselorTag -> accountCounselorTag.tag.tag } + .filter { tag -> tag.tag.isActive } + .map { tag -> tag.tag.tag } .toList() ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt index 0ee28c6..b193a82 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/kickout/LiveRoomKickOutService.kt @@ -14,7 +14,7 @@ import org.springframework.stereotype.Service class LiveRoomKickOutService( private val roomInfoRepository: LiveRoomInfoRedisRepository, private val repository: LiveRoomKickOutRedisRepository, - private val accountRepository: MemberRepository, + private val memberRepository: MemberRepository, private val roomRepository: LiveRoomRepository, @Value("\${cloud.aws.cloud-front.host}") @@ -43,7 +43,7 @@ class LiveRoomKickOutService( liveRoomKickOut.kickOut(request.userId) repository.save(liveRoomKickOut) - val kickOutUser = accountRepository.findByIdOrNull(request.userId) + val kickOutUser = memberRepository.findByIdOrNull(request.userId) if (kickOutUser != null) { roomInfo.removeSpeaker(kickOutUser) roomInfo.removeListener(kickOutUser) From b556219e3629a09b4a09e1bf4d9de4f0123ae17e Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 14:47:00 +0900 Subject: [PATCH 72/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=9E=85?= =?UTF-8?q?=EC=9E=A5=20-=20=ED=87=B4=EC=9E=A5=20=ED=9A=9F=EC=88=98=20?= =?UTF-8?q?=EC=B2=B4=ED=81=AC,=20=EC=B0=A8=EB=8B=A8=EB=90=9C=20=EC=9C=A0?= =?UTF-8?q?=EC=A0=80=20=EC=B2=B4=ED=81=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 3244c86..171c8a3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -42,6 +42,7 @@ import kr.co.vividnext.sodalive.member.Gender import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable @@ -63,6 +64,7 @@ class LiveRoomService( private val roomInfoRepository: LiveRoomInfoRedisRepository, private val roomCancelRepository: LiveRoomCancelRepository, private val kickOutService: LiveRoomKickOutService, + private val blockMemberRepository: BlockMemberRepository, private val useCanCalculateRepository: UseCanCalculateRepository, private val reservationRepository: LiveReservationRepository, @@ -419,6 +421,12 @@ class LiveRoomService( throw SodaException("비밀번호가 일치하지 않습니다.\n다시 확인 후 입력해주세요.") } + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = room.member!!.id!!) + if (isBlocked) throw SodaException("${room.member!!.nickname}님의 요청으로 라이브에 입장할 수 없습니다.") + + val kickOutCount = kickOutService.getKickOutCount(roomId = room.id!!, userId = member.id!!) + if (kickOutCount >= 2) throw SodaException("${room.member!!.nickname}님의 요청으로 라이브에 참여할 수 없습니다.") + val lock = getOrCreateLock(memberId = member.id!!) lock.write { var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) From 14220ff6dc4760287fd8575dee9750b93e2012ac Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 15:36:22 +0900 Subject: [PATCH 73/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/admin/member/AdminMemberController.kt | 8 ++++++++ .../sodalive/admin/member/AdminMemberService.kt | 12 ++++++++++++ .../sodalive/admin/member/UpdateMemberRequest.kt | 8 ++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/UpdateMemberRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt index 43675a7..436e23f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -4,6 +4,8 @@ import kr.co.vividnext.sodalive.common.ApiResponse import org.springframework.data.domain.Pageable import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PutMapping +import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @@ -12,6 +14,12 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/admin/member") @PreAuthorize("hasRole('ADMIN')") class AdminMemberController(private val service: AdminMemberService) { + @PutMapping + fun updateMember(@RequestBody request: UpdateMemberRequest) = ApiResponse.ok( + service.updateMember(request = request), + "수정되었습니다." + ) + @GetMapping("/list") fun getMemberList(pageable: Pageable) = ApiResponse.ok(service.getMemberList(pageable)) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt index 0550575..8b81fbc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -5,7 +5,9 @@ import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRole import org.springframework.beans.factory.annotation.Value import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -16,6 +18,16 @@ class AdminMemberService( @Value("\${cloud.aws.cloud-front.host}") private val cloudFrontHost: String ) { + @Transactional + fun updateMember(request: UpdateMemberRequest) { + val member = repository.findByIdOrNull(request.id) + ?: throw SodaException("해당 유저가 없습니다.") + + if (member.role != request.userType) { + member.role = request.userType + } + } + fun getMemberList(pageable: Pageable): GetAdminMemberListResponse { val totalCount = repository.getMemberTotalCount() val memberList = processMemberListToGetAdminMemberListResponseItemList( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/UpdateMemberRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/UpdateMemberRequest.kt new file mode 100644 index 0000000..687ca6a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/UpdateMemberRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.admin.member + +import kr.co.vividnext.sodalive.member.MemberRole + +data class UpdateMemberRequest( + val id: Long, + val userType: MemberRole +) From f7cdf4097672552208103f02bf20e13c3f8f2f2d Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 18:20:00 +0900 Subject: [PATCH 74/94] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=ED=83=90=EC=83=89=20=EB=A9=94=EB=89=B4=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/explorer/AdminExplorerController.kt | 39 +++++ .../AdminExplorerSectionRepository.kt | 43 ++++++ .../admin/explorer/AdminExplorerService.kt | 144 ++++++++++++++++++ .../explorer/CreateExplorerSectionRequest.kt | 9 ++ .../GetAdminExplorerSectionResponse.kt | 16 ++ .../UpdateExplorerSectionOrdersRequest.kt | 6 + .../explorer/UpdateExplorerSectionRequest.kt | 11 ++ .../member/tag/MemberTagRepository.kt | 4 +- 8 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerSectionRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/CreateExplorerSectionRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/GetAdminExplorerSectionResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionOrdersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerController.kt new file mode 100644 index 0000000..59db936 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerController.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.admin.explorer + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.data.domain.Pageable +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.GetMapping +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 +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/admin/explorer") +@PreAuthorize("hasRole('ADMIN')") +class AdminExplorerController(private val service: AdminExplorerService) { + @PostMapping + fun createExplorerSection(@RequestBody request: CreateExplorerSectionRequest): ApiResponse { + service.createExplorerSection(request) + return ApiResponse.ok(null, "등록되었습니다.") + } + + @PutMapping + fun updateExplorerSection(@RequestBody request: UpdateExplorerSectionRequest) = ApiResponse.ok( + service.updateExplorerSection(request), + "수정되었습니다." + ) + + @PutMapping("/orders") + fun updateExplorerSectionOrders( + @RequestBody request: UpdateExplorerSectionOrdersRequest + ) = ApiResponse.ok( + service.updateExplorerSectionOrders(request.firstOrders, request.ids), + "수정되었습니다." + ) + + @GetMapping + fun getExplorerSections(pageable: Pageable) = ApiResponse.ok(service.getExplorerSections(pageable)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerSectionRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerSectionRepository.kt new file mode 100644 index 0000000..347da74 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerSectionRepository.kt @@ -0,0 +1,43 @@ +package kr.co.vividnext.sodalive.admin.explorer + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.explorer.section.ExplorerSection +import kr.co.vividnext.sodalive.explorer.section.QExplorerSection.explorerSection +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface AdminExplorerSectionRepository : JpaRepository, AdminExplorerSectionQueryRepository + +interface AdminExplorerSectionQueryRepository { + fun findByTitle(title: String): ExplorerSection? + fun findAllWithPaging(offset: Long, limit: Long): List + fun totalCount(): Int +} + +class AdminExplorerSectionQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AdminExplorerSectionQueryRepository { + override fun findByTitle(title: String): ExplorerSection? { + return queryFactory + .selectFrom(explorerSection) + .where(explorerSection.title.eq(title)) + .fetchFirst() + } + + override fun findAllWithPaging(offset: Long, limit: Long): List { + return queryFactory + .selectFrom(explorerSection) + .offset(offset) + .limit(limit) + .orderBy(explorerSection.orders.asc()) + .fetch() + } + + override fun totalCount(): Int { + return queryFactory + .selectFrom(explorerSection) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerService.kt new file mode 100644 index 0000000..7bab12d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/AdminExplorerService.kt @@ -0,0 +1,144 @@ +package kr.co.vividnext.sodalive.admin.explorer + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.explorer.section.ExplorerSection +import kr.co.vividnext.sodalive.explorer.section.ExplorerSectionCreatorTag +import kr.co.vividnext.sodalive.member.tag.MemberTagRepository +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class AdminExplorerService( + private val repository: AdminExplorerSectionRepository, + private val memberTagRepository: MemberTagRepository +) { + @Transactional + fun createExplorerSection(request: CreateExplorerSectionRequest): Long { + if (request.title.isBlank()) throw SodaException("제목을 입력하세요.") + + val findExplorerSection = repository.findByTitle(request.title) + if (findExplorerSection != null) throw SodaException("동일한 제목이 있습니다.") + + val explorerSection = ExplorerSection(title = request.title, isAdult = request.isAdult) + explorerSection.coloredTitle = request.coloredTitle + explorerSection.color = request.color + + val tags = mutableListOf() + request.tagList.forEach { + val findTag = memberTagRepository.findByTag(it) + if (findTag != null) { + val tag = ExplorerSectionCreatorTag() + tag.explorerSection = explorerSection + tag.tag = findTag + tags.add(tag) + } + } + + if (tags.size <= 0) throw SodaException("관심사를 선택하세요.") + explorerSection.tags = tags + + return repository.save(explorerSection).id!! + } + + @Transactional + fun updateExplorerSection(request: UpdateExplorerSectionRequest) { + if ( + request.title == null && + request.isAdult == null && + request.tagList == null && + request.color == null && + request.coloredTitle == null && + request.isActive == null + ) { + throw SodaException("변경사항이 없습니다.") + } + + val explorerSection = repository.findByIdOrNull(request.id) + ?: throw SodaException("해당하는 섹션이 없습니다.") + + if (request.title != null) { + if (request.title.isBlank()) throw SodaException("올바른 제목을 입력하세요.") + explorerSection.title = request.title + } + + if (request.isActive != null) { + explorerSection.isActive = request.isActive + } + + if (request.isAdult != null) { + explorerSection.isAdult = request.isAdult + } + + if (request.color != null) { + explorerSection.color = request.color + } + + if (request.coloredTitle != null) { + explorerSection.coloredTitle = request.coloredTitle + } + + if (request.tagList != null) { + val requestTagList = request.tagList.toMutableList() + val tags = explorerSection.tags.filter { + requestTagList.contains(it.tag!!.tag) + requestTagList.remove(it.tag!!.tag) + }.toMutableList() + + requestTagList.forEach { + val findTag = memberTagRepository.findByTag(it) + if (findTag != null) { + val tag = ExplorerSectionCreatorTag() + tag.explorerSection = explorerSection + tag.tag = findTag + tags.add(tag) + } + } + + if (tags.size <= 0) throw SodaException("관심사를 입력하세요.") + if (tags != explorerSection.tags) { + explorerSection.tags.clear() + explorerSection.tags.addAll(tags) + } + } + } + + @Transactional + fun updateExplorerSectionOrders(firstOrders: Int, ids: List) { + for (index in ids.indices) { + val explorerSection = repository.findByIdOrNull(ids[index]) + + if (explorerSection != null) { + explorerSection.orders = firstOrders + index + } + } + } + + fun getExplorerSections(pageable: Pageable): GetAdminExplorerSectionResponse { + val totalCount = repository.totalCount() + val explorerSectionItemList = repository + .findAllWithPaging(pageable.offset, pageable.pageSize.toLong()) + .map { + GetAdminExplorerSectionResponseItem( + id = it.id!!, + title = it.title, + coloredTitle = it.coloredTitle ?: "", + color = it.color ?: "", + isAdult = it.isAdult, + isActive = it.isActive, + tags = it.tags + .asSequence() + .filter { explorerSectionTag -> explorerSectionTag.tag!!.isActive } + .map { explorerSectionTag -> explorerSectionTag.tag!!.tag } + .toList() + ) + } + + return GetAdminExplorerSectionResponse( + totalCount = totalCount, + explorerSectionItemList = explorerSectionItemList + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/CreateExplorerSectionRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/CreateExplorerSectionRequest.kt new file mode 100644 index 0000000..7e43118 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/CreateExplorerSectionRequest.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.admin.explorer + +data class CreateExplorerSectionRequest( + val title: String, + val isAdult: Boolean, + val tagList: List, + val coloredTitle: String? = null, + val color: String? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/GetAdminExplorerSectionResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/GetAdminExplorerSectionResponse.kt new file mode 100644 index 0000000..049be02 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/GetAdminExplorerSectionResponse.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.admin.explorer + +data class GetAdminExplorerSectionResponse( + val totalCount: Int, + val explorerSectionItemList: List +) + +data class GetAdminExplorerSectionResponseItem( + val id: Long, + val title: String, + val coloredTitle: String, + val color: String, + val isAdult: Boolean, + val isActive: Boolean, + val tags: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionOrdersRequest.kt new file mode 100644 index 0000000..ff3f2c2 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionOrdersRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.admin.explorer + +data class UpdateExplorerSectionOrdersRequest( + val firstOrders: Int, + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionRequest.kt new file mode 100644 index 0000000..ee3d38f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/explorer/UpdateExplorerSectionRequest.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.admin.explorer + +data class UpdateExplorerSectionRequest( + val id: Long, + val title: String? = null, + val isAdult: Boolean? = null, + val tagList: List? = null, + val coloredTitle: String? = null, + val color: String? = null, + val isActive: Boolean? = null +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt index 84a2e62..078a252 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt @@ -5,7 +5,9 @@ import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag import org.springframework.beans.factory.annotation.Value import org.springframework.data.jpa.repository.JpaRepository -interface MemberTagRepository : JpaRepository, MemberTagQueryRepository +interface MemberTagRepository : JpaRepository, MemberTagQueryRepository { + fun findByTag(tag: String): CreatorTag? +} interface MemberTagQueryRepository { fun getTags(): List From 771dbeced040c5d1f993fd1f68b94cad3dce7945 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 8 Aug 2023 12:47:49 +0900 Subject: [PATCH 75/94] =?UTF-8?q?=EB=A0=88=EB=94=94=EC=8A=A4=20JWT=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=A0=80=EC=9E=A5=EC=86=8C=20-=20tokenLis?= =?UTF-8?q?t=20=EA=B0=80=20=EA=B3=84=EC=86=8D=20null=20=EC=9D=B4=20?= =?UTF-8?q?=EB=90=98=EB=A9=B4=EC=84=9C=20=EC=B4=88=EA=B8=B0=ED=99=94?= =?UTF-8?q?=EA=B0=80=20=EB=90=98=EC=A7=80=20=EC=95=8A=EC=95=84=20set=20?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98=EA=B3=A0=20def?= =?UTF-8?q?ault=20value=20=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt | 8 +++----- .../kr/co/vividnext/sodalive/member/MemberService.kt | 4 +--- .../kr/co/vividnext/sodalive/member/token/MemberToken.kt | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt index 2393dff..0ec16ac 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt @@ -66,11 +66,9 @@ class TokenProvider( val lock = getOrCreateLock(memberId = memberId) lock.write { val memberToken = tokenRepository.findByIdOrNull(memberId) - ?: MemberToken(id = memberId, tokenList = mutableListOf()) + ?: MemberToken(id = memberId) - val memberTokenSet = memberToken.tokenList.toMutableSet() - memberTokenSet.add(token) - memberToken.tokenList = memberTokenSet.toMutableList() + memberToken.tokenSet.add(token) tokenRepository.save(memberToken) } @@ -89,7 +87,7 @@ class TokenProvider( val memberToken = tokenRepository.findByIdOrNull(id = claims.subject.toLong()) ?: throw SodaException("로그인 정보를 확인해주세요.") - if (!memberToken.tokenList.contains(token)) throw SodaException("로그인 정보를 확인해주세요.") + if (!memberToken.tokenSet.contains(token)) throw SodaException("로그인 정보를 확인해주세요.") val member = repository.findByIdOrNull(id = claims.subject.toLong()) ?: throw SodaException("로그인 정보를 확인해주세요.") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 3dc8967..a916159 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -371,9 +371,7 @@ class MemberService( val memberToken = tokenRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") - val memberTokenSet = memberToken.tokenList.toMutableSet() - memberTokenSet.remove(token) - memberToken.tokenList = memberTokenSet.toMutableList() + memberToken.tokenSet.remove(token) tokenRepository.save(memberToken) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt index 1027928..1af6273 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/token/MemberToken.kt @@ -7,5 +7,5 @@ import org.springframework.data.redis.core.RedisHash data class MemberToken( @Id val id: Long, - var tokenList: MutableList + var tokenSet: MutableSet = mutableSetOf() ) From 705bf0b6b24dd8c342959f3129ee88ab81286971 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 8 Aug 2023 16:46:30 +0900 Subject: [PATCH 76/94] =?UTF-8?q?=ED=91=B8=EC=8B=9C=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4,=20=EA=B0=9C=EB=B3=84,=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=20=EC=83=9D=EC=84=B1,=20=EB=9D=BC=EC=9D=B4=EB=B8=8C?= =?UTF-8?q?=20=EC=8B=9C=EC=9E=91,=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1,=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 3 + .../sodalive/configs/FirebaseConfig.kt | 25 +++ .../sodalive/content/AudioContentService.kt | 15 ++ .../vividnext/sodalive/fcm/FcmController.kt | 47 +++++ .../kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 137 +++++++++++++++ .../co/vividnext/sodalive/fcm/FcmService.kt | 39 +++++ .../GetMessageRecipientPushTokenResponse.kt | 8 + .../co/vividnext/sodalive/fcm/PushRequest.kt | 7 + .../sodalive/live/room/LiveRoomService.kt | 30 ++++ .../sodalive/member/MemberRepository.kt | 164 +++++++++++++++++- .../member/block/BlockMemberRepository.kt | 12 ++ .../sodalive/message/MessageService.kt | 22 +++ src/main/resources/application.yml | 3 + 13 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt diff --git a/build.gradle.kts b/build.gradle.kts index dfb9490..84666cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,6 +55,9 @@ dependencies { implementation("org.json:json:20230227") implementation("com.google.code.findbugs:jsr305:3.0.2") + // firebase admin sdk + implementation("com.google.firebase:firebase-admin:9.2.0") + developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") runtimeOnly("com.mysql:mysql-connector-j") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt new file mode 100644 index 0000000..abf3132 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/FirebaseConfig.kt @@ -0,0 +1,25 @@ +package kr.co.vividnext.sodalive.configs + +import com.google.auth.oauth2.GoogleCredentials +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration +import java.io.FileInputStream +import javax.annotation.PostConstruct + +@Configuration +class FirebaseConfig( + @Value("\${firebase.secret-key-path}") + private val secretKeyPath: String +) { + + @PostConstruct + fun initialize() { + FirebaseOptions.builder() + .setCredentials(GoogleCredentials.fromStream(FileInputStream(secretKeyPath))) + .build() + + FirebaseApp.initializeApp() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 1bceec0..acaf24c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -17,10 +17,13 @@ import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository +import kr.co.vividnext.sodalive.fcm.FcmEvent +import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -45,6 +48,7 @@ class AudioContentService( private val s3Uploader: S3Uploader, private val objectMapper: ObjectMapper, private val audioContentCloudFront: AudioContentCloudFront, + private val applicationEventPublisher: ApplicationEventPublisher, @Value("\${cloud.aws.s3.content-bucket}") private val audioContentBucket: String, @@ -260,6 +264,17 @@ class AudioContentService( audioContent.isActive = true audioContent.content = content audioContent.duration = duration + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.UPLOAD_CONTENT, + title = audioContent.member!!.nickname, + message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", + isAuth = audioContent.isAdult, + contentId = contentId, + creatorId = audioContent.member!!.id + ) + ) } fun getDetail(id: Long, member: Member, timezone: String): GetAudioContentDetailResponse { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt new file mode 100644 index 0000000..a35b138 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmController.kt @@ -0,0 +1,47 @@ +package kr.co.vividnext.sodalive.fcm + +import org.springframework.context.ApplicationEventPublisher +import org.springframework.security.access.prepost.PreAuthorize +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 +@RequestMapping("/push") +@PreAuthorize("hasRole('ADMIN')") +class FcmController(private val applicationEventPublisher: ApplicationEventPublisher) { + @PostMapping + fun send( + @RequestBody request: PushRequest + ) = run { + if (request.memberIds.isNotEmpty()) { + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.INDIVIDUAL, + title = request.title, + message = request.message, + recipients = request.memberIds + ) + ) + } else { + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.ALL, + title = request.title, + message = request.message, + container = "ios" + ) + ) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.ALL, + title = request.title, + message = request.message, + container = "aos" + ) + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt new file mode 100644 index 0000000..9707f86 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -0,0 +1,137 @@ +package kr.co.vividnext.sodalive.fcm + +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.context.event.EventListener +import org.springframework.stereotype.Component + +enum class FcmEventType { + ALL, INDIVIDUAL, CREATE_LIVE, START_LIVE, UPLOAD_CONTENT, SEND_MESSAGE +} + +class FcmEvent( + val type: FcmEventType, + val title: String, + val message: String, + val container: String = "", + val recipients: List = listOf(), + val isAuth: Boolean = false, + val roomId: Long? = null, + val contentId: Long? = null, + val messageId: Long? = null, + val creatorId: Long? = null +) + +@Component +class FcmSendListener( + private val pushService: FcmService, + private val memberRepository: MemberRepository +) { + @EventListener + fun send(fcmEvent: FcmEvent) { + when (fcmEvent.type) { + FcmEventType.ALL -> { + if (fcmEvent.container.isNotBlank()) { + val pushTokens = memberRepository.getAllRecipientPushTokens( + fcmEvent.isAuth, + fcmEvent.container + ) + + for (tokens in pushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container + ) + } + } + } + + FcmEventType.INDIVIDUAL -> { + if (fcmEvent.recipients.isNotEmpty()) { + val pushTokens = memberRepository.getIndividualRecipientPushTokens( + recipients = fcmEvent.recipients, + isAuth = fcmEvent.isAuth + ) + + val iosPushTokens = pushTokens["ios"] + val aosPushToken = pushTokens["aos"] + + if (iosPushTokens != null) { + for (tokens in iosPushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container + ) + } + } + + if (aosPushToken != null) { + for (tokens in aosPushToken) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container + ) + } + } + } + } + + FcmEventType.CREATE_LIVE, FcmEventType.START_LIVE -> { + if (fcmEvent.container.isNotBlank()) { + val pushTokens = memberRepository.getCreateLiveRoomNotificationRecipientPushTokens( + creatorId = fcmEvent.creatorId!!, + isAuth = fcmEvent.isAuth, + container = fcmEvent.container + ) + + for (tokens in pushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container, + roomId = fcmEvent.roomId + ) + } + } + } + + FcmEventType.UPLOAD_CONTENT -> { + if (fcmEvent.container.isNotBlank()) { + val pushTokens = memberRepository.getUploadContentNotificationRecipientPushTokens( + creatorId = fcmEvent.creatorId!!, + isAuth = fcmEvent.isAuth, + container = fcmEvent.container + ) + + for (tokens in pushTokens) { + pushService.send( + tokens = tokens, + title = fcmEvent.title, + message = fcmEvent.message, + container = fcmEvent.container, + contentId = fcmEvent.contentId + ) + } + } + } + + FcmEventType.SEND_MESSAGE -> { + val response = memberRepository.getMessageRecipientPushToken(messageId = fcmEvent.messageId!!) + + pushService.send( + tokens = listOf(response.pushToken), + title = fcmEvent.title, + message = fcmEvent.message, + container = response.container, + messageId = fcmEvent.messageId + ) + } + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt new file mode 100644 index 0000000..7a678fa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt @@ -0,0 +1,39 @@ +package kr.co.vividnext.sodalive.fcm + +import com.google.firebase.messaging.FirebaseMessaging +import com.google.firebase.messaging.MulticastMessage +import org.springframework.scheduling.annotation.Async +import org.springframework.stereotype.Service + +@Service +class FcmService { + @Async + fun send( + tokens: List, + title: String, + message: String, + container: String, + roomId: Long? = null, + messageId: Long? = null, + contentId: Long? = null + ) { + val multicastMessage = MulticastMessage.builder() + .putData("title", title) + .putData("message", message) + .addAllTokens(tokens) + + if (roomId != null) { + multicastMessage.putData("room_id", roomId.toString()) + } + + if (messageId != null) { + multicastMessage.putData("message_id", messageId.toString()) + } + + if (contentId != null) { + multicastMessage.putData("content_id", contentId.toString()) + } + + FirebaseMessaging.getInstance().sendEachForMulticast(multicastMessage.build()) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt new file mode 100644 index 0000000..e20dd2e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/GetMessageRecipientPushTokenResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.fcm + +import com.querydsl.core.annotations.QueryProjection + +data class GetMessageRecipientPushTokenResponse @QueryProjection constructor( + val pushToken: String, + val container: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt new file mode 100644 index 0000000..c0966b4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/PushRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.fcm + +data class PushRequest( + val memberIds: List, + val title: String, + val message: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 171c8a3..50d6a21 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -19,6 +19,8 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.extensions.convertLocalDateTime +import kr.co.vividnext.sodalive.fcm.FcmEvent +import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancel @@ -45,6 +47,7 @@ import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -66,6 +69,7 @@ class LiveRoomService( private val kickOutService: LiveRoomKickOutService, private val blockMemberRepository: BlockMemberRepository, + private val applicationEventPublisher: ApplicationEventPublisher, private val useCanCalculateRepository: UseCanCalculateRepository, private val reservationRepository: LiveReservationRepository, private val explorerQueryRepository: ExplorerQueryRepository, @@ -231,6 +235,21 @@ class LiveRoomService( room.bgImage = request.coverImageUrl } + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.CREATE_LIVE, + title = createdRoom.member!!.nickname, + message = if (createdRoom.channelName != null) { + "라이브를 시작했습니다. - ${createdRoom.title}" + } else { + "라이브를 개설했습니다. - ${createdRoom.title}" + }, + isAuth = createdRoom.isAdult, + roomId = createdRoom.id, + creatorId = createdRoom.member!!.id + ) + ) + return CreateLiveRoomResponse(createdRoom.id, createdRoom.channelName) } @@ -351,6 +370,17 @@ class LiveRoomService( room.channelName = "SODA_LIVE_CHANNEL_" + "${member.id}_${dateTime.year}_${dateTime.month}_${dateTime.dayOfMonth}_" + "${dateTime.hour}_${dateTime.minute}" + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.START_LIVE, + title = room.member!!.nickname, + message = "라이브를 시작했습니다 - ${room.title}", + isAuth = room.isAdult, + roomId = room.id, + creatorId = room.member!!.id + ) + ) } @Transactional diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 6eaa3fd..9561f1d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -1,7 +1,14 @@ package kr.co.vividnext.sodalive.member import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.fcm.GetMessageRecipientPushTokenResponse +import kr.co.vividnext.sodalive.fcm.QGetMessageRecipientPushTokenResponse import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.member.auth.QAuth.auth +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing +import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification +import kr.co.vividnext.sodalive.message.QMessage.message import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -15,10 +22,28 @@ interface MemberQueryRepository { fun findByPushToken(pushToken: String): List fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List fun findCreatorByIdOrNull(memberId: Long): Member? + fun getAllRecipientPushTokens(isAuth: Boolean, container: String): List> + fun getCreateLiveRoomNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> + + fun getUploadContentNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> + + fun getMessageRecipientPushToken(messageId: Long): GetMessageRecipientPushTokenResponse + fun getIndividualRecipientPushTokens(recipients: List, isAuth: Boolean): Map>> } @Repository -class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MemberQueryRepository { +class MemberQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + private val blockMemberRepository: BlockMemberRepository +) : MemberQueryRepository { override fun findByPushToken(pushToken: String): List { return queryFactory .selectFrom(member) @@ -47,4 +72,141 @@ class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Mem ) .fetchFirst() } + + override fun getAllRecipientPushTokens(isAuth: Boolean, container: String): List> { + var where = member.isActive.isTrue + .and(member.email.notIn("admin@sodalive.net")) + .and(member.container.eq(container)) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + return queryFactory + .select(member.pushToken) + .from(member) + .leftJoin(member.auth, auth) + .where(where) + .fetch() + .toSet() + .chunked(500) + } + + override fun getCreateLiveRoomNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> { + val member = QMember.member + val creator = QMember.member + + var where = creatorFollowing.isActive.isTrue + .and(creator.id.eq(creatorId)) + .and(member.email.notIn("admin@sodalive.net")) + .and(member.container.eq(container)) + .and(memberNotification.live.isTrue) + .and( + member.id.notIn( + blockMemberRepository.getBlockedMemberList(creatorId) + ) + ) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + return queryFactory + .select(member.pushToken) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, creator) + .innerJoin(creatorFollowing.member, member) + .innerJoin(member.notification, memberNotification) + .leftJoin(member.auth, auth) + .where(where) + .fetch() + .toSet() + .chunked(500) + } + + override fun getUploadContentNotificationRecipientPushTokens( + creatorId: Long, + isAuth: Boolean, + container: String + ): List> { + val member = QMember.member + val creator = QMember.member + + var where = creatorFollowing.isActive.isTrue + .and(creator.id.eq(creatorId)) + .and(member.email.notIn("admin@sodalive.net")) + .and(member.container.eq(container)) + .and(memberNotification.uploadContent.isTrue) + .and( + member.id.notIn( + blockMemberRepository.getBlockedMemberList(creatorId) + ) + ) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + return queryFactory + .select(member.pushToken) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, creator) + .innerJoin(creatorFollowing.member, member) + .innerJoin(member.notification, memberNotification) + .leftJoin(member.auth, auth) + .where(where) + .fetch() + .toSet() + .chunked(500) + } + + override fun getMessageRecipientPushToken(messageId: Long): GetMessageRecipientPushTokenResponse { + return queryFactory + .select( + QGetMessageRecipientPushTokenResponse( + member.pushToken, + member.container + ) + ) + .from(message) + .innerJoin(message.recipient, member) + .where(message.id.eq(messageId)) + .fetchFirst() + } + + override fun getIndividualRecipientPushTokens( + recipients: List, + isAuth: Boolean + ): Map>> { + var where = member.isActive.isTrue + .and(member.email.notIn("admin@sodalive.net")) + + if (isAuth) { + where = where.and(member.auth.isNotNull) + } + + val aosPushTokens = queryFactory + .select(member.pushToken) + .from(member) + .leftJoin(member.auth, auth) + .where(where.and(member.container.eq("aos"))) + .fetch() + .toSet() + .chunked(500) + + val iosPushTokens = queryFactory + .select(member.pushToken) + .from(member) + .leftJoin(member.auth, auth) + .where(where.and(member.container.eq("ios"))) + .fetch() + .toSet() + .chunked(500) + + return mapOf("aos" to aosPushTokens, "ios" to iosPushTokens) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt index 29eb50e..f81572d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/block/BlockMemberRepository.kt @@ -11,6 +11,7 @@ interface BlockMemberRepository : JpaRepository, BlockMemberQ interface BlockMemberQueryRepository { fun getBlockAccount(blockedMemberId: Long, memberId: Long): BlockMember? fun isBlocked(blockedMemberId: Long, memberId: Long): Boolean + fun getBlockedMemberList(creatorId: Long): List } @Repository @@ -39,4 +40,15 @@ class BlockMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) return blockedAccount != null } + + override fun getBlockedMemberList(creatorId: Long): List { + return queryFactory + .select(blockMember.blockedMemberId) + .from(blockMember) + .where( + blockMember.memberId.eq(creatorId) + .and(blockMember.isActive.isTrue) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt index a6e3ec5..07f22c8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt @@ -4,11 +4,14 @@ import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.fcm.FcmEvent +import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value +import org.springframework.context.ApplicationEventPublisher import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -24,6 +27,7 @@ class MessageService( private val memberRepository: MemberRepository, private val blockMemberRepository: BlockMemberRepository, + private val applicationEventPublisher: ApplicationEventPublisher, private val objectMapper: ObjectMapper, private val s3Uploader: S3Uploader, @@ -56,6 +60,15 @@ class MessageService( message.recipient = recipient repository.save(message) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.SEND_MESSAGE, + title = "메시지", + message = "${sender.nickname}님으로 부터 문자메시지가 도착했습니다.", + messageId = message.id + ) + ) } fun getSentTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { @@ -115,6 +128,15 @@ class MessageService( ) message.voiceMessage = messagePath + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.SEND_MESSAGE, + title = "메시지", + message = "${sender.nickname}님으로 부터 음성메시지가 도착했습니다.", + messageId = message.id + ) + ) } fun getSentVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetVoiceMessageResponse { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c33f8dc..1941134 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,6 +20,9 @@ agora: appId: ${AGORA_APP_ID} appCertificate: ${AGORA_APP_CERTIFICATE} +firebase: + secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS} + cloud: aws: credentials: From e9723d37bad183dc4a77d454bd2d61f3326a64ad Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 06:54:25 +0900 Subject: [PATCH 77/94] =?UTF-8?q?=EC=B0=A8=EB=8B=A8=EC=9C=A0=EC=A0=80=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../live/recommend/LiveRecommendRepository.kt | 7 ++++++- .../live/recommend/LiveRecommendService.kt | 15 ++++++++++++--- .../sodalive/live/room/LiveRoomService.kt | 14 +++++++++++--- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 7895ea5..7f12513 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -44,6 +44,7 @@ class LiveRecommendRepository( fun getOnAirRecommendChannelList( memberId: Long, + isBlocked: (Long) -> Boolean, isAdult: Boolean ): List { var where = member.role.eq(MemberRole.CREATOR) @@ -75,12 +76,16 @@ class LiveRecommendRepository( .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) .limit(20) .fetch() + .asSequence() + .filter { !isBlocked(it.creatorId) } + .toList() } fun getRecommendChannelList( memberId: Long, withOutCreatorList: List, - limit: Long + limit: Long, + isBlocked: (Long) -> Boolean ): List { val where = member.role.eq(MemberRole.CREATOR) .and(member.isActive.isTrue) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt index 9b400ae..5ffe915 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -1,10 +1,14 @@ package kr.co.vividnext.sodalive.live.recommend import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import org.springframework.stereotype.Service @Service -class LiveRecommendService(private val repository: LiveRecommendRepository) { +class LiveRecommendService( + private val repository: LiveRecommendRepository, + private val blockMemberRepository: BlockMemberRepository +) { fun getRecommendLive(member: Member): List { return repository.getRecommendLive( @@ -14,7 +18,11 @@ class LiveRecommendService(private val repository: LiveRecommendRepository) { } fun getRecommendChannelList(member: Member): List { - val onAirChannelList = repository.getOnAirRecommendChannelList(member.id!!, isAdult = member.auth != null) + val onAirChannelList = repository.getOnAirRecommendChannelList( + member.id!!, + isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) }, + isAdult = member.auth != null + ) if (onAirChannelList.size >= 20) { return onAirChannelList @@ -27,7 +35,8 @@ class LiveRecommendService(private val repository: LiveRecommendRepository) { val notOnAirCreatorList = repository.getRecommendChannelList( member.id!!, withOutCreatorList = onAirCreatorIdList, - limit = (20 - onAirChannelList.size).toLong() + limit = (20 - onAirChannelList.size).toLong(), + isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) } ) return onAirChannelList + notOnAirCreatorList diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 50d6a21..4de90c2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -115,9 +115,13 @@ class LiveRoomService( isAdult = member.auth != null ) .asSequence() + .filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } .map { val roomInfo = roomInfoRepository.findByIdOrNull(it.id!!) + val reservations = it.reservations + .filter { reservation -> reservation.member!!.id!! == member.id!! && reservation.isActive } + val beginDateTime = it.beginDateTime .atZone(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of(timezone)) @@ -138,13 +142,17 @@ class LiveRoomService( channelName = it.channelName, managerNickname = it.member!!.nickname, managerId = it.member!!.id!!, - tags = listOf(), + tags = it.tags + .asSequence() + .filter { tag -> tag.tag.isActive } + .map { tag -> tag.tag.tag } + .toList(), coverImageUrl = if (it.coverImage!!.startsWith("https://")) { it.coverImage!! } else { "$cloudFrontHost/${it.coverImage!!}" }, - isReservation = false, + isReservation = reservations.isNotEmpty(), isPrivateRoom = it.type == LiveRoomType.PRIVATE ) } @@ -742,7 +750,7 @@ class LiveRoomService( isSpeaker = isSpeaker, isManager = isManager, isFollowing = isFollowing, - isBlock = false + isBlock = blockMemberRepository.isBlocked(blockedMemberId = userResponse.id, memberId = memberResponse.id) ) } From ba3444fd26de0b983a474d7cc727118f43edd6a8 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 07:14:54 +0900 Subject: [PATCH 78/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20-=20=ED=8C=94=EB=A1=9C=EC=9E=89=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../live/recommend/LiveRecommendController.kt | 15 +++- .../live/recommend/LiveRecommendRepository.kt | 76 +++++++++++++++++++ .../live/recommend/LiveRecommendService.kt | 25 ++++++ 3 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt index 3c38eef..5400225 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt @@ -5,11 +5,13 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController +@RequestMapping("/live/recommend") class LiveRecommendController(private val service: LiveRecommendService) { - @GetMapping("/live/recommend") + @GetMapping fun getRecommendLive( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { @@ -18,7 +20,7 @@ class LiveRecommendController(private val service: LiveRecommendService) { ApiResponse.ok(service.getRecommendLive(member)) } - @GetMapping("/live/recommend/channel") + @GetMapping("/channel") fun getRecommendChannelList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? ) = run { @@ -26,4 +28,13 @@ class LiveRecommendController(private val service: LiveRecommendService) { ApiResponse.ok(service.getRecommendChannelList(member)) } + + @GetMapping("/following/channel/list") + fun getFollowingChannelList( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getFollowingChannelList(member)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 7f12513..5a7c1c5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member +import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -106,4 +107,79 @@ class LiveRecommendRepository( .limit(limit) .fetch() } + + fun getOnAirFollowingChannelList( + memberId: Long, + isBlocked: (Long) -> Boolean, + isAdult: Boolean + ): List { + var where = member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + + if (!isAdult) { + where = where.and(liveRoom.isAdult.isFalse) + } + + return queryFactory + .select( + Projections.constructor( + GetRecommendChannelResponse::class.java, + member.id, + member.nickname, + member.profileImage.prepend("/").prepend(cloudFrontHost), + Expressions.asBoolean(true) + ) + ) + .from(liveRoom, creatorFollowing) + .rightJoin(liveRoom.member, member) + .innerJoin(creatorFollowing.creator, member) + .where( + where + .and(liveRoom.member.id.eq(creatorFollowing.creator.id)) + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.member.id.eq(memberId)) + .and(liveRoom.isActive.isTrue) + .and(liveRoom.channelName.isNotNull) + .and(liveRoom.channelName.isNotEmpty) + ) + .groupBy(member.id) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .limit(20) + .fetch() + .asSequence() + .filter { !isBlocked(it.creatorId) } + .toList() + } + + fun getFollowingChannelList( + memberId: Long, + withOutCreatorList: List, + limit: Long, + isBlocked: (Long) -> Boolean + ): List { + val where = member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + + return queryFactory + .select( + Projections.constructor( + GetRecommendChannelResponse::class.java, + member.id, + member.nickname, + member.profileImage.prepend("/").prepend(cloudFrontHost), + Expressions.asBoolean(false) + ) + ) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, member) + .where( + where + .and(member.id.notIn(withOutCreatorList)) + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.member.id.eq(memberId)) + ) + .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) + .limit(limit) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt index 5ffe915..279718e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -41,4 +41,29 @@ class LiveRecommendService( return onAirChannelList + notOnAirCreatorList } + + fun getFollowingChannelList(member: Member): List { + val onAirFollowingChannelList = repository.getOnAirFollowingChannelList( + memberId = member.id!!, + isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) }, + isAdult = member.auth != null + ) + + if (onAirFollowingChannelList.size >= 20) { + return onAirFollowingChannelList + } + + val onAirCreatorIdList = onAirFollowingChannelList.asSequence() + .map { it.creatorId } + .toList() + + val notOnAirFollowingChannelList = repository.getFollowingChannelList( + memberId = member.id!!, + withOutCreatorList = onAirCreatorIdList, + limit = (20 - onAirCreatorIdList.size).toLong(), + isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) } + ) + + return onAirFollowingChannelList + notOnAirFollowingChannelList + } } From 777f4755bed4b7861ff3dac2c928b1225d815790 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 07:48:43 +0900 Subject: [PATCH 79/94] =?UTF-8?q?=ED=8C=94=EB=A1=9C=EC=9E=89=20=EC=B1=84?= =?UTF-8?q?=EB=84=90=20=EC=A0=84=EC=B2=B4=EB=B3=B4=EA=B8=B0=20API=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetCreatorFollowingAllListResponse.kt | 13 +++++ .../live/recommend/LiveRecommendController.kt | 11 ++++ .../live/recommend/LiveRecommendRepository.kt | 52 +++++++++++++++++++ .../live/recommend/LiveRecommendService.kt | 20 +++++++ 4 files changed, 96 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetCreatorFollowingAllListResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetCreatorFollowingAllListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetCreatorFollowingAllListResponse.kt new file mode 100644 index 0000000..2ae3bc4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/GetCreatorFollowingAllListResponse.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.live.recommend + +data class GetCreatorFollowingAllListResponse( + val totalCount: Int, + val items: List +) + +data class GetCreatorFollowingAllListItem( + val creatorId: Long, + val nickname: String, + val profileImageUrl: String, + val isFollow: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt index 5400225..a7c005c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendController.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.live.recommend import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping @@ -37,4 +38,14 @@ class LiveRecommendController(private val service: LiveRecommendService) { ApiResponse.ok(service.getFollowingChannelList(member)) } + + @GetMapping("/following/channel/all/list") + fun getFollowingAllChannelList( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getFollowingAllChannelList(member, pageable)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt index 5a7c1c5..3287f3e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepository.kt @@ -182,4 +182,56 @@ class LiveRecommendRepository( .limit(limit) .fetch() } + + fun getCreatorFollowingAllListTotalCount(memberId: Long, isBlocked: (Long) -> Boolean): Int { + val where = member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.member.id.eq(memberId)) + + return queryFactory + .select(creatorFollowing.creator.id) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, member) + .where(where) + .groupBy(member.id) + .fetch() + .asSequence() + .filter { !isBlocked(it) } + .toList() + .size + } + + fun getCreatorFollowingAllList( + memberId: Long, + offset: Long, + limit: Long, + isBlocked: (Long) -> Boolean + ): List { + val where = member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + .and(creatorFollowing.isActive.isTrue) + .and(creatorFollowing.member.id.eq(memberId)) + + return queryFactory + .select( + Projections.constructor( + GetCreatorFollowingAllListItem::class.java, + member.id, + member.nickname, + member.profileImage.prepend("/").prepend(cloudFrontHost), + Expressions.asBoolean(true) + ) + ) + .from(creatorFollowing) + .innerJoin(creatorFollowing.creator, member) + .where(where) + .groupBy(member.id) + .offset(offset) + .limit(limit) + .fetch() + .asSequence() + .filter { !isBlocked(it.creatorId) } + .toList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt index 279718e..a842415 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendService.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.live.recommend import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service @Service @@ -66,4 +67,23 @@ class LiveRecommendService( return onAirFollowingChannelList + notOnAirFollowingChannelList } + + fun getFollowingAllChannelList(member: Member, pageable: Pageable): GetCreatorFollowingAllListResponse { + val totalCount = repository.getCreatorFollowingAllListTotalCount( + memberId = member.id!!, + isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) } + ) + + val items = repository.getCreatorFollowingAllList( + memberId = member.id!!, + offset = pageable.offset, + limit = pageable.pageSize.toLong(), + isBlocked = { blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it) } + ) + + return GetCreatorFollowingAllListResponse( + totalCount = totalCount, + items = items + ) + } } From eb09251955db47b35f2351a491311524ba7d26bf Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 09:16:27 +0900 Subject: [PATCH 80/94] =?UTF-8?q?debug=20logger=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index 9707f86..eec8a93 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.fcm import kr.co.vividnext.sodalive.member.MemberRepository +import org.slf4j.LoggerFactory import org.springframework.context.event.EventListener import org.springframework.stereotype.Component @@ -26,6 +27,8 @@ class FcmSendListener( private val pushService: FcmService, private val memberRepository: MemberRepository ) { + private val logger = LoggerFactory.getLogger(FcmSendListener::class.java) + @EventListener fun send(fcmEvent: FcmEvent) { when (fcmEvent.type) { @@ -36,6 +39,8 @@ class FcmSendListener( fcmEvent.container ) + logger.info("tokens::: $pushTokens") + for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -54,6 +59,8 @@ class FcmSendListener( isAuth = fcmEvent.isAuth ) + logger.info("tokens::: $pushTokens") + val iosPushTokens = pushTokens["ios"] val aosPushToken = pushTokens["aos"] @@ -89,6 +96,8 @@ class FcmSendListener( container = fcmEvent.container ) + logger.info("tokens::: $pushTokens") + for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -109,6 +118,8 @@ class FcmSendListener( container = fcmEvent.container ) + logger.info("tokens::: $pushTokens") + for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -124,6 +135,8 @@ class FcmSendListener( FcmEventType.SEND_MESSAGE -> { val response = memberRepository.getMessageRecipientPushToken(messageId = fcmEvent.messageId!!) + logger.info("tokens::: ${response.pushToken}") + pushService.send( tokens = listOf(response.pushToken), title = fcmEvent.title, From 11f1f781a0c68dc39b0eca7b2e72e909c7bf6390 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 10:06:06 +0900 Subject: [PATCH 81/94] =?UTF-8?q?EventListener=20->=20TransactionalEventLi?= =?UTF-8?q?stener=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index eec8a93..f393651 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -2,8 +2,8 @@ package kr.co.vividnext.sodalive.fcm import kr.co.vividnext.sodalive.member.MemberRepository import org.slf4j.LoggerFactory -import org.springframework.context.event.EventListener import org.springframework.stereotype.Component +import org.springframework.transaction.event.TransactionalEventListener enum class FcmEventType { ALL, INDIVIDUAL, CREATE_LIVE, START_LIVE, UPLOAD_CONTENT, SEND_MESSAGE @@ -29,7 +29,7 @@ class FcmSendListener( ) { private val logger = LoggerFactory.getLogger(FcmSendListener::class.java) - @EventListener + @TransactionalEventListener fun send(fcmEvent: FcmEvent) { when (fcmEvent.type) { FcmEventType.ALL -> { From b331e6e152ff53f47394b8ac8beba4703ee97743 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 10:13:28 +0900 Subject: [PATCH 82/94] =?UTF-8?q?TransactionalEventListener=20->=20EventLi?= =?UTF-8?q?stener=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index f393651..fc2e324 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -2,8 +2,9 @@ package kr.co.vividnext.sodalive.fcm import kr.co.vividnext.sodalive.member.MemberRepository import org.slf4j.LoggerFactory +import org.springframework.context.event.EventListener +import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component -import org.springframework.transaction.event.TransactionalEventListener enum class FcmEventType { ALL, INDIVIDUAL, CREATE_LIVE, START_LIVE, UPLOAD_CONTENT, SEND_MESSAGE @@ -29,7 +30,8 @@ class FcmSendListener( ) { private val logger = LoggerFactory.getLogger(FcmSendListener::class.java) - @TransactionalEventListener + @Async + @EventListener fun send(fcmEvent: FcmEvent) { when (fcmEvent.type) { FcmEventType.ALL -> { From 2548c92aa1d121fd7425b6b2e930787779bf33ca Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 10:29:08 +0900 Subject: [PATCH 83/94] =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=EC=9C=A0=EC=A0=80?= =?UTF-8?q?=20=ED=91=B8=EC=8B=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 9561f1d..7fe2237 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -184,6 +184,7 @@ class MemberQueryRepositoryImpl( ): Map>> { var where = member.isActive.isTrue .and(member.email.notIn("admin@sodalive.net")) + .and(member.id.`in`(*recipients.toTypedArray())) if (isAuth) { where = where.and(member.auth.isNotNull) From ccb32a73ddf83b80ca12f4dbb91e3c3895977545 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 10:36:40 +0900 Subject: [PATCH 84/94] =?UTF-8?q?=ED=91=B8=EC=8B=9C=20=EB=B0=9C=EC=86=A1?= =?UTF-8?q?=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20-=20container=EA=B0=80=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=EC=97=90=20con?= =?UTF-8?q?tainer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/content/AudioContentService.kt | 15 +++++++- .../kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 16 ++++----- .../sodalive/live/room/LiveRoomService.kt | 34 +++++++++++++++++-- 3 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index acaf24c..f849a25 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -272,7 +272,20 @@ class AudioContentService( message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", isAuth = audioContent.isAdult, contentId = contentId, - creatorId = audioContent.member!!.id + creatorId = audioContent.member!!.id, + container = "ios" + ) + ) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.UPLOAD_CONTENT, + title = audioContent.member!!.nickname, + message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", + isAuth = audioContent.isAdult, + contentId = contentId, + creatorId = audioContent.member!!.id, + container = "aos" ) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index fc2e324..f2b3df9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -35,14 +35,13 @@ class FcmSendListener( fun send(fcmEvent: FcmEvent) { when (fcmEvent.type) { FcmEventType.ALL -> { + logger.info("ALL") if (fcmEvent.container.isNotBlank()) { val pushTokens = memberRepository.getAllRecipientPushTokens( fcmEvent.isAuth, fcmEvent.container ) - logger.info("tokens::: $pushTokens") - for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -55,14 +54,13 @@ class FcmSendListener( } FcmEventType.INDIVIDUAL -> { + logger.info("INDIVIDUAL") if (fcmEvent.recipients.isNotEmpty()) { val pushTokens = memberRepository.getIndividualRecipientPushTokens( recipients = fcmEvent.recipients, isAuth = fcmEvent.isAuth ) - logger.info("tokens::: $pushTokens") - val iosPushTokens = pushTokens["ios"] val aosPushToken = pushTokens["aos"] @@ -91,6 +89,8 @@ class FcmSendListener( } FcmEventType.CREATE_LIVE, FcmEventType.START_LIVE -> { + logger.info("CREATE_LIVE") + logger.info("START_LIVE") if (fcmEvent.container.isNotBlank()) { val pushTokens = memberRepository.getCreateLiveRoomNotificationRecipientPushTokens( creatorId = fcmEvent.creatorId!!, @@ -98,8 +98,6 @@ class FcmSendListener( container = fcmEvent.container ) - logger.info("tokens::: $pushTokens") - for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -113,6 +111,7 @@ class FcmSendListener( } FcmEventType.UPLOAD_CONTENT -> { + logger.info("UPLOAD_CONTENT") if (fcmEvent.container.isNotBlank()) { val pushTokens = memberRepository.getUploadContentNotificationRecipientPushTokens( creatorId = fcmEvent.creatorId!!, @@ -120,8 +119,6 @@ class FcmSendListener( container = fcmEvent.container ) - logger.info("tokens::: $pushTokens") - for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -135,10 +132,9 @@ class FcmSendListener( } FcmEventType.SEND_MESSAGE -> { + logger.info("SEND_MESSAGE") val response = memberRepository.getMessageRecipientPushToken(messageId = fcmEvent.messageId!!) - logger.info("tokens::: ${response.pushToken}") - pushService.send( tokens = listOf(response.pushToken), title = fcmEvent.title, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 4de90c2..a894b70 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -254,7 +254,24 @@ class LiveRoomService( }, isAuth = createdRoom.isAdult, roomId = createdRoom.id, - creatorId = createdRoom.member!!.id + creatorId = createdRoom.member!!.id, + container = "ios" + ) + ) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.CREATE_LIVE, + title = createdRoom.member!!.nickname, + message = if (createdRoom.channelName != null) { + "라이브를 시작했습니다. - ${createdRoom.title}" + } else { + "라이브를 개설했습니다. - ${createdRoom.title}" + }, + isAuth = createdRoom.isAdult, + roomId = createdRoom.id, + creatorId = createdRoom.member!!.id, + container = "aos" ) ) @@ -386,7 +403,20 @@ class LiveRoomService( message = "라이브를 시작했습니다 - ${room.title}", isAuth = room.isAdult, roomId = room.id, - creatorId = room.member!!.id + creatorId = room.member!!.id, + container = "ios" + ) + ) + + applicationEventPublisher.publishEvent( + FcmEvent( + type = FcmEventType.START_LIVE, + title = room.member!!.nickname, + message = "라이브를 시작했습니다 - ${room.title}", + isAuth = room.isAdult, + roomId = room.id, + creatorId = room.member!!.id, + container = "aos" ) ) } From e5531bbef7bb96e5b640aca896425c3c53e71bd9 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 10:43:39 +0900 Subject: [PATCH 85/94] debug logger --- src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index f2b3df9..e6dff28 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -35,7 +35,6 @@ class FcmSendListener( fun send(fcmEvent: FcmEvent) { when (fcmEvent.type) { FcmEventType.ALL -> { - logger.info("ALL") if (fcmEvent.container.isNotBlank()) { val pushTokens = memberRepository.getAllRecipientPushTokens( fcmEvent.isAuth, @@ -54,7 +53,6 @@ class FcmSendListener( } FcmEventType.INDIVIDUAL -> { - logger.info("INDIVIDUAL") if (fcmEvent.recipients.isNotEmpty()) { val pushTokens = memberRepository.getIndividualRecipientPushTokens( recipients = fcmEvent.recipients, @@ -89,8 +87,6 @@ class FcmSendListener( } FcmEventType.CREATE_LIVE, FcmEventType.START_LIVE -> { - logger.info("CREATE_LIVE") - logger.info("START_LIVE") if (fcmEvent.container.isNotBlank()) { val pushTokens = memberRepository.getCreateLiveRoomNotificationRecipientPushTokens( creatorId = fcmEvent.creatorId!!, @@ -98,6 +94,8 @@ class FcmSendListener( container = fcmEvent.container ) + logger.info("pushTokens: $pushTokens") + for (tokens in pushTokens) { pushService.send( tokens = tokens, @@ -111,7 +109,6 @@ class FcmSendListener( } FcmEventType.UPLOAD_CONTENT -> { - logger.info("UPLOAD_CONTENT") if (fcmEvent.container.isNotBlank()) { val pushTokens = memberRepository.getUploadContentNotificationRecipientPushTokens( creatorId = fcmEvent.creatorId!!, @@ -132,7 +129,6 @@ class FcmSendListener( } FcmEventType.SEND_MESSAGE -> { - logger.info("SEND_MESSAGE") val response = memberRepository.getMessageRecipientPushToken(messageId = fcmEvent.messageId!!) pushService.send( From 0fa16762bd0d6d5967dec5951053e032d7be45ee Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 10:59:36 +0900 Subject: [PATCH 86/94] debug logger --- .../sodalive/member/MemberRepository.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 7fe2237..db53dc1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification import kr.co.vividnext.sodalive.message.QMessage.message +import org.slf4j.LoggerFactory import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -92,6 +93,8 @@ class MemberQueryRepositoryImpl( .chunked(500) } + private val logger = LoggerFactory.getLogger(MemberRepository::class.java) + override fun getCreateLiveRoomNotificationRecipientPushTokens( creatorId: Long, isAuth: Boolean, @@ -100,13 +103,16 @@ class MemberQueryRepositoryImpl( val member = QMember.member val creator = QMember.member + logger.info("creatorId: $creatorId") + logger.info("isAuth: $isAuth") + logger.info("container: $container") + var where = creatorFollowing.isActive.isTrue - .and(creator.id.eq(creatorId)) - .and(member.email.notIn("admin@sodalive.net")) - .and(member.container.eq(container)) - .and(memberNotification.live.isTrue) + .and(creatorFollowing.creator.id.eq(creatorId)) + .and(creatorFollowing.member.notification.live.isTrue) + .and(creatorFollowing.member.email.notIn("admin@sodalive.net")) .and( - member.id.notIn( + creatorFollowing.member.id.notIn( blockMemberRepository.getBlockedMemberList(creatorId) ) ) @@ -116,7 +122,7 @@ class MemberQueryRepositoryImpl( } return queryFactory - .select(member.pushToken) + .select(creatorFollowing.member.pushToken) .from(creatorFollowing) .innerJoin(creatorFollowing.creator, creator) .innerJoin(creatorFollowing.member, member) From c876378b219a575d0e6f5286f04158b89c8c2c65 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 11:56:15 +0900 Subject: [PATCH 87/94] =?UTF-8?q?=ED=91=B8=EC=8B=9C=20=EB=B0=9C=EC=86=A1?= =?UTF-8?q?=20=EB=8C=80=EC=83=81=20=EC=9C=A0=EC=A0=80=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/member/MemberRepository.kt | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index db53dc1..615774c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -9,7 +9,6 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification import kr.co.vividnext.sodalive.message.QMessage.message -import org.slf4j.LoggerFactory import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -93,8 +92,6 @@ class MemberQueryRepositoryImpl( .chunked(500) } - private val logger = LoggerFactory.getLogger(MemberRepository::class.java) - override fun getCreateLiveRoomNotificationRecipientPushTokens( creatorId: Long, isAuth: Boolean, @@ -103,13 +100,10 @@ class MemberQueryRepositoryImpl( val member = QMember.member val creator = QMember.member - logger.info("creatorId: $creatorId") - logger.info("isAuth: $isAuth") - logger.info("container: $container") - var where = creatorFollowing.isActive.isTrue .and(creatorFollowing.creator.id.eq(creatorId)) .and(creatorFollowing.member.notification.live.isTrue) + .and(creatorFollowing.member.container.eq(container)) .and(creatorFollowing.member.email.notIn("admin@sodalive.net")) .and( creatorFollowing.member.id.notIn( @@ -143,12 +137,12 @@ class MemberQueryRepositoryImpl( val creator = QMember.member var where = creatorFollowing.isActive.isTrue - .and(creator.id.eq(creatorId)) - .and(member.email.notIn("admin@sodalive.net")) - .and(member.container.eq(container)) - .and(memberNotification.uploadContent.isTrue) + .and(creatorFollowing.creator.id.eq(creatorId)) + .and(creatorFollowing.member.email.notIn("admin@sodalive.net")) + .and(creatorFollowing.member.container.eq(container)) + .and(creatorFollowing.member.notification.uploadContent.isTrue) .and( - member.id.notIn( + creatorFollowing.member.id.notIn( blockMemberRepository.getBlockedMemberList(creatorId) ) ) @@ -158,7 +152,7 @@ class MemberQueryRepositoryImpl( } return queryFactory - .select(member.pushToken) + .select(creatorFollowing.member.pushToken) .from(creatorFollowing) .innerJoin(creatorFollowing.creator, creator) .innerJoin(creatorFollowing.member, member) From c9970ce7ca2acc751045e26f53c78fca5b004a9f Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 9 Aug 2023 12:17:37 +0900 Subject: [PATCH 88/94] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20lo?= =?UTF-8?q?gger=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt index e6dff28..721a8b7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt @@ -1,7 +1,6 @@ package kr.co.vividnext.sodalive.fcm import kr.co.vividnext.sodalive.member.MemberRepository -import org.slf4j.LoggerFactory import org.springframework.context.event.EventListener import org.springframework.scheduling.annotation.Async import org.springframework.stereotype.Component @@ -28,8 +27,6 @@ class FcmSendListener( private val pushService: FcmService, private val memberRepository: MemberRepository ) { - private val logger = LoggerFactory.getLogger(FcmSendListener::class.java) - @Async @EventListener fun send(fcmEvent: FcmEvent) { @@ -94,8 +91,6 @@ class FcmSendListener( container = fcmEvent.container ) - logger.info("pushTokens: $pushTokens") - for (tokens in pushTokens) { pushService.send( tokens = tokens, From c8764be69f9f87d666882302abc319f556702c37 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 13 Aug 2023 19:47:28 +0900 Subject: [PATCH 89/94] =?UTF-8?q?coin=20->=20can=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/admin/can/AdminCanService.kt | 2 +- .../sodalive/can/payment/CanPaymentService.kt | 12 ++++++------ .../sodalive/content/AudioContentService.kt | 2 +- .../content/comment/AudioContentCommentRepository.kt | 4 ++-- .../comment/GetAudioContentCommentListResponse.kt | 2 +- .../sodalive/explorer/ExplorerQueryRepository.kt | 6 +++--- .../explorer/MemberDonationRankingResponse.kt | 2 +- .../kr/co/vividnext/sodalive/faq/FaqController.kt | 2 +- .../vividnext/sodalive/live/room/LiveRoomService.kt | 8 ++++---- .../donation/GetLiveRoomDonationTotalResponse.kt | 2 +- .../sodalive/notice/ServiceNoticeController.kt | 2 +- 11 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt index 3cdd6e1..786138b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/can/AdminCanService.kt @@ -38,7 +38,7 @@ class AdminCanService( val member = memberRepository.findByIdOrNull(request.memberId) ?: throw SodaException("잘못된 회원번호 입니다.") - if (request.can <= 0) throw SodaException("0 코인 이상 입력하세요.") + if (request.can <= 0) throw SodaException("1 캔 이상 입력하세요.") if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.") val charge = Charge(0, request.can, status = ChargeStatus.ADMIN) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index d9dbbe0..fd3a937 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -90,15 +90,15 @@ class CanPaymentService( useCanRepository.save(useCan) - setUseCoinCalculate(recipientId, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.PG) - setUseCoinCalculate( + setUseCanCalculate(recipientId, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.PG) + setUseCanCalculate( recipientId, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.GOOGLE_IAP ) - setUseCoinCalculate( + setUseCanCalculate( recipientId, useRewardCan, useChargeCan, @@ -107,7 +107,7 @@ class CanPaymentService( ) } - private fun setUseCoinCalculate( + private fun setUseCanCalculate( recipientId: Long?, useRewardCan: TotalSpentCan, useChargeCan: TotalSpentCan?, @@ -271,8 +271,8 @@ class CanPaymentService( ) ?: throw SodaException("잘못된 예약정보 입니다.") useCan.isRefund = true - val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) - useCoinCalculates.forEach { + val useCanCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) + useCanCalculates.forEach { it.status = UseCanCalculateStatus.REFUND val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) charge.title = "${it.can} 캔" diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index f849a25..48dbe84 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -150,7 +150,7 @@ class AudioContentService( val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId) ?: throw SodaException("잘못된 테마입니다. 다시 선택해 주세요.") - if (request.price in 1..9) throw SodaException("콘텐츠의 최소금액은 10코인 입니다.") + if (request.price in 1..9) throw SodaException("콘텐츠의 최소금액은 10캔 입니다.") // DB에 값 추가 val audioContent = AudioContent( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt index d09397c..b58aa2d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentRepository.kt @@ -67,7 +67,7 @@ class AudioContentCommentQueryRepositoryImpl( "$cloudFrontHost/profile/default-profile.png" }, comment = it.comment, - donationCoin = it.donationCan ?: 0, + donationCan = it.donationCan ?: 0, date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), replyCount = commentReplyCountByAudioContentCommentId(it.id!!) ) @@ -131,7 +131,7 @@ class AudioContentCommentQueryRepositoryImpl( "$cloudFrontHost/profile/default-profile.png" }, comment = it.comment, - donationCoin = it.donationCan ?: 0, + donationCan = it.donationCan ?: 0, date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), replyCount = 0 ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt index 08be9e4..bbdd725 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/comment/GetAudioContentCommentListResponse.kt @@ -11,7 +11,7 @@ data class GetAudioContentCommentListItem( val nickname: String, val profileUrl: String, val comment: String, - val donationCoin: Int, + val donationCan: Int, val date: String, val replyCount: Int ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt index 33fb311..db641b4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt @@ -58,7 +58,7 @@ class ExplorerQueryRepository( creatorId: Long, limit: Long, offset: Long = 0, - withDonationCoin: Boolean + withDonationCan: Boolean ): List { val creator = QMember("creator") val member = QMember("user") @@ -82,7 +82,7 @@ class ExplorerQueryRepository( .fetch() .map { val account = it.get(member)!! - val donationCoin = it.get(donation)!! + val donationCan = it.get(donation)!! MemberDonationRankingResponse( account.id!!, account.nickname, @@ -91,7 +91,7 @@ class ExplorerQueryRepository( } else { "$cloudFrontHost/profile/default-profile.png" }, - if (withDonationCoin) donationCoin else 0 + if (withDonationCan) donationCan else 0 ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt index 3e6e3f9..343c56a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt @@ -4,5 +4,5 @@ data class MemberDonationRankingResponse( val userId: Long, val nickname: String, val profileImage: String, - val donationCoin: Int + val donationCan: Int ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt index 0e835a1..195d3ea 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/faq/FaqController.kt @@ -31,7 +31,7 @@ class FaqController(private val service: FaqService) { @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN')") - fun deleteCoin(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") + fun deleteCan(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") @GetMapping fun getFaqList(@RequestParam("category") category: String) = ApiResponse.ok(service.getFaqList(category)) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index a894b70..77c0255 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -642,7 +642,7 @@ class LiveRoomService( .getMemberDonationRanking( room.member!!.id!!, 3, - withDonationCoin = false + withDonationCan = false ) .asSequence() .map { it.userId } @@ -786,7 +786,7 @@ class LiveRoomService( fun getDonationTotal(roomId: Long): GetLiveRoomDonationTotalResponse { return GetLiveRoomDonationTotalResponse( - totalDonationCoin = repository.getDonationTotal(roomId = roomId) ?: 0 + totalDonationCan = repository.getDonationTotal(roomId = roomId) ?: 0 ) } @@ -908,8 +908,8 @@ class LiveRoomService( ) ?: throw SodaException("후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") useCan.isRefund = true - val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) - useCoinCalculates.forEach { + val useCanCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) + useCanCalculates.forEach { it.status = UseCanCalculateStatus.REFUND val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) charge.title = "${it.can} 캔" diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt index 8dc9556..2db7f9b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/donation/GetLiveRoomDonationTotalResponse.kt @@ -1,3 +1,3 @@ package kr.co.vividnext.sodalive.live.room.donation -data class GetLiveRoomDonationTotalResponse(val totalDonationCoin: Int) +data class GetLiveRoomDonationTotalResponse(val totalDonationCan: Int) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt index f1f7921..e4f258f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeController.kt @@ -31,7 +31,7 @@ class ServiceNoticeController(private val service: ServiceNoticeService) { @DeleteMapping("/{id}") @PreAuthorize("hasRole('ADMIN')") - fun deleteCoin(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") + fun deleteCan(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.") @GetMapping fun getNoticeList(pageable: Pageable, timezone: String) = ApiResponse.ok(service.getNoticeList(pageable, timezone)) From 4bf8617102c25b77e168649c97f2e954adb281ce Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 13 Aug 2023 20:24:48 +0900 Subject: [PATCH 90/94] =?UTF-8?q?coin=20->=20can=20=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/content/donation/AudioContentDonationService.kt | 4 ++-- .../kr/co/vividnext/sodalive/content/order/OrderService.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt index 043e6ca..d8e1e46 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/donation/AudioContentDonationService.kt @@ -12,7 +12,7 @@ import org.springframework.transaction.annotation.Transactional @Service class AudioContentDonationService( - private val coinPaymentService: CanPaymentService, + private val canPaymentService: CanPaymentService, private val queryRepository: AudioContentRepository, private val commentRepository: AudioContentCommentRepository ) { @@ -24,7 +24,7 @@ class AudioContentDonationService( val audioContent = queryRepository.findByIdAndActive(request.contentId) ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") - coinPaymentService.spendCan( + canPaymentService.spendCan( memberId = member.id!!, needCan = request.donationCan, canUsage = CanUsage.DONATION, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt index 1112fcd..e16f567 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt @@ -17,7 +17,7 @@ import java.time.LocalDateTime @Transactional(readOnly = true) class OrderService( private val repository: OrderRepository, - private val coinPaymentService: CanPaymentService, + private val canPaymentService: CanPaymentService, private val audioContentRepository: AudioContentRepository, private val audioContentCommentQueryRepository: AudioContentCommentRepository, private val audioContentLikeQueryRepository: AudioContentLikeRepository, @@ -42,7 +42,7 @@ class OrderService( repository.save(order) - coinPaymentService.spendCan( + canPaymentService.spendCan( memberId = member.id!!, needCan = order.can, canUsage = CanUsage.ORDER_CONTENT, From 6f40838d09ba1a6e4a2b8dece38673655fb7b452 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 14 Aug 2023 18:58:16 +0900 Subject: [PATCH 91/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20-=20dateformat=EC=97=90=20locale=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 77c0255..429fed4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -57,6 +57,7 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Date +import java.util.Locale import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.write @@ -290,6 +291,9 @@ class LiveRoomService( .atZone(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of(timezone)) + val dateFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd E a hh:mm") + .withLocale(Locale(timezone)) + val response = GetRoomDetailResponse( roomId = roomId, title = room.title, @@ -299,7 +303,7 @@ class LiveRoomService( numberOfParticipantsTotal = room.numberOfPeople, numberOfParticipants = 0, channelName = room.channelName, - beginDateTime = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), + beginDateTime = beginDateTime.format(dateFormatter), isPaid = false, isPrivateRoom = room.type == LiveRoomType.PRIVATE, password = room.password From ba69f862956b7b24d5281fb1018201ce893eaa7b Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 14 Aug 2023 19:14:51 +0900 Subject: [PATCH 92/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20-=20dateformat=20=EC=9B=90=EC=83=81=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 429fed4..77c0255 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -57,7 +57,6 @@ import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.util.Date -import java.util.Locale import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.write @@ -291,9 +290,6 @@ class LiveRoomService( .atZone(ZoneId.of("UTC")) .withZoneSameInstant(ZoneId.of(timezone)) - val dateFormatter = DateTimeFormatter.ofPattern("yyyy.MM.dd E a hh:mm") - .withLocale(Locale(timezone)) - val response = GetRoomDetailResponse( roomId = roomId, title = room.title, @@ -303,7 +299,7 @@ class LiveRoomService( numberOfParticipantsTotal = room.numberOfPeople, numberOfParticipants = 0, channelName = room.channelName, - beginDateTime = beginDateTime.format(dateFormatter), + beginDateTime = beginDateTime.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")), isPaid = false, isPrivateRoom = room.type == LiveRoomType.PRIVATE, password = room.password From c84a9bc473f3ad0c04565360afd4b8fcb24f570d Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 14 Aug 2023 22:49:41 +0900 Subject: [PATCH 93/94] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20-=20=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomService.kt | 16 +++++----------- .../live/room/info/GetRoomInfoResponse.kt | 10 ++++------ 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 77c0255..c4c1c4b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -630,11 +630,7 @@ class LiveRoomService( expireTimestamp.toInt() ) - val tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList() - val isRadioMode = tags.contains("라디오") or tags.contains("콘서트") - val isAvailableDonation = room.member!!.id!! != member.id!! && - room.member!!.role == MemberRole.CREATOR - val isFollowingManager = explorerQueryRepository + val isFollowing = explorerQueryRepository .getNotificationUserIds(room.member!!.id!!) .contains(member.id) @@ -668,22 +664,20 @@ class LiveRoomService( channelName = room.channelName!!, rtcToken = rtcToken, rtmToken = rtmToken, - managerId = room.member!!.id!!, - managerNickname = room.member!!.nickname, - managerProfileUrl = if (room.member!!.profileImage != null) { + creatorId = room.member!!.id!!, + creatorNickname = room.member!!.nickname, + creatorProfileUrl = if (room.member!!.profileImage != null) { "$cloudFrontHost/${room.member!!.profileImage}" } else { "$cloudFrontHost/profile/default-profile.png" }, - isFollowingManager = isFollowingManager, + isFollowing = isFollowing, participantsCount = roomInfo.listenerCount + roomInfo.speakerCount + roomInfo.managerCount, totalAvailableParticipantsCount = room.numberOfPeople, speakerList = roomInfo.speakerList, listenerList = roomInfo.listenerList, managerList = roomInfo.managerList, donationRankingTop3UserIds = donationRankingTop3UserIds, - isRadioMode = isRadioMode, - isAvailableDonation = isAvailableDonation, isPrivateRoom = room.type == LiveRoomType.PRIVATE, password = room.password ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt index 0bede8c..69ad47d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt @@ -8,18 +8,16 @@ data class GetRoomInfoResponse( val channelName: String, val rtcToken: String, val rtmToken: String, - val managerId: Long, - val managerNickname: String, - val managerProfileUrl: String, - val isFollowingManager: Boolean, + val creatorId: Long, + val creatorNickname: String, + val creatorProfileUrl: String, + val isFollowing: Boolean, val participantsCount: Int, val totalAvailableParticipantsCount: Int, val speakerList: List, val listenerList: List, val managerList: List, val donationRankingTop3UserIds: List, - val isRadioMode: Boolean = false, - val isAvailableDonation: Boolean = false, val isPrivateRoom: Boolean = false, val password: String? = null ) From 9236aece18b09ddbfe7e051fc017163707dd1747 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 15 Aug 2023 00:12:27 +0900 Subject: [PATCH 94/94] manager -> creator --- .../kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt | 4 ++-- .../kr/co/vividnext/sodalive/live/room/LiveRoomService.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt index 84f8796..34edc54 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/GetRoomListResponse.kt @@ -12,8 +12,8 @@ data class GetRoomListResponse( val price: Int, val tags: List, val channelName: String?, - val managerNickname: String, - val managerId: Long, + val creatorNickname: String, + val creatorId: Long, val isReservation: Boolean, val isPrivateRoom: Boolean ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index c4c1c4b..6f04a46 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -140,8 +140,8 @@ class LiveRoomService( isAdult = it.isAdult, price = it.price, channelName = it.channelName, - managerNickname = it.member!!.nickname, - managerId = it.member!!.id!!, + creatorNickname = it.member!!.nickname, + creatorId = it.member!!.id!!, tags = it.tags .asSequence() .filter { tag -> tag.tag.isActive }