Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/light-trainers-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"es-hangul": patch
---

fix: '자음군 단순화' 음운규칙을 적용하여 standardizePronunciation 오류 해결
3 changes: 1 addition & 2 deletions benchmarks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,5 @@
},
"devDependencies": {
"@types/josa": "^3"
},
"version": null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,22 @@ console.log(standardizePronunciation('곧이듣다', { hardConversion: false }))
```

</Sandpack>

## Notice on Exception Cases

According to the [Regulations on Korean Orthography](https://korean.go.kr/kornorms/regltn/regltnView.do?regltn_code=0002&regltn_no=346#a422), Chapter 7 "Addition of Sounds" - Article 29 states:

"In compound words and derivatives, when the final sound of the first word or prefix is a consonant and the first syllable of the following word or suffix begins with '이, 야, 여, 요, 유' (i, ya, yeo, yo, yu), the sound 'ㄴ' (n) is added, making it '니, 냐, 녀, 뇨, 뉴' (ni, nya, nyeo, nyo, nyu)."

For example:

맨입 (maen-ip) is pronounced as 맨닙 (maen-nip)

꽃잎 (kkot-ip) is pronounced as 꼰닙 (kkon-nip)

However, in the case of 전역 (jeon-yeok), according to this rule, it should be pronounced as 전녁 (jeon-nyeok).
But since 전역 is not a compound or derivative word, it is exceptionally pronounced as 저녁 (jeo-nyeok).

Since it is impossible to accurately determine whether a word is compound or derivative using only pure TypeScript logic without external linguistic data, these exceptions are managed in a separate constant list.

Therefore, if you come across any additional words that require such exception handling, we highly encourage your contributions and feedback.
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,21 @@ console.log(standardizePronunciation('곧이듣다', { hardConversion: false }))
```

</Sandpack>

## 예외사항 안내

[한국어 어문 규범](https://korean.go.kr/kornorms/regltn/regltnView.do?regltn_code=0002&regltn_no=346#a422) 제7장 음의 첨가 - 제29항에서는

> "합성어 및 파생어에서 앞 단어나 접두사의 끝이 자음이고 뒤 단어나 접미사의 첫음절이 '이, 야, 여, 요, 유'인 경우에는, 'ㄴ'음을 첨가하여 니, 냐, 녀, 뇨, 뉴로 발음한다"
> 라고 규정하고 있습니다.

예를 들면,

- 맨입 - 맨닙
- 꽃잎 - 꼰닙

처럼 발음이 변합니다.

하지만 **전역**의 경우, 이 규칙에 따르면 **전녁**이 되어야 할 것 같지만, **전역**은 합성어나 파생어가 아니기 때문에 예외적으로 **저녁**으로 발음하는 것이 맞습니다.

외부의 도움 없이 순수 타입스크립트 로직만으로 주어진 단어가 합성어/파생어인지 단일어인지 정확히 판별할 수 없으므로, 이러한 예외적인 단어들은 별도의 상수 목록으로 관리하고 있습니다. 이에 따라 예외 처리가 필요한 단일어가 추가로 있다면 적극적인 제보와 기여를 부탁드립니다.
32 changes: 32 additions & 0 deletions src/pronunciation/standardizePronunciation/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,35 @@ export const 된소리_받침 = [

// 24항, 25항
export const 어간_받침 = ['ㄴ', 'ㄴㅈ', 'ㅁ', 'ㄹㅁ', 'ㄹㅂ', 'ㄹㅌ'] as const;

/**
* @description 'ㄹㅁ'과 같은 복합 받침은 발음 과정에서 단순화될 수 있다.
* @see {@link https://encykorea.aks.ac.kr/Article/E0074523 자음군 단순화 설명 링크}
*/
export const 자음군_단순화 = [
'ㄹㅁ',
'ㄱㅅ',
'ㄹㄱ',
'ㄹㅂ',
'ㄹㅅ',
'ㅂㅅ',
'ㄴㅈ',
'ㄴㅎ',
'ㄹㅌ',
'ㄹㅍ',
'ㄹㅎ',
] as const;

export const 자음군_단순화_결과 = {
ㄱㅅ: 'ㄱ',
ㄴㅈ: 'ㄴ',
ㄴㅎ: 'ㄴ',
ㄹㄱ: 'ㄱ',
ㄹㅁ: 'ㅁ',
ㄹㅂ: 'ㄹ', // 상황에 따라 'ㅂ'이 남기도 하지만, 기본은 'ㄹ'로 정리
ㄹㅅ: 'ㄹ',
ㄹㅌ: 'ㄹ',
ㄹㅍ: 'ㄹ',
ㄹㅎ: 'ㄹ',
ㅂㅅ: 'ㅂ',
} as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const 단일어_예외사항_단어모음: Record<string, string> = {
전역: '저녁',
};
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,30 @@ describe('transformHardConversion', () => {
},
});
});

