fix(frontend): 화면 문구를 사용자에게 말하는 말투로 고친다

설계 문서처럼 규칙을 적어 둔 문구가 많다는 사용자 지적(2026-11-05) — "메일 서버에 계정이 있는 직원입니다 —
이름과 주소만 보입니다." · "「@도메인」은 그 도메인에서 오는 메일 모두입니다." 같은 글. 화면 상수 · 템플릿 ·
문장을 만드는 유틸의 236곳을 무엇이 되는지 · 무엇을 하면 되는지로 다시 썼다: 줄표로 잇지 않고, 「」 대신
[단추 이름], 메일 서버 · 엔진 · 판정 같은 만든 쪽 말을 빼고, 존댓말은 그대로. 한도 · 날짜 · 개수는 빼지 않았다.
그 문구를 그대로 보던 spec 을 함께 맞췄다(lint · type-check · 단위 2,354 · TZ=UTC · build 통과).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 12:44:43 +09:00
parent 7d0d5f34c0
commit ddfb2d14ab
74 changed files with 503 additions and 503 deletions
@@ -151,9 +151,9 @@ describe('LoginPage — 제한된 세션 (13단계)', () => {
expect(wrapper.find('#restricted-password-temporary').exists()).toBe(false)
expect(wrapper.find('#restricted-password-code').exists()).toBe(false)
expect(wrapper.get('label[for="restricted-password-next"]').text()).toBe('새 비밀번호')
expect(wrapper.text()).toContain('8자 이상 · 임시 비밀번호와 달라야 합니다. 영문 · 숫자 · 기호를 섞으면 더 안전합니다.')
expect(wrapper.text()).toContain('8자 이상이고 임시 비밀번호와 달라야 합니다. 영문, 숫자, 기호를 섞으면 더 안전합니다.')
expect(wrapper.get('[data-restricted-submit]').text()).toBe('비밀번호 바꾸고 계속')
expect(wrapper.text()).toContain('메일 프로그램(아웃룩 등)을 연결해 두었다면 바꾼 뒤 다시 연결해야 합니다.')
expect(wrapper.text()).toContain('아웃룩 같은 메일 프로그램을 연결해 두었다면 비밀번호를 바꾼 뒤 다시 연결해 주세요.')
expect(document.activeElement?.id).toBe('restricted-password-next')
// 비밀번호는 칸 · 반응형 상태에 남기지 않는다 — 로그인 칸은 사라졌다
expect(wrapper.find('#login-password').exists()).toBe(false)
@@ -279,7 +279,7 @@ describe('LoginPage — 제한된 세션 (13단계)', () => {
expect(wrapper.get('h1').text()).toBe('새 비밀번호를 정해 주세요')
expect(wrapper.find('#restricted-password-temporary').exists()).toBe(true)
expect(wrapper.get('label[for="restricted-password-code"]').text()).toBe('인증 앱 코드')
expect(wrapper.text()).toContain('로그인할 때 넣은 코드는 다시 쓸 수 없습니다 — 앱에 다음 코드가 뜨면 넣어 주세요.')
expect(wrapper.text()).toContain('로그인할 때 넣은 코드는 다시 쓸 수 없으니, 앱에 다음 코드가 뜨면 넣어 주세요.')
expect(urls()).toContain('GET /two-factor')
})
@@ -366,16 +366,16 @@ describe('LoginPage — 제한된 세션 (13단계)', () => {
it('새 키를 받아 QR · 키를 그린다 — 지금 비밀번호는 묻지 않고(메모리) 문의 줄 · 아래 안내', async () => {
const { wrapper } = await atSetupStep({ supportContact: () => ok({ text: CONTACT }) })
expect(wrapper.get('h1').text()).toBe('2단계 인증을 켜야 합니다')
expect(wrapper.get('h1').text()).toBe('2단계 인증을 켜 주세요')
expect(urls()).toContain('POST /two-factor/enrollments')
expect(wrapper.find('[data-qr-code]').exists()).toBe(true)
expect(wrapper.get('[data-setup-key]').text().replace(/\s/g, '')).toBe(enrollment(1).secret)
expect(wrapper.text()).toContain(`앱에는 「ARA SYSTEM (${USER.email})」로 저장됩니다.`)
expect(wrapper.text()).toContain(`앱에는 ‘ARA SYSTEM (${USER.email})’로 저장됩니다.`)
expect(wrapper.get('label[for="restricted-setup-code"]').text()).toBe('앱에 표시된 6자리 코드')
expect(wrapper.find('#restricted-setup-password').exists()).toBe(false)
expect(wrapper.get('[data-support-line]').text()).toBe(`인증 앱을 쓸 수 없나요?${CONTACT}`)
expect(wrapper.get('[data-support-contact]').text()).toBe(CONTACT)
expect(wrapper.text()).toContain('Outlook · 휴대폰 메일 앱은 이 코드를 묻지 않습니다.')
expect(wrapper.text()).toContain('Outlook이나 휴대폰 메일 앱은 이 코드를 묻지 않습니다.')
expect(document.activeElement?.id).toBe('restricted-setup-code')
})
@@ -428,7 +428,7 @@ describe('LoginPage — 제한된 세션 (13단계)', () => {
const { wrapper } = await mountLogin({ activate: () => fail(400, 'AUTH_005') })
expect(wrapper.get('label[for="restricted-setup-password"]').text()).toBe('지금 비밀번호')
expect(wrapper.text()).toContain('새로고침해서 다시 넣어야 합니다.')
expect(wrapper.text()).toContain('새로고침한 뒤에는 지금 비밀번호를 한 번 더 넣어 주세요.')
await wrapper.get('#restricted-setup-code').setValue('482913')
await submitStep(wrapper)
expect(bodyOf('/two-factor/activation')).toEqual([])
@@ -453,7 +453,7 @@ describe('LoginPage — 제한된 세션 (13단계)', () => {
await submitStep(wrapper)
expect(router.currentRoute.value.name).toBe('login')
expect(wrapper.get('h1').text()).toBe('2단계 인증을 켜야 합니다')
expect(wrapper.get('h1').text()).toBe('2단계 인증을 켜 주세요')
expect(useToastStore().toasts.map((toast) => toast.message)).toEqual(['새 비밀번호를 정했습니다.'])
expect(wrapper.find('#restricted-setup-password').exists()).toBe(false)
expect(document.activeElement?.id).toBe('restricted-setup-code')
@@ -491,7 +491,7 @@ describe('LoginPage — 제한된 세션 (13단계)', () => {
})
expect(router.currentRoute.value.name).toBe('login')
expect(wrapper.get('h1').text()).toBe('2단계 인증을 켜야 합니다')
expect(wrapper.get('h1').text()).toBe('2단계 인증을 켜 주세요')
expect(wrapper.find('#restricted-setup-password').exists()).toBe(false)
await wrapper.get('#restricted-setup-code').setValue('482913')
await submitStep(wrapper)
+1 -1
View File
@@ -206,7 +206,7 @@ describe('LoginPage — 2단계 인증 코드 단계', () => {
expect(input.attributes('maxlength')).toBeUndefined()
expect(document.activeElement?.id).toBe('login-code')
expect(wrapper.text()).toContain('인증 앱을 쓸 수 없나요? 관리자에게 문의해 주세요.')
expect(wrapper.text()).toContain('Outlook · 휴대폰 메일 앱은 이 코드를 묻지 않습니다.')
expect(wrapper.text()).toContain('Outlook이나 휴대폰 메일 앱은 이 코드를 묻지 않습니다.')
})
it('문의 연락처가 있으면(13단계) 「인증 앱을 쓸 수 없나요?」 다음 줄에 연락처', async () => {
+2 -2
View File
@@ -449,7 +449,7 @@ void supportContactStore.ensureLoaded()
로그인
</h1>
<p class="login__subtitle">
회사 메일 계정(@{{ COMPANY_MAIL_DOMAIN }})으로 로그인하세요.
회사 메일 주소(@{{ COMPANY_MAIL_DOMAIN }})로 로그인해 주세요.
</p>
</div>
@@ -524,7 +524,7 @@ void supportContactStore.ensureLoaded()
:contact="supportContactStore.text"
/>
<p class="login__note">
Outlook이나 휴대폰 기본 메일 앱에서도 같은 계정으로 메일을 쓸 수 있습니다. (IMAP · SMTP 설정)
Outlook이나 휴대폰 기본 메일 앱에서도 IMAP, SMTP 설정으로 같은 메일을 쓸 수 있습니다.
</p>
</template>
</div>
@@ -387,7 +387,7 @@ describe('ContactsPage (주소록 모듈)', () => {
const row = app.get('[data-contacts-row="c2"]')
expect(row.get('.contacts-row__meta').text()).toBe('(주)한빛상사 · 영업팀')
expect(row.get('.contacts-row__email').text()).toBe('jihoon.park@hanbit.example')
expect(app.get('[data-contact-none]').text()).toContain('연락처를 고르세요')
expect(app.get('[data-contact-none]').text()).toContain('연락처를 골라 주세요')
await click(row.get('a'))
expect(router.currentRoute.value.fullPath).toBe('/contacts/all/c2')
@@ -624,11 +624,11 @@ describe('ContactsPage (주소록 모듈)', () => {
const { app } = await mountApp('/contacts/directory/people/s5')
expect(app.get('#contacts-list-title').text()).toBe('직원')
expect(app.get('.contacts-list__note').text()).toBe('메일 서버에 계정이 있는 직원입니다 — 이름과 주소만 보입니다.')
expect(app.get('.contacts-list__note').text()).toBe('회사 메일을 쓰는 직원이라 이름과 메일 주소만 보입니다.')
const detail = app.get('[data-directory-detail="s5"]')
expect(detail.get('.contact-detail__subtitle').text()).toBe('사내 직원')
expect(detail.findAll('.tool-button').map((button) => button.text())).toEqual(['메일 쓰기', '내 주소록에 추가'])
expect(detail.get('.contact-detail__hint').text()).toContain('사내 목록은 메일 서버의 계정에서 옵니다')
expect(detail.get('.contact-detail__hint').text()).toContain('사내 목록은 이름과 주소만 보이고 여기서 고칠 수 없습니다')
await click(detail.get('[data-contact-add-mine]'))
expect(isOpen(app, '#contact-dialog')).toBe(true)
@@ -693,7 +693,7 @@ describe('ContactsPage (주소록 모듈)', () => {
await click(app.get('[data-group-row="g1"] a'))
const input = app.get('[data-contacts-search]')
expect(input.attributes('aria-label')).toBe('주소록에서 찾기')
expect(input.attributes('placeholder')).toBe('이름 · 주소 · 회사 · 전화번호 — 초성도 됩니다 (ㅂㅈㅎ)')
expect(input.attributes('placeholder')).toBe('이름, 주소, 회사, 전화번호, 초성(ㅂㅈㅎ)으로 찾기')
await input.setValue('박')
clock.advance(CONTACTS_SEARCH_DELAY_MS - 1)
@@ -721,7 +721,7 @@ describe('ContactsPage (주소록 모듈)', () => {
await flushPromises()
const empty = app.get('[data-contacts-empty="search"]')
expect(empty.text()).toContain('‘없는사람’에 맞는 연락처가 없습니다')
expect(empty.text()).toContain('이름 · 주소 · 회사 · 전화번호의 일부나 이름의 초성으로 찾습니다.')
expect(empty.text()).toContain('이름, 주소, 회사, 전화번호의 일부나 이름의 초성으로 찾을 수 있습니다.')
// Esc 는 찾을 글을 지운다 — 목록이 다시 그룹 전체로
await input.trigger('keydown', { key: 'Escape' })
@@ -742,7 +742,7 @@ describe('ContactsPage (주소록 모듈)', () => {
await click(app.get('[data-contacts-source="directory:person"]'))
expect(router.currentRoute.value.fullPath).toBe('/contacts/directory/people')
expect((input.element as HTMLInputElement).value).toBe('')
expect(input.attributes('placeholder')).toBe('이름 · 주소 — 초성도 됩니다 (ㅂㅈㅎ)')
expect(input.attributes('placeholder')).toBe('이름, 주소, 초성(ㅂㅈㅎ)으로 찾기')
})
it('그룹 ⋯ — 이름 바꾸기 · 그룹에 메일 쓰기 · 그룹 지우기(빨간 글), 구성원이 없으면 메일 쓰기를 끈다 · 지우기는 확인 없이 + [되돌리기]', async () => {
@@ -764,7 +764,7 @@ describe('ContactsPage (주소록 모듈)', () => {
expect(router.currentRoute.value.fullPath).toBe('/contacts')
expect(app.find('[data-group-row="g1"]').exists()).toBe(false)
const [toast] = useToastStore().toasts
expect(toast?.message).toBe('‘거래처’ 그룹을 지웠습니다 — 연락처 3명은 그대로 있습니다.')
expect(toast?.message).toBe('‘거래처’ 그룹을 지웠습니다. 그룹에 있던 연락처 3명은 그대로 남아 있습니다.')
expect(toast?.action?.label).toBe('되돌리기')
if (toast?.action && 'run' in toast.action) toast.action.run()
@@ -953,8 +953,8 @@ describe('ContactsPage (주소록 모듈)', () => {
const empty = app.get('[data-contacts-empty="first"]')
expect(empty.text()).toContain('아직 연락처가 없습니다')
expect(empty.text()).toContain('자주 메일을 주고받는 사람을 넣어 두면 받는 사람 칸에서 바로 찾습니다.')
expect(empty.text()).toContain('메일을 읽다가 ⋯ › 「보낸 사람을 주소록에 추가」로도 넣을 수 있습니다.')
expect(empty.text()).toContain('자주 메일을 주고받는 사람을 넣어 두면 받는 사람 칸에서 바로 찾을 수 있습니다.')
expect(empty.text()).toContain('메일을 읽다가 [⋯] → [보낸 사람을 주소록에 추가]로도 넣을 수 있습니다.')
await click(empty.get('button'))
expect(isOpen(app, '#contact-dialog')).toBe(true)
expect(app.get('#contact-dialog-title').text()).toBe('연락처 추가')
@@ -1001,7 +1001,7 @@ describe('ContactsPage (주소록 모듈)', () => {
await click(app.get('[data-group-row="g2"] button[data-mailbox-more]'))
await click(app.findAll('[role="menuitem"]')[2] ?? app.get('[role="menuitem"]'))
expect(useToastStore().toasts.map((toast) => [toast.message, toast.action ?? null])).toEqual([
['‘프로젝트 알파’ 그룹을 지웠습니다 — 연락처 1명은 그대로 있습니다.', null],
['‘프로젝트 알파’ 그룹을 지웠습니다. 그룹에 있던 연락처 1명은 그대로 남아 있습니다.', null],
])
})
@@ -446,7 +446,7 @@ describe('ComposePage — 임시 저장 · 서명 (2단계)', () => {
expect(wrapper.get('.compose__send').text()).toBe('보내기')
// 꺼진 단추는 aria-disabled (11단계 — 초점은 받는다) · 까닭은 단추 옆 글
expect(wrapper.get('.compose__send').attributes('aria-disabled')).toBe('true')
expect(wrapper.get('#compose-send-why').text()).toBe('보낼 수 없는 파일이 있습니다 — 빨간 줄의 파일을 빼 주세요.')
expect(wrapper.get('#compose-send-why').text()).toBe('보낼 수 없는 파일이 있습니다. 빨간 줄의 파일을 빼 주세요.')
expect(wrapper.text()).toContain(ERROR_CODE_MESSAGES.MAIL_004)
expect(document.activeElement?.textContent?.trim()).toBe('파일 첨부')
expect(useToastStore().toasts).toEqual([])
@@ -239,9 +239,9 @@ describe('ComposePage — 대용량 첨부', () => {
expect(phases(wrapper)).toEqual(['ready', 'waiting-file', 'missing'])
expect(lineOf(wrapper, 'ready')).toBe('1.2 GB · 이 임시 저장본에 11월 25일까지 남습니다')
expect(lineOf(wrapper, 'waiting-file')).toContain('올리다 멈춘 파일 — 같은 파일을 다시 고르면 이어서 올립니다')
expect(lineOf(wrapper, 'waiting-file')).toContain('올리다 멈춤 · 160 MB / 612 MB 올림 · 같은 파일을 다시 고르면 이어서 올립니다')
expect(wrapper.find('[data-phase="waiting-file"] [data-large-repick]').exists()).toBe(true)
expect(lineOf(wrapper, 'missing')).toBe('설정 › 대용량 첨부에서 지운 파일입니다 — 빼고 다시 올려 주세요.')
expect(lineOf(wrapper, 'missing')).toBe('[설정] → [대용량 첨부]에서 지운 파일입니다. 빼고 다시 올려 주세요.')
const send = () => wrapper.get('.compose__send')
expect(send().attributes('aria-disabled')).toBe('true')
@@ -283,7 +283,7 @@ describe('ComposePage — 대용량 첨부', () => {
expect((sent?.data as Record<string, unknown> | undefined)?.largeFileIds).toEqual(['lf-1'])
expect(calls(requests)).toContain('GET /file-uploads/lf-1')
expect(phases(wrapper)).toEqual(['missing'])
expect(lineOf(wrapper, 'missing')).toBe('서버에서 찾을 수 없는 파일입니다 — 빼고 다시 올려 주세요.')
expect(lineOf(wrapper, 'missing')).toBe('파일을 찾을 수 없습니다. 빼고 다시 올려 주세요.')
expect(wrapper.get('.compose__send').attributes('aria-disabled')).toBe('true')
expect(wrapper.get('#compose-send-why').text()).toBe(SEND_BLOCK_TEXT.unsendable)
expect(useToastStore().toasts).toEqual([])
@@ -308,7 +308,7 @@ describe('ComposePage — 대용량 첨부', () => {
const lookup = requests.find((request) => request.url === '/share-link-lookups')
expect(lookup?.data).toEqual({ urls: [URL1, URL2] })
expect(wrapper.findAll('[data-quoted-link]').map((notice) => notice.text())).toEqual([
'원래 메일의 대용량 첨부 2개는 인용 속 링크로 함께 갑니다 — 11월 29일까지',
'원래 메일의 대용량 첨부 2개는 인용 속 링크로 함께 갑니다. 11월 29일까지 받을 수 있습니다.',
'인용 속 대용량 첨부 링크는 공유가 멈춰 받을 수 없습니다.',
])
// 원래 메일의 대용량 첨부는 다시 싣지 않는다 — 링크 묶음이 없다
@@ -379,7 +379,7 @@ describe('ComposePage — 대용량 첨부', () => {
await flushPromises()
expect(phases(wrapper)).toEqual(['ready'])
expect(lineOf(wrapper, 'ready')).toBe('2 MB · 보안 검사 중 — 끝나지 않아도 보낼 수 있습니다')
expect(lineOf(wrapper, 'ready')).toBe('2 MB · 보안 검사 중이지만 보낼 수 있습니다')
expect(wrapper.find('#compose-send-why').exists()).toBe(false)
})
@@ -216,7 +216,7 @@ describe('ComposePage — 보내기 취소 · 예약 발송 (9단계)', () => {
expect(toasts().map((toast) => toast.kind)).toEqual(['error'])
})
it('일부가 받지 못하면 빨간 알림 "보냈지만 1명은 받지 못합니다 — …" 에도 [보내기 취소] (같은 마감)', async () => {
it('일부가 받지 못하면 빨간 알림 "보냈지만 1명은 받지 못합니다. …" 에도 [보내기 취소] (같은 마감)', async () => {
const { wrapper } = await mountCompose(server([undoReply([NOBODY])]))
await addRecipient(wrapper, 'doyun.lee@arasystem.it.kr')
await addRecipient(wrapper, NOBODY.email)
@@ -226,7 +226,7 @@ describe('ComposePage — 보내기 취소 · 예약 발송 (9단계)', () => {
expect(toasts()).toEqual([
expect.objectContaining({
kind: 'error',
message: '보냈지만 1명은 받지 못합니다 — nobody@arasystem.it.kr (없는 주소)',
message: '보냈지만 1명은 받지 못합니다. nobody@arasystem.it.kr (없는 주소)',
action: expect.objectContaining({ label: '보내기 취소' }),
expiresAt: NOW + 10_000,
}),
@@ -369,7 +369,7 @@ describe('ComposePage — 보내기 취소 · 예약 발송 (9단계)', () => {
const dialog = wrapper.get('#compose-schedule-title').element.closest('dialog')
expect(dialog?.hasAttribute('open')).toBe(true)
// 이 PC 의 검사로는 맞는 시각이다(시계가 어긋남) — 가까운 쪽(5분) 한도로 알린다
expect(wrapper.get('#compose-schedule-error').text()).toBe('지금부터 5분 뒤 이후의 시각을 고르세요.')
expect(wrapper.get('#compose-schedule-error').text()).toBe('5분 뒤보다 이른 시각은 고를 수 없습니다. 더 늦은 시각을 골라 주세요.')
expect((wrapper.get('[data-schedule-preset="custom"]').element as HTMLInputElement).checked).toBe(true)
const chosen = new Date(preset.at)
expect((wrapper.get('[data-schedule-day]').element as HTMLInputElement).value).toBe(formatDate(chosen))
+5 -5
View File
@@ -319,7 +319,7 @@ describe('ComposePage', () => {
expect(useToastStore().toasts).toEqual([
expect.objectContaining({
kind: 'error',
message: '보냈지만 1명은 받지 못했습니다 — nobody@arasystem.it.kr (없는 주소)',
message: '보냈지만 1명은 받지 못했습니다. nobody@arasystem.it.kr (없는 주소)',
// 성공 알림과 같은 곳 — 링크가 있으니 10초 (toast.store)
action: { label: '보낸편지함 보기', to: { name: 'mail-mailbox', params: { mailboxId: 'sent' } } },
}),
@@ -538,7 +538,7 @@ describe('ComposePage', () => {
const { wrapper } = await mountCompose('/mail/compose', replyApi)
expect(wrapper.get('.compose__attach-hint').text()).toBe(
'파일당 최대 10MB · 합계 30MB · 최대 20개 · 더 큰 파일은 링크로 보냅니다 (파일당 2GB · 10개까지)',
'파일당 최대 10MB · 합계 30MB · 최대 20개 · 더 큰 파일은 파일당 2GB, 10개까지 링크로 보냅니다',
)
})
@@ -547,7 +547,7 @@ describe('ComposePage', () => {
expect(useMailStore().quota).toBeNull()
expect(wrapper.get('.compose__attach-hint').text()).toBe(
'파일당 최대 20MB · 합계 25MB · 최대 20개 · 더 큰 파일은 링크로 보냅니다 (파일당 2GB · 10개까지)',
'파일당 최대 20MB · 합계 25MB · 최대 20개 · 더 큰 파일은 파일당 2GB, 10개까지 링크로 보냅니다',
)
})
@@ -1483,7 +1483,7 @@ describe('ComposePage', () => {
expect(dialogOpen(wrapper)).toBe(false)
expect(blocksUnload()).toBe(false)
expect(useToastStore().toasts.map((toast) => [toast.kind, toast.message])).toEqual([
['error', '보냈지만 1명은 받지 못했습니다 — nobody@arasystem.it.kr (받는 쪽이 거절)'],
['error', '보냈지만 1명은 받지 못했습니다. nobody@arasystem.it.kr (받는 쪽이 거절)'],
])
})
@@ -1567,7 +1567,7 @@ describe('ComposePage', () => {
await flushPromises()
expect(useToastStore().toasts.map((toast) => [toast.kind, toast.message])).toEqual([
['error', '보냈지만 1명은 받지 못했습니다 — infra@arasystem.it.kr (지금은 받을 수 없음)'],
['error', '보냈지만 1명은 받지 못했습니다. infra@arasystem.it.kr (지금은 받을 수 없음)'],
])
expect(router.currentRoute.value.fullPath).toBe('/mail/sent')
})
+1 -1
View File
@@ -1256,7 +1256,7 @@ watch(hasMissingLarge, (missing) => {
v-model="subject"
type="text"
class="compose__subject"
placeholder="제목을 입력하세요"
placeholder="제목을 입력해 주세요"
autocomplete="off"
:maxlength="MAIL_SUBJECT_MAX_LENGTH"
:disabled="preparing"
@@ -126,12 +126,12 @@ describe('예약됨 · 보낸편지함 (9단계)', () => {
}))
}
it('분할 보기 — 예약한 메일을 보낼 시각 차례로: 받는 사람 · 제목 · 보낼 시각(청록) · 머리 "예약됨 · 2통 · 보낼 시각 차례"', async () => {
it('분할 보기 — 예약한 메일을 보낼 시각 차례로: 받는 사람 · 제목 · 보낼 시각(청록) · 머리 "예약됨 · 2통 · 보낼 시각이 이른 순서"', async () => {
const { wrapper } = await mountAt('/mail/scheduled')
expect(requests.find((request) => request.url === '/scheduled-messages')?.params).toEqual({ limit: 20 })
expect(wrapper.get('#scheduled-list-title').text()).toBe('예약됨')
expect(wrapper.get('.list-header__count').text()).toBe('2통 · 보낼 시각 차례')
expect(wrapper.get('.list-header__count').text()).toBe('2통 · 보낼 시각이 이른 순서')
expect(rows(wrapper)).toEqual([
{
who: '받는 사람: 이도윤',
@@ -114,7 +114,7 @@ describe('MailboxPage — 검색 결과', () => {
expect(wrapper.text()).toContain('‘회의’에 맞는 메일이 없습니다')
expect(wrapper.text()).toContain(
'낱말 단위로 찾습니다. 붙여 쓴 말의 일부(‘서버이전’의 ‘서버’)나 제목 맨 앞의 말머리([공지] · Re:)로는 찾지 못합니다.',
'낱말 단위로 찾습니다. ‘서버이전’의 ‘서버’처럼 붙여 쓴 말의 일부나, 제목 맨 앞의 [공지]나 Re: 같은 말머리로는 찾지 못합니다.',
)
await wrapper.get('.search-empty__all').trigger('click')
await flushPromises()
@@ -124,7 +124,7 @@ describe('MailboxPage — 검색 결과', () => {
expect(listRequests().at(-1)?.params).toEqual({ q: '회의', limit: 20 })
// 전체에서 찾았으면 더 넓힐 곳이 없다 — 안내 줄만
expect(wrapper.find('.search-empty__all').exists()).toBe(false)
expect(wrapper.get('.search-scope__hint').text()).toBe('휴지통 · 스팸메일함은 빼고 찾습니다')
expect(wrapper.get('.search-scope__hint').text()).toBe('휴지통과 스팸메일함은 빼고 찾습니다')
})
it('범위 줄에서 "전체 메일함"을 누르면 기록을 쌓지 않고 전체에서 다시 찾고, "받은편지함"을 누르면 돌아온다', async () => {
@@ -167,7 +167,7 @@ describe('MailboxPage — 검색 결과', () => {
useMailStore().rememberSearchOrigin({ mailboxId: 'sent', location: '/mail/sent' })
const { wrapper, router } = await mountAt(`/mail/search?q=${Q}&in=sent`)
expect(wrapper.get('.search-header__close').attributes('title')).toBe('검색 닫기 — 보낸편지함으로')
expect(wrapper.get('.search-header__close').attributes('title')).toBe('검색을 닫고 보낸편지함으로 돌아가기')
await wrapper.get('.search-header__close').trigger('click')
await flushPromises()
@@ -147,10 +147,10 @@ describe('AccountAppPasswordsPage', () => {
const settings = wrapper.get('[data-mail-client-settings]').text()
expect(settings).toContain('mx1.arasystem.it.kr · 포트 993 · SSL/TLS')
expect(settings).toContain('mx1.arasystem.it.kr · 포트 465 · SSL/TLS')
expect(settings).toContain('ara.kim@arasystem.it.kr — 메일 주소 전체')
expect(settings).toContain('여기서 만든 앱 비밀번호 — 웹메일 비밀번호가 아닙니다')
expect(settings).toContain('ara.kim@arasystem.it.kr · 메일 주소 전체')
expect(settings).toContain('웹메일 비밀번호가 아닌, 여기서 만든 앱 비밀번호')
expect(wrapper.get('[data-app-password-notes]').text()).toContain(
'웹메일 비밀번호를 바꾸면 앱 비밀번호도 모두 지워집니다 — 메일 프로그램마다 새로 만들어 주세요.',
'웹메일 비밀번호를 바꾸면 앱 비밀번호도 모두 지워지니, 메일 프로그램마다 새로 만들어 주세요.',
)
})
@@ -214,7 +214,7 @@ describe('AccountAppPasswordsPage', () => {
expect(dialogOpen('app-password-create-title')).toBe(true)
expect(document.activeElement?.id).toBe('app-password-name')
expect(wrapper.get('#app-password-name').attributes('maxlength')).toBe('40')
expect(wrapper.get('#app-password-name-help').text()).toBe('어느 프로그램 · 기기에서 쓰는지 알아볼 이름입니다. 40자까지.')
expect(wrapper.get('#app-password-name-help').text()).toBe('어느 프로그램이나 기기에서 쓰는지 알아볼 수 있게 지어 주세요. 40자까지 쓸 수 있습니다.')
await create(wrapper, ' ')
expect(wrapper.get('#app-password-name-help').text()).toBe('이름을 넣어 주세요.')
@@ -237,7 +237,7 @@ describe('AccountAppPasswordsPage', () => {
expect(dialogOpen('app-password-create-title')).toBe(false)
expect(dialogOpen('app-password-result-title')).toBe(true)
expect(document.querySelector('#app-password-result-message')?.textContent?.trim()).toBe(
'‘집 노트북 메일’ — 메일 프로그램의 비밀번호 칸에 아래 값을 넣어 주세요.',
'‘집 노트북 메일’ 앱 비밀번호입니다. 메일 프로그램의 비밀번호 칸에 아래 값을 넣어 주세요.',
)
expect(wrapper.findAll('.result__group').map((group) => group.text())).toEqual([
'app_',
@@ -370,7 +370,7 @@ describe('AccountAppPasswordsPage', () => {
replies[0]?.(fail(400, 'VAL_001'))
await flushPromises()
expect(wrapper.get('#app-password-name-help').text()).toBe('어느 프로그램 · 기기에서 쓰는지 알아볼 이름입니다. 40자까지.')
expect(wrapper.get('#app-password-name-help').text()).toBe('어느 프로그램이나 기기에서 쓰는지 알아볼 수 있게 지어 주세요. 40자까지 쓸 수 있습니다.')
expect(toastMessages()).toEqual([])
})
@@ -193,7 +193,7 @@ describe('AccountLoginHistoryPage', () => {
])
expect(wrapper.get('[data-sessions-end-others]').text()).toBe('다른 곳 모두 로그아웃')
const note = wrapper.get('[data-sessions-note]')
expect(note.text()).toBe('웹메일 로그인만 보입니다. 메일 프로그램은 앱 비밀번호를 지워 끊습니다.')
expect(note.text()).toBe('웹메일 로그인만 보입니다. 메일 프로그램의 연결을 끊으려면 앱 비밀번호를 지워 주세요.')
expect(note.get('a').attributes('href')).toBe('/settings/account/app-passwords')
})
@@ -204,7 +204,7 @@ describe('AccountLoginHistoryPage', () => {
expect(wrapper.get('[data-session="old"] [data-session-device]').text()).toBe('알 수 없는 브라우저')
})
it('줄의 [로그아웃]은 묻지 않고 바로 끊는다 — 「‘Edge · Windows’의 로그인을 끊었습니다.」 · 줄이 빠지고 초점은 다음 줄', async () => {
it('줄의 [로그아웃]은 묻지 않고 바로 끊는다 — 「‘Edge · Windows’에서 로그아웃했습니다.」 · 줄이 빠지고 초점은 다음 줄', async () => {
const wrapper = await mountPage()
const button = wrapper.findAll('[data-session-end]')[0]
;(button?.element as HTMLButtonElement).focus()
@@ -214,7 +214,7 @@ describe('AccountLoginHistoryPage', () => {
expect(requests.filter((request) => request.method === 'DELETE').map((request) => request.url)).toEqual([
'/auth/sessions/edge',
])
expect(toastMessages()).toEqual(['‘Edge · Windows’의 로그인을 끊었습니다.'])
expect(toastMessages()).toEqual(['‘Edge · Windows’에서 로그아웃했습니다.'])
expect(sessionRows(wrapper).map((row) => row[0])).toEqual(['Chrome · Windows', 'Safari · iOS'])
expect(wrapper.get('[data-sessions-count]').text()).toBe('웹메일 2곳')
expect(document.activeElement?.getAttribute('aria-label')).toBe('‘Safari · iOS’ 로그아웃')
@@ -225,11 +225,11 @@ describe('AccountLoginHistoryPage', () => {
await wrapper.findAll('[data-session-end]')[1]?.trigger('click')
await flushPromises()
expect(toastMessages()).toEqual(['‘Safari · iOS’의 로그인을 끊었습니다.'])
expect(toastMessages()).toEqual(['‘Safari · iOS’에서 로그아웃했습니다.'])
expect(sessionRows(wrapper)).toHaveLength(2)
})
it('[다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」 · 끊을 곳의 이름) → 「다른 곳의 로그인 2곳을 끊었습니다.」 — 지금 이 브라우저만 남는다', async () => {
it('[다른 곳 모두 로그아웃] → 확인 창(처음 초점 「취소」 · 끊을 곳의 이름) → 「다른 2곳에서 로그아웃했습니다.」 — 지금 이 브라우저만 남는다', async () => {
const wrapper = await mountPage()
await wrapper.get('[data-sessions-end-others]').trigger('click')
await flushPromises()
@@ -238,9 +238,9 @@ describe('AccountLoginHistoryPage', () => {
expect(dialog?.hasAttribute('open')).toBe(true)
expect(dialog?.getAttribute('role')).toBe('alertdialog')
expect(document.querySelector('#end-other-sessions-message')?.textContent?.trim()).toBe(
'이 브라우저를 뺀 2곳(Edge · Windows, Safari · iOS)의 웹메일 로그인을 끊습니다. 끊긴 곳은 다음 동작에서 바로 로그인 화면으로 돌아갑니다.',
'이 브라우저를 뺀 2곳(Edge · Windows, Safari · iOS)의 웹메일에서 로그아웃합니다. 그곳에서 다음에 무엇이든 누르면 바로 로그인 화면이 나옵니다.',
)
expect(dialog?.textContent).toContain('메일 프로그램은 끊기지 않습니다 — 앱 비밀번호를 지워 끊어 주세요.')
expect(dialog?.textContent).toContain('메일 프로그램은 로그아웃되지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요.')
expect(document.activeElement?.hasAttribute('data-end-others-cancel')).toBe(true)
// 데이터를 지우는 일이 아니다 — 청록 주 단추
expect(wrapper.get('[data-end-others-confirm]').classes()).not.toContain('modal-dialog__action--danger')
@@ -249,12 +249,12 @@ describe('AccountLoginHistoryPage', () => {
await flushPromises()
expect(requests.filter((request) => request.method === 'DELETE').map((request) => request.url)).toEqual(['/auth/sessions'])
expect(toastMessages()).toEqual(['다른 곳의 로그인 2곳을 끊었습니다.'])
expect(toastMessages()).toEqual(['다른 2곳에서 로그아웃했습니다.'])
expect(dialog?.hasAttribute('open')).toBe(false)
expect(sessionRows(wrapper).map((row) => row[0])).toEqual(['Chrome · Windows'])
expect(wrapper.find('[data-sessions-end-others]').exists()).toBe(false)
expect(wrapper.get('[data-sessions-note]').text()).toBe(
'다른 곳에 로그인된 브라우저가 없습니다. 웹메일 로그인만 보입니다. 메일 프로그램은 앱 비밀번호를 지워 끊습니다.',
'다른 곳에 로그인된 브라우저가 없습니다. 웹메일 로그인만 보입니다. 메일 프로그램의 연결을 끊으려면 앱 비밀번호를 지워 주세요.',
)
expect(document.activeElement?.id).toBe('login-sessions-title')
})
@@ -273,10 +273,10 @@ describe('AccountLoginHistoryPage', () => {
await wrapper.get('[data-sessions-end-others]').trigger('click')
await flushPromises()
expect(document.querySelector('#end-other-sessions-title')?.textContent?.trim()).toBe(
'이 브라우저를 포함해 모든 곳의 로그인을 끊을까요?',
'이 브라우저를 포함해 모든 곳에서 로그아웃할까요?',
)
expect(document.querySelector('#end-other-sessions-message')?.textContent?.trim()).toBe(
'이 브라우저를 알아보지 못해 이 브라우저를 포함한 3곳(Chrome · Windows, Edge · Windows, Safari · iOS)의 웹메일 로그인을 끊습니다. 이 브라우저도 로그아웃되어 로그인 화면으로 돌아갑니다.',
'이 브라우저를 알아보지 못해서 이 브라우저를 포함한 3곳(Chrome · Windows, Edge · Windows, Safari · iOS)의 웹메일에서 로그아웃합니다. 이 브라우저도 로그아웃되어 로그인 화면으로 돌아갑니다.',
)
await wrapper.get('[data-end-others-confirm]').trigger('click')
@@ -288,7 +288,7 @@ describe('AccountLoginHistoryPage', () => {
'GET /auth/sessions',
'POST /auth/refresh',
])
expect(toastMessages()).toContain('이 브라우저를 포함한 3곳의 로그인을 끊었습니다.')
expect(toastMessages()).toContain('이 브라우저를 포함해 3곳에서 로그아웃했습니다.')
// 빈 목록을 그려 두지 않는다 — 다시 받은 요청이 세션 만료로 이어졌다
expect(wrapper.find('[data-session]').exists()).toBe(false)
expect(useAuthStore().sessionExpired).toBe(true)
@@ -329,11 +329,11 @@ describe('AccountLoginHistoryPage', () => {
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 웹메일과 메일 프로그램')
expect(eventRows(wrapper)).toEqual([
[time(0), '웹메일', '211.186.95.252', 'Chrome · Windows', '성공 · 2단계 인증'],
[time(1), '웹메일', '211.186.95.252', 'Chrome · Windows', '실패 — 코드 틀림'],
[time(0), '웹메일', '211.186.95.252', 'Chrome · Windows', '2단계 인증으로 성공'],
[time(1), '웹메일', '211.186.95.252', 'Chrome · Windows', '코드가 틀려 실패'],
[time(2), '메일 프로그램 · IMAP앱 비밀번호 ‘회사 PC 아웃룩’', '211.186.95.252', '—알 수 없음', '성공'],
[time(3), '메일 프로그램 · SMTP', '45.133.10.21', '—알 수 없음', '실패 — 앱 비밀번호 필요'],
[time(4), '웹메일', '211.186.95.252', '알 수 없는 브라우저', '실패 — 비밀번호 틀림'],
[time(3), '메일 프로그램 · SMTP', '45.133.10.21', '—알 수 없음', '앱 비밀번호가 필요해 실패'],
[time(4), '웹메일', '211.186.95.252', '알 수 없는 브라우저', '비밀번호가 틀려 실패'],
])
expect(eventRows(wrapper)[0]?.[0]).toBe('오늘 오전 9:30')
expect(wrapper.findAll('[data-event-result]').map((cell) => cell.classes().includes('events__result--failure'))).toEqual([
@@ -363,9 +363,9 @@ describe('AccountLoginHistoryPage', () => {
expect(all.attributes('aria-pressed')).toBe('false')
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 실패 4번')
expect(eventRows(wrapper).map((row) => row[4])).toEqual([
'실패 — 코드 틀림',
'실패 — 앱 비밀번호 필요',
'실패 — 비밀번호 틀림',
'코드가 틀려 실패',
'앱 비밀번호가 필요해 실패',
'비밀번호가 틀려 실패',
])
// 같은 보기를 다시 누르면 받지 않는다
@@ -374,7 +374,7 @@ describe('AccountLoginHistoryPage', () => {
expect(eventRequests()).toHaveLength(2)
})
it('빈 상태(실패만) — "최근 90일 동안 실패한 로그인이 없습니다" / "[전체]를 누르면 성공한 로그인도 봅니다."', async () => {
it('빈 상태(실패만) — "최근 90일 동안 실패한 로그인이 없습니다" / "[전체]를 누르면 성공한 로그인도 볼 수 있습니다."', async () => {
const wrapper = await mountPage({
events: (request) => (request.params.isSuccess === false ? ok(pageOf([], null, 0)) : ok(pageOf(EVENTS))),
})
@@ -383,7 +383,7 @@ describe('AccountLoginHistoryPage', () => {
expect(wrapper.find('[data-login-event]').exists()).toBe(false)
expect(wrapper.get('[data-events-empty]').text()).toContain('최근 90일 동안 실패한 로그인이 없습니다')
expect(wrapper.get('[data-events-empty]').text()).toContain('[전체]를 누르면 성공한 로그인도 봅니다.')
expect(wrapper.get('[data-events-empty]').text()).toContain('[전체]를 누르면 성공한 로그인도 볼 수 있습니다.')
expect(wrapper.get('[data-events-hint]').text()).toBe('최근 90일 · 실패 0번')
})
@@ -398,7 +398,7 @@ describe('AccountLoginHistoryPage', () => {
},
})
expect(wrapper.get('[data-events-more]').text()).toBe('더 보기')
expect(wrapper.text()).toContain('최근 순서 · 20개씩. 90일이 지난 기록은 지웁니다.')
expect(wrapper.text()).toContain('최근 순서로 20개씩 보입니다. 90일이 지난 기록은 지워집니다.')
await wrapper.get('[data-events-filter="failures"]').trigger('click')
await flushPromises()
@@ -53,12 +53,12 @@ describe('AccountMailClientsPage', () => {
const wrapper = await mountPage()
expect(wrapper.get('h1').text()).toBe('메일 프로그램 연결')
expect(wrapper.text()).toContain('아웃룩 · 썬더버드 · 휴대폰 메일 앱에서 회사 메일을 보는 방법입니다.')
expect(wrapper.text()).toContain('아웃룩, 썬더버드, 휴대폰 메일 앱에서 회사 메일을 보는 방법입니다.')
expect(settingsRows(wrapper)).toEqual([
'mx1.arasystem.it.kr · 포트 993 · SSL/TLS',
'mx1.arasystem.it.kr · 포트 465 · SSL/TLS',
`${USER.email} — 메일 주소 전체`,
'앱 비밀번호 — 웹메일 비밀번호가 아닙니다',
`${USER.email} · 메일 주소 전체`,
'웹메일 비밀번호가 아닌 앱 비밀번호',
])
expect(requests.map((request) => request.url)).toEqual(['/two-factor'])
})
@@ -72,10 +72,10 @@ describe('AccountMailClientsPage', () => {
expect(wrapper.get('[data-mail-clients-create]').attributes('href')).toBe('/settings/account/app-passwords')
})
it('켜지 않은 직원 — 비밀번호 줄은 「앱 비밀번호(권장) …」, 청록 상자 「앱 비밀번호로 연결하기를 권합니다」', async () => {
it('켜지 않은 직원 — 비밀번호 줄은 「앱 비밀번호를 권합니다. …」, 청록 상자 「앱 비밀번호로 연결하기를 권합니다」', async () => {
const wrapper = await mountPage(() => ok(OFF))
expect(settingsRows(wrapper).at(-1)).toBe('앱 비밀번호(권장) — 2단계 인증을 켜지 않았으면 웹메일 비밀번호도 됩니다')
expect(settingsRows(wrapper).at(-1)).toBe('앱 비밀번호를 권합니다. 2단계 인증을 켜지 않았다면 웹메일 비밀번호로도 연결됩니다.')
const notice = wrapper.get('[data-mail-clients-notice]')
expect(notice.attributes('data-mail-clients-notice')).toBe('recommend')
expect(notice.text()).toContain('앱 비밀번호로 연결하기를 권합니다')
@@ -92,7 +92,7 @@ describe('AccountMailClientsPage', () => {
const wrapper = await mountPage()
expect(wrapper.get('[data-mail-clients-cloud-warning]').text()).toBe(
'새 Outlook · 휴대폰 Outlook 앱은 메일을 Microsoft 클라우드를 거쳐 받습니다 — 회사 정책에 따라 쓰지 않는 것이 좋습니다.',
'새 Outlook과 휴대폰 Outlook 앱은 메일을 Microsoft 클라우드를 거쳐 받습니다. 회사 정책에 따라 쓰지 않는 것이 좋습니다.',
)
const buttons = wrapper.findAll('[data-mail-client] h3 button')
expect(buttons.map((button) => button.text())).toEqual([
@@ -118,14 +118,14 @@ describe('AccountMailClientsPage', () => {
expect(panel.attributes('role')).toBe('region')
expect(panel.attributes('aria-labelledby')).toBe('mail-client-outlook-button')
expect(panel.findAll('li')).toHaveLength(5)
expect(panel.text()).toContain('Outlook 에서 [파일] → [계정 추가]를 누릅니다.')
expect(panel.text()).toContain('Outlook에서 [파일] → [계정 추가]를 누릅니다.')
// 호스트 이름은 굵게
expect(panel.get('strong').text()).toBe('mx1.arasystem.it.kr')
await wrapper.get('#mail-client-android-button').trigger('click')
expect(outlook.attributes('aria-expanded')).toBe('false')
expect(wrapper.find('#mail-client-outlook-panel').exists()).toBe(false)
expect(wrapper.get('#mail-client-android-panel').text()).toContain('보안 유형은 「SSL/TLS」를 넣고 [다음]을 누릅니다.')
expect(wrapper.get('#mail-client-android-panel').text()).toContain('보안 유형은 [SSL/TLS]를 고른 뒤 [다음]을 누릅니다.')
await wrapper.get('#mail-client-android-button').trigger('click')
expect(wrapper.findAll('[role="region"]')).toHaveLength(0)
@@ -136,6 +136,6 @@ describe('AccountMailClientsPage', () => {
const notes = wrapper.get('[data-mail-clients-notes]').findAll('p').map((note) => note.text())
expect(notes).toHaveLength(4)
expect(notes[1]).toContain('POP3 로는 연결할 수 없습니다 — 계정 유형은 IMAP 을 골라 주세요.')
expect(notes[1]).toContain('POP3로는 연결할 수 없으니 계정 유형은 IMAP을 골라 주세요.')
})
})
@@ -79,18 +79,18 @@ describe('AccountPasswordPage', () => {
expect(wrapper.get('#password-current').attributes('autocomplete')).toBe('current-password')
expect(wrapper.get('#password-next').attributes('autocomplete')).toBe('new-password')
expect(wrapper.get('#password-next-hint').text()).toBe(
'8자 이상 · 지금 비밀번호와 달라야 합니다. 영문 · 숫자 · 기호를 섞으면 더 안전합니다.',
'8자 이상이고 지금 비밀번호와 달라야 합니다. 영문, 숫자, 기호를 섞으면 더 안전합니다.',
)
// 모든 직원 — 다른 곳 로그인이 끊기고 앱 비밀번호도 모두 지워진다 (12단계)
expect(wrapper.get('[data-password-note]').text()).toContain(
'바꾸면 이 브라우저를 뺀 다른 기기 · 브라우저의 웹메일 로그인이 끊깁니다.',
'바꾸면 이 브라우저를 빼고 다른 기기와 브라우저의 웹메일에서 로그아웃됩니다.',
)
expect(wrapper.get('[data-password-note]').text()).toContain(
'앱 비밀번호도 모두 지워집니다 — 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요.',
'앱 비밀번호도 모두 지워지니, 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요.',
)
// 2단계 인증이 꺼진 직원 — 머리 설명은 그대로, 코드 칸이 없다
expect(wrapper.get('.settings-page__description').text()).toBe(
'웹메일과 휴대폰 메일 앱(IMAP)이 함께 쓰는 메일 계정의 비밀번호입니다.',
'웹메일에 로그인할 때와 휴대폰 메일 앱을 연결할 때 함께 쓰는 비밀번호입니다.',
)
expect(wrapper.find('#password-code').exists()).toBe(false)
@@ -148,7 +148,7 @@ describe('AccountPasswordPage', () => {
// 서버가 엔진을 두 번 부르고 다른 세션을 끊는다 — 요청 한도는 보통(API_TIMEOUT_MS)보다 긴 30초
expect(PASSWORD_CHANGE_TIMEOUT_MS).toBe(30_000)
expect(passwordRequests()[0]?.config.timeout).toBe(PASSWORD_CHANGE_TIMEOUT_MS)
expect(wrapper.get('#password-status').text()).toBe('비밀번호를 바꿨습니다. 다른 기기 · 브라우저의 로그인은 끊었습니다.')
expect(wrapper.get('#password-status').text()).toBe('비밀번호를 바꿨습니다. 다른 기기와 브라우저에서는 로그아웃했습니다.')
for (const id of ['password-current', 'password-next', 'password-confirm']) {
expect((wrapper.get(`#${id}`).element as HTMLInputElement).value).toBe('')
}
@@ -234,7 +234,7 @@ describe('AccountPasswordPage', () => {
autocomplete: 'one-time-code',
'aria-describedby': 'password-code-hint',
})
expect(wrapper.get('#password-code-hint').text()).toBe('코드는 30초마다 바뀝니다 — 다른 칸을 다 쓴 뒤 넣어 주세요.')
expect(wrapper.get('#password-code-hint').text()).toBe('코드는 30초마다 바뀌니 다른 칸을 다 채운 뒤에 넣어 주세요.')
})
it('코드는 숫자만 받는다 — 붙여 넣은 "123 456" · "123-456" 은 다듬고, 6자리가 아니면 보내지 않는다(사전 문구 AUTH_007)', async () => {
@@ -120,10 +120,10 @@ describe('AccountTwoFactorPage', () => {
expect(wrapper.find('[data-two-factor-notes]').exists()).toBe(false)
})
it('켜짐 — 「켜짐 — 2026년 10월 31일부터」 · [2단계 인증 끄기] · 안내 상자 / 켠 때를 모르면 「켜짐」', async () => {
it('켜짐 — 「2026년 10월 31일부터 켜짐」 · [2단계 인증 끄기] · 안내 상자 / 켠 때를 모르면 「켜짐」', async () => {
const wrapper = await mountPage({ status: () => ok(ON) })
expect(wrapper.get('[data-two-factor-label]').text()).toBe('켜짐 — 2026년 10월 31일부터')
expect(wrapper.get('[data-two-factor-label]').text()).toBe('2026년 10월 31일부터 켜짐')
expect(wrapper.get('[data-two-factor-label]').classes()).toContain('two-factor__state-label--on')
expect(wrapper.get('[data-two-factor-disable]').text()).toBe('2단계 인증 끄기')
expect(wrapper.find('[data-two-factor-warning]').exists()).toBe(false)
@@ -176,7 +176,7 @@ describe('AccountTwoFactorPage', () => {
['K7QF', '2M4X', 'HB6R', 'TZ3N'],
['PW5C', 'YL7D', 'JQ2V', 'MGAS'],
])
expect(wrapper.text()).toContain('앱에는 「ARA SYSTEM (ara.kim@arasystem.it.kr)」로 저장됩니다.')
expect(wrapper.text()).toContain('앱에는 ‘ARA SYSTEM (ara.kim@arasystem.it.kr)’로 저장됩니다.')
expect(document.activeElement?.id).toBe('two-factor-enable-code')
expect(wrapper.get('#two-factor-enable-code').attributes()).toMatchObject({
inputmode: 'numeric',
@@ -243,11 +243,11 @@ describe('AccountTwoFactorPage', () => {
// 서버가 엔진을 여러 번 부른다 — 한도는 보통(10초)보다 긴 30초
expect(calls('/two-factor/activation')[0]?.config.timeout).toBe(TWO_FACTOR_CHANGE_TIMEOUT_MS)
expect(enableDialog()?.hasAttribute('open')).toBe(false)
expect(wrapper.get('[data-two-factor-label]').text()).toBe('켜짐 — 2026년 10월 31일부터')
expect(wrapper.get('[data-two-factor-label]').text()).toBe('2026년 10월 31일부터 켜짐')
const done = wrapper.get('[data-two-factor-done]')
expect(done.attributes('role')).toBe('status')
expect(done.text()).toContain('2단계 인증을 켰습니다')
expect(done.text()).toContain('메일 프로그램(Outlook · 썬더버드 · 휴대폰 메일 앱)은 다시 연결할 때 앱 비밀번호가 필요합니다.')
expect(done.text()).toContain('Outlook, 썬더버드, 휴대폰 메일 앱 같은 메일 프로그램은 다시 연결할 때 앱 비밀번호가 필요합니다.')
expect(done.get('a').text()).toBe('앱 비밀번호 만들기')
expect(done.get('a').attributes('href')).toBe('/settings/account/app-passwords')
expect(document.activeElement).toBe(done.element)
@@ -668,7 +668,7 @@ describe('AccountTwoFactorPage — 필수인 직원 (13단계)', () => {
expect(wrapper.get('[data-two-factor-locked]').text()).toBe('회사 정책에 따라 끌 수 없습니다')
expect(wrapper.find('[data-two-factor-grace]').exists()).toBe(false)
const notes = wrapper.get('[data-two-factor-notes]').text()
expect(notes).toContain('휴대폰을 바꿀 때는 관리자에게 2단계 인증 해제를 요청해 주세요 — 다음 로그인 때 새 휴대폰으로 다시 켭니다.')
expect(notes).toContain('휴대폰을 바꿀 때는 관리자에게 2단계 인증을 풀어 달라고 요청해 주세요. 다음에 로그인할 때 새 휴대폰으로 다시 켜면 됩니다.')
expect(notes).toContain('휴대폰을 잃어버렸거나 인증 앱을 지웠으면 관리자에게 문의해 주세요.')
})
})
@@ -119,11 +119,11 @@ describe('MailBlocksPage', () => {
expect(empty?.[1]).not.toMatch(/display:\s*none/)
})
it('비었으면 "차단한 주소가 없습니다. 메일의 ⋯ › 보낸 사람 차단으로도 막을 수 있습니다."', async () => {
it('비었으면 "차단한 주소가 없습니다. 메일을 읽다가 [⋯] → [보낸 사람 차단]으로도 막을 수 있습니다."', async () => {
const wrapper = await mountPage({ blocks: [] })
expect(wrapper.get('[data-block-empty]').text()).toBe(
'차단한 주소가 없습니다. 메일의 ⋯ › 보낸 사람 차단으로도 막을 수 있습니다.',
'차단한 주소가 없습니다. 메일을 읽다가 [⋯] → [보낸 사람 차단]으로도 막을 수 있습니다.',
)
expect(wrapper.get('[data-block-count]').text()).toBe('0 / 500')
})
@@ -150,12 +150,12 @@ describe('MailBlocksPage', () => {
it.each([
['형식', 'kim@', '메일 주소나 @도메인 형식으로 써 주세요.'],
['이미 차단함', 'NEWS@cloudbrief.example', '이미 차단한 주소입니다.'],
['내 주소', 'ara.kim@arasystem.it.kr', '내 주소 · 사내 도메인은 차단할 수 없습니다.'],
['사내 도메인', '@arasystem.it.kr', '내 주소 · 사내 도메인은 차단할 수 없습니다.'],
['내 주소', 'ara.kim@arasystem.it.kr', '내 주소와 사내 도메인은 차단할 수 없습니다.'],
['사내 도메인', '@arasystem.it.kr', '내 주소와 사내 도메인은 차단할 수 없습니다.'],
[
'여러 사람이 쓰는 메일 서비스',
'@gmail.com',
'gmail.com 처럼 여러 사람이 쓰는 메일 서비스는 도메인으로 막을 수 없습니다 — 주소로 막아 주세요.',
'gmail.com처럼 여러 사람이 쓰는 메일 서비스는 도메인 전체를 막을 수 없습니다. 주소를 하나씩 넣어 막아 주세요.',
],
])('보내기 전 검사 — %s: 칸 아래 오류(빨강) · 잘못된 입력, 보내지 않는다', async (_label, value, text) => {
const wrapper = await mountPage()
@@ -247,7 +247,7 @@ describe('MailBlocksPage', () => {
})
describe('엔진에서 우리 규칙이 꺼져 있으면(isActive: false) 머리 아래 노란 줄 — 단추 없음', () => {
const INACTIVE = '지금은 차단이 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 — 주소를 차단하거나 풀면 다시 켜집니다.'
const INACTIVE = '다른 곳에서 메일 설정이 바뀌어 지금은 차단이 되지 않습니다. 주소를 차단하거나 풀면 다시 켜집니다.'
it('차단한 주소가 있을 때만 보인다(role=status) — 켜져 있거나 비었으면 없다', async () => {
const off = await mountPage({ isActive: false })
@@ -237,24 +237,24 @@ describe('MailFilterPage', () => {
expect(wrapper.get('ol').attributes('aria-busy')).toBeUndefined()
expect(rows(wrapper).map((item) => item.classes('is-off'))).toEqual([true, false, false, true, false])
expect(wrapper.findAll('[data-filter-missing]').map((item) => item.text())).toEqual([
'옮길 메일함이 지워져 받은편지함으로 들어옵니다 — 눌러서 고치세요.',
'옮길 메일함이 지워져 메일이 받은편지함으로 들어옵니다. 눌러서 고쳐 주세요.',
])
expect(row(wrapper, 'r5').get('[data-filter-edit]').attributes('aria-label')).toBe('‘알파 프로젝트’ 필터 고치기')
// 맨 위의 ▲ · 맨 아래의 ▼ 는 누를 수 없다 (초점은 그대로 받는다)
expect(row(wrapper, 'r1').get('[data-filter-up]').attributes('aria-disabled')).toBe('true')
expect(row(wrapper, 'r1').get('[data-filter-down]').attributes('aria-disabled')).toBeUndefined()
expect(row(wrapper, 'r5').get('[data-filter-down]').attributes('aria-disabled')).toBe('true')
expect(wrapper.get('[data-filter-count]').text()).toBe('필터 5개 · 50개까지 만들 수 있습니다. 행을 누르면 고칩니다.')
expect(wrapper.get('[data-filter-count]').text()).toBe('필터 5개를 만들었고, 50개까지 만들 수 있습니다. 줄을 누르면 고칠 수 있습니다.')
expect(wrapper.get('[data-filter-notes]').findAll('p').map((note) => note.text())).toEqual([
'·위에서부터 차례로 적용합니다. 메일함으로 옮기는 필터에 맞으면 그 아래 필터는 적용하지 않습니다 — 순서는 ▲ ▼ 로 바꿉니다.',
'·이미 받은 메일에도 적용하려면 필터를 저장할 때 「받은편지함에 이미 있는 메일에도 적용」을 고릅니다. 받은편지함의 메일만 보고, 그 필터 하나만 적용합니다.',
'·스팸으로 판정되어 들어오는 메일에는 필터를 적용하지 않습니다 — 그대로 스팸메일함으로 갑니다.',
'·전달은 사내 주소로만, 새로 오는 메일만 합니다. 메일 한 통은 한 곳으로만 전달됩니다(위의 필터가 먼저). 전달해도 내 메일함에 그대로 남습니다.',
'·부재중 자동 응답은 필터와 함께 동작합니다.',
'·위에 있는 필터부터 차례로 메일을 정리합니다. 메일함으로 옮기는 필터에 맞은 메일은 그 아래 필터를 거치지 않습니다. 순서는 ▲ ▼ 로 바꿀 수 있습니다.',
'·이미 받은 메일도 정리하려면 필터를 저장할 때 [받은편지함에 이미 있는 메일에도 적용]을 골라 주세요. 받은편지함에 있는 메일만 그 필터 하나로 정리합니다.',
'·스팸으로 분류된 메일은 필터를 거치지 않고 그대로 스팸메일함으로 갑니다.',
'·전달은 새로 오는 메일만 사내 주소로 합니다. 메일 한 통은 한 곳으로만 전달되고, 전달하는 필터가 여럿이면 위에 있는 필터가 먼저입니다. 전달해도 내 메일함에 그대로 남습니다.',
'·필터가 메일을 옮겨도 부재중 자동 응답은 그대로 보냅니다.',
])
expect(wrapper.get('[data-filter-note-apply] strong').text()).toBe('「받은편지함에 이미 있는 메일에도 적용」')
expect(wrapper.get('[data-filter-note-apply] strong').text()).toBe('[받은편지함에 이미 있는 메일에도 적용]')
expect(wrapper.get('.settings-page__description').text()).toBe(
'받은 메일을 조건에 따라 메일함으로 옮기거나, 읽음 · 중요로 표시하거나, 동료에게 전달합니다. 새로 오는 메일에 적용하고, 고를 때는 받은편지함에 이미 있는 메일에도 적용합니다.',
'받은 메일을 조건에 따라 메일함으로 옮기거나, 읽음이나 중요로 표시하거나, 동료에게 전달합니다. 새로 오는 메일을 정리하고, 저장할 때 고르면 받은편지함에 이미 있는 메일도 함께 정리합니다.',
)
expect(wrapper.find('[data-filter-empty]').exists()).toBe(false)
expect(wrapper.find('[data-filter-conflict]').exists()).toBe(false)
@@ -403,7 +403,7 @@ describe('MailFilterPage', () => {
await save(wrapper)
expect(wrapper.get('#mail-filter-condition-0-error').text()).toBe(
'‘${’ 는 쓸 수 없습니다 — 메일 서버가 다른 뜻으로 읽습니다.',
'‘${’는 필터에 쓸 수 없습니다. 이 글자를 빼고 넣어 주세요.',
)
expect(wrapper.get('#mail-filter-forward-help').text()).toBe('사내 주소(@arasystem.it.kr)로만 전달할 수 있습니다.')
expect(document.activeElement).toBe(value.element)
@@ -477,7 +477,7 @@ describe('MailFilterPage', () => {
const select = wrapper.get('#mail-filter-move-target')
const options = select.findAll('option')
expect(options.map((option) => option.text())).toEqual([
'(지워진 메일함) — 다른 메일함을 고르세요',
'지워진 메일함이니 다른 메일함을 골라 주세요',
'받은편지함',
'거래처',
'전체 공지',
@@ -721,7 +721,7 @@ describe('MailFilterPage', () => {
const { wrapper } = await mountPage({ filters: { ...SET, isActive: false } })
const bar = wrapper.get('[data-filter-inactive]')
expect(bar.text()).toContain('지금은 필터와 부재중 자동 응답이 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다.')
expect(bar.text()).toContain('다른 곳에서 메일 설정이 바뀌어 지금은 필터와 부재중 자동 응답이 되지 않습니다.')
await click(bar.get('button'))
expect(puts()).toEqual([{ rules: RULES, version: 'v1' }])
@@ -820,7 +820,7 @@ describe('MailFilterPage', () => {
const applyHelp = (wrapper: VueWrapper) => wrapper.get('#mail-filter-apply-help').text()
const runs = () =>
requests.filter((request) => request.url === '/mail-filter-runs').map((request) => request.data as MailFilterRunRequest)
const HELP = '저장한 뒤 받은편지함에서 조건에 맞는 메일을 찾아 바로 적용합니다.'
const HELP = '저장하면 받은편지함에서 조건에 맞는 메일을 찾아 바로 정리합니다.'
it('할 일 아래 — 창을 열 때마다 꺼져 있고 설명은 칸의 aria-describedby, 전달도 하면 설명에 더한다 · 상자 아래 스팸 한 줄', async () => {
const { wrapper } = await mountPage()
@@ -831,7 +831,7 @@ describe('MailFilterPage', () => {
expect(applyBox(wrapper).attributes('aria-describedby')).toBe('mail-filter-apply-help')
expect(wrapper.get('[data-filter-apply] label').text()).toBe('받은편지함에 이미 있는 메일에도 적용')
expect(applyHelp(wrapper)).toBe(HELP)
expect(wrapper.get('[data-filter-apply-spam]').text()).toBe('스팸으로 판정되어 들어오는 메일에는 적용하지 않습니다.')
expect(wrapper.get('[data-filter-apply-spam]').text()).toBe('스팸으로 분류된 메일은 필터를 거치지 않습니다.')
// 6단계의 안내 띠는 없다
expect(wrapper.text()).not.toContain('새로 오는 메일에만 적용됩니다')
@@ -852,7 +852,7 @@ describe('MailFilterPage', () => {
expect(applyBox(wrapper).attributes('disabled')).toBeDefined()
expect(wrapper.get('[data-filter-apply] label').classes()).toContain('is-disabled')
expect(applyHelp(wrapper)).toBe('전달만 하는 필터는 이미 받은 메일에 할 일이 없습니다 — 전달은 새로 오는 메일에만 합니다.')
expect(applyHelp(wrapper)).toBe('전달은 새로 오는 메일에만 해서, 전달만 하는 필터는 이미 받은 메일에 할 일이 없습니다.')
await wrapper.get('#mail-filter-read').setValue(true)
expect(applyBox(wrapper).attributes('disabled')).toBeUndefined()
@@ -863,7 +863,7 @@ describe('MailFilterPage', () => {
const { wrapper } = await mountPage()
await openRule(wrapper, 'r5')
expect(applyBox(wrapper).attributes('disabled')).toBeDefined()
expect(applyHelp(wrapper)).toBe('옮길 메일함이 지워져 이미 받은 메일에 할 일이 없습니다 — 다른 메일함을 고르세요.')
expect(applyHelp(wrapper)).toBe('옮길 메일함이 지워져 이미 받은 메일에 할 일이 없습니다. 다른 메일함을 골라 주세요.')
await click(wrapper.get('#mail-filter-cancel'))
await openRule(wrapper, 'r2')
@@ -927,7 +927,7 @@ describe('MailFilterPage', () => {
{ ...rule, cursor: 'c1' },
])
expect(useToastStore().toasts.map((toast) => [toast.kind, toast.message])).toEqual([
['progress', '‘거래처 메일’ 필터를 받은편지함에 적용하는 중 · 4,500 / 8,412통 확인'],
['progress', '‘거래처 메일’ 필터로 받은편지함을 정리하는 중 · 4,500 / 8,412통 확인'],
])
second.finish(step(3912, 7, 0, null))
@@ -936,7 +936,7 @@ describe('MailFilterPage', () => {
expect(useToastStore().toasts).toHaveLength(1)
expect(done).toMatchObject({
kind: 'success',
message: '‘거래처 메일’ 필터를 받은편지함의 메일 37통에 적용했습니다.',
message: '‘거래처 메일’ 필터로 받은편지함의 메일 37통을 정리했습니다.',
action: { label: '거래처 보기', to: { name: 'mail-mailbox', params: { mailboxId: 'vendor' } } },
})
expect(toasts()).not.toContain('필터를 저장했습니다.')
@@ -952,7 +952,7 @@ describe('MailFilterPage', () => {
await openRule(wrapper, 'r2')
expect(applyBox(wrapper).attributes('disabled')).toBeDefined()
expect(applyHelp(wrapper)).toBe('다른 필터를 받은편지함에 적용하는 중입니다. 끝난 뒤에 고를 수 있습니다.')
expect(applyHelp(wrapper)).toBe('다른 필터로 받은편지함을 정리하는 중입니다. 끝난 뒤에 고를 수 있습니다.')
await click(wrapper.get('#mail-filter-cancel'))
// 다른 화면으로 가도 이어 한다
@@ -960,7 +960,7 @@ describe('MailFilterPage', () => {
wrapper.unmount()
first.finish(step(8412, 12, 0, null))
await flushPromises()
expect(toasts()).toEqual(['‘결재 알림’ 필터를 받은편지함의 메일 12통에 적용했습니다.'])
expect(toasts()).toEqual(['‘결재 알림’ 필터로 받은편지함의 메일 12통을 정리했습니다.'])
expect(useToastStore().toasts[0]?.action).toBeNull()
const { wrapper: again } = await mountPage()
@@ -981,7 +981,7 @@ describe('MailFilterPage', () => {
reply.finish(echo(requests.find((request) => request.method === 'PUT') as MockRequest))
await flushPromises()
expect(runs().map((request) => request.cursor)).toEqual([null])
expect(toasts()).toEqual(['‘거래처 메일’ 필터를 받은편지함의 메일 37통에 적용했습니다.'])
expect(toasts()).toEqual(['‘거래처 메일’ 필터로 받은편지함의 메일 37통을 정리했습니다.'])
useToastStore().clear()
const leaving = deferred()
@@ -997,7 +997,7 @@ describe('MailFilterPage', () => {
leaving.finish(echo(requests.find((request) => request.method === 'PUT') as MockRequest))
await flushPromises()
expect(runs().map((request) => request.cursor)).toEqual([null])
expect(toasts()).toEqual(['‘거래처 메일’ 필터를 받은편지함의 메일 37통에 적용했습니다.'])
expect(toasts()).toEqual(['‘거래처 메일’ 필터로 받은편지함의 메일 37통을 정리했습니다.'])
})
it('저장하는 중에 세션이 바뀌면 적용 요청을 보내지 않는다 — 알림도 없다', async () => {
@@ -158,7 +158,7 @@ describe('MailLargeFilesPage', () => {
expect(view.get('[data-large-usage]').text()).toBe('6.2 / 10 GB')
expect(view.get('meter').attributes('aria-valuetext')).toBe('62%')
expect(view.get('.large-usage__note').text()).toBe(
'보낸 링크 4.3 GB · 보내지 않은 파일 1.9 GB(임시 저장 · 올리다 멈춤). 기간이 지나거나, 링크를 끊거나, 보내지 않은 파일을 지우면 바로 줄어듭니다.',
'보낸 링크 4.3 GB, 임시 저장했거나 올리다 멈춘 파일 1.9 GB입니다. 기간이 지나거나, 링크를 끊거나, 보내지 않은 파일을 지우면 바로 줄어듭니다.',
)
})
@@ -199,7 +199,7 @@ describe('MailLargeFilesPage', () => {
await view.get('[data-unsent-delete]').trigger('click')
await flushPromises()
expect(view.get('#large-delete-message').text()).toBe(
'‘시공도면_v4.dwg’(620 MB) 파일을 지웁니다. 이 파일을 실은 임시 저장본이 있으면 열 때 첨부 칸에 ‘지운 파일’로 보입니다. 지운 파일은 되돌릴 수 없습니다.',
'‘시공도면_v4.dwg’(620 MB) 파일을 지웁니다. 이 파일을 실은 임시 저장본을 열면 첨부 칸에 지운 파일로 보입니다. 지운 파일은 되돌릴 수 없습니다.',
)
expect(document.activeElement?.hasAttribute('data-delete-cancel')).toBe(true)
@@ -56,13 +56,13 @@ describe('MailNotifyPage (설정 › 메일 › 알림)', () => {
expect(wrapper.text()).toContain('새 메일은 1분을 기다리지 않고 바로 목록에 나타납니다.')
})
it('묻기 전 — 칸은 비었고 "켜면 브라우저가 … 「허용」을 눌러 주세요.", 보일 것 · 시험 알림은 흐리게', async () => {
it('묻기 전 — 칸은 비었고 "켜면 브라우저가 … [허용]을 눌러 주세요.", 보일 것 · 시험 알림은 흐리게', async () => {
const wrapper = await mountPage()
expect(enableInput(wrapper).checked).toBe(false)
expect(enableInput(wrapper).disabled).toBe(false)
expect(wrapper.get('#notify-enabled-note').text()).toBe(
'켜면 브라우저가 알림을 허락할지 한 번 묻습니다 — 「허용」을 눌러 주세요.',
'켜면 브라우저가 알림을 허락할지 한 번 묻습니다. [허용]을 눌러 주세요.',
)
expect(wrapper.get('fieldset').attributes('disabled')).toBeDefined()
expect(wrapper.get('#notify-test').attributes('disabled')).toBeDefined()
@@ -101,7 +101,7 @@ describe('MailNotifyPage (설정 › 메일 › 알림)', () => {
expect(enableInput(wrapper).disabled).toBe(true)
const note = wrapper.get('#notify-enabled-note')
expect(note.text()).toBe(
'브라우저가 이 사이트의 알림을 막고 있습니다. 주소창 왼쪽의 사이트 정보(자물쇠) › 알림을 「허용」으로 바꾼 뒤 이 화면을 새로 고쳐 주세요.',
'브라우저가 이 사이트의 알림을 막고 있습니다. 주소창 왼쪽의 사이트 정보(자물쇠)를 눌러 [알림]을 [허용]으로 바꾼 뒤 이 화면을 새로 고쳐 주세요.',
)
expect(note.classes()).toContain('notify-form__note--danger')
expect(useMailNotifyStore().settings.enabled).toBe(false)
@@ -200,7 +200,7 @@ describe('MailNotifyPage (설정 › 메일 › 알림)', () => {
const subject = wrapper.get('#notify-show-subject')
expect((subject.element as HTMLInputElement).checked).toBe(true)
expect(wrapper.text()).toContain('끄면 보낸 사람 이름만 보입니다 — 화면을 다른 사람과 함께 보는 자리라면 꺼 두세요.')
expect(wrapper.text()).toContain('끄면 보낸 사람 이름만 보입니다. 다른 사람과 화면을 함께 보는 자리라면 꺼 두는 것이 좋습니다.')
await subject.setValue(false)
expect(useMailNotifyStore().settings.showSubject).toBe(false)
@@ -213,7 +213,7 @@ describe('MailNotifyPage (설정 › 메일 › 알림)', () => {
const status = () => wrapper.get('#notify-realtime-status')
expect(wrapper.get('h2').text()).toBe('실시간 연결')
expect(status().text()).toBe('연결되어 있습니다 — 새 메일이 오면 바로 목록에 나타납니다.')
expect(status().text()).toBe('연결되어 있어 새 메일이 오면 바로 목록에 나타납니다.')
expect(status().find('.realtime-link__dot').classes()).toContain('is-live')
expect(wrapper.find('#notify-realtime-notice').exists()).toBe(false)
@@ -225,7 +225,7 @@ describe('MailNotifyPage (설정 › 메일 › 알림)', () => {
// 끊긴 동안 — 상태 줄 바로 아래, "저절로 다시 잇습니다…" 안내 앞
expect(wrapper.get('#notify-realtime-notice').text()).toBe('연결될 때까지 바탕화면 알림은 오지 않습니다.')
expect(wrapper.find('[role="status"] #notify-realtime-notice').exists()).toBe(true)
expect(wrapper.text()).toContain('저절로 다시 잇습니다.')
expect(wrapper.text()).toContain('저절로 다시 연결합니다.')
realtime.setStatus('connecting')
await flushPromises()
@@ -238,9 +238,9 @@ describe('MailNotifyPage (설정 › 메일 › 알림)', () => {
const notes = wrapper.get('[data-notify-notes]')
expect(notes.findAll('p').map((note) => note.findAll('span').at(-1)?.text())).toEqual([
'알림 설정은 이 브라우저에만 적용됩니다. 다른 PC · 브라우저에서는 그곳에서 따로 켜 주세요.',
'알림 설정은 이 브라우저에만 저장됩니다. 다른 PC나 브라우저에서는 그곳에서 따로 켜 주세요.',
'웹메일 탭이 하나라도 열려 있어야 알림이 옵니다. 탭을 모두 닫으면 오지 않습니다.',
'웹메일 화면을 보고 있을 때는 바탕화면 알림을 보내지 않습니다 — 목록에 바로 보이므로.',
'웹메일 화면을 보고 있을 때는 새 메일이 목록에 바로 보이므로 바탕화면 알림을 보내지 않습니다.',
])
expect(notes.get('strong').text()).toBe('이 브라우저에만')
})
@@ -78,15 +78,15 @@ describe('MailSendReadPage', () => {
['30초', 'false'],
])
expect(wrapper.get('#send-read-undo-hint').text()).toBe(
'[보내기]를 누른 뒤 이 시간 동안 [보내기 취소]로 되돌릴 수 있습니다. 그동안 메일은 메일 서버에서 기다리므로 창을 닫아도 보내집니다.',
'[보내기]를 누른 뒤 이 시간 동안 [보내기 취소]로 되돌릴 수 있습니다. 그사이 창을 닫아도 시간이 지나면 메일은 그대로 보내집니다.',
)
expect(wrapper.findAll('.send-read__option-label').map((label) => label.text())).toEqual([
'늘 묻기',
'주소록 · 사내 직원의 메일은 늘 보기',
'주소록과 사내 직원의 메일은 늘 보기',
'늘 보기',
])
expect(checkedImages(wrapper)).toEqual(['trusted'])
expect(wrapper.get('[data-images-note]').text()).toContain('사내 메일 서버가 대신 받아 보여 줍니다')
expect(wrapper.get('[data-images-note]').text()).toContain('외부 이미지는 회사를 거쳐 불러오므로 보낸 사람은 이 PC의 주소를 알 수 없습니다.')
expect(wrapper.get('[data-settings-status]').text()).toBe('')
})
@@ -189,7 +189,7 @@ describe('MailSignaturePage', () => {
isForNew: true,
isForReply: false,
})
expect(wrapper.get('#signature-save-status').text()).toBe('저장했습니다. 새로 여는 쓰기 화면부터 들어갑니다.')
expect(wrapper.get('#signature-save-status').text()).toBe('저장했습니다. 다음에 메일을 쓸 때부터 들어갑니다.')
expect(useMailSignatureStore().signature?.isForReply).toBe(false)
await wrapper.get('#signature-for-new').setValue(false)
@@ -466,10 +466,10 @@ describe('MailSignaturePage', () => {
expect(replyMode.attributes('aria-pressed')).toBe('true')
expect(wrapper.find('.signature-preview__quote').exists()).toBe(true)
expect(wrapper.get('[data-signature-preview-note]').text()).toBe('답장 · 전달은 인용한 원문 위에 서명을 넣습니다.')
expect(wrapper.get('[data-signature-preview-note]').text()).toBe('답장과 전달에는 인용한 원문 위에 서명을 넣습니다.')
await wrapper.get('#signature-for-reply').setValue(false)
expect(wrapper.get('[data-signature-preview]').isVisible()).toBe(false)
expect(wrapper.get('[data-signature-preview-note]').text()).toBe('답장 · 전달에는 서명을 넣지 않습니다.')
expect(wrapper.get('[data-signature-preview-note]').text()).toBe('답장과 전달에는 서명을 넣지 않습니다.')
})
})
@@ -102,7 +102,7 @@ describe('MailVacationPage', () => {
// 엔진이 바이트로 막는 한도 안 — 한글로 채워도 저장된다 (계약 2절 "부재중 자동 응답"의 "길이 한도의 근거")
expect(wrapper.get('#vacation-subject').attributes('maxlength')).toBe('150')
expect(wrapper.get('#vacation-body').attributes('maxlength')).toBe('600')
expect(wrapper.get('[data-vacation-preview-note]').text()).toBe('꺼져 있습니다 — 자동으로 답하지 않습니다.')
expect(wrapper.get('[data-vacation-preview-note]').text()).toBe('꺼져 있어 자동으로 답하지 않습니다.')
})
it('저장한 값으로 채운다 — 날짜는 브라우저 시간대의 날짜로, 미리 보기는 보낸 사람 · 제목 · 내용', async () => {
@@ -112,7 +112,7 @@ describe('MailVacationPage', () => {
expect(value(wrapper, '#vacation-from')).toBe('2026-10-30')
expect(value(wrapper, '#vacation-to')).toBe('2026-11-03')
expect(wrapper.get('#vacation-period-hint').text()).toBe(
'시작일 0시부터 끝나는 날 자정까지 답합니다 (이 브라우저의 시간대 기준).',
'시작일 0시부터 끝나는 날 자정까지 답합니다. 시각은 이 브라우저의 시간대를 따릅니다.',
)
expect(value(wrapper, '#vacation-subject')).toBe(SAVED.subject)
const preview = wrapper.get('[data-vacation-preview]')
@@ -120,7 +120,7 @@ describe('MailVacationPage', () => {
expect(preview.get('[data-vacation-preview-subject]').text()).toBe(SAVED.subject)
expect(preview.get('[data-vacation-preview-body]').element.textContent).toBe(SAVED.textBody)
expect(wrapper.get('[data-vacation-preview-note]').text()).toBe(
'받은 메일의 보낸 사람에게 위 글로 답합니다. 같은 사람에게 매번 보내지는 않습니다(메일 엔진 규칙).',
'받은 메일의 보낸 사람에게 위 글로 답합니다. 같은 사람에게는 매번 보내지 않습니다.',
)
})
@@ -215,7 +215,7 @@ describe('MailVacationPage', () => {
expect(puts()).toEqual([{ ...SAVED, isEnabled: false, textBody: longBody }])
expect(wrapper.find('#vacation-body-error').exists()).toBe(false)
expect(wrapper.get('#vacation-save-status').text()).toBe('저장했습니다. 자동 응답을 껐습니다 — 써 둔 글은 남겨 둡니다.')
expect(wrapper.get('#vacation-save-status').text()).toBe('저장했습니다. 자동 응답을 껐고, 써 둔 글은 그대로 남아 있습니다.')
expect(useMailVacationStore().vacation?.isEnabled).toBe(false)
expect(value(wrapper, '#vacation-body')).toBe(longBody)
})
@@ -227,7 +227,7 @@ describe('MailVacationPage', () => {
await save(wrapper)
expect(puts()).toEqual([{ ...SAVED, isEnabled: false }])
expect(wrapper.get('#vacation-save-status').text()).toBe('저장했습니다. 자동 응답을 껐습니다 — 써 둔 글은 남겨 둡니다.')
expect(wrapper.get('#vacation-save-status').text()).toBe('저장했습니다. 자동 응답을 껐고, 써 둔 글은 그대로 남아 있습니다.')
// 설정한 적 없는 계정을 그대로 꺼 둔 채 저장해도 된다
const { wrapper: fresh } = await mountPage()
@@ -279,7 +279,7 @@ describe('MailVacationPage', () => {
describe('엔진에서 규칙이 꺼졌을 때 (7단계 — 디자인 원본 MailFilterApplyStates)', () => {
const INACTIVE_TEXT =
'켜 둔 자동 응답이 지금은 동작하지 않습니다. 메일 서버의 규칙이 다른 곳에서 바뀌었습니다 — 아래 [저장]을 누르면 다시 켜집니다.'
'다른 곳에서 메일 설정이 바뀌어 켜 둔 자동 응답이 지금은 나가지 않습니다. 아래 [저장]을 누르면 다시 켜집니다.'
it('켜 두었는데 isActive: false 면 머리 아래 노란 줄(단추 없음) — 저장하면 isActive 없이 보내고 응답의 값으로 내린다', async () => {
const { wrapper } = await mountPage({
+8 -8
View File
@@ -108,11 +108,11 @@ describe('SharePage', () => {
// 이름과 주소 사이 띄어쓰기는 괄호 칸 안에 둔다 — 좁은 화면은 괄호와 함께 감추고 주소를 다음 줄로 (디자인 원본 SharePage "김아라 <ara.kim@…>")
expect(view.get('.share__intro').text()).toContain('보낸 사람 김아라 <ara.kim@arasystem.it.kr> · ARA SYSTEM')
expect(view.get('.share__intro').text()).toContain(
'2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간) · 30일 남음',
'한국 시간으로 2026년 11월 29일(일)까지 내려받을 수 있습니다. 30일 남았습니다.',
)
expect(view.findAll('.share-file__meta').map((meta) => meta.text())).toEqual([
'1.2 GB · 보안 검사 · 이상 없음',
'612 MB · 보안 검사 · 이상 없음',
'1.2 GB · 보안 검사 결과 이상 없음',
'612 MB · 보안 검사 결과 이상 없음',
])
const download = view.get<HTMLAnchorElement>('[data-share-file="f1"] a.share-file__download')
expect(download.attributes('href')).toBe(`/api/public-links/${TOKEN}/files/f1`)
@@ -122,11 +122,11 @@ describe('SharePage', () => {
)
expect(view.get('.share__report').text()).toBe('받을 이유가 없는 파일이면 security@arasystem.it.kr 로 알려 주세요.')
expect(view.get('.share__english').text()).toBe(
'Files from 김아라 (ara.kim@arasystem.it.kr) — download by Nov 29, 2026 (KST). This page never asks for a login or password.',
'Files from 김아라 (ara.kim@arasystem.it.kr). Download by Nov 29, 2026 (KST). This page will never ask you to log in or enter a password.',
)
// 메일 제목 · 본문 · 받는 사람 · 받은 수는 보이지 않는다
expect(view.text()).not.toContain('회 받음')
expect(document.title).toBe('김아라 님이 보낸 파일 — ARA SYSTEM')
expect(document.title).toBe('김아라 님이 보낸 파일 · ARA SYSTEM')
})
it('링크 정보는 쿠키 없이 · 캐시 없이 · 주소를 싣지 않고 묻는다 — 검색 엔진 · referrer 막기 머리도 둔다', async () => {
@@ -192,7 +192,7 @@ describe('SharePage', () => {
const view = await mountAt(TOKEN, { link: publicLink({ files: [{ ...DESIGN, status: 'scanning' }] }) }, { link: publicLink() })
expect(view.get('[data-share-view="scanning"] h1').text()).toBe('보안 검사 중입니다')
expect(view.get('.share__english').text()).toBe('Security scan in progress — please check again in a few minutes.')
expect(view.get('.share__english').text()).toBe('A security scan is in progress. Please check again in a few minutes.')
await view.get('[data-share-recheck]').trigger('click')
await flushPromises()
@@ -213,7 +213,7 @@ describe('SharePage', () => {
const infected = view.get('[data-share-file="f1"]')
expect(infected.classes()).toContain('share-file--danger')
expect(infected.get('.share-file__meta').text()).toBe('1.2 GB · 위험 — 막았습니다')
expect(infected.get('.share-file__meta').text()).toBe('1.2 GB · 위험한 내용이 있어 막았습니다')
expect(infected.find('.share-file__download').exists()).toBe(false)
const unscannable = view.get('[data-share-file="f2"]')
expect(unscannable.classes()).toContain('share-file--warning')
@@ -287,7 +287,7 @@ describe('SharePage', () => {
expect(view.get('h1').text()).toBe('ara.kim@arasystem.it.kr 님이 보낸 파일 1개 (1.2 GB)')
expect(view.find('.share__email').exists()).toBe(false)
expect(document.title).toBe('ara.kim@arasystem.it.kr 님이 보낸 파일 — ARA SYSTEM')
expect(document.title).toBe('ara.kim@arasystem.it.kr 님이 보낸 파일 · ARA SYSTEM')
})
it('불러오는 중 — 자리 그림은 숨기고 "불러오는 중"을 화면 낭독기에 읽어 준다 (리뷰 L9)', async () => {
+1 -1
View File
@@ -267,7 +267,7 @@ watch(
<span class="share__org"> · {{ APP_NAME }}</span>
</p>
<p class="share__line">
<strong>{{ untilText }}</strong>{{ untilTail }}
{{ SHARE_PAGE_TEXT.untilBefore }}<strong>{{ untilText }}</strong>{{ untilTail }}
</p>
</div>
<ul class="share__files">