generated from ttipo/project-base
feat(flutter): 조사 도우미가 호환 자모 · 숫자의 받침도 고른다
웹 조사 도우미(frontend shared/utils/korean.ts)와 짝으로 맞춘다. 초성 검색어(‘ㄱㅁ’)나 숫자로 끝나는 말이 "(으)로" · "이(가)" 꼴로 나오지 않게, 호환 자모 자음은 받침 있음 (ㄹ 은 "로"), 모음은 받침 없음, 숫자는 읽는 소리(영 · 일 · 이 …)로 고른다. 라틴 글자 · 기호로 끝나면 지금처럼 괄호 꼴이다. 알려진 한계: 라틴 글자 뒤의 숫자도 한국어 소리로 센다(‘녹음.mp3’은). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,39 +39,93 @@ const int _noJongseong = 0;
|
||||
/// ㄹ 받침 — 방향 조사가 "로"다(서울로 · 메일로)
|
||||
const int _jongseongRieul = 8;
|
||||
|
||||
/// 마지막 글자의 받침 번호 — 한글 음절로 끝나지 않으면(영문 · 숫자 · 빈 글) null
|
||||
int? _lastJongseong(String word) {
|
||||
/// 한글 호환 자모의 자음 ㄱ~ㅎ(겹자음 포함) · 모음 ㅏ~ㅣ (U+3131~U+3163)
|
||||
const int _jamoConsonantFirst = 0x3131;
|
||||
const int _jamoConsonantLast = 0x314E;
|
||||
const int _jamoVowelFirst = 0x314F;
|
||||
const int _jamoVowelLast = 0x3163;
|
||||
|
||||
/// 자음 ㄹ(리을) — 자음 가운데 방향 조사가 "로"인 것
|
||||
const int _jamoRieul = 0x3139;
|
||||
|
||||
/// 숫자 0~9 (ASCII)
|
||||
const int _digitZero = 0x30;
|
||||
const int _digitNine = 0x39;
|
||||
|
||||
/// 끝소리 — 조사를 고르는 데 쓴다
|
||||
enum _Coda {
|
||||
/// 받침 없음 (프로젝트 · ㅏ · 2)
|
||||
none,
|
||||
|
||||
/// ㄹ 받침 — 방향 조사가 "로"다 (메일 · ㄹ · 1)
|
||||
rieul,
|
||||
|
||||
/// 그 밖의 받침 (휴지통 · ㄱ · 0)
|
||||
other,
|
||||
}
|
||||
|
||||
/// 숫자 0~9 의 끝소리 — 읽는 소리 영 · 일 · 이 · 삼 · 사 · 오 · 육 · 칠 · 팔 · 구
|
||||
const List<_Coda> _digitCodas = <_Coda>[
|
||||
_Coda.other, // 영
|
||||
_Coda.rieul, // 일
|
||||
_Coda.none, // 이
|
||||
_Coda.other, // 삼
|
||||
_Coda.none, // 사
|
||||
_Coda.none, // 오
|
||||
_Coda.other, // 육
|
||||
_Coda.rieul, // 칠
|
||||
_Coda.rieul, // 팔
|
||||
_Coda.none, // 구
|
||||
];
|
||||
|
||||
/// 마지막 글자의 끝소리 — 앞뒤 공백은 떼고 본다(따옴표 · 괄호는 떼지 않는다). 소리를 알 수 없으면(라틴 글자 · 기호 · 빈 글) null
|
||||
/// - 한글 음절: 그 받침
|
||||
/// - 한글 호환 자모: 자음은 받침 있음(이름 기역 · 니은 · 리을 … 이 모두 받침으로 끝난다 — ㄹ 은 [_Coda.rieul]), 모음은 받침 없음
|
||||
/// - 숫자: 읽는 소리로 ([_digitCodas])
|
||||
/// (웹 `korean.ts` 와 같은 규칙 — 2026-11-07 자모 · 숫자를 함께 넓혔다)
|
||||
_Coda? _lastCoda(String word) {
|
||||
final String trimmed = word.trim();
|
||||
if (trimmed.isEmpty) return null;
|
||||
final int last = trimmed.runes.last;
|
||||
if (!_isHangulSyllable(last)) return null;
|
||||
return (last - _hangulFirst) % _jongseongCount;
|
||||
if (_isHangulSyllable(last)) {
|
||||
final int jongseong = (last - _hangulFirst) % _jongseongCount;
|
||||
if (jongseong == _noJongseong) return _Coda.none;
|
||||
return jongseong == _jongseongRieul ? _Coda.rieul : _Coda.other;
|
||||
}
|
||||
if (last >= _jamoConsonantFirst && last <= _jamoConsonantLast) {
|
||||
return last == _jamoRieul ? _Coda.rieul : _Coda.other;
|
||||
}
|
||||
if (last >= _jamoVowelFirst && last <= _jamoVowelLast) return _Coda.none;
|
||||
if (last >= _digitZero && last <= _digitNine) {
|
||||
return _digitCodas[last - _digitZero];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 주격 조사 "이/가" — 받침이 있으면 "이", 없으면 "가"(휴지통이 · 받은편지함이 · 프로젝트가).
|
||||
/// 한글로 끝나지 않는 이름(영문 · 숫자)은 소리를 알 수 없어 "이(가)" 다 (웹 `korean.ts` 의 `subjectParticle` 과 같다).
|
||||
String subjectParticle(String word) {
|
||||
final int? jongseong = _lastJongseong(word);
|
||||
if (jongseong == null) return '이(가)';
|
||||
return jongseong == _noJongseong ? '가' : '이';
|
||||
}
|
||||
/// 주격 조사 "이/가" — 받침이 있으면 "이", 없으면 "가"(휴지통이 · 받은편지함이 · 프로젝트가 · ‘ㄱㅁ’이 · 2가).
|
||||
/// 소리를 알 수 없는 끝 글자(라틴 글자 · 기호)면 "이(가)" 다 (웹 `korean.ts` 의 `subjectParticle` 과 같다).
|
||||
String subjectParticle(String word) => switch (_lastCoda(word)) {
|
||||
null => '이(가)',
|
||||
_Coda.none => '가',
|
||||
_Coda.rieul || _Coda.other => '이',
|
||||
};
|
||||
|
||||
/// 방향 조사 "으로/로" — 받침이 있으면 "으로", 없거나 ㄹ 받침이면 "로"(휴지통으로 · 보낸편지함으로 · 프로젝트로).
|
||||
/// 한글로 끝나지 않는 이름은 "(으)로" 다 (웹 `korean.ts` 의 `directionParticle` 과 같다).
|
||||
String directionParticle(String word) {
|
||||
final int? jongseong = _lastJongseong(word);
|
||||
if (jongseong == null) return '(으)로';
|
||||
return jongseong == _noJongseong || jongseong == _jongseongRieul ? '로' : '으로';
|
||||
}
|
||||
/// 방향 조사 "으로/로" — 받침이 있으면 "으로", 없거나 ㄹ 받침이면 "로"(휴지통으로 · 보낸편지함으로 · 프로젝트로 · 프로젝트1로).
|
||||
/// 소리를 알 수 없는 끝 글자면 "(으)로" 다 (웹 `korean.ts` 의 `directionParticle` 과 같다).
|
||||
String directionParticle(String word) => switch (_lastCoda(word)) {
|
||||
null => '(으)로',
|
||||
_Coda.none || _Coda.rieul => '로',
|
||||
_Coda.other => '으로',
|
||||
};
|
||||
|
||||
/// 보조사 "은/는" — 받침이 있으면 "은", 없으면 "는"(사진은 · 보고서는 · 외 2개는).
|
||||
/// 한글로 끝나지 않는 이름(영문 · 숫자 — 확장자로 끝나는 파일 이름)은 소리를 알 수 없어 "은(는)" 이다
|
||||
/// 소리를 알 수 없는 끝 글자(라틴 글자 · 기호 — 확장자로 끝나는 파일 이름)면 "은(는)" 이다
|
||||
/// (앱에서 더함 — 웹 `korean.ts` 에는 없다. [subjectParticle] 과 같은 규칙)
|
||||
String topicParticle(String word) {
|
||||
final int? jongseong = _lastJongseong(word);
|
||||
if (jongseong == null) return '은(는)';
|
||||
return jongseong == _noJongseong ? '는' : '은';
|
||||
}
|
||||
String topicParticle(String word) => switch (_lastCoda(word)) {
|
||||
null => '은(는)',
|
||||
_Coda.none => '는',
|
||||
_Coda.rieul || _Coda.other => '은',
|
||||
};
|
||||
|
||||
/// 띄어쓰기 · 제어 문자를 뺀 ASCII 글자 — 영문 · 숫자 · 기호
|
||||
bool _isAsciiGraphic(int rune) => rune >= 0x21 && rune <= 0x7E;
|
||||
|
||||
@@ -87,7 +87,7 @@ abstract final class MailComposeTexts {
|
||||
'$limit보다 큰 파일은 앱에서 아직 보낼 수 없습니다. 웹메일에서 대용량 첨부로 보내 주세요.';
|
||||
|
||||
/// 합계 한도를 넘게 만드는 파일 — "첨부를 모두 합치면 25MB를 넘어 ‘사진.jpg’은(는) 넣지 못했습니다. …" (계획).
|
||||
/// 여럿이면 "‘사진.jpg’ 외 2개는"(앱에서 더함). 조사는 `topicParticle` — 확장자로 끝나는 이름은 소리를 몰라 "은(는)"
|
||||
/// 여럿이면 "‘사진.jpg’ 외 2개는"(앱에서 더함). 조사는 `topicParticle` — 라틴 글자 확장자로 끝나는 이름은 소리를 몰라 "은(는)"(숫자로 끝나면 그 숫자의 소리로)
|
||||
static String overTotal(String total, String name, {int others = 0}) {
|
||||
final String target = others > 0
|
||||
? '‘$name’ 외 ${formatCount(others)}개'
|
||||
|
||||
@@ -250,7 +250,7 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('은/는 — 받침에 맞추고, 한글로 끝나지 않으면 "은(는)"', () {
|
||||
test('은/는 — 받침에 맞추고, 소리를 모르는 끝 글자(라틴 글자 · 기호)면 "은(는)"', () {
|
||||
expect(topicParticle('사진'), '은');
|
||||
expect(topicParticle('보고서'), '는');
|
||||
expect(topicParticle('외 2개'), '는');
|
||||
|
||||
@@ -108,6 +108,49 @@ void main() {
|
||||
expect(directionParticle('프로젝트'), '로');
|
||||
expect(directionParticle('Archive'), '(으)로');
|
||||
});
|
||||
|
||||
test('한글 자모 · 숫자는 읽는 소리로 — 자음은 받침 있음(ㄹ 은 "로"), 모음은 받침 없음', () {
|
||||
// 끝 글자 → 이/가 · 으로/로 · 은/는
|
||||
for (final (String last, String subject, String direction, String topic)
|
||||
in <(String, String, String, String)>[
|
||||
('ㄱ', '이', '으로', '은'), // 기역
|
||||
('ㄹ', '이', '로', '은'), // 리을
|
||||
('ㅁ', '이', '으로', '은'), // 미음
|
||||
('ㄳ', '이', '으로', '은'), // 기역시옷 — 겹자음도 받침 있음
|
||||
('ㅏ', '가', '로', '는'), // 아
|
||||
('0', '이', '으로', '은'), // 영
|
||||
('1', '이', '로', '은'), // 일
|
||||
('2', '가', '로', '는'), // 이
|
||||
('3', '이', '으로', '은'), // 삼
|
||||
('4', '가', '로', '는'), // 사
|
||||
('5', '가', '로', '는'), // 오
|
||||
('6', '이', '으로', '은'), // 육
|
||||
('7', '이', '로', '은'), // 칠
|
||||
('8', '이', '로', '은'), // 팔
|
||||
('9', '가', '로', '는'), // 구
|
||||
]) {
|
||||
expect(subjectParticle(last), subject, reason: last);
|
||||
expect(directionParticle(last), direction, reason: last);
|
||||
expect(topicParticle(last), topic, reason: last);
|
||||
}
|
||||
// 낱말의 끝 글자로 본다 — 초성 검색어 · 번호로 끝나는 이름
|
||||
expect(directionParticle('ㄱㅁ'), '으로');
|
||||
expect(subjectParticle('ㄱㅁ'), '이');
|
||||
expect(directionParticle('프로젝트1'), '로');
|
||||
expect(subjectParticle('2026'), '이');
|
||||
// 라틴 글자 · 기호는 소리를 몰라 지금처럼
|
||||
expect(subjectParticle('ARA'), '이(가)');
|
||||
expect(directionParticle('100%'), '(으)로');
|
||||
expect(topicParticle('v'), '은(는)');
|
||||
});
|
||||
|
||||
test('끝의 공백은 떼고 보고, 따옴표 · 괄호는 떼지 않는다 — 그 글자로 끝나면 소리를 몰라 "이(가)" 꼴', () {
|
||||
expect(subjectParticle('계약 '), '이');
|
||||
expect(directionParticle('ㄹ '), '로');
|
||||
expect(subjectParticle('계약)'), '이(가)');
|
||||
expect(subjectParticle('"계약"'), '이(가)');
|
||||
expect(directionParticle('(2)'), '(으)로');
|
||||
});
|
||||
});
|
||||
|
||||
group('여러 통 처리의 알림 글 (웹 mail-bulk.ts 와 같다)', () {
|
||||
|
||||
Reference in New Issue
Block a user