fix(frontend): 묶은 휴지통의 영구 삭제 글과 띄어 쓴 조사를 고친다

- 대화로 묶은 휴지통에서 줄을 골라 영구 삭제하면 확인 창 · 알림을
  "휴지통에 있는 메일 N통을 …"으로 쓴다 — 줄의 통 수는 대화 전체라서
  (옮기기 알림 "{메일함}에 있는 메일 N통을 …"과 같은 결)
- 값을 끼워 넣는 글의 띄어 쓴 조사를 붙인다(9단계 "@도메인에서",
  11단계 "25MB를" · "2GB까지" · "20MB가"), 공개 내려받기 화면의 신고
  주소는 끝 글자로 조사를 고른다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 02:13:33 +09:00
parent 08aafdef5c
commit 749997cb18
16 changed files with 139 additions and 37 deletions
+1 -1
View File
@@ -636,7 +636,7 @@ describe('ComposePage', () => {
])
expect(wrapper.get('[data-large-upload]').text()).toContain('도면.pdf')
expect(wrapper.get('[data-large-switch]').text()).toBe(
'‘도면.pdf’(5 MB)는 메일에 담으면 합계가 15MB 를 넘어 링크로 보냅니다.',
'‘도면.pdf’(5 MB)는 메일에 담으면 합계가 15MB를 넘어 링크로 보냅니다.',
)
expect(requests.filter((request) => request.url === '/mail-attachments')).toHaveLength(1)
expect(useToastStore().toasts).toEqual([])
@@ -116,6 +116,8 @@ describe('MailboxPage — 대화로 묶어 보기 (14단계)', () => {
let loadSettings: (() => MockResult) | null
/** GET /messages 의 응답을 바꾼다 (붙잡아 두기 등) */
let listMessages: (() => MockResult | Promise<MockResult>) | null
/** 읽기 화면이 여는 메일(GET /messages/{id})의 메일함 — 처음은 받은편지함 */
let openedMailboxIds: string[]
const mounted: VueWrapper[] = []
beforeEach(() => {
@@ -130,6 +132,7 @@ describe('MailboxPage — 대화로 묶어 보기 (14단계)', () => {
patchSettings = null
loadSettings = null
listMessages = null
openedMailboxIds = ['inbox']
requests = installMockApi((request) => {
const { url, method } = request
if (url === '/mail-state') return ok({ state })
@@ -176,7 +179,7 @@ describe('MailboxPage — 대화로 묶어 보기 (14단계)', () => {
}
if (url.endsWith('/conversation')) return ok({ items: [], pageInfo: { hasNext: false, nextCursor: null, total: 1 }, basis: 'index' })
const opened = /^\/messages\/([^/]+)$/.exec(url)?.[1]
if (opened && method === 'GET') return ok(detail(summary(opened)))
if (opened && method === 'GET') return ok(detail(summary(opened, { mailboxIds: openedMailboxIds })))
return ok(null)
})
})
@@ -396,23 +399,51 @@ describe('MailboxPage — 대화로 묶어 보기 (14단계)', () => {
expect(useToastStore().toasts.map((item) => item.message)).toContain('메일 2통을 받은편지함으로 되돌렸습니다.')
})
it('휴지통에서 고른 대화를 영구 삭제 — 확인 창의 수는 메일 수이고 그 줄의 메일을 POST /message-deletions', async () => {
it('휴지통에서 고른 대화를 영구 삭제 — 확인 창 · 알림은 "휴지통에 있는 메일 N통을 …"(줄의 통 수는 대화 전체라서)이고 그 줄의 메일을 POST /message-deletions', async () => {
rememberGrouped()
rows = [conversation('t1', ['m8', 'm7'], { latest: summary('m8', { mailboxIds: ['trash'] }) }), conversation('t2', ['m1'])]
rows = [
conversation('t1', ['m8', 'm7'], { latest: summary('m8', { mailboxIds: ['trash'] }), messageCount: 5 }),
conversation('t2', ['m1']),
]
const { wrapper } = await mountMail('/mail/trash')
// 줄은 대화 전체의 "5통" — 지우는 것은 휴지통의 2통이다 (2026-11-07)
expect(wrapper.findAll('.thread-row')[0]?.get('[data-thread-count]').text()).toBe('5통')
await wrapper.findAll('.thread-row')[0]?.get('[role="checkbox"]').trigger('click')
await wrapper.get('[data-tool="delete"]').trigger('click')
await settle()
const dialog = wrapper.get('#bulk-purge-title')
expect(dialog.text()).toBe('메일 2통을 영구 삭제할까요?')
expect(dialog.text()).toBe('휴지통에 있는 메일 2통을 영구 삭제할까요?')
expect(wrapper.get('#bulk-purge-message').text()).toBe('휴지통에서 지운 메일은 되돌릴 수 없습니다.')
await wrapper.get('[data-purge-confirm]').trigger('click')
await settle()
expect(requests.filter((request) => request.url === '/message-deletions').map((request) => request.data)).toEqual([
{ ids: ['m8', 'm7'] },
])
expect(useToastStore().toasts.map((toast) => toast.message)).toContain('메일 2통을 영구 삭제했습니다.')
expect(useToastStore().toasts.map((toast) => toast.message)).toContain('휴지통에 있는 메일 2통을 영구 삭제했습니다.')
})
it('묶은 휴지통이어도 읽기 화면의 영구 삭제(한 통)는 지금 글 그대로 — "메일 1통을 영구 삭제할까요?" · "메일 1통을 영구 삭제했습니다."', async () => {
rememberGrouped()
rows = [conversation('t1', ['m8', 'm7'], { latest: summary('m8', { mailboxIds: ['trash'] }), messageCount: 5 })]
openedMailboxIds = ['trash']
const { wrapper } = await mountMail('/mail/trash/m8')
const remove = wrapper.get('[data-reader-delete]')
expect(remove.text()).toBe('영구 삭제')
await remove.trigger('click')
await settle()
expect(wrapper.get('#reader-purge-title').text()).toBe('메일 1통을 영구 삭제할까요?')
// 고른 줄이 없어 도구 줄(과 그 확인 창)은 없다 — 확인 단추는 읽기 화면의 것 하나
expect(wrapper.find('.bulk-bar').exists()).toBe(false)
await wrapper.get('[data-purge-confirm]').trigger('click')
await settle()
expect(requests.filter((request) => request.url === '/message-deletions').map((request) => request.data)).toEqual([
{ ids: ['m8'] },
])
expect(useToastStore().toasts.map((toast) => toast.message)).toContain('메일 1통을 영구 삭제했습니다.')
})
it('목록 보기 — 크기 칸이 없고 머리 고르기는 "불러온 대화 모두 고르기". 연 메일의 위치는 대화 기준이고 [다음]은 다음 대화의 마지막 메일', async () => {
@@ -276,7 +276,7 @@ describe('MailLargeFilesPage', () => {
expect(view.get('[data-large-usage]').text()).toBe('0 / 10 GB')
expect(view.get('[data-large-empty]').text()).toContain('링크로 보낸 파일이 없습니다')
expect(view.get('[data-large-empty]').text()).toContain('메일 쓰기에서 20MB 가 넘는 파일을 고르면 링크로 보냅니다.')
expect(view.get('[data-large-empty]').text()).toContain('메일 쓰기에서 20MB가 넘는 파일을 고르면 링크로 보냅니다.')
expect(view.find('#large-unsent-title').exists()).toBe(false)
})
+1 -1
View File
@@ -120,7 +120,7 @@ describe('SharePage', () => {
expect(view.get('.share__safety').text()).toBe(
'이 화면은 로그인이나 비밀번호를 묻지 않습니다. 묻는 화면이 나오면 닫고 보낸 사람에게 확인해 주세요.',
)
expect(view.get('.share__report').text()).toBe('받을 이유가 없는 파일이면 security@arasystem.it.kr 로 알려 주세요.')
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 will never ask you to log in or enter a password.',
)
+6 -1
View File
@@ -22,6 +22,7 @@ import { APP_NAME } from '@/shared/constants/brand'
import { SHARE_PAGE_TEXT, SHARE_SCAN_POLL_DELAYS_MS, SHARE_SCAN_POLL_LIMIT } from '@/shared/constants/large-file'
import type { PublicLink, PublicLinkFile } from '@/shared/types/large-file'
import { formatCount, formatFileSize } from '@/shared/utils/format'
import { directionParticle } from '@/shared/utils/korean'
import {
formatKstEnglishDate,
formatKstFullDate,
@@ -71,6 +72,10 @@ const view = computed(() => {
return 'active'
})
/** 신고 주소 뒤의 글 — 조사는 주소에 붙이고 받침에 맞춘다 (영문으로 끝나면 "(으)로") */
const reportAfter = computed(() =>
link.value ? SHARE_PAGE_TEXT.reportAfter(directionParticle(link.value.reportAddress)) : '',
)
const untilText = computed(() => (link.value ? SHARE_PAGE_TEXT.until(formatKstFullDate(link.value.expiresAt)) : ''))
const untilTail = computed(() => {
if (!link.value) return ''
@@ -395,7 +400,7 @@ watch(
v-if="link && view !== 'loading'"
class="share__report"
>
{{ SHARE_PAGE_TEXT.reportBefore }}<a :href="`mailto:${link.reportAddress}`">{{ link.reportAddress }}</a>{{ SHARE_PAGE_TEXT.reportAfter }}
{{ SHARE_PAGE_TEXT.reportBefore }}<a :href="`mailto:${link.reportAddress}`">{{ link.reportAddress }}</a>{{ reportAfter }}
</p>
<p
v-if="english"