/** * 한글 이름을 영문(로마자)으로 변환하는 유틸리티 * * 사용 예시: * hangulToRoman('홍길동') => 'hong.gildong' * hangulToRoman('김철수') => 'kim.cheolsu' */ // 한글 로마자 변환 매핑 (국립국어원 표기법 기준) const CHOSUNG_MAP = { 'ㄱ': 'g', 'ㄲ': 'kk', 'ㄴ': 'n', 'ㄷ': 'd', 'ㄸ': 'tt', 'ㄹ': 'r', 'ㅁ': 'm', 'ㅂ': 'b', 'ㅃ': 'pp', 'ㅅ': 's', 'ㅆ': 'ss', 'ㅇ': '', 'ㅈ': 'j', 'ㅉ': 'jj', 'ㅊ': 'ch', 'ㅋ': 'k', 'ㅌ': 't', 'ㅍ': 'p', 'ㅎ': 'h' }; const JUNGSUNG_MAP = { 'ㅏ': 'a', 'ㅐ': 'ae', 'ㅑ': 'ya', 'ㅒ': 'yae', 'ㅓ': 'eo', 'ㅔ': 'e', 'ㅕ': 'yeo', 'ㅖ': 'ye', 'ㅗ': 'o', 'ㅘ': 'wa', 'ㅙ': 'wae', 'ㅚ': 'oe', 'ㅛ': 'yo', 'ㅜ': 'u', 'ㅝ': 'wo', 'ㅞ': 'we', 'ㅟ': 'wi', 'ㅠ': 'yu', 'ㅡ': 'eu', 'ㅢ': 'ui', 'ㅣ': 'i' }; const JONGSUNG_MAP = { '': '', 'ㄱ': 'k', 'ㄲ': 'k', 'ㄳ': 'k', 'ㄴ': 'n', 'ㄵ': 'n', 'ㄶ': 'n', 'ㄷ': 't', 'ㄹ': 'l', 'ㄺ': 'k', 'ㄻ': 'm', 'ㄼ': 'p', 'ㄽ': 'l', 'ㄾ': 'l', 'ㄿ': 'p', 'ㅀ': 'l', 'ㅁ': 'm', 'ㅂ': 'p', 'ㅄ': 'p', 'ㅅ': 't', 'ㅆ': 't', 'ㅇ': 'ng', 'ㅈ': 't', 'ㅊ': 't', 'ㅋ': 'k', 'ㅌ': 't', 'ㅍ': 'p', 'ㅎ': 't' }; const CHOSUNG = [ 'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' ]; const JUNGSUNG = [ 'ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ', 'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ' ]; const JONGSUNG = [ '', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ', 'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ', 'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ' ]; /** * 한글 한 글자를 초성, 중성, 종성으로 분리 * @param {string} char - 한글 한 글자 * @returns {object} { cho, jung, jong } */ function decomposeHangul(char) { const code = char.charCodeAt(0) - 0xAC00; if (code < 0 || code > 11171) { return null; // 한글이 아님 } const choIndex = Math.floor(code / 588); const jungIndex = Math.floor((code % 588) / 28); const jongIndex = code % 28; return { cho: CHOSUNG[choIndex], jung: JUNGSUNG[jungIndex], jong: JONGSUNG[jongIndex] }; } /** * 한글 이름을 로마자로 변환 * @param {string} koreanName - 한글 이름 * @returns {string} 로마자 이름 (예: 'hong.gildong') */ function hangulToRoman(koreanName) { if (!koreanName || typeof koreanName !== 'string') { return ''; } // 공백 제거 const trimmed = koreanName.trim(); // 성과 이름 분리 (첫 글자를 성으로 간주) const surname = trimmed[0]; const givenName = trimmed.substring(1); // 각 부분을 로마자로 변환 const romanSurname = convertToRoman(surname); const romanGivenName = convertToRoman(givenName); // 점(.)으로 연결 return `${romanSurname}.${romanGivenName}`.toLowerCase(); } /** * 한글 문자열을 로마자로 변환 * @param {string} text - 한글 문자열 * @returns {string} 로마자 문자열 */ function convertToRoman(text) { let result = ''; for (let i = 0; i < text.length; i++) { const char = text[i]; const decomposed = decomposeHangul(char); if (decomposed) { result += CHOSUNG_MAP[decomposed.cho] || ''; result += JUNGSUNG_MAP[decomposed.jung] || ''; result += JONGSUNG_MAP[decomposed.jong] || ''; } else { // 한글이 아닌 경우 그대로 추가 result += char; } } return result; } /** * 사용자명 생성 (중복 확인 및 처리) * @param {string} koreanName - 한글 이름 * @param {object} db - Database connection (mysql2 pool or knex) * @returns {Promise} 고유한 username */ async function generateUniqueUsername(koreanName, db) { const baseUsername = hangulToRoman(koreanName); let username = baseUsername; let counter = 1; // 중복 확인 while (true) { let existing; // mysql2 pool 또는 knex 모두 지원 if (typeof db === 'function') { // Knex existing = await db('users') .where('username', username) .first(); } else { // mysql2 pool const [rows] = await db.query('SELECT username FROM users WHERE username = ?', [username]); existing = rows[0]; } if (!existing) { break; // 중복 없음 } // 중복 시 숫자 추가 username = `${baseUsername}${counter}`; counter++; } return username; } module.exports = { hangulToRoman, convertToRoman, generateUniqueUsername, decomposeHangul };