feat(flutter): 사람 고르기(core/people) — 주소 칩 · 받는 사람 추천 맞추기를 더한다

쓰기 화면의 받는 사람 칸이 쓰고, 나중의 캘린더 · 파일 모듈도 함께 쓰게
core 에 둔다. 주소 검사는 웹 mail-address.ts, 추천 맞추기(초성 · 겹자음 ·
조합 중 글자)는 웹 recipient-match.ts 와 짝이고, 표본 표는 웹 · 백엔드와
같은 70행을 같은 차례로 옮겼다(세 쪽 짝 — 한쪽을 고치면 모두).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 02:11:00 +09:00
parent 20cde01454
commit 695a343878
8 changed files with 1485 additions and 0 deletions
@@ -0,0 +1,188 @@
import 'package:arasystem_app/core/people/address_input.dart';
import 'package:arasystem_app/core/people/person_address.dart';
import 'package:flutter_test/flutter_test.dart';
// 짝: 웹 `frontend/src/shared/utils/mail-address.spec.ts` — 같은 표본을 옮겼다
void main() {
group('isValidEmail', () {
for (final String email in <String>[
'doyun.lee@arasystem.it.kr',
'ara.kim@arasystem.test',
'a+tag@b.co',
]) {
test('$email 는 올바른 주소', () => expect(isValidEmail(email), isTrue));
}
for (final String email in <String>[
'kim@arasystem',
'이도윤',
'a@b',
'a b@c.kr',
'<a@b.kr>',
]) {
test('$email 는 형식 오류', () => expect(isValidEmail(email), isFalse));
}
});
group('parseRecipient', () {
test('"이름 <주소>" 를 이름과 주소로 나눈다', () {
expect(
parseRecipient('이도윤 <doyun.lee@arasystem.it.kr>'),
const RecipientToken(
label: '이도윤 <doyun.lee@arasystem.it.kr>',
address: PersonAddress(
email: 'doyun.lee@arasystem.it.kr',
name: '이도윤',
),
valid: true,
),
);
});
test('따옴표로 감싼 이름도 받는다', () {
expect(
parseRecipient('"Kim, Ara" <ara.kim@arasystem.it.kr>')?.address,
const PersonAddress(email: 'ara.kim@arasystem.it.kr', name: 'Kim, Ara'),
);
});
test('주소만 쓰면 이름은 null', () {
expect(
parseRecipient(' junho.park@arasystem.it.kr ')?.address,
const PersonAddress(email: 'junho.park@arasystem.it.kr'),
);
});
test('형식이 틀리면 칩은 만들되 valid=false (빨간 칩)', () {
final RecipientToken? token = parseRecipient('kim@arasystem');
expect(token?.label, 'kim@arasystem');
expect(token?.valid, isFalse);
});
test('빈 입력은 null', () => expect(parseRecipient(' '), isNull));
});
group('splitRecipientInput', () {
test('쉼표 · 세미콜론 · 줄바꿈으로 나눈다', () {
expect(splitRecipientInput('a@b.kr, c@d.kr;e@f.kr\ng@h.kr'), <String>[
'a@b.kr',
'c@d.kr',
'e@f.kr',
'g@h.kr',
]);
});
test('따옴표 · 꺾쇠 안의 쉼표는 나누지 않는다', () {
expect(splitRecipientInput('"Kim, Ara" <ara@b.kr>, <x,y@b.kr>'), <String>[
'"Kim, Ara" <ara@b.kr>',
'<x,y@b.kr>',
]);
});
});
group('splitPendingRecipients — 칸의 글이 바뀔 때(앱에만 있다)', () {
test('구분자가 없으면 글 그대로 이어 쓴다', () {
final ({List<String> complete, String rest}) split =
splitPendingRecipients('doyun@ara');
expect(split.complete, isEmpty);
expect(split.rest, 'doyun@ara');
});
test('마지막 구분자까지는 다 쓴 주소 · 뒤는 이어 쓰는 글(앞 공백은 지운다)', () {
final ({List<String> complete, String rest}) split =
splitPendingRecipients('a@b.kr, c@d.kr; e@f');
expect(split.complete, <String>['a@b.kr', 'c@d.kr']);
expect(split.rest, 'e@f');
});
test('쉼표로 끝나면 이어 쓰는 글은 비었다', () {
final ({List<String> complete, String rest}) split =
splitPendingRecipients('a@b.kr, ');
expect(split.complete, <String>['a@b.kr']);
expect(split.rest, isEmpty);
});
test('따옴표 · 꺾쇠가 열린 동안의 쉼표는 구분자가 아니다', () {
final ({List<String> complete, String rest}) split =
splitPendingRecipients('"Kim, Ara');
expect(split.complete, isEmpty);
expect(split.rest, '"Kim, Ara');
expect(
splitPendingRecipients('"Kim, Ara" <ara@b.kr>,').complete,
<String>['"Kim, Ara" <ara@b.kr>'],
);
});
});
group('hasOpenRecipientGroup', () {
test('따옴표나 꺾쇠가 열린 채면 참', () {
expect(hasOpenRecipientGroup('"Kim'), isTrue);
expect(hasOpenRecipientGroup('Kim <ara'), isTrue);
expect(hasOpenRecipientGroup('"Kim" <ara@b.kr>'), isFalse);
});
});
group('mergeRecipientTokens', () {
test('이미 있는 주소(대소문자 무시)는 다시 넣지 않는다', () {
final List<RecipientToken> existing = <RecipientToken>[
tokenFromAddress(
const PersonAddress(email: 'doyun.lee@arasystem.it.kr', name: '이도윤'),
),
];
final List<RecipientToken> incoming = <RecipientToken>[
?parseRecipient('DOYUN.LEE@arasystem.it.kr'),
?parseRecipient('junho.park@arasystem.it.kr'),
];
expect(
mergeRecipientTokens(
existing,
incoming,
).map((RecipientToken token) => token.address.email),
<String>['doyun.lee@arasystem.it.kr', 'junho.park@arasystem.it.kr'],
);
});
test('형식이 틀린 칩은 넣은 글로 겹침을 가른다', () {
final List<RecipientToken> tokens = mergeRecipientTokens(
<RecipientToken>[?parseRecipient('kim@ara')],
<RecipientToken>[
?parseRecipient('kim@ara'),
?parseRecipient('KIM@ara'),
],
);
expect(tokens.map((RecipientToken t) => t.label), <String>[
'kim@ara',
'KIM@ara',
]);
});
});
group('표시', () {
test('formatAddress — 이름이 있으면 "이름 <주소>"', () {
expect(
formatAddress(const PersonAddress(email: 'a@b.kr', name: '김아라')),
'김아라 <a@b.kr>',
);
expect(
formatAddress(const PersonAddress(email: 'a@b.kr', name: ' ')),
'a@b.kr',
);
expect(
formatAddressList(const <PersonAddress>[
PersonAddress(email: 'a@b.kr', name: '김아라'),
PersonAddress(email: 'c@d.kr'),
]),
'김아라 <a@b.kr>, c@d.kr',
);
});
test('uniqueAddresses — 같은 주소(대소문자 무시)는 처음 것만', () {
expect(
uniqueAddresses(const <PersonAddress>[
PersonAddress(email: 'a@b.kr', name: '처음'),
PersonAddress(email: 'A@B.KR', name: '나중'),
]),
const <PersonAddress>[PersonAddress(email: 'a@b.kr', name: '처음')],
);
});
});
}
@@ -0,0 +1,120 @@
/// 받는 사람 맞추기 표본 — 시험 전용(앱 코드에서 import 하지 않는다).
///
/// ⚠️ **같은 표 셋**: 웹 `frontend/src/test-utils/recipient-match-samples.ts` · 백엔드 `src/modules/mail/recipient-match.spec.ts` 의
/// `RECIPIENT_MATCH_SAMPLES` 와 행 · 차례까지 같다. 한쪽을 고치면 셋 다 고친다(맞추기 규칙을 바꾸면 세 `recipient-match` 와 세 표를 함께).
/// 웹의 `normalize('NFD')` 는 Dart 에 없어 한글 음절을 같은 규칙(유니코드의 한글 분해)으로 [_nfd] 가 푼다.
library;
/// [찾는 글(q), 이름(없으면 null), 주소, 맞는가]
typedef RecipientMatchSample = (
String query,
String? name,
String email,
bool matches,
);
final List<RecipientMatchSample> kRecipientMatchSamples =
<RecipientMatchSample>[
('도윤', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('DOYUN', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('Lee', 'Doyun Lee', 'd.lee@example.com', true),
('lEE', 'Doyun Lee', 'd.lee@example.com', true),
('윤도', '이도윤', 'doyun.lee@arasystem.it.kr', false),
('박', '이도윤', 'doyun.lee@arasystem.it.kr', false),
('ㄷㅇ', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('ㅇㄷㅇ', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('ㅂㅈㅎ', '박지훈', 'jihoon.park@hanbit.co.kr', true),
('ㅈㅎ', '박지훈', 'jihoon.park@hanbit.co.kr', true),
('ㅎㅈ', '박지훈', 'jihoon.park@hanbit.co.kr', false),
('ㄲ', '꽃님', 'flower@example.com', true),
('ㄱ', '꽃님', 'flower@example.com', false),
('ㅃ', '빵집', 'bread@example.com', true),
('도ㅇ', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('ㄷ윤', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('이ㄷ', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('도유', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('이도유', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('동', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('이동', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('돈', '이도윤', 'doyun.lee@arasystem.it.kr', false),
('도요', '이도윤', 'doyun.lee@arasystem.it.kr', false),
('유', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('윤', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('도윤이', '이도윤', 'doyun.lee@arasystem.it.kr', false),
('지후', '박지훈', 'jihoon.park@hanbit.co.kr', true),
('박지후', '박지훈', 'jihoon.park@hanbit.co.kr', true),
('박짛', '박지훈', 'jihoon.park@hanbit.co.kr', true),
('박지', '박지훈', 'jihoon.park@hanbit.co.kr', true),
('닭', '달걀', 'egg@example.com', true),
('넋', '너서', 'neo@example.com', false),
('값', '가비', 'gabi@example.com', false),
('값', '갑시다', 'gapsi@example.com', true),
('갑', '갑시다', 'gapsi@example.com', true),
('ㄳ', '넋', 'neok@example.com', false),
('ㄱ', '넋', 'neok@example.com', false),
('김 아', '김 아라', 'ara.kim@arasystem.it.kr', true),
('김아', '김 아라', 'ara.kim@arasystem.it.kr', false),
('ㄱ ㅇ', '김 아라', 'ara.kim@arasystem.it.kr', true),
('ㄱㅇ', '김 아라', 'ara.kim@arasystem.it.kr', false),
(' 도윤 ', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('arasystem', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('DOYUN.LEE@', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('doyun.lee@arasystem.it.kr', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('doyun.lee@other.example', '이도윤', 'doyun.lee@arasystem.it.kr', true),
('doyun.leee@other.example', '이도윤', 'doyun.lee@arasystem.it.kr', false),
('@arasystem', '', 'doyun.lee@arasystem.it.kr', true),
('lee@', 'Doyun', 'doyun.lee@arasystem.it.kr', true),
('a@b@c', '', 'a@x.example', true),
('Doyun.Lee@AraSystem.it.kr', null, 'doyun.lee@arasystem.it.kr', true),
('kr', null, 'doyun.lee@arasystem.it.kr', true),
('istanbul', null, 'İstanbul@example.com', false),
('example', null, 'İstanbul@example.com', false),
('도윤', null, 'doyun.lee@arasystem.it.kr', false),
('도윤', '', 'x@example.com', false),
('😀', '팀😀', 'team@example.com', true),
('팀😀', '팀😀', 'team@example.com', true),
('😀팀', '팀😀', 'team@example.com', false),
('𠀋', '𠀋田', 'kanji@example.com', true),
('ㅏ', '아라', 'ara@example.com', false),
('ㅇㅏ', '아라', 'ara@example.com', false),
('2팀', '영업2팀', 'sales2@example.com', true),
('(주)', '(주)한빛', 'hanbit@example.com', true),
(_nfd('도윤'), '이도윤', 'doyun.lee@arasystem.it.kr', false),
('도윤', _nfd('이도윤'), 'x@example.com', false),
(
'ㄱ' * 33,
'ㄱㄱㄱㄱㄱ가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가가',
'long@example.com',
true,
),
('${'가' * 32}나', '${'가' * 40}나', 'long@example.com', true),
('${'가' * 31}나다', '${'가' * 40}나다', 'long@example.com', true),
('${'가' * 31}나라', '${'가' * 40}나다', 'long@example.com', false),
];
/// 한글 음절을 첫소리 · 가운뎃소리 · 끝소리 낱자로 푼다 — 웹 `String.prototype.normalize('NFD')` 의 한글 부분과 같다
String _nfd(String value) {
const int base = 0xAC00;
const int last = 0xD7A3;
const int leadBase = 0x1100;
const int vowelBase = 0x1161;
const int tailBase = 0x11A7;
const int vowelCount = 21;
const int tailCount = 28;
final StringBuffer out = StringBuffer();
for (final int rune in value.runes) {
if (rune < base || rune > last) {
out.writeCharCode(rune);
continue;
}
final int index = rune - base;
out
..writeCharCode(leadBase + index ~/ (vowelCount * tailCount))
..writeCharCode(
vowelBase + (index % (vowelCount * tailCount)) ~/ tailCount,
);
final int tail = index % tailCount;
if (tail != 0) out.writeCharCode(tailBase + tail);
}
return out.toString();
}
@@ -0,0 +1,369 @@
import 'package:arasystem_app/core/people/address_input.dart';
import 'package:arasystem_app/core/people/people_texts.dart';
import 'package:arasystem_app/core/people/person_address.dart';
import 'package:arasystem_app/core/people/recipient_match.dart';
import 'package:arasystem_app/core/people/recipient_suggestion.dart';
import 'package:flutter_test/flutter_test.dart';
import 'recipient_match_samples.dart';
const RecipientSuggestion _doyun = RecipientSuggestion(
name: '이도윤',
email: 'doyun.lee@arasystem.it.kr',
source: RecipientSource.directory,
);
const RecipientSuggestion _doyunKim = RecipientSuggestion(
name: '김도윤',
email: 'doyun.kim@arasystem.it.kr',
source: RecipientSource.directory,
);
const RecipientSuggestion _partner = RecipientSuggestion(
name: '최도윤',
email: 'choi@partner.example',
source: RecipientSource.recent,
);
const RecipientSuggestion _noName = RecipientSuggestion(
email: 'ops@vendor.example',
source: RecipientSource.recent,
);
/// 맞은 부분 글자
String? _matched(String text, MatchRange? range) =>
range == null ? null : text.substring(range.start, range.end);
void main() {
// 웹 · 서버의 q 맞추기와 같은 표 — 행 · 차례까지 같다(recipient_match_samples.dart)
group('표본 표 — 웹 · 서버(GET /recipient-suggestions?q=)와 같은 맞추기', () {
for (final (String query, String? name, String email, bool matches)
in kRecipientMatchSamples) {
test('q=$query 이름=$name 주소=$email → $matches', () {
final bool byName = matchName(query, name ?? '') != null;
final bool byEmail = matchEmail(query, email) != null;
expect(byName || byEmail, matches);
});
}
});
group('matchName — 이름(대소문자 무시 · 초성 · 조합 중인 마지막 글자)', () {
for (final (String query, String name, String expected)
in <(String, String, String)>[
('도윤', '이도윤', '도윤'),
('이도', '이도윤', '이도'),
('ㄷㅇ', '이도윤', '도윤'),
('ㅇㄷㅇ', '이도윤', '이도윤'),
('도ㅇ', '이도윤', '도윤'),
('ㅇ도', '이도윤', '이도'),
// 마지막 글자가 조합 중 — "윤"을 치는 중이면 "유", 다음 글자의 ㅇ 이 받침으로 붙으면 "동"
('이도유', '이도윤', '이도윤'),
('이동', '이도윤', '이도윤'),
('읻', '이도윤', '이도'),
('닭', '달기봉', '달기'),
('kim', 'Ara KIM', 'KIM'),
(' 도윤 ', '이도윤', '도윤'),
]) {
test('$query → $name 에서 "$expected"', () {
expect(_matched(name, matchName(query, name)), expected);
});
}
for (final (String query, String name) in <(String, String)>[
('도윤', '김아라'),
('ㄷㅈ', '이도윤'),
('동윤', '이도윤'),
('이동', '이도'),
('윤도', '이도윤'),
('', '이도윤'),
]) {
test('$query 는 $name 에 맞지 않는다', () {
expect(matchName(query, name), isNull);
});
}
test('조합 중인 글자는 마지막에만 — 가운데 글자는 그대로 맞아야 한다', () {
expect(matchName('유ㄴ', '윤나'), isNull);
expect(matchName('윤ㄴ', '윤나'), const MatchRange(0, 2));
});
test('이모지 같은 두 칸 글자가 있어도 범위는 UTF-16 위치다', () {
const String name = '😀이도윤';
expect(_matched(name, matchName('도윤', name)), '도윤');
});
});
group('matchEmail — 주소(대소문자 무시)', () {
test('친 글자가 들어 있는 곳', () {
expect(
_matched(_doyun.email, matchEmail('doyun', _doyun.email)),
'doyun',
);
expect(
_matched(_doyun.email, matchEmail('ARASYSTEM', _doyun.email)),
'arasystem',
);
expect(matchEmail('xyz', _doyun.email), isNull);
});
test('완전한 주소를 쳤으면 아이디(@ 앞, @ 포함)로도 찾는다', () {
expect(
_matched(
_doyun.email,
matchEmail('doyun.lee@partner.example', _doyun.email),
),
'doyun.lee@',
);
expect(matchEmail('@partner.example', _doyun.email), isNull);
});
test('İ 는 JS 처럼 두 글자로 낮춘다 — 길이가 바뀌는 주소는 맞추지 않는다', () {
expect(jsLowerCase('İ').length, 2);
expect(matchEmail('stanbul', 'İstanbul@example.com'), isNull);
});
});
group('filterRecipientSuggestions', () {
const List<RecipientSuggestion> pool = <RecipientSuggestion>[
_partner,
_doyun,
_noName,
_doyunKim,
];
List<String> emails(List<RecipientMatch> matches) => <String>[
for (final RecipientMatch match in matches) match.suggestion.email,
];
test('사내를 먼저, 그 안에서는 받은 차례대로', () {
expect(
emails(filterRecipientSuggestions('도윤', pool, <String>{})),
<String>[_doyun.email, _doyunKim.email, _partner.email],
);
});
test('주소록이 사내보다 먼저 — 주소록 → 사내 → 최근, 묶음 안은 받은 차례대로', () {
const RecipientSuggestion contact = RecipientSuggestion(
name: '도윤 거래처',
email: 'doyun@vendor.example',
source: RecipientSource.contact,
);
expect(
emails(
filterRecipientSuggestions('도윤', const <RecipientSuggestion>[
_partner,
_doyun,
contact,
_doyunKim,
], <String>{}),
),
<String>[contact.email, _doyun.email, _doyunKim.email, _partner.email],
);
});
test('이미 넣은 주소(대소문자 무시)는 빼고, 같은 주소는 한 번만', () {
const RecipientSuggestion again = RecipientSuggestion(
name: '이도윤',
email: 'Doyun.Lee@arasystem.it.kr',
source: RecipientSource.recent,
);
expect(
emails(
filterRecipientSuggestions(
'도윤',
<RecipientSuggestion>[...pool, again],
<String>{normalizeEmail(_doyunKim.email)},
),
),
<String>[_doyun.email, _partner.email],
);
});
test('이름이 없으면 주소로만 맞춘다', () {
expect(
filterRecipientSuggestions(
'vendor',
pool,
<String>{},
).map((RecipientMatch match) => match.suggestion).toList(),
<RecipientSuggestion>[_noName],
);
});
test('빈 글자는 아무것도 보이지 않고, 최대 8개까지', () {
expect(filterRecipientSuggestions(' ', pool, <String>{}), isEmpty);
final List<RecipientSuggestion> many = <RecipientSuggestion>[
for (int index = 0; index < 12; index++)
RecipientSuggestion(
name: '도윤$index',
email: 'doyun$index@arasystem.it.kr',
source: RecipientSource.directory,
),
];
expect(
filterRecipientSuggestions('도윤', many, <String>{}),
hasLength(kRecipientSuggestionLimit),
);
});
});
group('그룹 — matchContactGroups · arrangeRecipientOptions', () {
const ContactGroupSummary vendors = ContactGroupSummary(
id: 'g1',
name: '거래처',
memberCount: 4,
);
const ContactGroupSummary team = ContactGroupSummary(
id: 'g2',
name: '도윤 팀',
memberCount: 2,
);
const ContactGroupSummary empty = ContactGroupSummary(
id: 'g3',
name: '거래 준비',
memberCount: 0,
);
const RecipientSuggestion contact = RecipientSuggestion(
name: '이도윤',
email: 'doyun@vendor.example',
source: RecipientSource.contact,
);
test('그룹 이름으로 찾는다 — 초성 · 조합 중 글자도, 구성원이 없는 그룹은 뺀다', () {
expect(
matchContactGroups('거래', const <ContactGroupSummary>[
vendors,
team,
empty,
]).map((RecipientGroupMatch match) => match.group.id),
<String>['g1'],
);
final List<RecipientGroupMatch> byInitials = matchContactGroups(
'ㄱㄹ',
const <ContactGroupSummary>[vendors, empty],
);
expect(byInitials.single.group, vendors);
expect(byInitials.single.name, const MatchRange(0, 2));
expect(
matchContactGroups(' ', const <ContactGroupSummary>[vendors]),
isEmpty,
);
});
test('줄의 차례는 주소록 → 그룹 → 사내 → 최근, 모두 합쳐 8개까지', () {
final List<RecipientOption> options = arrangeRecipientOptions(
filterRecipientSuggestions('도윤', const <RecipientSuggestion>[
_partner,
_doyun,
contact,
], <String>{}),
matchContactGroups('도윤', const <ContactGroupSummary>[team]),
);
expect(options.map(recipientOptionGroupOf), <RecipientOptionGroup>[
RecipientOptionGroup.contact,
RecipientOptionGroup.group,
RecipientOptionGroup.directory,
RecipientOptionGroup.recent,
]);
final List<RecipientSuggestion> many = <RecipientSuggestion>[
for (int index = 0; index < 10; index++)
RecipientSuggestion(
name: '도윤$index',
email: 'doyun$index@vendor.example',
source: RecipientSource.contact,
),
];
final List<RecipientOption> full = arrangeRecipientOptions(
filterRecipientSuggestions('도윤', many, <String>{}),
matchContactGroups('도윤', const <ContactGroupSummary>[team]),
);
// 주소록 줄이 8개를 채우면 그룹 줄은 밀려난다(차례가 앞선다)
expect(full, hasLength(kRecipientSuggestionLimit));
expect(
full.every((RecipientOption option) => option is AddressOption),
isTrue,
);
});
});
group('보조 함수', () {
test('highlightParts — 맞은 부분만 굵게', () {
expect(highlightParts('이도윤', const MatchRange(1, 3)), <HighlightPart>[
(text: '이', strong: false),
(text: '도윤', strong: true),
]);
expect(highlightParts('abc', null), <HighlightPart>[
(text: 'abc', strong: false),
]);
// 범위가 글 밖이면 굵게 하지 않는다
expect(highlightParts('abc', const MatchRange(2, 9)), <HighlightPart>[
(text: 'abc', strong: false),
]);
});
test('주소록 줄의 설명 — 회사 주소는 회사 이름, 그 밖은 종류(웹 contactSuggestionDetail)', () {
RecipientSuggestion contact({
String? company,
ContactEmailLabel? label,
}) => RecipientSuggestion(
email: 'a@b.example',
source: RecipientSource.contact,
company: company,
emailLabel: label,
);
expect(
contactSuggestionDetail(
contact(company: '한빛', label: ContactEmailLabel.work),
),
'한빛',
);
expect(
contactSuggestionDetail(contact(label: ContactEmailLabel.work)),
'회사',
);
expect(
contactSuggestionDetail(
contact(company: '한빛', label: ContactEmailLabel.personal),
),
'개인',
);
expect(contactSuggestionDetail(contact(company: '한빛')), '한빛');
expect(contactSuggestionDetail(_doyun), isNull);
});
test('그룹 구성원 — 대표 주소만, 겹치면 한 번, 주소가 없는 사람은 센다', () {
final GroupRecipients recipients = groupRecipientsOf(<GroupMember>[
(name: ' 이도윤 ', email: 'doyun@x.example'),
(name: null, email: 'DOYUN@x.example'),
(name: '박지훈', email: null),
(name: '', email: 'jihoon@x.example'),
]);
expect(recipients.addresses, const <PersonAddress>[
PersonAddress(email: 'doyun@x.example', name: '이도윤'),
PersonAddress(email: 'jihoon@x.example'),
]);
expect(recipients.withoutEmail, 1);
});
test('그룹을 넣은 뒤 칸 아래 글 — 넣은 수 · 뺀 까닭(웹과 같은 글)', () {
expect(
PeopleTexts.groupInserted(
'거래처',
'받는 사람',
added: 4,
withoutEmail: 1,
already: 0,
),
'‘거래처’ 그룹의 4명을 받는 사람에 넣었습니다. 주소가 없는 1명은 뺐습니다.',
);
expect(
PeopleTexts.groupInserted(
'거래처',
'참조',
added: 0,
withoutEmail: 0,
already: 2,
),
'‘거래처’ 그룹에서 참조에 더 넣을 주소가 없습니다. 이미 넣은 2명은 뺐습니다.',
);
});
});
}