From 3216c73ee821870a1dc9202ff9ec0526a96127b8 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 2 Mar 2025 23:45:08 +0900 Subject: [PATCH 01/13] =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=97=90=20=EA=B4=91=EA=B3=A0=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=98=ED=82=B9=20=EC=A0=81=EC=9A=A9=20-=20=EA=B4=91?= =?UTF-8?q?=EA=B3=A0=20=ED=8A=B8=EB=9E=98=ED=82=B9=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?Entity=20=EC=B6=94=EA=B0=80=20-=20pid=EA=B0=80=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=20=EA=B4=91=EA=B3=A0=20=EC=A4=91=EC=9D=B8=20pid?= =?UTF-8?q?=EC=9D=B8=20=EA=B2=BD=EC=9A=B0=20=ED=8A=B8=EB=9E=98=ED=82=B9=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../sodalive/marketing/AdMediaPartner.kt | 22 ++++++++++ .../marketing/AdMediaPartnerRepository.kt | 15 +++++++ .../sodalive/marketing/AdTrackingHistory.kt | 44 +++++++++++++++++++ .../marketing/AdTrackingRepository.kt | 5 +++ .../sodalive/marketing/AdTrackingService.kt | 41 +++++++++++++++++ .../kr/co/vividnext/sodalive/member/Member.kt | 7 +++ .../sodalive/member/MemberController.kt | 22 +++++++++- .../sodalive/member/MemberService.kt | 14 +++++- .../sodalive/member/signUp/SignUpRequest.kt | 1 + .../sodalive/member/signUp/SignUpResponse.kt | 9 ++++ 11 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpResponse.kt diff --git a/build.gradle.kts b/build.gradle.kts index b260db1..46061c5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -66,6 +66,7 @@ dependencies { implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0") implementation("org.apache.poi:poi-ooxml:5.2.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("com.h2database:h2") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt new file mode 100644 index 0000000..6b37c53 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.marketing + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated + +@Entity +data class AdMediaPartner( + val mediaGroup: String, + val pid: String, + val pidName: String, + @Enumerated(value = EnumType.STRING) + val type: AdMediaPartnerType, + val utmSource: String, + val utmMedium: String, + val isActive: Boolean = true +) : BaseEntity() + +enum class AdMediaPartnerType { + SERIES, CONTENT, LIVE, CHANNEL, MAIN +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt new file mode 100644 index 0000000..f491b39 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.marketing + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.marketing.QAdMediaPartner.adMediaPartner +import org.springframework.stereotype.Repository + +@Repository +class AdMediaPartnerRepository(private val queryFactory: JPAQueryFactory) { + fun findByPid(pid: String): AdMediaPartner? { + return queryFactory + .selectFrom(adMediaPartner) + .where(adMediaPartner.pid.eq(pid), adMediaPartner.isActive.isTrue) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt new file mode 100644 index 0000000..1046c4c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt @@ -0,0 +1,44 @@ +package kr.co.vividnext.sodalive.marketing + +import java.io.Serializable +import java.time.LocalDateTime +import javax.persistence.Embeddable +import javax.persistence.EmbeddedId +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.PreUpdate + +@Entity +data class AdTrackingHistory( + @EmbeddedId + val id: AdTrackingHistoryId, + val price: Double = 0.toDouble(), + val locale: String? = null, + var updatedAt: LocalDateTime = LocalDateTime.now() +) { + @PreUpdate + fun preUpdate() { + updatedAt = LocalDateTime.now() + } +} + +@Embeddable +data class AdTrackingHistoryId( + val pid: String, + val memberId: Long, + @Enumerated(value = EnumType.STRING) + val type: AdTrackingHistoryType, + val createdAt: LocalDateTime = LocalDateTime.now() +) : Serializable + +enum class AdTrackingHistoryType { + // 회원가입 + SIGNUP, + + // 첫결제 + FIRST_PAYMENT, + + // 재결제 + REPEAT_PAYMENT +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingRepository.kt new file mode 100644 index 0000000..6d99bd4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingRepository.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.marketing + +import org.springframework.data.jpa.repository.JpaRepository + +interface AdTrackingRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt new file mode 100644 index 0000000..1d91534 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt @@ -0,0 +1,41 @@ +package kr.co.vividnext.sodalive.marketing + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.springframework.stereotype.Service + +@Service +class AdTrackingService( + private val repository: AdTrackingRepository, + private val mediaPartnerRepository: AdMediaPartnerRepository +) { + + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + fun saveTrackingHistory( + pid: String, + type: AdTrackingHistoryType, + memberId: Long, + price: Double? = null, + locale: String? = null + ) { + coroutineScope.launch { + try { + val mediaPartner = mediaPartnerRepository.findByPid(pid) + + if (mediaPartner != null) { + val id = AdTrackingHistoryId(pid = pid, memberId = memberId, type = type) + val trackingHistory = AdTrackingHistory( + id = id, + price = price ?: 0.toDouble(), + locale = locale + ) + repository.save(trackingHistory) + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index fa268fc..793b9ae 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.member.following.CreatorFollowing import kr.co.vividnext.sodalive.member.notification.MemberNotification import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag +import java.time.LocalDateTime import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity @@ -29,6 +30,12 @@ data class Member( @Enumerated(value = EnumType.STRING) var role: MemberRole = MemberRole.USER, + @Column(nullable = true) + var activePid: String? = null, + + @Column(nullable = true) + var partnerExpirationDateTime: LocalDateTime? = null, + var isVisibleDonationRank: Boolean = true, var isActive: Boolean = true, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 024981c..3d8378a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -2,9 +2,12 @@ package kr.co.vividnext.sodalive.member import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType +import kr.co.vividnext.sodalive.marketing.AdTrackingService import kr.co.vividnext.sodalive.member.block.MemberBlockRequest import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest import kr.co.vividnext.sodalive.member.login.LoginRequest +import kr.co.vividnext.sodalive.member.login.LoginResponse import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -23,7 +26,10 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/member") -class MemberController(private val service: MemberService) { +class MemberController( + private val service: MemberService, + private val trackingService: AdTrackingService +) { @GetMapping("/check/email") fun checkEmail(@RequestParam email: String) = service.duplicateCheckEmail(email) @@ -40,7 +46,19 @@ class MemberController(private val service: MemberService) { fun signUp( @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, @RequestPart("request") requestString: String - ) = service.signUp(profileImage, requestString) + ): ApiResponse { + val response = service.signUp(profileImage, requestString) + + if (!response.marketingPid.isNullOrBlank()) { + trackingService.saveTrackingHistory( + pid = response.marketingPid, + type = AdTrackingHistoryType.SIGNUP, + memberId = response.memberId + ) + } + + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) + } @PostMapping("/login") fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index b7fd155..8a1392f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -27,6 +27,7 @@ import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLogRepository import kr.co.vividnext.sodalive.member.notification.MemberNotificationService import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequest +import kr.co.vividnext.sodalive.member.signUp.SignUpResponse import kr.co.vividnext.sodalive.member.signUp.SignUpValidator import kr.co.vividnext.sodalive.member.stipulation.Stipulation import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree @@ -97,7 +98,7 @@ class MemberService( fun signUp( profileImage: MultipartFile?, requestString: String - ): ApiResponse { + ): SignUpResponse { val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") @@ -117,7 +118,11 @@ class MemberService( member.profileImage = uploadProfileImage(profileImage = profileImage, memberId = member.id!!) agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy) - return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = login(request.email, request.password)) + return SignUpResponse( + memberId = member.id!!, + marketingPid = request.marketingPid, + loginResponse = login(request.email, request.password) + ) } fun login(request: LoginRequest): ApiResponse { @@ -289,6 +294,11 @@ class MemberService( container = request.container ) + if (!request.marketingPid.isNullOrBlank()) { + member.activePid = request.marketingPid + member.partnerExpirationDateTime = LocalDateTime.now().plusYears(1) + } + return repository.save(member) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt index 1d962ab..42c7350 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpRequest.kt @@ -7,6 +7,7 @@ data class SignUpRequest( val password: String, val nickname: String, val gender: Gender, + val marketingPid: String? = null, val isAgreeTermsOfService: Boolean, val isAgreePrivacyPolicy: Boolean, val container: String = "api" diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpResponse.kt new file mode 100644 index 0000000..eacae08 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/signUp/SignUpResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.member.signUp + +import kr.co.vividnext.sodalive.member.login.LoginResponse + +data class SignUpResponse( + val memberId: Long, + val marketingPid: String?, + val loginResponse: LoginResponse +) -- 2.40.1 From 83ed4b6961377c4a9adce11e8b1a9be9d4c6c052 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 12:27:17 +0900 Subject: [PATCH 02/13] =?UTF-8?q?marketing=20info=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20API=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/MarketingInfoUpdateRequest.kt | 6 ++++++ .../sodalive/member/MemberController.kt | 16 ++++++++++++++++ .../vividnext/sodalive/member/MemberService.kt | 15 +++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/MarketingInfoUpdateRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MarketingInfoUpdateRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MarketingInfoUpdateRequest.kt new file mode 100644 index 0000000..6ba3dd8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MarketingInfoUpdateRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.member + +data class MarketingInfoUpdateRequest( + val adid: String, + val pid: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index 3d8378a..54f1f8b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -128,6 +128,22 @@ class MemberController( ) } + @PutMapping("/marketing-info/update") + fun updateMarketingInfo( + @RequestBody request: MarketingInfoUpdateRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.updateMarketingInfo( + memberId = member.id!!, + adid = request.adid, + pid = request.pid + ) + ) + } + @PutMapping("/adid/update") fun updateAdid( @RequestBody request: AdidUpdateRequest, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 8a1392f..511121e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -646,4 +646,19 @@ class MemberService( private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } } + + @Transactional + fun updateMarketingInfo(memberId: Long, adid: String, pid: String) { + val member = repository.findByIdOrNull(id = memberId) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (adid != member.adid) { + member.adid = adid + } + + if (pid != member.activePid) { + member.activePid = pid + member.partnerExpirationDateTime = LocalDateTime.now().plusYears(1) + } + } } -- 2.40.1 From f918e89307e9f5c0b78434804c4738df37a5ad31 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 13:34:56 +0900 Subject: [PATCH 03/13] DateTime -> Datetime --- src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index 793b9ae..c99c541 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -34,7 +34,7 @@ data class Member( var activePid: String? = null, @Column(nullable = true) - var partnerExpirationDateTime: LocalDateTime? = null, + var partnerExpirationDatetime: LocalDateTime? = null, var isVisibleDonationRank: Boolean = true, -- 2.40.1 From 81b11976a7ddcf196ad0d2801ce8b57d9e596e4d Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 13:40:23 +0900 Subject: [PATCH 04/13] DateTime -> Datetime --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 511121e..6592c6d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -296,7 +296,7 @@ class MemberService( if (!request.marketingPid.isNullOrBlank()) { member.activePid = request.marketingPid - member.partnerExpirationDateTime = LocalDateTime.now().plusYears(1) + member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1) } return repository.save(member) @@ -658,7 +658,7 @@ class MemberService( if (pid != member.activePid) { member.activePid = pid - member.partnerExpirationDateTime = LocalDateTime.now().plusYears(1) + member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1) } } } -- 2.40.1 From 72d10f9443478caa22106a3380f5f51de48202a1 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 17:05:41 +0900 Subject: [PATCH 05/13] =?UTF-8?q?=EC=BA=94=20=EC=B6=A9=EC=A0=84=20-=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=98=ED=82=B9=20=EB=A1=9C=EC=A7=81=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../can/charge/ChargeCompleteResponse.kt | 7 ++ .../sodalive/can/charge/ChargeController.kt | 84 +++++++++++++++---- .../sodalive/can/charge/ChargeRepository.kt | 16 ++++ .../sodalive/can/charge/ChargeService.kt | 41 +++++++-- 4 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt new file mode 100644 index 0000000..066cd8f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeCompleteResponse.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.can.charge + +data class ChargeCompleteResponse( + val price: Double, + val currencyCode: String, + val isFirstCharged: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt index cd6b8da..3748c96 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt @@ -3,17 +3,22 @@ package kr.co.vividnext.sodalive.can.charge import kr.co.vividnext.sodalive.can.payment.PaymentGateway import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType +import kr.co.vividnext.sodalive.marketing.AdTrackingService 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 +import java.time.LocalDateTime @RestController @RequestMapping("/charge") -class ChargeController(private val service: ChargeService) { +class ChargeController( + private val service: ChargeService, + private val trackingService: AdTrackingService +) { @PostMapping fun charge( @@ -30,14 +35,30 @@ class ChargeController(private val service: ChargeService) { @PostMapping("/verify") fun verify( @RequestBody verifyRequest: VerifyRequest, - @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.verify(user, verifyRequest)) + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val response = service.verify(memberId = member.id!!, verifyRequest) + trackingCharge(member, response) + ApiResponse.ok(Unit) + } @PostMapping("/verify/hecto") fun verifyHecto( @RequestBody verifyRequest: VerifyRequest, - @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.verifyHecto(user, verifyRequest)) + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val response = service.verifyHecto(memberId = member.id!!, verifyRequest) + trackingCharge(member, response) + ApiResponse.ok(Unit) + } @PostMapping("/apple") fun appleCharge( @@ -54,8 +75,16 @@ class ChargeController(private val service: ChargeService) { @PostMapping("/apple/verify") fun appleVerify( @RequestBody verifyRequest: AppleVerifyRequest, - @AuthenticationPrincipal user: User - ) = ApiResponse.ok(service.appleVerify(user, verifyRequest)) + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) { + throw SodaException("로그인 정보를 확인해주세요.") + } + + val response = service.appleVerify(memberId = member.id!!, verifyRequest) + trackingCharge(member, response) + ApiResponse.ok(Unit) + } @PostMapping("/google") fun googleCharge( @@ -78,17 +107,40 @@ class ChargeController(private val service: ChargeService) { paymentGateway = request.paymentGateway ) - ApiResponse.ok( - service.processGoogleIap( - memberId = member.id!!, - chargeId = chargeId, - productId = request.productId, - purchaseToken = request.purchaseToken, - paymentGateway = request.paymentGateway - ) + val response = service.processGoogleIap( + memberId = member.id!!, + chargeId = chargeId, + productId = request.productId, + purchaseToken = request.purchaseToken, + paymentGateway = request.paymentGateway ) + + trackingCharge(member, response) + ApiResponse.ok(Unit) } else { throw SodaException("결제정보에 오류가 있습니다.") } } + + private fun trackingCharge( + member: Member, + response: ChargeCompleteResponse + ) { + if ( + !member.activePid.isNullOrBlank() && + member.partnerExpirationDatetime?.isAfter(LocalDateTime.now()) == true + ) { + trackingService.saveTrackingHistory( + pid = member.activePid!!, + type = if (response.isFirstCharged) { + AdTrackingHistoryType.FIRST_PAYMENT + } else { + AdTrackingHistoryType.REPEAT_PAYMENT + }, + memberId = member.id!!, + price = response.price, + locale = response.currencyCode + ) + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt index 1b65e1a..dbb2128 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeRepository.kt @@ -18,6 +18,7 @@ interface ChargeQueryRepository { fun getOldestChargeWhereRewardCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? fun getOldestChargeWhereChargeCanGreaterThan0(chargeId: Long, memberId: Long, container: String): Charge? fun getChargeCountAfterDate(memberId: Long, date: LocalDateTime): Int + fun isFirstCharged(memberId: Long): Boolean } class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : ChargeQueryRepository { @@ -76,6 +77,21 @@ class ChargeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Cha .size } + override fun isFirstCharged(memberId: Long): Boolean { + return queryFactory + .select(charge.id) + .from(charge) + .innerJoin(charge.member, member) + .innerJoin(charge.payment, payment) + .where( + member.id.eq(memberId), + charge.status.eq(ChargeStatus.CHARGE), + payment.status.eq(PaymentStatus.COMPLETE) + ) + .fetch() + .size <= 1 + } + private fun getPaymentGatewayCondition(container: String): BooleanExpression? { val paymentGatewayCondition = when (container) { "aos" -> { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index 64529b0..4b8486e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -23,9 +23,10 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpHeaders import org.springframework.retry.annotation.Backoff import org.springframework.retry.annotation.Retryable -import org.springframework.security.core.userdetails.User import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.math.RoundingMode @Service @Transactional(readOnly = true) @@ -102,10 +103,10 @@ class ChargeService( } @Transactional - fun verify(user: User, verifyRequest: VerifyRequest) { + fun verify(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) ?: throw SodaException("결제정보에 오류가 있습니다.") - val member = memberRepository.findByEmail(user.username) + val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") if (charge.payment!!.paymentGateway == PaymentGateway.PG) { @@ -130,6 +131,12 @@ class ChargeService( memberId = member.id!! ) ) + + return ChargeCompleteResponse( + price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(), + currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW", + isFirstCharged = chargeRepository.isFirstCharged(memberId) + ) } else { throw SodaException("결제정보에 오류가 있습니다.") } @@ -142,10 +149,10 @@ class ChargeService( } @Transactional - fun verifyHecto(user: User, verifyRequest: VerifyRequest) { + fun verifyHecto(memberId: Long, verifyRequest: VerifyRequest): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong()) ?: throw SodaException("결제정보에 오류가 있습니다.") - val member = memberRepository.findByEmail(user.username) + val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") if (charge.payment!!.paymentGateway == PaymentGateway.PG) { @@ -174,6 +181,12 @@ class ChargeService( memberId = member.id!! ) ) + + return ChargeCompleteResponse( + price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(), + currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW", + isFirstCharged = chargeRepository.isFirstCharged(memberId) + ) } else { throw SodaException("결제정보에 오류가 있습니다.") } @@ -208,10 +221,10 @@ class ChargeService( } @Transactional - fun appleVerify(user: User, verifyRequest: AppleVerifyRequest) { + fun appleVerify(memberId: Long, verifyRequest: AppleVerifyRequest): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId) ?: throw SodaException("결제정보에 오류가 있습니다.") - val member = memberRepository.findByEmail(user.username) + val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) { @@ -228,6 +241,12 @@ class ChargeService( memberId = member.id!! ) ) + + return ChargeCompleteResponse( + price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(), + currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW", + isFirstCharged = chargeRepository.isFirstCharged(memberId) + ) } else { throw SodaException("결제정보에 오류가 있습니다.") } @@ -271,7 +290,7 @@ class ChargeService( productId: String, purchaseToken: String, paymentGateway: PaymentGateway - ) { + ): ChargeCompleteResponse { val charge = chargeRepository.findByIdOrNull(id = chargeId) ?: throw SodaException("결제정보에 오류가 있습니다.") val member = memberRepository.findByIdOrNull(id = memberId) @@ -290,6 +309,12 @@ class ChargeService( memberId = member.id!! ) ) + + return ChargeCompleteResponse( + price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(), + currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW", + isFirstCharged = chargeRepository.isFirstCharged(memberId) + ) } else { throw SodaException("구매를 하지 못했습니다.\n고객센터로 문의해 주세요") } -- 2.40.1 From be12148d040eb07aecad6c532fde9eab30bd8551 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 17:33:01 +0900 Subject: [PATCH 06/13] =?UTF-8?q?=ED=8A=B8=EB=9E=98=ED=82=B9=20-=20mediaGr?= =?UTF-8?q?oup=EA=B3=BC=20pidName=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt | 2 ++ .../kr/co/vividnext/sodalive/marketing/AdTrackingService.kt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt index 1046c4c..5a1eb2b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingHistory.kt @@ -13,6 +13,8 @@ import javax.persistence.PreUpdate data class AdTrackingHistory( @EmbeddedId val id: AdTrackingHistoryId, + val mediaGroup: String, + val pidName: String, val price: Double = 0.toDouble(), val locale: String? = null, var updatedAt: LocalDateTime = LocalDateTime.now() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt index 1d91534..13379a4 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdTrackingService.kt @@ -28,6 +28,8 @@ class AdTrackingService( val id = AdTrackingHistoryId(pid = pid, memberId = memberId, type = type) val trackingHistory = AdTrackingHistory( id = id, + mediaGroup = mediaPartner.mediaGroup, + pidName = mediaPartner.pidName, price = price ?: 0.toDouble(), locale = locale ) -- 2.40.1 From d74f1ddb81ba06e57e46da8344a1d15d9da173b7 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 23:36:31 +0900 Subject: [PATCH 07/13] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EB=A7=A4=EC=B2=B4=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C(pid)=20=EB=93=B1=EB=A1=9D=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminAdMediaPartnerController.kt | 18 ++++++++++++++ .../marketing/AdminAdMediaPartnerService.kt | 12 ++++++++++ .../marketing/CreateAdMediaPartnerRequest.kt | 24 +++++++++++++++++++ .../marketing/AdMediaPartnerRepository.kt | 11 +++++++-- 4 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/CreateAdMediaPartnerRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt new file mode 100644 index 0000000..a33509b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt @@ -0,0 +1,18 @@ +package kr.co.vividnext.sodalive.admin.marketing + +import kr.co.vividnext.sodalive.common.ApiResponse +import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@PreAuthorize("hasRole('ADMIN')") +@RequestMapping("/admin/marketing/media-partner") +class AdminAdMediaPartnerController(private val service: AdminAdMediaPartnerService) { + @PostMapping + fun createMediaPartner( + @RequestBody request: CreateAdMediaPartnerRequest + ) = ApiResponse.ok(service.createMediaPartner(request)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt new file mode 100644 index 0000000..a910963 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.admin.marketing + +import kr.co.vividnext.sodalive.marketing.AdMediaPartnerRepository +import org.springframework.stereotype.Service + +@Service +class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepository) { + fun createMediaPartner(request: CreateAdMediaPartnerRequest) { + val mediaPartner = request.toEntity() + repository.save(mediaPartner) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/CreateAdMediaPartnerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/CreateAdMediaPartnerRequest.kt new file mode 100644 index 0000000..e3f9620 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/CreateAdMediaPartnerRequest.kt @@ -0,0 +1,24 @@ +package kr.co.vividnext.sodalive.admin.marketing + +import kr.co.vividnext.sodalive.marketing.AdMediaPartner +import kr.co.vividnext.sodalive.marketing.AdMediaPartnerType + +data class CreateAdMediaPartnerRequest( + val mediaGroup: String, + val pid: String, + val pidName: String, + val type: AdMediaPartnerType, + val utmSource: String, + val utmMedium: String +) { + fun toEntity(): AdMediaPartner { + return AdMediaPartner( + mediaGroup = mediaGroup, + pid = pid, + pidName = pidName, + type = type, + utmSource = utmSource, + utmMedium = utmMedium + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt index f491b39..955b70e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt @@ -2,11 +2,18 @@ package kr.co.vividnext.sodalive.marketing import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.marketing.QAdMediaPartner.adMediaPartner +import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository +interface AdMediaPartnerRepository : JpaRepository, AdMediaPartnerQueryRepository + +interface AdMediaPartnerQueryRepository { + fun findByPid(pid: String): AdMediaPartner? +} + @Repository -class AdMediaPartnerRepository(private val queryFactory: JPAQueryFactory) { - fun findByPid(pid: String): AdMediaPartner? { +class AdMediaPartnerQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdMediaPartnerQueryRepository { + override fun findByPid(pid: String): AdMediaPartner? { return queryFactory .selectFrom(adMediaPartner) .where(adMediaPartner.pid.eq(pid), adMediaPartner.isActive.isTrue) -- 2.40.1 From 5db181aa743caf3a24a0bed00f944c35e5c412ca Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 4 Mar 2025 23:53:22 +0900 Subject: [PATCH 08/13] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EB=A7=A4=EC=B2=B4=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C(pid)=20=EC=88=98=EC=A0=95=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminAdMediaPartnerController.kt | 6 +++ .../marketing/AdminAdMediaPartnerService.kt | 38 +++++++++++++++++++ .../marketing/UpdateAdMediaPartnerRequest.kt | 14 +++++++ .../sodalive/marketing/AdMediaPartner.kt | 14 +++---- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/UpdateAdMediaPartnerRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt index a33509b..64a7454 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.admin.marketing import kr.co.vividnext.sodalive.common.ApiResponse import org.springframework.security.access.prepost.PreAuthorize 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 @@ -15,4 +16,9 @@ class AdminAdMediaPartnerController(private val service: AdminAdMediaPartnerServ fun createMediaPartner( @RequestBody request: CreateAdMediaPartnerRequest ) = ApiResponse.ok(service.createMediaPartner(request)) + + @PutMapping + fun updateMediaPartner( + @RequestBody request: UpdateAdMediaPartnerRequest + ) = ApiResponse.ok(service.updateMediaPartner(request)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt index a910963..eb6f5fc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt @@ -1,12 +1,50 @@ package kr.co.vividnext.sodalive.admin.marketing +import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.marketing.AdMediaPartnerRepository +import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepository) { + @Transactional fun createMediaPartner(request: CreateAdMediaPartnerRequest) { val mediaPartner = request.toEntity() repository.save(mediaPartner) } + + @Transactional + fun updateMediaPartner(request: UpdateAdMediaPartnerRequest) { + val entity = repository.findByIdOrNull(request.id) + ?: throw SodaException("잘못된 접근입니다") + + if (request.mediaGroup != null) { + entity.mediaGroup = request.mediaGroup + } + + if (request.pid != null) { + entity.pid = request.pid + } + + if (request.pidName != null) { + entity.pidName = request.pidName + } + + if (request.type != null) { + entity.type = request.type + } + + if (request.utmSource != null) { + entity.utmSource = request.utmSource + } + + if (request.utmMedium != null) { + entity.utmMedium = request.utmMedium + } + + if (request.isActive != null) { + entity.isActive = request.isActive + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/UpdateAdMediaPartnerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/UpdateAdMediaPartnerRequest.kt new file mode 100644 index 0000000..2bff5d7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/UpdateAdMediaPartnerRequest.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.admin.marketing + +import kr.co.vividnext.sodalive.marketing.AdMediaPartnerType + +data class UpdateAdMediaPartnerRequest( + val id: Long, + val mediaGroup: String?, + val pid: String?, + val pidName: String?, + val type: AdMediaPartnerType?, + val utmSource: String?, + val utmMedium: String?, + val isActive: Boolean? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt index 6b37c53..9dc5c6a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartner.kt @@ -7,14 +7,14 @@ import javax.persistence.Enumerated @Entity data class AdMediaPartner( - val mediaGroup: String, - val pid: String, - val pidName: String, + var mediaGroup: String, + var pid: String, + var pidName: String, @Enumerated(value = EnumType.STRING) - val type: AdMediaPartnerType, - val utmSource: String, - val utmMedium: String, - val isActive: Boolean = true + var type: AdMediaPartnerType, + var utmSource: String, + var utmMedium: String, + var isActive: Boolean = true ) : BaseEntity() enum class AdMediaPartnerType { -- 2.40.1 From a17a6a41dad51438502da478ddabbf1193237e97 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 5 Mar 2025 00:39:49 +0900 Subject: [PATCH 09/13] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20?= =?UTF-8?q?=EB=A7=A4=EC=B2=B4=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C(pid)=20=EC=A1=B0=ED=9A=8C=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminAdMediaPartnerController.kt | 10 +++++ .../marketing/AdminAdMediaPartnerService.kt | 35 +++++++++++++++ .../GetAdminAdMediaPartnerResponse.kt | 17 +++++++ .../marketing/AdMediaPartnerRepository.kt | 44 +++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt index 64a7454..e83d063 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerController.kt @@ -1,7 +1,9 @@ package kr.co.vividnext.sodalive.admin.marketing 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 @@ -21,4 +23,12 @@ class AdminAdMediaPartnerController(private val service: AdminAdMediaPartnerServ fun updateMediaPartner( @RequestBody request: UpdateAdMediaPartnerRequest ) = ApiResponse.ok(service.updateMediaPartner(request)) + + @GetMapping + fun getMediaPartnerList(pageable: Pageable) = ApiResponse.ok( + service.getMediaPartnerList( + offset = pageable.offset, + limit = pageable.pageSize.toLong() + ) + ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt index eb6f5fc..ba02fe0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt @@ -2,9 +2,11 @@ package kr.co.vividnext.sodalive.admin.marketing import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.marketing.AdMediaPartnerRepository +import kr.co.vividnext.sodalive.marketing.AdMediaPartnerType import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.net.URLEncoder @Service class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepository) { @@ -47,4 +49,37 @@ class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepositor entity.isActive = request.isActive } } + + fun getMediaPartnerList(offset: Long, limit: Long): List { + return repository.getMediaPartnerList(offset, limit) + .map { + val deepLinkValue = when (it.type) { + AdMediaPartnerType.SERIES -> "series" + AdMediaPartnerType.CONTENT -> "content" + AdMediaPartnerType.LIVE -> "live" + AdMediaPartnerType.CHANNEL -> "channel" + AdMediaPartnerType.MAIN -> "main" + } + + val link = URLEncoder.encode( + "$oneLinkHost?af_dp=voiceon%3A%2F%2F" + + "&deep_link_value=$deepLinkValue" + + "&deep_link_sub1=${it.pid}" + + "&deep_link_sub2=${it.utmSource}" + + "&deep_link_sub3=${it.utmMedium}" + + "&deep_link_sub4=${it.pidName}" + + "&utm_source=${it.utmSource}" + + "&utm_medium=${it.utmMedium}" + + "&utm_campaign=${it.pidName}", + "UTF-8" + ) + + it.link = link + it + } + } + + companion object { + private val oneLinkHost = "https://voiceon.onelink.me/RkTm" + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt new file mode 100644 index 0000000..14e0b6d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.admin.marketing + +import com.querydsl.core.annotations.QueryProjection +import kr.co.vividnext.sodalive.marketing.AdMediaPartnerType + +data class GetAdminAdMediaPartnerResponse @QueryProjection constructor( + val id: Long, + val mediaGroup: String, + val pid: String, + val pidName: String, + val type: AdMediaPartnerType, + val utmSource: String, + val utmMedium: String, + val isActive: Boolean, + val createdAt: String, + var link: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt index 955b70e..1970387 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt @@ -1,14 +1,21 @@ package kr.co.vividnext.sodalive.marketing +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.admin.marketing.GetAdminAdMediaPartnerResponse +import kr.co.vividnext.sodalive.admin.marketing.QGetAdminAdMediaPartnerResponse import kr.co.vividnext.sodalive.marketing.QAdMediaPartner.adMediaPartner import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository +import java.time.LocalDateTime interface AdMediaPartnerRepository : JpaRepository, AdMediaPartnerQueryRepository interface AdMediaPartnerQueryRepository { fun findByPid(pid: String): AdMediaPartner? + fun getMediaPartnerList(offset: Long, limit: Long): List } @Repository @@ -19,4 +26,41 @@ class AdMediaPartnerQueryRepositoryImpl(private val queryFactory: JPAQueryFactor .where(adMediaPartner.pid.eq(pid), adMediaPartner.isActive.isTrue) .fetchFirst() } + + override fun getMediaPartnerList(offset: Long, limit: Long): List { + return queryFactory + .select( + QGetAdminAdMediaPartnerResponse( + adMediaPartner.id, + adMediaPartner.mediaGroup, + adMediaPartner.pid, + adMediaPartner.pidName, + adMediaPartner.type, + adMediaPartner.utmSource, + adMediaPartner.utmMedium, + adMediaPartner.isActive, + getFormattedDate(adMediaPartner.createdAt), + Expressions.constant("") + ) + ) + .from(adMediaPartner) + .orderBy(adMediaPartner.isActive.desc(), adMediaPartner.id.asc()) + .offset(offset) + .limit(limit) + .fetch() + } + + private fun getFormattedDate(dateTimePath: DateTimePath): StringTemplate { + return Expressions.stringTemplate( + "DATE_FORMAT({0}, {1})", + Expressions.dateTimeTemplate( + LocalDateTime::class.java, + "CONVERT_TZ({0},{1},{2})", + dateTimePath, + "UTC", + "Asia/Seoul" + ), + "%Y-%m-%d %H:%i:%s" + ) + } } -- 2.40.1 From 2dd75ae7e8c0226c41df78bee781a2dd25022ebb Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 5 Mar 2025 00:49:50 +0900 Subject: [PATCH 10/13] =?UTF-8?q?marketing=20info=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20API=20-=20pid=EA=B0=80=20=EB=B9=88?= =?UTF-8?q?=EC=B9=B8=EC=9D=B4=EB=A9=B4=20=EB=93=B1=EB=A1=9D=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/kr/co/vividnext/sodalive/member/MemberService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 6592c6d..e646fac 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -656,7 +656,7 @@ class MemberService( member.adid = adid } - if (pid != member.activePid) { + if (pid != member.activePid && pid.isNotBlank()) { member.activePid = pid member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1) } -- 2.40.1 From 2ce13afc0a964a6f95086d8c2d517d65c6dbb843 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 5 Mar 2025 15:53:25 +0900 Subject: [PATCH 11/13] =?UTF-8?q?=EB=A7=88=EC=BC=80=ED=8C=85=20=EB=A7=A4?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20-=20link=20=EC=9D=B8=EC=BD=94=EB=94=A9=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20-=20host=20=EB=8A=94=20=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=94=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EA=B3=A0=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EB=A7=8C=20=EC=9D=B8=EC=BD=94=EB=94=A9=20=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/admin/marketing/AdminAdMediaPartnerService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt index ba02fe0..9dc6f7c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt @@ -61,8 +61,8 @@ class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepositor AdMediaPartnerType.MAIN -> "main" } - val link = URLEncoder.encode( - "$oneLinkHost?af_dp=voiceon%3A%2F%2F" + + val link = "$oneLinkHost?" + URLEncoder.encode( + "af_dp=voiceon%3A%2F%2F" + "&deep_link_value=$deepLinkValue" + "&deep_link_sub1=${it.pid}" + "&deep_link_sub2=${it.utmSource}" + -- 2.40.1 From d454acdcbebd82b3aaeeb038c07bd6b030fd8fb6 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 5 Mar 2025 16:10:11 +0900 Subject: [PATCH 12/13] =?UTF-8?q?=EB=A7=88=EC=BC=80=ED=8C=85=20=EB=A7=A4?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20-=20=EC=A0=84=EC=B2=B4=20=EA=B0=9C?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../marketing/AdminAdMediaPartnerService.kt | 10 ++++++++-- .../GetAdminAdMediaPartnerResponse.kt | 7 ++++++- .../marketing/AdMediaPartnerRepository.kt | 19 ++++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt index 9dc6f7c..0d25be8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt @@ -50,8 +50,9 @@ class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepositor } } - fun getMediaPartnerList(offset: Long, limit: Long): List { - return repository.getMediaPartnerList(offset, limit) + fun getMediaPartnerList(offset: Long, limit: Long): GetAdminAdMediaPartnerResponse { + val totalCount = repository.getMediaPartnerListTotalCount() + val items = repository.getMediaPartnerList(offset, limit) .map { val deepLinkValue = when (it.type) { AdMediaPartnerType.SERIES -> "series" @@ -77,6 +78,11 @@ class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepositor it.link = link it } + + return GetAdminAdMediaPartnerResponse( + totalCount = totalCount, + items = items + ) } companion object { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt index 14e0b6d..5b0bd18 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/GetAdminAdMediaPartnerResponse.kt @@ -3,7 +3,12 @@ package kr.co.vividnext.sodalive.admin.marketing import com.querydsl.core.annotations.QueryProjection import kr.co.vividnext.sodalive.marketing.AdMediaPartnerType -data class GetAdminAdMediaPartnerResponse @QueryProjection constructor( +data class GetAdminAdMediaPartnerResponse( + val totalCount: Int, + val items: List +) + +data class GetAdminAdMediaPartnerResponseItem @QueryProjection constructor( val id: Long, val mediaGroup: String, val pid: String, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt index 1970387..2886fbc 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/marketing/AdMediaPartnerRepository.kt @@ -4,8 +4,8 @@ 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.admin.marketing.GetAdminAdMediaPartnerResponse -import kr.co.vividnext.sodalive.admin.marketing.QGetAdminAdMediaPartnerResponse +import kr.co.vividnext.sodalive.admin.marketing.GetAdminAdMediaPartnerResponseItem +import kr.co.vividnext.sodalive.admin.marketing.QGetAdminAdMediaPartnerResponseItem import kr.co.vividnext.sodalive.marketing.QAdMediaPartner.adMediaPartner import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -15,7 +15,8 @@ interface AdMediaPartnerRepository : JpaRepository, AdMedi interface AdMediaPartnerQueryRepository { fun findByPid(pid: String): AdMediaPartner? - fun getMediaPartnerList(offset: Long, limit: Long): List + fun getMediaPartnerList(offset: Long, limit: Long): List + fun getMediaPartnerListTotalCount(): Int } @Repository @@ -27,10 +28,10 @@ class AdMediaPartnerQueryRepositoryImpl(private val queryFactory: JPAQueryFactor .fetchFirst() } - override fun getMediaPartnerList(offset: Long, limit: Long): List { + override fun getMediaPartnerList(offset: Long, limit: Long): List { return queryFactory .select( - QGetAdminAdMediaPartnerResponse( + QGetAdminAdMediaPartnerResponseItem( adMediaPartner.id, adMediaPartner.mediaGroup, adMediaPartner.pid, @@ -50,6 +51,14 @@ class AdMediaPartnerQueryRepositoryImpl(private val queryFactory: JPAQueryFactor .fetch() } + override fun getMediaPartnerListTotalCount(): Int { + return queryFactory + .select(adMediaPartner.id) + .from(adMediaPartner) + .fetch() + .size + } + private fun getFormattedDate(dateTimePath: DateTimePath): StringTemplate { return Expressions.stringTemplate( "DATE_FORMAT({0}, {1})", -- 2.40.1 From 0f68b297a095d723bed7e1a7f0782853c9cb8d86 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 5 Mar 2025 17:16:05 +0900 Subject: [PATCH 13/13] =?UTF-8?q?=EB=A7=88=EC=BC=80=ED=8C=85=20=EB=A7=A4?= =?UTF-8?q?=EC=B2=B4=20=ED=8C=8C=ED=8A=B8=EB=84=88=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20-=20=EB=A7=81=ED=81=AC=20=EC=9D=B8?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../marketing/AdminAdMediaPartnerService.kt | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt index 0d25be8..62d0759 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/marketing/AdminAdMediaPartnerService.kt @@ -6,7 +6,6 @@ import kr.co.vividnext.sodalive.marketing.AdMediaPartnerType import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.net.URLEncoder @Service class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepository) { @@ -62,18 +61,16 @@ class AdminAdMediaPartnerService(private val repository: AdMediaPartnerRepositor AdMediaPartnerType.MAIN -> "main" } - val link = "$oneLinkHost?" + URLEncoder.encode( - "af_dp=voiceon%3A%2F%2F" + - "&deep_link_value=$deepLinkValue" + - "&deep_link_sub1=${it.pid}" + - "&deep_link_sub2=${it.utmSource}" + - "&deep_link_sub3=${it.utmMedium}" + - "&deep_link_sub4=${it.pidName}" + - "&utm_source=${it.utmSource}" + - "&utm_medium=${it.utmMedium}" + - "&utm_campaign=${it.pidName}", - "UTF-8" - ) + val link = "$oneLinkHost?" + + "af_dp=voiceon://" + + "&deep_link_value=$deepLinkValue" + + "&deep_link_sub1=${it.pid}" + + "&deep_link_sub2=${it.utmSource}" + + "&deep_link_sub3=${it.utmMedium}" + + "&deep_link_sub4=${it.pidName}" + + "&utm_source=${it.utmSource}" + + "&utm_medium=${it.utmMedium}" + + "&utm_campaign=${it.pidName}" it.link = link it -- 2.40.1