Compare commits

..

3 Commits

Author SHA1 Message Date
37d2e0de73 일별 전체 회원 수에 애플 계정으로 회원 가입 수 추가 2026-02-08 16:26:28 +09:00
9779c1b50b 일본어 닉네임 생성 기능 추가
generateUniqueNickname에 Lang 파라미터를 추가하여
언어 설정이 일본어일 때 일본어 단어 조합으로
닉네임을 생성한다.
2026-02-08 16:15:51 +09:00
23c219c672 닉네임 생성 형용사 및 명사 단어 목록 교체
NicknameGenerateService의 adjectives, nouns 리스트를
새로운 단어 목록으로 전체 교체한다.
형용사 140개, 명사 160개를 신규 단어로 구성한다.
2026-02-08 16:02:32 +09:00
5 changed files with 157 additions and 49 deletions

View File

@@ -68,6 +68,19 @@ class AdminMemberStatisticsRepository(private val queryFactory: JPAQueryFactory)
.size .size
} }
fun getTotalSignUpAppleCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory
.select(member.id)
.from(member)
.where(
member.createdAt.goe(startDate),
member.createdAt.loe(endDate),
member.provider.eq(MemberProvider.APPLE)
)
.fetch()
.size
}
fun getTotalSignUpLineCount(startDate: LocalDateTime, endDate: LocalDateTime): Int { fun getTotalSignUpLineCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory return queryFactory
.select(member.id) .select(member.id)
@@ -202,6 +215,25 @@ class AdminMemberStatisticsRepository(private val queryFactory: JPAQueryFactory)
.fetch() .fetch()
} }
fun getSignUpAppleCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
return queryFactory
.select(
QDateAndMemberCount(
getFormattedDate(member.createdAt),
member.id.countDistinct().castToNum(Int::class.java)
)
)
.from(member)
.where(
member.createdAt.goe(startDate),
member.createdAt.loe(endDate),
member.provider.eq(MemberProvider.APPLE)
)
.groupBy(getFormattedDate(member.createdAt))
.orderBy(getFormattedDate(member.createdAt).desc())
.fetch()
}
fun getSignUpLineCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> { fun getSignUpLineCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
return queryFactory return queryFactory
.select( .select(

View File

@@ -58,6 +58,10 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
startDate = startDateTime, startDate = startDateTime,
endDate = endDateTime endDate = endDateTime
) )
val totalSignUpAppleCount = repository.getTotalSignUpAppleCount(
startDate = startDateTime,
endDate = endDateTime
)
val totalSignUpLineCount = repository.getTotalSignUpLineCount( val totalSignUpLineCount = repository.getTotalSignUpLineCount(
startDate = startDateTime, startDate = startDateTime,
endDate = endDateTime endDate = endDateTime
@@ -96,6 +100,11 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
endDate = endDateTime endDate = endDateTime
).associateBy({ it.date }, { it.memberCount }) ).associateBy({ it.date }, { it.memberCount })
val signUpAppleCountInRange = repository.getSignUpAppleCountInRange(
startDate = startDateTime,
endDate = endDateTime
).associateBy({ it.date }, { it.memberCount })
val signUpLineCountInRange = repository.getSignUpLineCountInRange( val signUpLineCountInRange = repository.getSignUpLineCountInRange(
startDate = startDateTime, startDate = startDateTime,
endDate = endDateTime endDate = endDateTime
@@ -130,6 +139,7 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
signUpEmailCount = signUpEmailCountInRange[date] ?: 0, signUpEmailCount = signUpEmailCountInRange[date] ?: 0,
signUpKakaoCount = signUpKakaoCountInRange[date] ?: 0, signUpKakaoCount = signUpKakaoCountInRange[date] ?: 0,
signUpGoogleCount = signUpGoogleCountInRange[date] ?: 0, signUpGoogleCount = signUpGoogleCountInRange[date] ?: 0,
signUpAppleCount = signUpAppleCountInRange[date] ?: 0,
signUpLineCount = signUpLineCountInRange[date] ?: 0, signUpLineCount = signUpLineCountInRange[date] ?: 0,
signOutCount = signOutCountInRange[date] ?: 0, signOutCount = signOutCountInRange[date] ?: 0,
paymentMemberCount = paymentMemberCountInRangeMap[date] ?: 0 paymentMemberCount = paymentMemberCountInRangeMap[date] ?: 0
@@ -144,6 +154,7 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
totalSignUpEmailCount = totalSignUpEmailCount, totalSignUpEmailCount = totalSignUpEmailCount,
totalSignUpKakaoCount = totalSignUpKakaoCount, totalSignUpKakaoCount = totalSignUpKakaoCount,
totalSignUpGoogleCount = totalSignUpGoogleCount, totalSignUpGoogleCount = totalSignUpGoogleCount,
totalSignUpAppleCount = totalSignUpAppleCount,
totalSignUpLineCount = totalSignUpLineCount, totalSignUpLineCount = totalSignUpLineCount,
totalSignOutCount = totalSignOutCount, totalSignOutCount = totalSignOutCount,
totalPaymentMemberCount = totalPaymentMemberCount, totalPaymentMemberCount = totalPaymentMemberCount,

View File

@@ -7,6 +7,7 @@ data class GetMemberStatisticsResponse(
val totalSignUpEmailCount: Int, val totalSignUpEmailCount: Int,
val totalSignUpKakaoCount: Int, val totalSignUpKakaoCount: Int,
val totalSignUpGoogleCount: Int, val totalSignUpGoogleCount: Int,
val totalSignUpAppleCount: Int,
val totalSignUpLineCount: Int, val totalSignUpLineCount: Int,
val totalSignOutCount: Int, val totalSignOutCount: Int,
val totalPaymentMemberCount: Int, val totalPaymentMemberCount: Int,
@@ -20,6 +21,7 @@ data class GetMemberStatisticsItem(
val signUpEmailCount: Int, val signUpEmailCount: Int,
val signUpKakaoCount: Int, val signUpKakaoCount: Int,
val signUpGoogleCount: Int, val signUpGoogleCount: Int,
val signUpAppleCount: Int,
val signUpLineCount: Int, val signUpLineCount: Int,
val signOutCount: Int, val signOutCount: Int,
val paymentMemberCount: Int val paymentMemberCount: Int

View File

@@ -130,7 +130,7 @@ class MemberService(
duplicateCheckEmail(request.email) duplicateCheckEmail(request.email)
validatePassword(request.password) validatePassword(request.password)
val nickname = nicknameGenerateService.generateUniqueNickname() val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member( val member = Member(
email = request.email, email = request.email,
password = passwordEncoder.encode(request.password), password = passwordEncoder.encode(request.password),
@@ -850,7 +850,7 @@ class MemberService(
val email = googleUserInfo.email val email = googleUserInfo.email
checkEmail(email) checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname() val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member( val member = Member(
googleId = googleUserInfo.sub, googleId = googleUserInfo.sub,
email = email, email = email,
@@ -907,7 +907,7 @@ class MemberService(
val email = kakaoUserInfo.email val email = kakaoUserInfo.email
checkEmail(email) checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname() val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member( val member = Member(
kakaoId = kakaoUserInfo.id, kakaoId = kakaoUserInfo.id,
email = email, email = email,
@@ -964,7 +964,7 @@ class MemberService(
val email = appleUserInfo.email val email = appleUserInfo.email
checkEmail(email) checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname() val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member( val member = Member(
appleId = appleUserInfo.sub, appleId = appleUserInfo.sub,
email = email, email = email,
@@ -1021,7 +1021,7 @@ class MemberService(
val email = lineUserInfo.email val email = lineUserInfo.email
checkEmail(email) checkEmail(email)
val nickname = nicknameGenerateService.generateUniqueNickname() val nickname = nicknameGenerateService.generateUniqueNickname(langContext.lang)
val member = Member( val member = Member(
lineId = lineUserInfo.sub, lineId = lineUserInfo.sub,
email = email, email = email,

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.member.nickname package kr.co.vividnext.sodalive.member.nickname
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import kotlin.random.Random import kotlin.random.Random
@@ -8,59 +9,121 @@ import kotlin.random.Random
@Service @Service
class NicknameGenerateService(private val repository: MemberRepository) { class NicknameGenerateService(private val repository: MemberRepository) {
private val adjectives = listOf( private val adjectives = listOf(
"따뜻한", "은은", "고요", "푸른", "맑은", "강한", "평온", "깊은", "고독한", "활기찬", "명랑", "씩씩", "용감한", "지혜론", "슬기론", "넉넉", "든든한", "알찬듯",
"거친", "빛바랜", "차가운", "꿈꾸는", "숨겨", "고귀", "깨어난", "끝없는", "청명한", "빠른듯", "느긋한", "당당한", "솔직한", "실한", "겸손", "성실한", "꼼꼼한", "야무진",
"어두운", "희미", "명한", "눈부신", "불타는", "차분한", "아련한", "선선한", "상쾌한", "온화한", "재빠른", "영리", "명한", "현명한", "착실한", "올곧은", "바른듯", "곧은듯", "힘찬듯", "굳센듯",
"포근", "황금빛", "청량", "시원한", "서늘", "우아", "단단한", "투명한", "가벼운", "조용한", "의젓", "점잖은", "듬직", "너그런", "관대", "인자", "자애론", "헌신적", "열정적", "적극적",
"화려", "찬란", "순수한", "흐릿", "고결", "달콤", "무한", "아득한", "화사한", "평안한", "능숙", "탁월", "뛰어난", "출중", "비범", "특별", "독특", "개성적", "창의적", "혁신적",
"웅장한", "황홀한", "빛나는", "쓸쓸", "청순한", "흐르는", "미묘", "그윽", "몽롱", "청아한", "진취적", "도전적", "패기찬", "호쾌", "시원찬", "통쾌한", "유능", "민첩", "기민", "재치론",
"섬세한", "촉촉한", "강렬한", "싱싱한", "부드런", "아늑한", "매운듯", "고운듯", "느린듯", "밝은듯", "센스찬", "감각적", "세련된", "품격찬", "격조찬", "기품찬", "위풍찬", "늠름한", "씩씩찬", "호탕한",
"짧은듯", "달큰", "깊은듯", "기쁜듯", "쌀쌀한", "무거운", "연한듯", "편안", "깨끗한", "말끔", "대범한", "거뜬", "가뿐한", "홀가분", "산뜻찬", "깔끔찬", "정갈한", "단정", "반짝찬", "영롱",
"뽀얀듯", "푸르른", "붉은", "노오란", "무딘듯", "살짝한", "상큼한", "시큰한", "고소", "나긋한", "찬란찬", "눈부찬", "환한", "밝은찬", "빛깔찬", "색다른", "새로운", "신선찬", "풋풋", "싱그런",
"화끈한", "중후", "정겨운", "날렵한", "기묘한", "참신한", "담백", "퉁명한", "꾸밈없", "소박한", "생기찬", "발랄", "경쾌한", "리듬찬", "율동적", "역동적", "활발", "생동찬", "약동찬", "힘있는",
"뾰족", "무심", "도도", "따끔", "무난한", "단호한", "냉정", "따스", "유연", "묵직", "건장", "튼튼", "건강", "탄탄", "단련된", "숙련된", "노련", "원숙", "성숙", "완숙",
"나른", "몽환적", "돈된", "쾌활", "날카론", "묘한듯", "예쁜듯", "뽀얗게", "다정", "푸근", "정확", "치밀한", "밀한", "철저", "완벽찬", "흠없는", "나무랄", "빈틈없", "알뜰", "꾸준",
"애틋", "낭만적", "", "훈훈", "섹시", "정적인", "유쾌한", "멍한듯", "혼란", "상냥한", "결찬", "변함없", "건한", "확고", "견고", "탄탄찬", "안정적", "평화론", "온유", "자비론",
"뚜렷한", "신비한", "허전", "그리운", "들뜬듯", "절실", "반듯", "반가운", "새하얀", "흐린듯", "배려찬", "사려찬", "깊숙", "심오한", "오묘한", "현묘", "신묘", "경이론", "놀라운", "대단한",
"엄숙", "깊잖은", "산뜻", "낯선듯" "훌륭", "멋스런", "근사", "기특한"
) )
private val nouns = listOf( private val nouns = listOf(
"소리", "울림", "공명", "음색", "감성", "리듬", "바람", "늑대", "태양", "대지", "다람쥐", "청설모", "두루미", "기러기", "올빼미", "부엉이", "딱따구", "꾀꼬리", "직박구", "동박새",
"", "하늘", "불꽃", "별빛", "나무", "", "달빛", "폭풍", "", "", "참새", "종달새", "제비", "뻐꾸기", "앵무새", "공작새", "원앙", "두더지", "고슴도", "족제비",
"노을", "물결", "노래", "파도", "구름", "사슴", "신비", "영혼", "선율", "평원", "오소리", "수리부", "해오라", "갈매기", "펭귄", "코알라", "알파카", "카멜레", "이구아", "플라밍",
"", "고래", "모래", "사자", "표범", "여우", "", "수달", "판다", "들소", "돌고래", "해달", "라쿤", "미어캣", "친칠라", "햄스터", "기니피", "토끼", "강아지", "고양이",
"까치", "", "솔개", "물총새", "철새", "황새", "은어", "붕어", "산양", "", "망아지", "송아지", "병아리", "올챙이", "개구리", "도롱뇽", "거북이", "앵무", "카나리", "둘기",
"설표", "물개", "자라", "나비", "노루", "해마", "백조", "청어", "호수", "", "참매", "독수리", "콘도르", "벌새", "홍학", "타조", "키위새", "투칸", "앵콩이", "까치",
"쿼카", "상어", "무드", "나노", "루프", "네온", "모아", "아토", "플로", "루미", "루비", "사파이", "에메랄", "자수정", "진주", "산호", "호박", "비취", "오팔", "토파즈",
"도트", "비트", "토브", "온기", "클리", "위드", "제로", "", "미오", "시그", "다이아", "크리스", "아쿠아", "코발트", "인디고", "라벤더", "마젠타", "터콰", "세룰리", "버밀리",
"쿠나", "오로", "폴라", "바움", "포잇", "누아", "오브", "파인", "조이", "아뜰", "카푸치", "에스프", "아메리", "마키아", "바닐라", "캐러멜", "시나몬", "민트", "자스민", "캐모마",
"티노", "소마", "하루", "밀크", "아린", "토로", "벨로", "위시", "뮤즈", "노블", "히비스", "라일락", "프리지", "튤립", "수선화", "동백", "매화", "목련", "벚꽃", "진달래",
"카노", "미카", "하라", "엘로", "피오", "라임", "노이", "루다", "이브", "마리", "철쭉", "개나리", "무궁화", "해바라", "코스모", "달리아", "작약", "모란", "연꽃", "수련",
"블루", "시온", "레아", "도르", "하노", "네리", "키노", "쿠키", "라노", "수이", "클로버", "민들레", "제비꽃", "은방울", "안개꽃", "라넌큘", "아네모", "델피니", "글라디", "프로테",
"우노", "파루", "크리", "포유", "코코", "아라", "토리", "", "보노", "페어", "유칼립", "로즈마", "바질", "타임", "오레가", "세이지", "", "파슬", "고수", "루꼴라",
"", "모리", "세리", "리브", "", "모카", "아이", "르네", "이로", "미노", "보카", "블루베", "라즈베", "크랜베", "아사", "망고", "파파야", "리치", "패션후", "구아바",
"다라", "노바", "디노", "오미", "카라", "", "루아", "네오", "하이", "레인", "석류", "무화과", "살구", "자두", "체리", "복숭", "포도", "감귤", "유자", "한라봉",
"피카", "유카", "제니", "이든", "", "아벨", "솔라", "쿠로", "시라", "리코" "천혜향", "레드향", "금귤", "모과", "", "대추", "", "호두", "", "은행"
) )
private fun generateRandomNickname(): String { private val jaAdjectives = listOf(
"元気な", "明るい", "優しい", "強い", "賢い", "穏やかな", "爽やかな", "楽しい",
"勇敢な", "素敵な", "可愛い", "美しい", "清らかな", "温かい", "輝く", "華やかな",
"凛とした", "朗らかな", "逞しい", "麗しい", "雅な", "粋な", "健やかな", "晴れやかな",
"鮮やかな", "煌めく", "微笑む", "誠実な", "丁寧な", "真っ直ぐな", "気高い", "聡明な",
"快活な", "軽やかな", "しなやかな", "伸びやかな", "瑞々しい", "初々しい", "艶やかな", "柔らかな",
"澄んだ", "静かな", "豊かな", "深い", "広い", "高い", "清い", "涼しい",
"眩しい", "暖かな", "和やかな", "安らかな", "のどかな", "ほがらかな", "すこやかな", "たくましい",
"ひたむきな", "まめな", "きらきらな", "ふわふわな", "にこにこな", "わくわくな", "すくすくな", "のびのびな",
"きりっとした", "はきはきな", "てきぱきな", "しっかりな", "どっしりな", "ゆったりな", "さっぱりな", "すっきりな",
"ぴかぴかな", "つやつやな", "さらさらな", "もちもちな", "ぷるぷるな", "ころころな", "ぽかぽかな", "そよそよな",
"堅実な", "勤勉な", "忠実な", "素直な", "謙虚な", "大胆な", "情熱的な", "積極的な",
"独創的な", "繊細な", "壮大な", "格調高い", "品のある", "風格ある", "趣のある", "奥深い",
"颯爽とした", "堂々とした", "悠々とした", "泰然とした", "毅然とした", "端正な", "清楚な", "典雅な",
"俊敏な", "機敏な", "敏捷な", "軽快な", "活発な", "溌剌とした", "生き生きな", "伸び伸びな",
"揺るぎない", "確かな", "頼もしい", "心強い", "力強い", "逞しき", "雄々しい", "凜々しい",
"慈しみの", "思いやりの", "気配りの", "心優しい", "情け深い", "懐の深い", "器の大きい", "包容力の"
)
private val jaNouns = listOf(
"うさぎ", "ねこ", "いぬ", "たぬき", "きつね", "しか", "りす", "ふくろう",
"つばめ", "すずめ", "ひばり", "うぐいす", "めじろ", "つる", "はと", "かもめ",
"いるか", "くじら", "らっこ", "ペンギン", "コアラ", "パンダ", "アルパカ", "ハムスター",
"かめ", "かえる", "ほたる", "ちょう", "とんぼ", "てんとう", "こねこ", "こいぬ",
"ひよこ", "こじか", "こぐま", "こうさぎ", "こりす", "こだぬき", "こぎつね", "こばと",
"さくら", "うめ", "もみじ", "つばき", "すみれ", "たんぽぽ", "ひまわり", "あじさい",
"コスモス", "ラベンダー", "チューリップ", "カーネーション", "バラ", "ユリ", "ダリア", "マーガレット",
"なでしこ", "あやめ", "ききょう", "はぎ", "ふじ", "ぼたん", "しゃくやく", "れんげ",
"ルビー", "サファイア", "エメラルド", "アメジスト", "パール", "オパール", "トパーズ", "ガーネット",
"ひかり", "そら", "うみ", "かぜ", "つき", "ほし", "にじ", "ゆめ",
"あかね", "みずき", "はるか", "あおい", "ひなた", "こはる", "いろは", "かなで",
"しずく", "つゆ", "あられ", "みぞれ", "こはく", "あかり", "ともしび", "かがやき",
"やまと", "みやび", "まこと", "ちはや", "あさひ", "ゆうひ", "あけぼの", "たそがれ",
"わかば", "あおば", "もえぎ", "ときわ", "さつき", "やよい", "きさらぎ", "むつき",
"抹茶", "桜餅", "団子", "大福", "最中", "羊羹", "煎餅", "饅頭",
"柚子", "梅干し", "味噌", "醤油", "わさび", "生姜", "山椒", "昆布",
"風鈴", "提灯", "扇子", "千鶴", "折鶴", "手毬", "万華鏡", "花火",
"雪うさぎ", "だるま", "こけし", "招き猫", "風車", "独楽", "竹とんぼ", "紙風船",
"朝露", "夕凪", "木漏れ日", "花吹雪", "月明かり", "星空", "天の川", "春風",
"小春日和", "花曇り", "薄紅", "若草", "深緑", "紺碧", "茜色", "藤色"
)
private fun getAdjectives(lang: Lang): List<String> = when (lang) {
Lang.JA -> jaAdjectives
else -> adjectives
}
private fun getNouns(lang: Lang): List<String> = when (lang) {
Lang.JA -> jaNouns
else -> nouns
}
private fun getParticle(lang: Lang): String = when (lang) {
Lang.JA -> ""
else -> ""
}
private fun generateRandomNickname(lang: Lang): String {
val adj = getAdjectives(lang)
val noun = getNouns(lang)
val particle = getParticle(lang)
val formatType = Random.nextInt(3) val formatType = Random.nextInt(3)
return when (formatType) { return when (formatType) {
0 -> "${adjectives.random()}${nouns.random()}" 0 -> "${adj.random()}${noun.random()}"
1 -> "${nouns.random()}${nouns.random()}" 1 -> "${noun.random()}${particle}${noun.random()}"
else -> "${adjectives.random()}${nouns.random()}${nouns.random()}" else -> "${adj.random()}${noun.random()}${particle}${noun.random()}"
} }
} }
private fun generateNonConflictingNickname(usedNicknames: Set<String>): String { private fun generateNonConflictingNickname(usedNicknames: Set<String>, lang: Lang): String {
val usedNicknameSet = HashSet(usedNicknames) // 해시셋으로 변환 (O(1) 조회 가능) val usedNicknameSet = HashSet(usedNicknames)
val availableNumbers = (1000..9999).shuffled() val availableNumbers = (1000..9999).shuffled()
val adj = getAdjectives(lang)
val noun = getNouns(lang)
for (num in availableNumbers) { // 숫자를 먼저 결정 (무작위) for (num in availableNumbers) {
for (adj in adjectives.shuffled()) { // 형용사 순서 랜덤화 for (a in adj.shuffled()) {
for (noun in nouns.shuffled()) { // 명사 순서 랜덤화 for (n in noun.shuffled()) {
val candidate = "$adj$noun$num" val candidate = "$a$n$num"
if (!usedNicknameSet.contains(candidate)) { if (!usedNicknameSet.contains(candidate)) {
return candidate return candidate
} }
@@ -70,13 +133,13 @@ class NicknameGenerateService(private val repository: MemberRepository) {
throw SodaException(messageKey = "member.signup.failed_retry") throw SodaException(messageKey = "member.signup.failed_retry")
} }
fun generateUniqueNickname(): String { fun generateUniqueNickname(lang: Lang = Lang.KO): String {
repeat(5) { repeat(5) {
val candidates = (1..10).map { generateRandomNickname() } val candidates = (1..10).map { generateRandomNickname(lang) }
val available = candidates.firstOrNull { !repository.existsByNickname(it) } val available = candidates.firstOrNull { !repository.existsByNickname(it) }
if (available != null) return available if (available != null) return available
} }
return generateNonConflictingNickname(repository.findNicknamesWithPrefix("").toSet()) return generateNonConflictingNickname(repository.findNicknamesWithPrefix("").toSet(), lang)
} }
} }