/** Nickname rules, docs/05-accounts-auth.md §2. Shared by server validation and client form. */ export const NICK_MIN = 2; export const NICK_MAX = 12; const ALLOWED = /^[0-9A-Za-z가-힣ㄱ-ㅎㅏ-ㅣ _.\-]+$/; // Minimal built-in list; the server may extend it. Matching ignores spaces and punctuation. const BANNED = ['시발', '씨발', '병신', '좆', '개새끼', '느금', '니애미', 'fuck', 'shit', 'bitch']; export function normalizeNickname(raw: string): string { return raw.normalize('NFC').trim().replace(/\s+/g, ' '); } export function containsProfanity(text: string): boolean { const squashed = text.normalize('NFC').toLowerCase().replace(/[\s_.\-*0-9]/g, ''); return BANNED.some((w) => squashed.includes(w)); } export function maskProfanity(text: string): string { let out = text; for (const w of BANNED) { out = out.replace(new RegExp(w.split('').map(escapeRe).join('[\\s_.\\-*]*'), 'gi'), (m) => '*'.repeat(m.length)); } return out; } function escapeRe(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** Returns a Korean error message, or null when valid. Input must already be normalized. */ export function nicknameError(nick: string): string | null { const len = [...nick].length; if (len < NICK_MIN) return `닉네임은 ${NICK_MIN}글자 이상이어야 해요.`; if (len > NICK_MAX) return `닉네임은 ${NICK_MAX}글자까지 쓸 수 있어요.`; if (!ALLOWED.test(nick)) return '한글, 영어, 숫자와 _ - . 만 쓸 수 있어요.'; if (containsProfanity(nick)) return '다른 닉네임을 써 주세요.'; return null; } const ADJ = ['용감한', '느긋한', '반짝이는', '씩씩한', '졸린', '신나는', '똑똑한', '배고픈', '귀여운', '날쌘', '든든한', '행복한']; const NOUN = ['호랑이', '고양이', '거북이', '다람쥐', '부엉이', '펭귄', '판다', '토끼', '여우', '돌고래', '곰돌이', '참새']; export function suggestNickname(rand: () => number = Math.random): string { const a = ADJ[Math.floor(rand() * ADJ.length)]!; const n = NOUN[Math.floor(rand() * NOUN.length)]!; return `${a} ${n}`; }