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..b9f7b19 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 = "사용 가능한 이메일 입니다.") } @@ -502,6 +549,7 @@ class MemberService( logoutAll(memberId = member.id!!) member.isActive = false + member.nickname = "deleted_${member.nickname}" val signOut = SignOut(reason = signOutRequest.reason) signOut.member = member diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt index 53ce8de..e736e1b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt @@ -64,6 +64,7 @@ class AuthService( fun signOut(memberId: Long) { val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") member.isActive = false + member.nickname = "deleted_${member.nickname}" val signOut = SignOut(reason = "운영정책을 위반하여 이용을 제한합니다.") signOut.member = member 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..a879128 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameGenerateService.kt @@ -0,0 +1,76 @@ +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( + "감성적인", "몽환적인", "깊이있는", "따뜻한", "서정적인", "소울풀한", "잔잔한", "리드미컬한", "감미로운", "은은한", + "울려퍼지는", "하모닉한", "레트로한", "아날로그적인", "빈티지한", "시간을넘는", "과거에서온", "미래를보는", "초월적인", "운명적인", + "신비로운", "마법같은", "고요한", "푸른", "맑은", "강한", "자유로운", "평온한", "깊은", "고독한", + "거친", "부드러운", "속삭이는", "빛바랜", "차가운", "꿈꾸는", "숨겨진", "고귀한", "기억속의", "깨어난", + "끝없는", "청명한", "환상적인", "어두운", "희미한", "선명한", "눈부신", "불타는", "차분한", "매혹적인", + "아련한", "선선한", "상쾌한", "온화한", "따사로운", "고혹적인", "포근한", "황금빛", "청량한", "시원한", + "서늘한", "우아한", "단단한", "투명한", "가벼운", "조용한", "비밀스러운", "화려한", "찬란한", "고동치는", + "폭발적인", "순수한", "어렴풋한", "흐릿한", "고결한", "신비에싸인", "달콤한", "무한한", "아득한", "화사한", + "평안한", "눈꽃같은", "선율적인", "고즈넉한", "웅장한", "황홀한", "빛나는", "쓸쓸한", "청순한", "흐르는", + "타오르는", "미묘한", "그윽한", "아름다운", "싱그러운", "몽롱한", "청아한", "섬세한", "촉촉한", "강렬한", + "싱싱한" + ) + + private val nouns = listOf( + "소리", "울림", "속삭임", "청취자", "메아리", "목소리", "공명", "음색", "감성", "멜로디", + "리듬", "사운드트랙", "나이트클럽", "라디오스타", "레코드판", "카세트테이프", "LP음악", "복고댄스", "클래식기타", "빈티지마이크", + "시간여행", "타임머신", "평행세계", "마법진", "바람", "늑대", "태양", "대지", "강", "하늘", + "불꽃", "별빛", "나무", "산", "달빛", "독수리", "폭풍", "눈", "밤", + "노을", "물결", "노래", "파도", "구름", "사슴", "호랑이", "부엉이", "신비", "영혼", + "선율", "하모니", "평원", "빛", "고래", "모래", "깊은숲", "까마귀", "사자", "코요테", + "표범", "재규어", "스라소니", "여우", "곰", "수달", "늑대개", "판다", "코끼리", "들소", + "바다사자", "살쾡이", "까치", "매", "카멜레온", "반달곰", "솔개", "바다표범", "늑대거북", "물총새", + "철새", "까투리", "매화쏘가리", "청둥오리", "황새", "알바트로스", "은어", "참다랑어", "도루묵", "붕어", + "송사리", "산양", "담비", "멧돼지", "설표", "물개", "칠면조", "담수어", "자라", "나비", + "풍뎅이", "하프물범", "노루", "사마귀", "장수말벌", "해마", "흰수염고래", "금붕어", "백조", "코뿔소", + "수리부엉이", "까막까치", "비단뱀", "청어", "산들바람", "은빛바다", "물안개", "자연의숨결", "신록숲", "호수", + "비밀정원", "파랑새", "바람개비", "샘물", "은하수", "구름다리", "폭포수", "쿼카", "캥거루", "상어", + "고라니", "휴지" + ) + + private fun generateRandomNickname(): String { + val formatType = Random.nextInt(3) + return when (formatType) { + 0 -> "${adjectives.random()}${nouns.random()}" + 1 -> "${nouns.random()}의${nouns.random()}" + else -> "${adjectives.random()}${nouns.random()}의${nouns.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" +)