Compare commits

...

5 Commits

Author SHA1 Message Date
klaus c7ec95f4bb Merge pull request 'test' (#292) from test into main
Reviewed-on: #292
2025-03-20 19:24:03 +00:00
Klaus bc822355df 회원탈퇴 시 닉네임 앞에 "deleted_"를 추가 2025-03-21 04:15:53 +09:00
Klaus 9535ff18de 닉네임 자동생성
- 닉네임을 더 유니크하게 생성할 수 있도록 형용사와 명사 추가
2025-03-21 04:11:35 +09:00
Klaus da0a83bb6d 닉네임 자동생성
- '의'가 들어간 단어 제거
2025-03-21 02:50:27 +09:00
Klaus 4977ee99df 회원가입 로직 개선
- 기본 프로필 이미지와 닉네임 자동생성을 통해 회원가입 단계 축소
2025-03-21 00:24:15 +09:00
7 changed files with 186 additions and 1 deletions

View File

@ -67,6 +67,7 @@ class SecurityConfig(
.antMatchers("/member/check/email").permitAll() .antMatchers("/member/check/email").permitAll()
.antMatchers("/member/check/nickname").permitAll() .antMatchers("/member/check/nickname").permitAll()
.antMatchers("/member/signup").permitAll() .antMatchers("/member/signup").permitAll()
.antMatchers("/member/signup/v2").permitAll()
.antMatchers("/member/login").permitAll() .antMatchers("/member/login").permitAll()
.antMatchers("/creator-admin/member/login").permitAll() .antMatchers("/creator-admin/member/login").permitAll()
.antMatchers("/member/forgot-password").permitAll() .antMatchers("/member/forgot-password").permitAll()

View File

@ -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.LoginRequest
import kr.co.vividnext.sodalive.member.login.LoginResponse import kr.co.vividnext.sodalive.member.login.LoginResponse
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest 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.data.domain.Pageable
import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.User
@ -42,6 +43,21 @@ class MemberController(
@AuthenticationPrincipal user: User @AuthenticationPrincipal user: User
) = ApiResponse.ok(service.updateNickname(profileUpdateRequest, user)) ) = ApiResponse.ok(service.updateNickname(profileUpdateRequest, user))
@PostMapping("/signup/v2")
fun signupV2(@RequestBody request: SignUpRequestV2): ApiResponse<LoginResponse> {
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") @PostMapping("/signup")
fun signUp( fun signUp(
@RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null,

View File

@ -59,6 +59,10 @@ interface MemberQueryRepository {
fun getAuditionNoticeRecipientPushTokens(isAuth: Boolean): Map<String, List<List<String>>> fun getAuditionNoticeRecipientPushTokens(isAuth: Boolean): Map<String, List<List<String>>>
fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse fun getMemberProfile(memberId: Long, myMemberId: Long): GetMemberProfileResponse
fun existsByEmail(email: String): Boolean
fun existsByNickname(nickname: String): Boolean
fun findNicknamesWithPrefix(prefix: String): List<String>
} }
@Repository @Repository
@ -479,4 +483,34 @@ class MemberQueryRepositoryImpl(
.where(member.id.eq(memberId)) .where(member.id.eq(memberId))
.fetchFirst() .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<String> {
return queryFactory
.select(member.nickname)
.from(member)
.where(
member.nickname.startsWith(prefix),
member.isActive.isTrue
)
.fetch()
}
} }

View File

@ -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.myPage.MyPageResponse
import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLog import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLog
import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLogRepository 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.MemberNotificationService
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
import kr.co.vividnext.sodalive.member.signUp.SignUpRequest 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.SignUpResponse
import kr.co.vividnext.sodalive.member.signUp.SignUpValidator import kr.co.vividnext.sodalive.member.signUp.SignUpValidator
import kr.co.vividnext.sodalive.member.stipulation.Stipulation import kr.co.vividnext.sodalive.member.stipulation.Stipulation
@ -77,6 +79,7 @@ class MemberService(
private val orderService: OrderService, private val orderService: OrderService,
private val emailService: SendEmailService, private val emailService: SendEmailService,
private val canPaymentService: CanPaymentService, private val canPaymentService: CanPaymentService,
private val nicknameGenerateService: NicknameGenerateService,
private val memberNotificationService: MemberNotificationService, private val memberNotificationService: MemberNotificationService,
private val s3Uploader: S3Uploader, private val s3Uploader: S3Uploader,
@ -96,6 +99,46 @@ class MemberService(
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf() private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = 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 @Transactional
fun signUp( fun signUp(
profileImage: MultipartFile?, profileImage: MultipartFile?,
@ -334,7 +377,11 @@ class MemberService(
fun duplicateCheckEmail(email: String): ApiResponse<Any> { fun duplicateCheckEmail(email: String): ApiResponse<Any> {
validateEmail(email) validateEmail(email)
repository.findByEmail(email)?.let { throw SodaException("이미 사용중인 이메일 입니다.", "email") }
if (repository.existsByEmail(email)) {
throw SodaException("이미 사용중인 이메일 입니다.", "email")
}
return ApiResponse.ok(message = "사용 가능한 이메일 입니다.") return ApiResponse.ok(message = "사용 가능한 이메일 입니다.")
} }
@ -502,6 +549,7 @@ class MemberService(
logoutAll(memberId = member.id!!) logoutAll(memberId = member.id!!)
member.isActive = false member.isActive = false
member.nickname = "deleted_${member.nickname}"
val signOut = SignOut(reason = signOutRequest.reason) val signOut = SignOut(reason = signOutRequest.reason)
signOut.member = member signOut.member = member

View File

@ -64,6 +64,7 @@ class AuthService(
fun signOut(memberId: Long) { fun signOut(memberId: Long) {
val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.") val member = memberRepository.findByIdOrNull(memberId) ?: throw SodaException("로그인 정보를 확인해주세요.")
member.isActive = false member.isActive = false
member.nickname = "deleted_${member.nickname}"
val signOut = SignOut(reason = "운영정책을 위반하여 이용을 제한합니다.") val signOut = SignOut(reason = "운영정책을 위반하여 이용을 제한합니다.")
signOut.member = member signOut.member = member

View File

@ -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>): 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())
}
}

View File

@ -12,3 +12,12 @@ data class SignUpRequest(
val isAgreePrivacyPolicy: Boolean, val isAgreePrivacyPolicy: Boolean,
val container: String = "api" 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"
)