diff --git a/.changeset/grumpy-singers-love.md b/.changeset/grumpy-singers-love.md new file mode 100644 index 00000000..067c194e --- /dev/null +++ b/.changeset/grumpy-singers-love.md @@ -0,0 +1,5 @@ +--- +"es-hangul": patch +--- + +fix. 겹모음과 관련하여 일부 메소드에서 잘못된 동작을 수정합니다. diff --git a/src/_internal/hangul.spec.ts b/src/_internal/hangul.spec.ts index f09f88b2..150360f7 100644 --- a/src/_internal/hangul.spec.ts +++ b/src/_internal/hangul.spec.ts @@ -89,6 +89,14 @@ describe('binaryAssembleHangulCharacters', () => { expect(binaryAssembleHangulCharacters('고', 'ㅏ')).toEqual('과'); }); + it('초성과 중성(겹모음)이 합쳐진 문자와 자음을 조합', () => { + expect(binaryAssembleHangulCharacters('과', 'ㄱ')).toEqual('곽'); + }); + + it('초성과 중성(겹모음)과 종성이 합쳐진 문자와 자음을 조합하여 겹받침 만들기', () => { + expect(binaryAssembleHangulCharacters('완', 'ㅈ')).toEqual('왅'); + }); + it('모음만 있는 문자와 모음을 조합하여 겹모음 만들기', () => { expect(binaryAssembleHangulCharacters('ㅗ', 'ㅏ')).toEqual('ㅘ'); }); @@ -97,6 +105,10 @@ describe('binaryAssembleHangulCharacters', () => { expect(binaryAssembleHangulCharacters('톳', 'ㅡ')).toEqual('토스'); }); + it('초성과 종성(겹모음)과 종성이 합쳐진 문자의 연음 법칙', () => { + expect(binaryAssembleHangulCharacters('왅', 'ㅓ')).toEqual('완저'); + }); + it('초성과 중성과 종성(겹받침)이 합쳐진 문자의 연음 법칙', () => { expect(binaryAssembleHangulCharacters('닭', 'ㅏ')).toEqual('달가'); expect(binaryAssembleHangulCharacters('깎', 'ㅏ')).toEqual('까까'); diff --git a/src/_internal/hangul.ts b/src/_internal/hangul.ts index 1a746f5b..dcb5c757 100644 --- a/src/_internal/hangul.ts +++ b/src/_internal/hangul.ts @@ -118,6 +118,7 @@ export function binaryAssembleHangulCharacters(source: string, nextCharacter: st } const [restJamos, lastJamo] = excludeLastElement(sourceJamos); + const secondaryLastJamo = excludeLastElement(restJamos)[1]; const needLinking = canBeChosung(lastJamo) && canBeJungsung(nextCharacter); if (needLinking) { @@ -131,12 +132,18 @@ export function binaryAssembleHangulCharacters(source: string, nextCharacter: st return combineJungsung(`${lastJamo}${nextCharacter}`)(); } + if (canBeJungsung(`${secondaryLastJamo}${lastJamo}`) && canBeJongsung(nextCharacter)) { + return combineJungsung(`${secondaryLastJamo}${lastJamo}`)(nextCharacter); + } + if (canBeJungsung(lastJamo) && canBeJongsung(nextCharacter)) { return combineJungsung(lastJamo)(nextCharacter); } const fixVowel = combineJungsung; - const combineJongsung = fixVowel(restJamos[1]); + const combineJongsung = fixVowel( + canBeJungsung(`${restJamos[1]}${restJamos[2]}`) ? `${restJamos[1]}${restJamos[2]}` : restJamos[1] + ); const lastConsonant = lastJamo; diff --git a/src/removeLastHangulCharacter.spec.ts b/src/removeLastHangulCharacter.spec.ts index 0445bc43..49e74fb9 100644 --- a/src/removeLastHangulCharacter.spec.ts +++ b/src/removeLastHangulCharacter.spec.ts @@ -7,15 +7,29 @@ describe('removeLastHangulCharacter', () => { }); it('마지막 문자가 초성과 중성의 조합으로 끝날 경우 초성만 남긴다.', () => { expect(removeLastHangulCharacter('프론트엔드')).toBe('프론트엔ㄷ'); + expect(removeLastHangulCharacter('끓다')).toBe('끓ㄷ'); + expect(removeLastHangulCharacter('관사')).toBe('관ㅅ'); + expect(removeLastHangulCharacter('괴사')).toBe('괴ㅅ'); }); it('마지막 문자가 초성과 중성과 종성의 조합으로 끝날 경우 초성과 중성이 조합된 문자만 남긴다.', () => { expect(removeLastHangulCharacter('일요일')).toBe('일요이'); + expect(removeLastHangulCharacter('완전')).toBe('완저'); + expect(removeLastHangulCharacter('왅전')).toBe('왅저'); expect(removeLastHangulCharacter('깎')).toBe('까'); }); it('마지막 문자가 초성과 중성의 조합으로 끝나며, 중성 입력 시 국제 표준 한글 레이아웃 기준 단일키로 처리되지 않는 이중모음 (ㅗ/ㅜ/ㅡ 계 이중모음) 인 경우 초성과 중성의 시작 모음만 남긴다.', () => { expect(removeLastHangulCharacter('전화')).toBe('전호'); expect(removeLastHangulCharacter('예의')).toBe('예으'); - expect(removeLastHangulCharacter("신세계")).toBe('신세ㄱ'); // 'ㅖ'의 경우 단일키 처리가 가능한 이중모음이므로 모음이 남지 않는다. + expect(removeLastHangulCharacter('신세계')).toBe('신세ㄱ'); // 'ㅖ'의 경우 단일키 처리가 가능한 이중모음이므로 모음이 남지 않는다. + }); + it('마지막 문자가 초성과 중성과 종성의 조합으로 끝나며, 중성 입력 시 국제 표준 한글 레이아웃 기준 단일키로 처리되지 않는 이중모음 (ㅗ/ㅜ/ㅡ 계 이중모음) 인 경우 초성과 중성만 남긴다.', () => { + expect(removeLastHangulCharacter('수확')).toBe('수화'); + }); + it('마지막 문자가 초성과 중성과 종성의 조합으로 끝나며, 종성이 겹자음인 경우 초성과 중성과 종성의 시작 자음만 남긴다.', () => { + expect(removeLastHangulCharacter('끓')).toBe('끌'); + }); + it('마지막 문자가 초성과 중성과 종성의 조합으로 끝나며, 중성 입력 시 국제 표준 한글 레이아웃 기준 단일키로 처리되지 않는 이중모음 (ㅗ/ㅜ/ㅡ 계 이중모음)이고 종성이 겹자음인 경우 초성과 중성과 종성의 시작 자음만 남긴다.', () => { + expect(removeLastHangulCharacter('왅')).toBe('완'); }); it('빈 문자열일 경우 빈 문자열을 반환한다.', () => { expect(removeLastHangulCharacter('')).toBe(''); diff --git a/src/removeLastHangulCharacter.ts b/src/removeLastHangulCharacter.ts index 59838522..5ac9c780 100644 --- a/src/removeLastHangulCharacter.ts +++ b/src/removeLastHangulCharacter.ts @@ -1,6 +1,7 @@ import { combineHangulCharacter } from './combineHangulCharacter'; import { disassembleHangulToGroups } from './disassemble'; import { excludeLastElement } from './_internal'; +import { canBeJungsung } from './utils'; /** * @name removeLastHangulCharacter @@ -24,9 +25,25 @@ export function removeLastHangulCharacter(words: string) { if (lastCharacter == null) { return ''; } - const disassembleLastCharacter = disassembleHangulToGroups(lastCharacter); - const [[first, middle, last]] = excludeLastElement(disassembleLastCharacter[0]); - const result = middle != null ? combineHangulCharacter(first, middle, last) : first; + + const result = (() => { + const disassembleLastCharacter = disassembleHangulToGroups(lastCharacter); + const [lastCharacterWithoutLastAlphabet] = excludeLastElement(disassembleLastCharacter[0]); + if (lastCharacterWithoutLastAlphabet.length <= 3) { + const [first, middle, last] = lastCharacterWithoutLastAlphabet; + if (middle != null) { + return canBeJungsung(last) + ? combineHangulCharacter(first, `${middle}${last}`) + : combineHangulCharacter(first, middle, last); + } + + return first; + } else { + const [first, firstJungsung, secondJungsung, firstJongsung] = lastCharacterWithoutLastAlphabet; + + return combineHangulCharacter(first, `${firstJungsung}${secondJungsung}`, firstJongsung); + } + })(); return [words.substring(0, words.length - 1), result].join(''); } diff --git a/src/utils.spec.ts b/src/utils.spec.ts index 58dc04e4..3d08e2b7 100644 --- a/src/utils.spec.ts +++ b/src/utils.spec.ts @@ -54,17 +54,26 @@ describe('hasSingleBatchim', () => { expect(hasSingleBatchim('핫')).toBe(true); expect(hasSingleBatchim('양')).toBe(true); expect(hasSingleBatchim('신')).toBe(true); + expect(hasSingleBatchim('확')).toBe(true); }); describe('홑받침이 아니라고 판단되는 경우', () => { it('겹받침을 받으면 false를 반환한다.', () => { expect(hasSingleBatchim('값')).toBe(false); expect(hasSingleBatchim('읊')).toBe(false); + expect(hasSingleBatchim('웱')).toBe(false); }); it('받침이 없는 문자를 받으면 false를 반환한다.', () => { expect(hasSingleBatchim('토')).toBe(false); expect(hasSingleBatchim('서')).toBe(false); + expect(hasSingleBatchim('와')).toBe(false); + }); + + it('한글 외의 문자를 입력하면 false를 반환한다.', () => { + expect(hasSingleBatchim('cat')).toBe(false); + expect(hasSingleBatchim('')).toBe(false); + expect(hasSingleBatchim('?')).toBe(false); }); it('한글 외의 문자를 입력하면 false를 반환한다.', () => { diff --git a/src/utils.ts b/src/utils.ts index eb336371..e17b58c6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -96,7 +96,8 @@ export function hasSingleBatchim(str: string) { * getChosung('띄어 쓰기') // 'ㄸㅇ ㅆㄱ' */ export function getChosung(word: string) { - return word.normalize('NFD') + return word + .normalize('NFD') .replace(EXTRACT_CHOSEONG_REGEX, '') // NFD ㄱ-ㅎ, NFC ㄱ-ㅎ 외 문자 삭제 .replace(CHOOSE_NFD_CHOSEONG_REGEX, $0 => HANGUL_CHARACTERS_BY_FIRST_INDEX[$0.charCodeAt(0) - 0x1100]); // NFD to NFC }