Compare commits
No commits in common. "d55514e3a7715bab0afe690d8834f883fef6e009" and "ca875b5baa8fcadcaad5b105a49bc1017b70d8e2" have entirely different histories.
d55514e3a7
...
ca875b5baa
|
@ -1,6 +1,5 @@
|
||||||
HELP.md
|
HELP.md
|
||||||
.gradle
|
.gradle
|
||||||
.envrc
|
|
||||||
build/
|
build/
|
||||||
!**/src/main/**/build/
|
!**/src/main/**/build/
|
||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
|
18
appspec.yml
18
appspec.yml
|
@ -1,18 +0,0 @@
|
||||||
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
|
|
|
@ -33,31 +33,11 @@ dependencies {
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
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 (추가 설정)
|
// querydsl (추가 설정)
|
||||||
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
|
implementation("com.querydsl:querydsl-jpa:$querydslVersion")
|
||||||
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")
|
kapt("com.querydsl:querydsl-apt:$querydslVersion:jpa")
|
||||||
kapt("org.springframework.boot:spring-boot-configuration-processor")
|
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")
|
|
||||||
|
|
||||||
// bootpay
|
|
||||||
implementation("io.github.bootpay:backend:2.2.1")
|
|
||||||
|
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.9.3")
|
|
||||||
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")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
runtimeOnly("com.h2database:h2")
|
runtimeOnly("com.h2database:h2")
|
||||||
runtimeOnly("com.mysql:mysql-connector-j")
|
runtimeOnly("com.mysql:mysql-connector-j")
|
||||||
|
@ -65,12 +45,6 @@ dependencies {
|
||||||
testImplementation("org.springframework.security:spring-security-test")
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
allOpen {
|
|
||||||
annotation("javax.persistence.Entity")
|
|
||||||
annotation("javax.persistence.MappedSuperclass")
|
|
||||||
annotation("javax.persistence.Embeddable")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
tasks.withType<KotlinCompile> {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs += "-Xjsr305=strict"
|
freeCompilerArgs += "-Xjsr305=strict"
|
||||||
|
|
|
@ -4,5 +4,3 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
org.gradle.daemon=true
|
|
||||||
org.gradle.configureondemand=true
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
#!/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
|
|
|
@ -1,12 +0,0 @@
|
||||||
#!/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 &
|
|
|
@ -2,12 +2,10 @@ package kr.co.vividnext.sodalive
|
||||||
|
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
import org.springframework.boot.runApplication
|
import org.springframework.boot.runApplication
|
||||||
import org.springframework.scheduling.annotation.EnableAsync
|
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableAsync
|
class SodaliveApplication
|
||||||
class SodaLiveApplication
|
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
runApplication<SodaLiveApplication>(*args)
|
runApplication<SodaliveApplication>(*args)
|
||||||
}
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.can
|
|
||||||
|
|
||||||
data class AdminCanChargeRequest(
|
|
||||||
val memberId: Long,
|
|
||||||
val method: String,
|
|
||||||
val can: Int
|
|
||||||
)
|
|
|
@ -1,24 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.can
|
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.can.Can
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
|
||||||
|
|
||||||
interface AdminCanRepository : JpaRepository<Can, Long>
|
|
|
@ -1,26 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
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("1 캔 이상 입력하세요.")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
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<GetChargeStatusQueryDto> {
|
|
||||||
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<GetChargeStatusDetailQueryDto> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
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<GetChargeStatusResponse> {
|
|
||||||
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<GetChargeStatusDetailResponse> {
|
|
||||||
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(
|
|
||||||
memberId = it.memberId,
|
|
||||||
nickname = it.nickname,
|
|
||||||
method = it.method,
|
|
||||||
amount = if (paymentGateway == PaymentGateway.APPLE_IAP) {
|
|
||||||
it.appleChargeAmount.toInt()
|
|
||||||
} else {
|
|
||||||
it.pgChargeAmount
|
|
||||||
},
|
|
||||||
datetime = it.datetime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.charge
|
|
||||||
|
|
||||||
import com.querydsl.core.annotations.QueryProjection
|
|
||||||
|
|
||||||
data class GetChargeStatusDetailQueryDto @QueryProjection constructor(
|
|
||||||
val memberId: Long,
|
|
||||||
val nickname: String,
|
|
||||||
val method: String,
|
|
||||||
val appleChargeAmount: Double,
|
|
||||||
val pgChargeAmount: Int,
|
|
||||||
val datetime: String
|
|
||||||
)
|
|
|
@ -1,9 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.charge
|
|
||||||
|
|
||||||
data class GetChargeStatusDetailResponse(
|
|
||||||
val memberId: Long,
|
|
||||||
val nickname: String,
|
|
||||||
val method: String,
|
|
||||||
val amount: Int,
|
|
||||||
val datetime: String
|
|
||||||
)
|
|
|
@ -1,12 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.charge
|
|
||||||
|
|
||||||
data class GetChargeStatusResponse(
|
|
||||||
val date: String,
|
|
||||||
val chargeAmount: Int,
|
|
||||||
val chargeCount: Long,
|
|
||||||
val pg: String
|
|
||||||
)
|
|
|
@ -1,30 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,119 +0,0 @@
|
||||||
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<AudioContent, Long>, AdminAudioContentQueryRepository
|
|
||||||
|
|
||||||
interface AdminAudioContentQueryRepository {
|
|
||||||
fun getAudioContentTotalCount(searchWord: String = ""): Int
|
|
||||||
fun getAudioContentList(offset: Long, limit: Long, searchWord: String = ""): List<GetAdminContentListItem>
|
|
||||||
fun getHashTagList(audioContentId: Long): List<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<GetAdminContentListItem> {
|
|
||||||
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<String> {
|
|
||||||
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<LocalDateTime>,
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.content
|
|
||||||
|
|
||||||
import com.querydsl.core.annotations.QueryProjection
|
|
||||||
|
|
||||||
data class GetAdminContentListResponse(
|
|
||||||
val totalCount: Int,
|
|
||||||
val items: List<GetAdminContentListItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
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 = ""
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
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?
|
|
||||||
)
|
|
|
@ -1,37 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
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<AudioContentBanner, Long>, AdminContentBannerQueryRepository
|
|
||||||
|
|
||||||
interface AdminContentBannerQueryRepository {
|
|
||||||
fun getAudioContentMainBannerList(): List<GetAdminContentBannerResponse>
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdminContentBannerQueryRepositoryImpl(
|
|
||||||
private val queryFactory: JPAQueryFactory,
|
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
|
||||||
private val cloudFrontHost: String
|
|
||||||
) : AdminContentBannerQueryRepository {
|
|
||||||
override fun getAudioContentMainBannerList(): List<GetAdminContentBannerResponse> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
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<Long>) {
|
|
||||||
for (index in ids.indices) {
|
|
||||||
val tag = repository.findByIdOrNull(ids[index])
|
|
||||||
|
|
||||||
if (tag != null) {
|
|
||||||
tag.orders = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAudioContentMainBannerList(): List<GetAdminContentBannerResponse> {
|
|
||||||
return repository.getAudioContentMainBannerList()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,16 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,5 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.content.banner
|
|
||||||
|
|
||||||
data class UpdateBannerOrdersRequest(
|
|
||||||
val ids: List<Long>
|
|
||||||
)
|
|
|
@ -1,13 +0,0 @@
|
||||||
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?
|
|
||||||
)
|
|
|
@ -1,33 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
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<AudioContentCuration, Long>,
|
|
||||||
AdminContentCurationQueryRepository
|
|
||||||
|
|
||||||
interface AdminContentCurationQueryRepository {
|
|
||||||
fun getAudioContentCurationList(): List<GetAdminContentCurationResponse>
|
|
||||||
fun findByIdAndActive(id: Long): AudioContentCuration?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
class AdminContentCurationQueryRepositoryImpl(
|
|
||||||
private val queryFactory: JPAQueryFactory
|
|
||||||
) : AdminContentCurationQueryRepository {
|
|
||||||
override fun getAudioContentCurationList(): List<GetAdminContentCurationResponse> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
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<Long>) {
|
|
||||||
for (index in ids.indices) {
|
|
||||||
val audioContentCuration = repository.findByIdOrNull(ids[index])
|
|
||||||
|
|
||||||
if (audioContentCuration != null) {
|
|
||||||
audioContentCuration.orders = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getContentCurationList(): List<GetAdminContentCurationResponse> {
|
|
||||||
return repository.getAudioContentCurationList()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
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<Long>
|
|
||||||
)
|
|
|
@ -1,10 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,36 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
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<AudioContentTheme, Long>, AdminContentThemeQueryRepository
|
|
||||||
|
|
||||||
interface AdminContentThemeQueryRepository {
|
|
||||||
fun findIdByTheme(theme: String): Long?
|
|
||||||
fun getActiveThemes(): List<GetAudioContentThemeResponse>
|
|
||||||
}
|
|
||||||
|
|
||||||
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<GetAudioContentThemeResponse> {
|
|
||||||
return queryFactory
|
|
||||||
.select(
|
|
||||||
QGetAudioContentThemeResponse(
|
|
||||||
audioContentTheme.id,
|
|
||||||
audioContentTheme.theme,
|
|
||||||
audioContentTheme.image.prepend("/").prepend(cloudFrontHost)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.from(audioContentTheme)
|
|
||||||
.where(audioContentTheme.isActive.isTrue)
|
|
||||||
.orderBy(audioContentTheme.orders.asc())
|
|
||||||
.fetch()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
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<Long>) {
|
|
||||||
for (index in ids.indices) {
|
|
||||||
val theme = repository.findByIdOrNull(ids[index])
|
|
||||||
|
|
||||||
if (theme != null) {
|
|
||||||
theme.orders = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getThemes(): List<GetAudioContentThemeResponse> {
|
|
||||||
return repository.getActiveThemes()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.content.theme
|
|
||||||
|
|
||||||
data class CreateContentThemeRequest(val theme: String)
|
|
|
@ -1,5 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.content.theme
|
|
||||||
|
|
||||||
data class UpdateThemeOrdersRequest(
|
|
||||||
val ids: List<Long>
|
|
||||||
)
|
|
|
@ -1,32 +0,0 @@
|
||||||
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<Any> {
|
|
||||||
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())
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
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<ChargeEvent, Long>, AdminChargeEventQueryRepository
|
|
||||||
|
|
||||||
interface AdminChargeEventQueryRepository {
|
|
||||||
fun getChargeEventList(): List<ChargeEvent>
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdminChargeEventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminChargeEventQueryRepository {
|
|
||||||
override fun getChargeEventList(): List<ChargeEvent> {
|
|
||||||
return queryFactory
|
|
||||||
.selectFrom(chargeEvent)
|
|
||||||
.orderBy(chargeEvent.createdAt.desc())
|
|
||||||
.fetch()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
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<GetChargeEventListResponse> {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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()
|
|
|
@ -1,9 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,39 +0,0 @@
|
||||||
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<Any> {
|
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
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<ExplorerSection, Long>, AdminExplorerSectionQueryRepository
|
|
||||||
|
|
||||||
interface AdminExplorerSectionQueryRepository {
|
|
||||||
fun findByTitle(title: String): ExplorerSection?
|
|
||||||
fun findAllWithPaging(offset: Long, limit: Long): List<ExplorerSection>
|
|
||||||
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<ExplorerSection> {
|
|
||||||
return queryFactory
|
|
||||||
.selectFrom(explorerSection)
|
|
||||||
.offset(offset)
|
|
||||||
.limit(limit)
|
|
||||||
.orderBy(explorerSection.orders.asc())
|
|
||||||
.fetch()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun totalCount(): Int {
|
|
||||||
return queryFactory
|
|
||||||
.selectFrom(explorerSection)
|
|
||||||
.fetch()
|
|
||||||
.size
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
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<ExplorerSectionCreatorTag>()
|
|
||||||
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<Long>) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.explorer
|
|
||||||
|
|
||||||
data class CreateExplorerSectionRequest(
|
|
||||||
val title: String,
|
|
||||||
val isAdult: Boolean,
|
|
||||||
val tagList: List<String>,
|
|
||||||
val coloredTitle: String? = null,
|
|
||||||
val color: String? = null
|
|
||||||
)
|
|
|
@ -1,16 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.explorer
|
|
||||||
|
|
||||||
data class GetAdminExplorerSectionResponse(
|
|
||||||
val totalCount: Int,
|
|
||||||
val explorerSectionItemList: List<GetAdminExplorerSectionResponseItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class GetAdminExplorerSectionResponseItem(
|
|
||||||
val id: Long,
|
|
||||||
val title: String,
|
|
||||||
val coloredTitle: String,
|
|
||||||
val color: String,
|
|
||||||
val isAdult: Boolean,
|
|
||||||
val isActive: Boolean,
|
|
||||||
val tags: List<String>
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.explorer
|
|
||||||
|
|
||||||
data class UpdateExplorerSectionOrdersRequest(
|
|
||||||
val firstOrders: Int,
|
|
||||||
val ids: List<Long>
|
|
||||||
)
|
|
|
@ -1,11 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.explorer
|
|
||||||
|
|
||||||
data class UpdateExplorerSectionRequest(
|
|
||||||
val id: Long,
|
|
||||||
val title: String? = null,
|
|
||||||
val isAdult: Boolean? = null,
|
|
||||||
val tagList: List<String>? = null,
|
|
||||||
val coloredTitle: String? = null,
|
|
||||||
val color: String? = null,
|
|
||||||
val isActive: Boolean? = null
|
|
||||||
)
|
|
|
@ -1,58 +0,0 @@
|
||||||
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
|
|
||||||
@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),
|
|
||||||
"수정되었습니다."
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
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
|
|
||||||
class AdminLiveRoomQueryRepository(private val queryFactory: JPAQueryFactory) {
|
|
||||||
fun getLiveRoomList(): List<LiveRoom> {
|
|
||||||
return queryFactory
|
|
||||||
.selectFrom(liveRoom)
|
|
||||||
.innerJoin(liveRoom.member, member)
|
|
||||||
.where(liveRoom.isActive.isTrue)
|
|
||||||
.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<RecommendLiveCreatorBanner> {
|
|
||||||
return queryFactory
|
|
||||||
.selectFrom(recommendLiveCreatorBanner)
|
|
||||||
.offset(pageable.offset)
|
|
||||||
.limit(pageable.pageSize.toLong())
|
|
||||||
.orderBy(recommendLiveCreatorBanner.orders.asc(), recommendLiveCreatorBanner.id.desc())
|
|
||||||
.fetch()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,240 +0,0 @@
|
||||||
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
|
|
||||||
) {
|
|
||||||
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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Long>) {
|
|
||||||
for (index in ids.indices) {
|
|
||||||
val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(id = ids[index])
|
|
||||||
|
|
||||||
if (recommendCreatorBanner != null) {
|
|
||||||
recommendCreatorBanner.orders = firstOrders + index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.live
|
|
||||||
|
|
||||||
data class GetAdminRecommendCreatorResponse(
|
|
||||||
val totalCount: Int,
|
|
||||||
val recommendCreatorList: List<GetAdminRecommendCreatorResponseItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
data class GetAdminRecommendCreatorResponseItem(
|
|
||||||
val id: Long,
|
|
||||||
val image: String,
|
|
||||||
val creatorId: Long,
|
|
||||||
val creatorNickname: String,
|
|
||||||
val startDate: String,
|
|
||||||
val endDate: String,
|
|
||||||
val isAdult: Boolean
|
|
||||||
)
|
|
|
@ -1,19 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.live
|
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.live.room.LiveRoomType
|
|
||||||
|
|
||||||
data class GetLiveResponse(
|
|
||||||
val liveList: List<GetLiveResponseItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.live
|
|
||||||
|
|
||||||
data class UpdateAdminRecommendCreatorBannerOrdersRequest(
|
|
||||||
val firstOrders: Int,
|
|
||||||
val ids: List<Long>
|
|
||||||
)
|
|
|
@ -1,43 +0,0 @@
|
||||||
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.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("/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))
|
|
||||||
|
|
||||||
@GetMapping("/search")
|
|
||||||
fun searchMember(
|
|
||||||
@RequestParam(value = "search_word") searchWord: String,
|
|
||||||
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))
|
|
||||||
|
|
||||||
@GetMapping("/creator/search")
|
|
||||||
fun searchCreator(
|
|
||||||
@RequestParam(value = "search_word") searchWord: String,
|
|
||||||
pageable: Pageable
|
|
||||||
) = ApiResponse.ok(service.searchCreator(searchWord, pageable))
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
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<Member, Long>, AdminMemberQueryRepository
|
|
||||||
|
|
||||||
interface AdminMemberQueryRepository {
|
|
||||||
fun getMemberTotalCount(role: MemberRole? = null): Int
|
|
||||||
fun getMemberList(offset: Long, limit: Long, role: MemberRole? = null): List<Member>
|
|
||||||
fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole? = null): List<Member>
|
|
||||||
|
|
||||||
fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int
|
|
||||||
fun getCreatorAllList(): List<GetAdminCreatorAllListResponse>
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository {
|
|
||||||
override fun getMemberList(offset: Long, limit: Long, role: MemberRole?): List<Member> {
|
|
||||||
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.role.ne(MemberRole.ADMIN)
|
|
||||||
.and(
|
|
||||||
if (role != null) {
|
|
||||||
member.role.eq(role)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.fetch()
|
|
||||||
.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole?): List<Member> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCreatorAllList(): List<GetAdminCreatorAllListResponse> {
|
|
||||||
return queryFactory
|
|
||||||
.select(
|
|
||||||
QGetAdminCreatorAllListResponse(
|
|
||||||
member.id,
|
|
||||||
member.nickname
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.from(member)
|
|
||||||
.where(
|
|
||||||
member.role.eq(MemberRole.CREATOR)
|
|
||||||
.and(member.isActive.isTrue)
|
|
||||||
)
|
|
||||||
.fetch()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
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.data.repository.findByIdOrNull
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
import org.springframework.transaction.annotation.Transactional
|
|
||||||
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
|
|
||||||
) {
|
|
||||||
@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(
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Member>
|
|
||||||
): List<GetAdminMemberListResponseItem> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCreatorAllList(): List<GetAdminCreatorAllListResponse> {
|
|
||||||
return repository.getCreatorAllList()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.member
|
|
||||||
|
|
||||||
import com.querydsl.core.annotations.QueryProjection
|
|
||||||
|
|
||||||
data class GetAdminCreatorAllListResponse @QueryProjection constructor(
|
|
||||||
val id: Long,
|
|
||||||
val nickname: String
|
|
||||||
)
|
|
|
@ -1,19 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.member
|
|
||||||
|
|
||||||
data class GetAdminMemberListResponse(
|
|
||||||
val totalCount: Int,
|
|
||||||
val items: List<GetAdminMemberListResponseItem>
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,8 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.member
|
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
|
||||||
|
|
||||||
data class UpdateMemberRequest(
|
|
||||||
val id: Long,
|
|
||||||
val userType: MemberRole
|
|
||||||
)
|
|
|
@ -1,39 +0,0 @@
|
||||||
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")
|
|
||||||
@PreAuthorize("hasRole('ADMIN')")
|
|
||||||
class AdminMemberTagController(private val service: AdminMemberTagService) {
|
|
||||||
@PostMapping
|
|
||||||
fun enrollmentCreatorTag(
|
|
||||||
@RequestPart("image") image: MultipartFile,
|
|
||||||
@RequestPart("request") requestString: String
|
|
||||||
) = ApiResponse.ok(service.uploadTagImage(image, requestString), "등록되었습니다.")
|
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
|
||||||
fun deleteCreatorTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.")
|
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
|
||||||
fun modifyCreatorTag(
|
|
||||||
@PathVariable id: Long,
|
|
||||||
@RequestPart("image") image: MultipartFile?,
|
|
||||||
@RequestPart("request") requestString: String
|
|
||||||
) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.")
|
|
||||||
|
|
||||||
@PutMapping("/orders")
|
|
||||||
fun updateTagOrders(
|
|
||||||
@RequestBody request: UpdateTagOrdersRequest
|
|
||||||
) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.")
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
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<CreatorTag, Long>, 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
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<Long>) {
|
|
||||||
for (index in ids.indices) {
|
|
||||||
val tag = repository.findByIdOrNull(ids[index])
|
|
||||||
|
|
||||||
if (tag != null) {
|
|
||||||
tag.orders = index + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.member.tag
|
|
||||||
|
|
||||||
data class CreateMemberTagRequest(val tag: String)
|
|
|
@ -1,3 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.admin.member.tag
|
|
||||||
|
|
||||||
data class UpdateTagOrdersRequest(val ids: List<Long>)
|
|
|
@ -1,171 +0,0 @@
|
||||||
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<Short, Int>
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
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<Short, String>): ByteBuf {
|
|
||||||
put(extra.size.toShort())
|
|
||||||
for ((key, value) in extra.entries) {
|
|
||||||
put(key)
|
|
||||||
put(value)
|
|
||||||
}
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
fun putIntMap(extra: TreeMap<Short, Int>): 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<Short, String> {
|
|
||||||
val map = TreeMap<Short, String>()
|
|
||||||
val length = readShort()
|
|
||||||
|
|
||||||
for (i in 0 until length) {
|
|
||||||
val k = readShort()
|
|
||||||
val v = readString()
|
|
||||||
map[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
fun readIntMap(): TreeMap<Short, Int> {
|
|
||||||
val map = TreeMap<Short, Int>()
|
|
||||||
val length = readShort()
|
|
||||||
|
|
||||||
for (i in 0 until length) {
|
|
||||||
val k = readShort()
|
|
||||||
val v = readInt()
|
|
||||||
map[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,256 +0,0 @@
|
||||||
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<Short, String>
|
|
||||||
): 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<Short, String>,
|
|
||||||
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<Short, String>()
|
|
||||||
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<Short, String>
|
|
||||||
) : 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<Short, String>? = null
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
serviceType: Short,
|
|
||||||
signature: String?,
|
|
||||||
appID: ByteArray,
|
|
||||||
unixTs: Int,
|
|
||||||
salt: Int,
|
|
||||||
expiredTs: Int,
|
|
||||||
extra: TreeMap<Short, String>
|
|
||||||
) : 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.agora
|
|
||||||
|
|
||||||
interface Packable {
|
|
||||||
fun marshal(out: ByteBuf): ByteBuf
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.agora
|
|
||||||
|
|
||||||
interface PackableEx : Packable {
|
|
||||||
fun unmarshal(input: ByteBuf)
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
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()
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
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(
|
|
||||||
"$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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
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<List<CanResponse>> {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,130 +0,0 @@
|
||||||
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.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
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
interface CanRepository : JpaRepository<Can, Long>, CanQueryRepository
|
|
||||||
|
|
||||||
interface CanQueryRepository {
|
|
||||||
fun findAllByStatus(status: CanStatus): List<CanResponse>
|
|
||||||
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
|
|
||||||
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
|
|
||||||
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
|
|
||||||
fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage = CanUsage.LIVE): UseCan?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Repository
|
|
||||||
class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQueryRepository {
|
|
||||||
override fun findAllByStatus(status: CanStatus): List<CanResponse> {
|
|
||||||
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<UseCan> {
|
|
||||||
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<Charge> {
|
|
||||||
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)
|
|
||||||
.leftJoin(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage): 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))
|
|
||||||
.and(useCan.isRefund.isFalse)
|
|
||||||
)
|
|
||||||
.orderBy(useCan.id.desc())
|
|
||||||
.fetchFirst()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
|
@ -1,118 +0,0 @@
|
||||||
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<CanResponse> {
|
|
||||||
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<GetCanUseStatusResponseItem> {
|
|
||||||
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<GetCanChargeStatusResponseItem> {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.can
|
|
||||||
|
|
||||||
data class GetCanChargeStatusResponseItem(
|
|
||||||
val canTitle: String,
|
|
||||||
val date: String,
|
|
||||||
val chargeMethod: String
|
|
||||||
)
|
|
|
@ -1,6 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.can
|
|
||||||
|
|
||||||
data class GetCanStatusResponse(
|
|
||||||
val chargeCan: Int,
|
|
||||||
val rewardCan: Int
|
|
||||||
)
|
|
|
@ -1,7 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.can
|
|
||||||
|
|
||||||
data class GetCanUseStatusResponseItem(
|
|
||||||
val title: String,
|
|
||||||
val date: String,
|
|
||||||
val can: Int
|
|
||||||
)
|
|
|
@ -1,45 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
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))
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
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)
|
|
|
@ -1,78 +0,0 @@
|
||||||
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<Charge, Long>, 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,203 +0,0 @@
|
||||||
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("결제를 완료하지 못했습니다.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.can.charge
|
|
||||||
|
|
||||||
enum class ChargeStatus {
|
|
||||||
CHARGE, REFUND_CHARGE, EVENT, CANCEL,
|
|
||||||
|
|
||||||
// 관리자 지급
|
|
||||||
ADMIN
|
|
||||||
}
|
|
|
@ -1,298 +0,0 @@
|
||||||
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
|
|
||||||
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.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
|
|
||||||
import org.springframework.stereotype.Service
|
|
||||||
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,
|
|
||||||
private val useCanCalculateRepository: UseCanCalculateRepository
|
|
||||||
) {
|
|
||||||
@Transactional
|
|
||||||
fun spendCan(
|
|
||||||
memberId: Long,
|
|
||||||
needCan: Int,
|
|
||||||
canUsage: CanUsage,
|
|
||||||
liveRoom: LiveRoom? = null,
|
|
||||||
order: Order? = null,
|
|
||||||
audioContent: AudioContent? = 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 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("잘못된 요청입니다.")
|
|
||||||
}
|
|
||||||
|
|
||||||
useCanRepository.save(useCan)
|
|
||||||
|
|
||||||
setUseCanCalculate(recipientId, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.PG)
|
|
||||||
setUseCanCalculate(
|
|
||||||
recipientId,
|
|
||||||
useRewardCan,
|
|
||||||
useChargeCan,
|
|
||||||
useCan,
|
|
||||||
paymentGateway = PaymentGateway.GOOGLE_IAP
|
|
||||||
)
|
|
||||||
setUseCanCalculate(
|
|
||||||
recipientId,
|
|
||||||
useRewardCan,
|
|
||||||
useChargeCan,
|
|
||||||
useCan,
|
|
||||||
paymentGateway = PaymentGateway.APPLE_IAP
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUseCanCalculate(
|
|
||||||
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<SpentCan>()
|
|
||||||
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.googleRewardCan -= 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.googleRewardCan -= 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<SpentCan>()
|
|
||||||
var chargeId = 0L
|
|
||||||
var total = 0
|
|
||||||
|
|
||||||
while (needCan - total > 0) {
|
|
||||||
val remainingNeedCan = needCan - total
|
|
||||||
val charge = chargeRepository.getOldestChargeWhereChargeCanGreaterThan0(chargeId, memberId, container)
|
|
||||||
?: break
|
|
||||||
|
|
||||||
if (charge.chargeCan >= remainingNeedCan) {
|
|
||||||
charge.chargeCan -= remainingNeedCan
|
|
||||||
|
|
||||||
when (charge.payment!!.paymentGateway) {
|
|
||||||
PaymentGateway.PG -> member.pgChargeCan -= remainingNeedCan
|
|
||||||
PaymentGateway.APPLE_IAP -> member.appleChargeCan -= remainingNeedCan
|
|
||||||
PaymentGateway.GOOGLE_IAP -> member.googleChargeCan -= remainingNeedCan
|
|
||||||
}
|
|
||||||
|
|
||||||
total += remainingNeedCan
|
|
||||||
|
|
||||||
spentCans.add(
|
|
||||||
SpentCan(
|
|
||||||
paymentGateway = charge.payment!!.paymentGateway,
|
|
||||||
can = remainingNeedCan
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
total += charge.chargeCan
|
|
||||||
spentCans.add(
|
|
||||||
SpentCan(
|
|
||||||
paymentGateway = charge.payment!!.paymentGateway,
|
|
||||||
can = charge.chargeCan
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
when (charge.payment!!.paymentGateway) {
|
|
||||||
PaymentGateway.PG -> member.pgChargeCan -= remainingNeedCan
|
|
||||||
PaymentGateway.APPLE_IAP -> member.appleChargeCan -= remainingNeedCan
|
|
||||||
PaymentGateway.GOOGLE_IAP -> member.pgChargeCan -= remainingNeedCan
|
|
||||||
}
|
|
||||||
|
|
||||||
charge.chargeCan = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
chargeId = charge.id!!
|
|
||||||
}
|
|
||||||
|
|
||||||
TotalSpentCan(spentCans, total)
|
|
||||||
} else {
|
|
||||||
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 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} 캔"
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
package kr.co.vividnext.sodalive.can.payment
|
|
||||||
|
|
||||||
enum class PaymentGateway {
|
|
||||||
PG, GOOGLE_IAP, APPLE_IAP
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue