0. 개요
한글판 워들 게임인 "한글 워들(Hangul Wordle)"을 개발하려고 한다.
"한글 워들"에서 플레이어의 입력 데이터를 분석하고 정답 판정을 위해 아래 2가지 로직이 필수적으로 필요하다.
1. 한글 자모음을 문자열을 입력하면 완전한 단어로 조립하는 로직
2. 단어를 입력하면 한글 자모음으로 분해하는 로직
위 2가지 로직을 구현해 보려고 한다.
한글 자모음의 유니코드를 이용하는 방식을 이용한다.
1. 한글 자음 / 모음 유니코드 분석
* "가" 유니코드
console.log("가".charCodeAt(0));
* 자음 유니코드 (ㄱ ~ ㅎ : 12593 ~ 12622)
console.log("ㄱ: " + "ㄱ".charCodeAt(0));
console.log("ㄴ: " + "ㄴ".charCodeAt(0));
console.log("ㄷ: " + "ㄷ".charCodeAt(0));
console.log("ㄹ: " + "ㄹ".charCodeAt(0));
"ㄱ" + 3 = "ㄴ"
"ㄴ" + 3 = "ㄷ"
"ㄷ" + 2 = "ㄹ"
값이 연속되지 않고 띄엄띄엄 위치한 것을 확인 할 수 있다.
let uni = 12593; // "ㄱㄱ"
while (uni) {
let hangul = String.fromCharCode(uni);
console.log("hangul : " + hangul + " - uni : " + uni);
if (hangul === "ㅎ") break;
uni++;
}
겹자음까지 포함하면 유니코드가 이어지는 것을 확인할 수 있다.
한글의 문자는 자음과 모음을 조합해 만든다.
이는 "초성" , "중성" , "종성" 으로 구분할 수 있다.
"초성" 과 "종성"은 자음이 오고, "중성"은 모음이 온다.
여기서 "초성"과 "종성"에 서로 올 수 있는 자음의 종류가 다르다.
"초성" , "중성" , "종성"에 위치 할 수 있는 자모음은 다음과 같다.
* 모음 유니코드 (ㅏ ~ ㅣ : 12623 ~ 12643)
console.log("ㅏ: " + "ㅏ".charCodeAt(0));
let uni = 12623; // "ㅏ"
while (uni) {
let hangul = String.fromCharCode(uni);
console.log(hangul + " : " + uni);
if (hangul === "ㅣ") break;
uni++;
}
* 유효한 '초성' , '중성' , '종성'
[국립 국어원 - '한글의 구성']
https://www.korean.go.kr/hangeul/principle/001.html
:::::::: 알고 싶은 한글 ::::::::
www.korean.go.kr
* 전역 변수 설정 ('초성' , '중성' , '종성' 리스트 + '겹모음' , '겹자음' JS Object)
// 초성 (19개)
const f = ['ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ',
'ㅂ', 'ㅃ', 'ㅅ', 'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ',
'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
// 중성 (21개)
const s = ['ㅏ', 'ㅐ', 'ㅑ', 'ㅒ', 'ㅓ', 'ㅔ', 'ㅕ',
'ㅖ', 'ㅗ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅛ', 'ㅜ',
'ㅝ', 'ㅞ', 'ㅟ', 'ㅠ', 'ㅡ', 'ㅢ', 'ㅣ'];
// 종성 (28개, 첫 번째 요소는 받침 없음)
const t = ['', 'ㄱ', 'ㄲ', 'ㄳ', 'ㄴ', 'ㄵ', 'ㄶ',
'ㄷ', 'ㄹ', 'ㄺ', 'ㄻ', 'ㄼ', 'ㄽ', 'ㄾ',
'ㄿ', 'ㅀ', 'ㅁ', 'ㅂ', 'ㅄ', 'ㅅ', 'ㅆ',
'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'];
// 겹모음 분해 (7종류)
const complexVowels = {
'ㅘ': ['ㅗ', 'ㅏ'], 'ㅙ': ['ㅗ', 'ㅐ'], 'ㅚ': ['ㅗ', 'ㅣ'],
'ㅝ': ['ㅜ', 'ㅓ'], 'ㅞ': ['ㅜ', 'ㅔ'], 'ㅟ': ['ㅜ', 'ㅣ'],
'ㅢ': ['ㅡ', 'ㅣ']
};
// 겹모음 조합 (7종류)
const complexVowelsReverse = {
'ㅗㅏ': 'ㅘ', 'ㅗㅐ': 'ㅙ', 'ㅗㅣ': 'ㅚ',
'ㅜㅓ': 'ㅝ', 'ㅜㅔ': 'ㅞ', 'ㅜㅣ': 'ㅟ',
'ㅡㅣ': 'ㅢ'
};
// 겹자음 분해 (11종류)
const complexConsonants = {
'ㄳ': ['ㄱ', 'ㅅ'], 'ㄵ': ['ㄴ', 'ㅈ'], 'ㄶ': ['ㄴ', 'ㅎ'],
'ㄺ': ['ㄹ', 'ㄱ'], 'ㄻ': ['ㄹ', 'ㅁ'], 'ㄼ': ['ㄹ', 'ㅂ'],
'ㄽ': ['ㄹ', 'ㅅ'], 'ㄾ': ['ㄹ', 'ㅌ'], 'ㄿ': ['ㄹ', 'ㅍ'],
'ㅀ': ['ㄹ', 'ㅎ'], 'ㅄ': ['ㅂ', 'ㅅ']
};
// 겹자음 조합 (11종류)
const complexConsonantsReverse = {
'ㄱㅅ': 'ㄳ', 'ㄴㅈ': 'ㄵ', 'ㄴㅎ': 'ㄶ',
'ㄹㄱ': 'ㄺ', 'ㄹㅁ': 'ㄻ', 'ㄹㅂ': 'ㄼ',
'ㄹㅅ': 'ㄽ', 'ㄹㅌ': 'ㄾ', 'ㄹㅍ': 'ㄿ',
'ㄹㅎ': 'ㅀ', 'ㅂㅅ': 'ㅄ'
};
2. 한글 단어 분해 메서드
- 한글 단어를 자음 / 모음으로 분해하는 메서드를 구성할 것이다.
- 예를 들어, "희망"은 "ㅎㅡㅣㅁㅏㅇ"으로 분해할 수 있다.
- 겹자음, 겹모음도 홑자음, 홑모음으로 분해할 것이다.
//분해 메서드
function decomposeHangul(hangulWord) {
let hangulStr = '';
const ga = 44032; //'가'
for (let i = 0; i < hangulWord.length; i++) {
let uni = hangulWord[i].charCodeAt(0);
uni = uni - ga;
let fn = parseInt(uni / 588); //초성 인덱스
let sn = parseInt((uni - (fn * 588)) / 28); //중성 인덱스
let tn = parseInt(uni % 28); //종성 인덱스
// 초성 (겹자음 없음)
hangulStr += f[fn];
// 중성
const vowel = s[sn];
if (complexVowels[vowel]) {
hangulStr += complexVowels[vowel][0];
hangulStr += complexVowels[vowel][1];
} else {
hangulStr += vowel;
}
// 종성
if (tn !== 0) {
const final = t[tn];
if (complexConsonants[final]) {
hangulStr += complexConsonants[final][0];
hangulStr += complexConsonants[final][1];
} else {
hangulStr += final;
}
}
}
return hangulStr;
}
// ✅ 테스트
var hangulWord = "희망";
var hangulStr = decomposeHangul(hangulWord);
console.log("자모 분해:", hangulStr); // "ㅎㅡㅣㅁㅏㅇ"
hangulWord = "뺨아리";
hangulStr = decomposeHangul(hangulWord);
console.log("자모 분해:", hangulStr);
hangulWord = "넓쭉하다";
hangulStr = decomposeHangul(hangulWord);
console.log("자모 분해:", hangulStr);
3. 한글 문자 조합 메서드
- 한글 문자(자음 / 모음)을 한글 단어로 조합하는 메서드를 구성할 것이다.
- 예를 들어, "ㅎㅡㅣㅁㅏㅇ"은 "희망"으로 조립할 수 있다.
- 연속된 홑모음은 겹모음으로, 연속된 겹자음은 선행문자의 받침인가 혹은 후행문자의 초성인가를 분석하여 조립해야 한다.
※ 전제 조건
1. 남는 자/모음없이 완전한 단어로 구성된다.
2. 초성은 겹자음이 올 수 없다. (쌍자음은 겹자음이 아닌 홑자음이다.)
경우의 수를 나눠서 메서드를 구성하였다.
//조합 메서드
function composeHangul(letters) {
if (letters.length === 0) return "";
let first = null, second = null, third = null;
var result = "";
for (let i = 0; i < letters.length; i++) {
var letter = letters[i];
//자음
if (f.includes(letter)) {
if (first === null) {
first = f.indexOf(letter);
}
else if (second !== null && third === null) {
/**다음문자 존재 */
if (i + 1 < letters.length) {
var nextLetter = letters[i + 1];
if (f.includes(nextLetter)) third = t.indexOf(letter);
else if (s.includes(nextLetter)) {
result += String.fromCharCode(0xAC00 + (first * 588) + (second * 28) + (third ?? 0));
first = f.indexOf(letter);
second = null;
third = null;
}
} else { /**letter : 마지막 문자 */
third = t.indexOf(letter);
}
}
else if (second !== null && third !== null) {
/**다음문자 존재 */
if (i + 1 < letters.length) {
var nextLetter = letters[i + 1];
if (f.includes(nextLetter)) {
var complexConsonant = complexConsonantsReverse[t[third] + letter];
third = t.indexOf(complexConsonant);
result += String.fromCharCode(0xAC00 + (first * 588) + (second * 28) + (third ?? 0));
first = null;
second = null;
third = null;
}
else if (s.includes(nextLetter)) {
result += String.fromCharCode(0xAC00 + (first * 588) + (second * 28) + (third ?? 0));
first = f.indexOf(letter);
second = null;
third = null;
}
} else { /**letter : 마지막 문자 */
var complexConsonant = complexConsonantsReverse[t[third] + letter];
third = t.indexOf(complexConsonant);
result += String.fromCharCode(0xAC00 + (first * 588) + (second * 28) + (third ?? 0));
first = null;
second = null;
third = null;
}
}
/**
* 1. first !== null && second === null && third === null
* 2. first !== null && second === null && third !== null
*/
else {
console.log("잘못된 입력 입니다.");
return "wrong!";
}
}
//모음
if (s.includes(letter)) {
if (second === null) {
second = s.indexOf(letter);
} else {
if (complexVowelsReverse[s[second] + letter]) {
var complexVowel = complexVowelsReverse[s[second] + letter];
second = s.indexOf(complexVowel);
} else {
console.log("겹모음이 될 수 없습니다! : 잘못된 입력입니다.");
return "wrong!";
}
}
}
}
/**마지막 문자 조합하기 */
result += String.fromCharCode(0xAC00 + (first * 588) + (second * 28) + (third ?? 0));
first = null;
second = null;
third = null;
// "ꯤ : 잘못된 입력입니다."
if (String.fromCharCode(44004) === result) {
console.log("ꯤ : 잘못된 입력입니다.");
return "wrong!";
}
return result;
}