From 3216c73ee821870a1dc9202ff9ec0526a96127b8 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 2 Mar 2025 23:45:08 +0900 Subject: [PATCH] =?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 +)