it('예외사항 - 현재 음절이 경음화를 적용할 수 있는 받침에 해당하지만, 다음 음절의 받침이 "자음군 단순화" 음운현상이 생긴다면 된소리로 발음하지 않는다', () => {
const current = defined(disassembleCompleteCharacter('힘'));
const next = defined(disassembleCompleteCharacter('듦'));

expect(transformHardConversion(current, next)).toEqual({
next: {
choseong: 'ㄷ',
jungseong: 'ㅡ',
jongseong: 'ㄹㅁ',
},
});
});

it('번외 - 현재 음절의 받침이 "자음군 단순화" 대상에 해당하지만, 다음 음절의 초성이 "음가가 없는 자음"일 경우에는 된소리로 발음하지 않는다', () => {
const current = defined(disassembleCompleteCharacter('삶'));
const next = defined(disassembleCompleteCharacter('은'));

expect(transformHardConversion(current, next)).toEqual({
next: {
choseong: 'ㅇ',
jungseong: 'ㅡ',
jongseong: 'ㄴ',
},
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { arrayIncludes, hasProperty } from '@/_internal';
import { 된소리, 된소리_받침, 어간_받침 } from '../constants';
import { 된소리, 된소리_받침, 어간_받침, 자음군_단순화 } from '../constants';
import type { ReturnSyllables, Syllable } from './rules.types';

/**
Expand All @@ -17,6 +17,15 @@ export function transformHardConversion(
const next = { ...nextSyllable };

if (hasProperty(된소리, next.choseong)) {
// [예외 처리]
// 다음 글자의 종성(받침)이 자음군 단순화 대상에 해당할 경우,
// 자음군이 단순화되면서 남은 받침이 비음화되거나 연음되어,
// 일반적인 된소리 현상이 발생하지 않는다.
// 따라서 이 경우에는 된소리 변화를 적용하지 않고 그대로 반환한다.
if (arrayIncludes(자음군_단순화, next.jongseong)) {
return { next };
}

const 제23항조건 = arrayIncludes(된소리_받침, currentSyllable.jongseong);
const 제24_25항조건 = arrayIncludes(어간_받침, currentSyllable.jongseong) && next.choseong !== 'ㅂ';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('transformNLAssimilation', () => {
});
});

it('ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다', () => {
it('예외사항 - ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다', () => {
const current = defined(disassembleCompleteCharacter('양'));
const next = defined(disassembleCompleteCharacter('이'));

Expand All @@ -74,4 +74,22 @@ describe('transformNLAssimilation', () => {
},
});
});

it('예외사항 - ㄴ/ㄹ이 되기 위한 조건이면서 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우지만, 현재 종성이 "자음군 단순화"의 대상이라면 연음규칙이 적용되지 않고 둘 중 하나의 자음만 남고 나머지 자음은 탈락한다', () => {
const current = defined(disassembleCompleteCharacter('듦'));
const next = defined(disassembleCompleteCharacter('이'));

expect(transformNLAssimilation(current, next)).toEqual({
current: {
choseong: 'ㄷ',
jungseong: 'ㅡ',
jongseong: 'ㅁ',
},
next: {
choseong: 'ㅇ',
jungseong: 'ㅣ',
jongseong: '',
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import {
ㄴㄹ이_덧나서_받침_ㄴ_변환,
ㄴㄹ이_덧나서_받침_ㄹ_변환,
음가가_없는_자음,
자음군_단순화,
자음군_단순화_결과,
} from '../constants';
import type { ReturnSyllables, Syllable } from './rules.types';

/**
* 'ㄴ,ㄹ'이 덧나는 경우(동화작용)를 적용합니다.
* @description 합성어에서 둘째 요소가 '야, 여, 요, 유, 얘, 예' 등으로 시작되는 말이면 'ㄴ, ㄹ'이 덧난다
* @description 합성어에서 둘째 요소가 '야, 여, 요, 유, 이, 얘, 예' 등으로 시작되는 말이면 'ㄴ, ㄹ'이 덧난다
* @link https://www.youtube.com/watch?v=Mm2JX2naqWk
* @link http://contents2.kocw.or.kr/KOCW/data/document/2020/seowon/choiyungon0805/12.pdf
* @param currentSyllable 현재 음절을 입력합니다.
Expand All @@ -23,7 +25,11 @@ export function transformNLAssimilation(currentSyllable: Syllable, nextSyllable:
const ㄴㄹ이덧나는조건 =
current.jongseong && next.choseong === 'ㅇ' && arrayIncludes(ㄴㄹ이_덧나는_후속음절_모음, next.jungseong);

const is이 = next.choseong === 음가가_없는_자음 && next.jungseong === 'ㅣ' && !next.jongseong;
const is이 =
next.choseong === 음가가_없는_자음 &&
next.jungseong === 'ㅣ' &&
!next.jongseong &&
!arrayIncludes(자음군_단순화, current.jongseong);

if (!ㄴㄹ이덧나는조건 || is이) {
return {
Expand Down Expand Up @@ -54,8 +60,13 @@ function applyㄴㄹ덧남(current: Syllable, next: Syllable): ReturnSyllables {
updatedNext.choseong = 'ㄹ';
}
} else {
// ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다
updatedNext.choseong = updatedCurrent.jongseong as typeof updatedNext.choseong;
if (arrayIncludes(자음군_단순화, updatedCurrent.jongseong)) {
// ㄴ/ㄹ이 되기 위한 조건이면서 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우지만, 현재 종성이 "자음군 단순화"의 대상이라면 연음규칙이 적용되지 않고 둘 중 하나의 자음만 남고 나머지 자음은 탈락한다
updatedCurrent.jongseong = 자음군_단순화_결과[updatedCurrent.jongseong];
} else {
// ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다
updatedNext.choseong = updatedCurrent.jongseong as typeof updatedNext.choseong;
}
}

return { current: updatedCurrent, next: updatedNext };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ describe('standardizePronunciation', () => {
expect(standardizePronunciation('고양이')).toBe('고양이');
expect(standardizePronunciation('윤여정')).toBe('윤녀정');
});

it('ㄴ/ㄹ이 되기 위한 조건이면서 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우지만, 현재 종성이 "자음군 단순화"의 대상이라면 연음규칙이 적용되지 않고 둘 중 하나의 자음만 남고 나머지 자음은 탈락한다', () => {
expect(standardizePronunciation('힘듦이 있다')).toBe('힘드미 읻따');
});
});

describe('19항', () => {
Expand Down Expand Up @@ -358,4 +362,10 @@ describe('standardizePronunciation', () => {
).toBe('널게');
});
});

describe('예외사항은 정의된 단어 모음에서 반환한다', () => {
it('파생어/합성어 예외사항 단어는 단어모음에서 찾아 반환한다', () => {
expect(standardizePronunciation('전역')).toBe('저녁');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isNotUndefined, joinString } from '@/_internal';
import { isHangulAlphabet, isHangulCharacter } from '@/_internal/hangul';
import { combineCharacter } from '@/core/combineCharacter';
import { disassembleCompleteCharacter } from '@/core/disassembleCompleteCharacter';
import { 단일어_예외사항_단어모음 } from './exceptionWords.constants';
import {
transform12th,
transform13And14th,
Expand Down Expand Up @@ -38,6 +39,10 @@ export function standardizePronunciation(hangul: string, options: Options = { ha
return '';
}

if (hangul in 단일어_예외사항_단어모음) {
return 단일어_예외사항_단어모음[hangul];
}

const processSyllables = (syllables: Syllable[], phrase: string, options: Options) =>
syllables.map((currentSyllable, index, array) => {
const nextSyllable = index < array.length - 1 ? array[index + 1] : null;
Expand Down Expand Up @@ -125,6 +130,7 @@ function applyRules(params: ApplyParameters): {
({ current, next } = transform17th(current, next));
({ next } = transform19th(current, next));
({ current, next } = transformNLAssimilation(current, next));

({ current } = transform18th(current, next));
({ current, next } = transform20th(current, next));
}
Expand Down