From 4977ee99df6b1510e240182227ed0bbddec055b0 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 21 Mar 2025 00:24:15 +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=20=EA=B0=9C=EC=84=A0=20-=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=EC=99=80=20=EB=8B=89=EB=84=A4=EC=9E=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=83=9D=EC=84=B1=EC=9D=84=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/configs/SecurityConfig.kt | 1 + .../sodalive/member/MemberController.kt | 16 +++++ .../sodalive/member/MemberRepository.kt | 34 ++++++++++ .../sodalive/member/MemberService.kt | 49 +++++++++++++- .../nickname/NicknameGenerateService.kt | 65 +++++++++++++++++++ .../sodalive/member/signUp/SignUpRequest.kt | 9 +++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt index 7c8a24e..6423bda 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt @@ -67,6 +67,7 @@ class SecurityConfig( .antMatchers("/member/check/email").permitAll() .antMatchers("/member/check/nickname").permitAll() .antMatchers("/member/signup").permitAll() + .antMatchers("/member/signup/v2").permitAll() .antMatchers("/member/login").permitAll() .antMatchers("/creator-admin/member/login").permitAll() .antMatchers("/member/forgot-password").permitAll() 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 61000c0..886c48a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -9,6 +9,7 @@ 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 kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2 import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.userdetails.User @@ -42,6 +43,21 @@ class MemberController( @AuthenticationPrincipal user: User ) = ApiResponse.ok(service.updateNickname(profileUpdateRequest, user)) + @PostMapping("/signup/v2") + fun signupV2(@RequestBody request: SignUpRequestV2): ApiResponse { + val response = service.signUpV2(request) + + if (!response.marketingPid.isNullOrBlank()) { + trackingService.saveTrackingHistory( + pid = response.marketingPid, + type = AdTrackingHistoryType.SIGNUP, + memberId = response.memberId + ) + } + + return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse) + } + @PostMapping("/signup") fun signUp( @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 9103990..e67a033 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -59,6 +59,10 @@ interface MemberQueryRepository { fun getAuditionNoticeRecipientPushTokens(isAuth: Boolean): Map>> fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse + + fun existsByEmail(email: String): Boolean + fun existsByNickname(nickname: String): Boolean + fun findNicknamesWithPrefix(prefix: String): List } @Repository @@ -479,4 +483,34 @@ class MemberQueryRepositoryImpl( .where(member.id.eq(memberId)) .fetchFirst() } + + override fun existsByEmail(email: String): Boolean { + return queryFactory + .selectOne() + .from(member) + .where(member.email.eq(email)) + .fetchFirst() != null + } + + override fun existsByNickname(nickname: String): Boolean { + return queryFactory + .selectOne() + .from(member) + .where( + member.nickname.eq(nickname), + member.isActive.isTrue + ) + .fetchFirst() != null + } + + override fun findNicknamesWithPrefix(prefix: String): List { + return queryFactory + .select(member.nickname) + .from(member) + .where( + member.nickname.startsWith(prefix), + member.isActive.isTrue + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 24628fd..2c54085 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -25,9 +25,11 @@ import kr.co.vividnext.sodalive.member.login.LoginResponse import kr.co.vividnext.sodalive.member.myPage.MyPageResponse import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLog import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLogRepository +import kr.co.vividnext.sodalive.member.nickname.NicknameGenerateService 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.SignUpRequestV2 import kr.co.vividnext.sodalive.member.signUp.SignUpResponse import kr.co.vividnext.sodalive.member.signUp.SignUpValidator import kr.co.vividnext.sodalive.member.stipulation.Stipulation @@ -77,6 +79,7 @@ class MemberService( private val orderService: OrderService, private val emailService: SendEmailService, private val canPaymentService: CanPaymentService, + private val nicknameGenerateService: NicknameGenerateService, private val memberNotificationService: MemberNotificationService, private val s3Uploader: S3Uploader, @@ -96,6 +99,46 @@ class MemberService( private val tokenLocks: MutableMap = mutableMapOf() + @Transactional + fun signUpV2(request: SignUpRequestV2): SignUpResponse { + val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID) + ?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.") + + if (!request.isAgreePrivacyPolicy || !request.isAgreeTermsOfService) { + throw SodaException("약관에 동의하셔야 회원가입이 가능합니다.") + } + + duplicateCheckEmail(request.email) + validatePassword(request.password) + + val nickname = nicknameGenerateService.generateUniqueNickname() + val member = Member( + email = request.email, + password = passwordEncoder.encode(request.password), + nickname = nickname, + profileImage = "profile/default-profile.png", + gender = Gender.NONE, + container = request.container + ) + + if (!request.marketingPid.isNullOrBlank()) { + member.activePid = request.marketingPid + member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1) + } + + repository.save(member) + agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy) + + return SignUpResponse( + memberId = member.id!!, + marketingPid = request.marketingPid, + loginResponse = login(request.email, request.password) + ) + } + @Transactional fun signUp( profileImage: MultipartFile?, @@ -334,7 +377,11 @@ class MemberService( fun duplicateCheckEmail(email: String): ApiResponse { validateEmail(email) - repository.findByEmail(email)?.let { throw SodaException("이미 사용중인 이메일 입니다.", "email") } + + if (repository.existsByEmail(email)) { + throw SodaException("이미 사용중인 이메일 입니다.", "email") + } + return ApiResponse.ok(message = "사용 가능한 이메일 입니다.") } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt new file mode 100644 index 0000000..ab4c230 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt @@ -0,0 +1,65 @@ +package kr.co.vividnext.sodalive.member.nickname + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.stereotype.Service +import kotlin.random.Random + +@Service +class NicknameGenerateService(private val repository: MemberRepository) { + private val adjectives = listOf( + "감성적인", "몽환적인", "깊이있는", "따뜻한", "서정적인", "소울풀한", "잔잔한", + "선율의", "리드미컬한", "감미로운", "은은한", "잔향의", "울려퍼지는", "하모닉한", + "레트로한", "복고풍의", "아날로그적인", "빈티지한", "90년대감성", "옛날느낌의", + "시간을넘는", "과거에서온", "시간여행자의", "미래를보는", "초월적인", "운명적인", + "신비로운", "마법같은", "전설속의", "별빛의", "달빛의" + ) + + private val nouns = listOf( + "소리", "울림", "속삭임", "청취자", "메아리", "목소리", "공명", "음색", "감성", + "멜로디", "선율", "리듬", "하모니", "사운드트랙", "나이트클럽", "라디오스타", + "레코드판", "카세트테이프", "LP음악", "복고댄스", "클래식기타", "빈티지마이크", + "시간여행", "타임머신", "평행세계", "운명의선택", "마법진", "신비한음색", + "달빛의향연", "별빛의꿈", "마법사의속삭임", "초월적인선율" + ) + + private val numberRange = 1000..9999 + + private fun generateRandomNickname(): String { + val formatType = Random.nextInt(5) + return when (formatType) { + 0 -> "${adjectives.random()}${nouns.random()}${numberRange.random()}" + 1 -> "${nouns.random()}의${nouns.random()}${numberRange.random()}" + 2 -> "${adjectives.random()}${nouns.random()}" + 3 -> "${nouns.random()}의${nouns.random()}" + else -> "${adjectives.random()}${nouns.random()}의${nouns.random()}${numberRange.random()}" + } + } + + private fun generateNonConflictingNickname(usedNicknames: Set): String { + val usedNicknameSet = HashSet(usedNicknames) // 해시셋으로 변환 (O(1) 조회 가능) + val availableNumbers = (1000..9999).shuffled() + + for (num in availableNumbers) { // 숫자를 먼저 결정 (무작위) + for (adj in adjectives.shuffled()) { // 형용사 순서 랜덤화 + for (noun in nouns.shuffled()) { // 명사 순서 랜덤화 + val candidate = "$adj$noun$num" + if (!usedNicknameSet.contains(candidate)) { + return candidate + } + } + } + } + throw SodaException("회원가입을 하지 못했습니다.\n다시 시도해 주세요.") + } + + fun generateUniqueNickname(): String { + repeat(5) { + val candidates = (1..10).map { generateRandomNickname() } + val available = candidates.firstOrNull { !repository.existsByNickname(it) } + if (available != null) return available + } + + return generateNonConflictingNickname(repository.findNicknamesWithPrefix("").toSet()) + } +} 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 42c7350..de4a6fa 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 @@ -12,3 +12,12 @@ data class SignUpRequest( val isAgreePrivacyPolicy: Boolean, val container: String = "api" ) + +data class SignUpRequestV2( + val email: String, + val password: String, + val marketingPid: String? = null, + val isAgreeTermsOfService: Boolean, + val isAgreePrivacyPolicy: Boolean, + val container: String = "api" +)