fix(backend): 서버가 만드는 글을 사용자에게 말하는 말투로 고친다

화면 문구(ddfb2d1)와 같은 기준으로 서버가 짓는 글을 고친다.
- 오류 문구 41개 가운데 7개: AUTH_006(「자동」) · VAL_001(괄호 꼬리표 — 길이 한도에도 나와 "형식이나 길이"로) ·
  FILE_003 · FILE_004 · MAIL_001(메일 서버 → 메일 서비스) · MAIL_013 · CONTACT_002. 코드 · 상태는 그대로이고
  sync:error-codes 로 frontend · flutter 사전을 다시 만들었다.
- 받은편지함 알림 메일 세 통: 줄표 · "이 계정의" · "판정"을 빼고, 메뉴 경로를 화면처럼 [설정] → [앱 비밀번호] 꼴로.
  링크를 넣지 않는 규칙 · 이스케이프 · 정화는 그대로다.
- 본문에 넣는 대용량 첨부 덩이의 기한 줄 · 영어 줄. 표시 · 표 모양은 그대로이고, 배포 전에 붙잡아 둔 발송의
  텍스트 덩이(옛 글)도 보내기 취소 때 떼도록 LEGACY_BLOCK_WORDINGS 를 둔다(7일 뒤 지워도 된다).

임시 복사본 Linux 컨테이너에서 lint · jest 3,209 · build, frontend lint · type-check · 단위 2,354,
flutter error_lexicon 테스트 · analyze 통과.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 14:04:45 +09:00
parent cdebcb85f9
commit 0021c0a1fc
10 changed files with 214 additions and 78 deletions
@@ -11,6 +11,7 @@ import {
insertHtmlBlock,
insertTextBlock,
isValidQuoteStart,
largeFileTextBlocks,
removeHtmlBlock,
removeTextBlock,
} from './large-file-block';
@@ -40,14 +41,14 @@ describe('buildLargeFileBlock — 덩이의 내용 (디자인 README "11단계
expect(block.html.endsWith(LARGE_FILES_BLOCK_END)).toBe(true);
for (const text of [
'대용량 첨부 2개 (합계 1.8 GB)',
'2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)',
'한국 시간으로 2026년 11월 29일(일)까지 내려받을 수 있습니다.',
'설계도_v3.dwg',
'1.2 GB',
'현장영상_1029.mp4',
'612 MB',
`<a href="${URL_}">${URL_}</a>`,
'내려받을 때 로그인이나 비밀번호를 묻지 않습니다.',
'Large files: download by Nov 29, 2026 (KST) — no login or password needed.',
'Large files: download by Nov 29, 2026 (KST). No login or password needed.',
]) {
expect(block.html).toContain(text);
}
@@ -57,6 +58,32 @@ describe('buildLargeFileBlock — 덩이의 내용 (디자인 README "11단계
it('텍스트 — 같은 내용을 대시 줄로 가른 줄로 (파일 "- 이름 (크기)", 주소 "내려받기: 주소")', () => {
expect(block.text.split('\n')).toEqual([
'-'.repeat(40),
'대용량 첨부 2개 (합계 1.8 GB)',
'한국 시간으로 2026년 11월 29일(일)까지 내려받을 수 있습니다.',
'- 설계도_v3.dwg (1.2 GB)',
'- 현장영상_1029.mp4 (612 MB)',
`내려받기: ${URL_}`,
'내려받을 때 로그인이나 비밀번호를 묻지 않습니다.',
'Large files: download by Nov 29, 2026 (KST). No login or password needed.',
'-'.repeat(40),
]);
});
it('사용자에게 말하는 말투 — 줄표(—)로 잇지 않고 「」 를 쓰지 않는다 (디자인 README "화면 문구")', () => {
expect(block.text).not.toMatch(/[—「」]/);
expect(block.html).not.toMatch(/[—「」]/);
});
it('떼기용 텍스트 덩이 — 지금 글이 먼저, 그다음 2026-11-05 앞의 글 그대로 (그 전에 붙잡아 둔 발송을 보내기 취소로 되돌릴 때)', () => {
const [current, legacy, ...rest] = largeFileTextBlocks({
files: FILES,
expiresAt: EXPIRES,
url: URL_,
});
expect(current).toBe(block.text);
expect(rest).toEqual([]);
expect(legacy.split('\n')).toEqual([
'-'.repeat(40),
'대용량 첨부 2개 (합계 1.8 GB)',
'2026년 11월 29일(일)까지 내려받을 수 있습니다 (한국 시간)',
+85 -23
View File
@@ -11,7 +11,10 @@ import { formatKstDate, formatKstDateEnglish } from '@/shared/utils/kst-date';
* 이 표시가 들어갈 수 없다(서버만 넣는다). 엔진은 저장한 메일의 주석을 그대로 둔다(2026-10-30 개발 엔진으로 확인 — 되돌린 초안의 rawHtmlBody 에 남는다)
* - **텍스트**: 같은 내용을 줄로 — 대시 줄로 가르고 앞뒤에 빈 줄
*
* ⚠️ 덩이를 고치면 그 전에 보낸 메일의 텍스트 덩이를 보내기 취소로 되돌릴 때 떼지 못한다(글 그대로일 때만 뗀다) — 표시의 판(v1)을 올리고 옛 판도 알아보게 한다
* ⚠️ 덩이를 고치면 그 전에 보낸 메일의 텍스트 덩이를 보내기 취소로 되돌릴 때 떼지 못한다(글 그대로일 때만 뗀다).
* - 글만 고칠 때: 옛 글을 `LEGACY_BLOCK_WORDINGS` 에 남겨 떼기가 함께 찾게 한다(largeFileTextBlocks — 2026-11-05 말투를 고칠 때 이렇게 했다).
* HTML 덩이는 표시 사이를 떼므로 글과 상관없다
* - 줄의 차례 · 표 모양을 바꿀 때: 표시의 판(v1)을 올리고 옛 판도 알아보게 한다
*/
/** 덩이의 앞뒤 표시 (HTML 주석) */
@@ -32,6 +35,34 @@ const TEXT_RULE = '-'.repeat(40);
const BLOCK_FONT =
"'Malgun Gothic', '맑은 고딕', 'Apple SD Gothic Neo', sans-serif";
/** 덩이의 로그인 안내 줄 (내려받기 화면이 로그인 · 비밀번호를 묻지 않는다는 것 — 묻는 화면이면 남이 꾸민 것이다) */
const NO_LOGIN_LINE = '내려받을 때 로그인이나 비밀번호를 묻지 않습니다.';
/** 덩이의 글 가운데 고쳐 온 줄 — 끝나는 날 · 영어 한 줄 (날짜는 한국 날짜로 받는다) */
interface BlockWording {
readonly until: (kstDate: string) => string;
readonly english: (kstDateEnglish: string) => string;
}
/** 지금 넣는 글 (디자인 README "11단계 › 본문 덩이" — 2026-11-05 사용자에게 말하는 말투로) */
const BLOCK_WORDING: BlockWording = {
until: (date) => `한국 시간으로 ${date}까지 내려받을 수 있습니다.`,
english: (date) =>
`Large files: download by ${date} (KST). No login or password needed.`,
};
/**
* 예전에 넣던 글 — 보내기 취소로 되돌린 발송의 텍스트 덩이를 뗄 때만 함께 찾는다 (largeFileTextBlocks).
* 2026-11-05 앞의 글이다. 그 전에 붙잡아 둔 발송(예약은 엔진 한도 7일)이 모두 나간 뒤에는 지워도 된다
*/
const LEGACY_BLOCK_WORDINGS: readonly BlockWording[] = [
{
until: (date) => `${date}까지 내려받을 수 있습니다 (한국 시간)`,
english: (date) =>
`Large files: download by ${date} (KST) — no login or password needed.`,
},
];
/** 덩이 하나의 재료 */
export interface LargeFileBlockInput {
/** 보낸 차례 */
@@ -56,15 +87,48 @@ function escapeHtml(text: string): string {
.replace(/'/g, '&#39;');
}
/** 덩이의 글 줄 — HTML 덩이와 텍스트 덩이가 같은 글을 쓴다 */
interface BlockLines {
readonly title: string;
readonly until: string;
readonly noLogin: string;
readonly english: string;
}
function blockLines(
input: LargeFileBlockInput,
wording: BlockWording,
): BlockLines {
const total = input.files.reduce((sum, file) => sum + file.size, 0);
return {
title: `대용량 첨부 ${input.files.length}개 (합계 ${formatFileSize(total)})`,
until: wording.until(formatKstDate(input.expiresAt)),
noLogin: NO_LOGIN_LINE,
english: wording.english(formatKstDateEnglish(input.expiresAt)),
};
}
/** 텍스트 덩이 — 대시 줄로 가른 같은 내용 (파일 "- 이름 (크기)", 주소 "내려받기: 주소") */
function textBlock(input: LargeFileBlockInput, lines: BlockLines): string {
return [
TEXT_RULE,
lines.title,
lines.until,
...input.files.map(
(file) => `- ${file.name} (${formatFileSize(file.size)})`,
),
`내려받기: ${input.url}`,
lines.noLogin,
lines.english,
TEXT_RULE,
].join('\n');
}
/** 덩이를 만든다 — HTML(앞뒤 표시 포함)과 텍스트에 같은 내용 */
export function buildLargeFileBlock(
input: LargeFileBlockInput,
): LargeFileBlock {
const total = input.files.reduce((sum, file) => sum + file.size, 0);
const title = `대용량 첨부 ${input.files.length}개 (합계 ${formatFileSize(total)})`;
const until = `${formatKstDate(input.expiresAt)}까지 내려받을 수 있습니다 (한국 시간)`;
const noLogin = '내려받을 때 로그인이나 비밀번호를 묻지 않습니다.';
const english = `Large files: download by ${formatKstDateEnglish(input.expiresAt)} (KST) — no login or password needed.`;
const lines = blockLines(input, BLOCK_WORDING);
const url = escapeHtml(input.url);
const cell = (style: string, content: string) =>
@@ -81,11 +145,11 @@ export function buildLargeFileBlock(
`<table style="border-collapse:collapse;border:1px solid #d0d5dd;margin:16px 0;font-family:${BLOCK_FONT}"><tbody>` +
cell(
'padding:12px 16px 2px 16px;font-size:14px;font-weight:bold;color:#101828',
escapeHtml(title),
escapeHtml(lines.title),
) +
cell(
'padding:0 16px 8px 16px;font-size:13px;color:#344054',
escapeHtml(until),
escapeHtml(lines.until),
) +
fileRows +
cell(
@@ -94,28 +158,26 @@ export function buildLargeFileBlock(
) +
cell(
'padding:6px 16px 0 16px;font-size:12px;color:#667085',
escapeHtml(noLogin),
escapeHtml(lines.noLogin),
) +
cell(
'padding:2px 16px 12px 16px;font-size:12px;color:#667085',
escapeHtml(english),
escapeHtml(lines.english),
) +
'</tbody></table>' +
LARGE_FILES_BLOCK_END;
const text = [
TEXT_RULE,
title,
until,
...input.files.map(
(file) => `- ${file.name} (${formatFileSize(file.size)})`,
),
`내려받기: ${input.url}`,
noLogin,
english,
TEXT_RULE,
].join('\n');
return { html, text };
return { html, text: textBlock(input, lines) };
}
/**
* 보내기 취소로 되돌린 발송에서 뗄 텍스트 덩이 — 지금 글 다음에 예전 글(LEGACY_BLOCK_WORDINGS) 차례.
* 텍스트 덩이는 글 그대로일 때만 떼므로(removeTextBlock), 글을 고치기 전에 붙잡아 둔 발송의 덩이도 찾게 한다
*/
export function largeFileTextBlocks(input: LargeFileBlockInput): string[] {
return [BLOCK_WORDING, ...LEGACY_BLOCK_WORDINGS].map((wording) =>
textBlock(input, blockLines(input, wording)),
);
}
/** 텍스트 덩이를 본문에 넣을 때의 모양 — 앞뒤에 빈 줄 (떼기도 이 모양 그대로 찾는다) */
@@ -21,6 +21,7 @@ import {
buildLargeFileBlock,
insertHtmlBlock,
insertTextBlock,
largeFileTextBlocks,
} from './large-file-block';
import { MAX_BODY_LENGTH } from './mail.constants';
import type { MailEventsService } from './mail-events.service';
@@ -538,6 +539,32 @@ describe('MailLargeFilesService — 메일과 대용량 첨부 (11단계)', () =
expect(kept.textBody).toBe(edited);
});
it('글을 고치기(2026-11-05) 전에 붙잡아 둔 발송 — 예전 글의 텍스트 덩이도 뗀다 (HTML 은 표시로 뗀다)', async () => {
const { service, sharing } = setup();
sharing.findBySentEmail.mockResolvedValue(
record({ canceledAt: SENT_AT }),
);
const [, legacyText] = largeFileTextBlocks({
files: VIEWS,
expiresAt: EXPIRES,
url: shareUrl(ORIGIN, TOKEN),
});
expect(legacyText).not.toBe(sentBlock.text);
const originalText = '내 글\n\n----- 원본 메일 -----\n> 인용';
const result = await service.draftView(
ARA,
draft({
isCanceledSend: true,
textBody: insertTextBlock(
originalText,
'내 글\n\n'.length,
legacyText,
),
}),
);
expect(result.textBody).toBe(originalText);
});
it('다른 링크의 덩이(토큰이 다름)는 떼지 않는다', async () => {
const { service, sharing } = setup();
sharing.findBySentEmail.mockResolvedValue(
@@ -41,6 +41,7 @@ import {
hrefsIn,
insertHtmlBlock,
insertTextBlock,
largeFileTextBlocks,
removeHtmlBlock,
removeTextBlock,
type LargeFileBlock,
@@ -565,7 +566,7 @@ export class MailLargeFilesService implements OnModuleInit, OnModuleDestroy {
/**
* 취소해 되돌린 발송의 덩이를 뗀다 — HTML 은 표시 사이(그 링크의 토큰이 있을 때만), 텍스트는 서버가 넣은 글 그대로일 때만.
* 토큰은 HTML 덩이의 링크에서(없으면 텍스트의 주소에서) 찾아 링크의 해시와 맞춘다 — 덩이를 다시 만들어 글 그대로 찾는다
* 토큰은 HTML 덩이의 링크에서(없으면 텍스트의 주소에서) 찾아 링크의 해시와 맞춘다 — 덩이를 다시 만들어(지금 글 · 예전 글) 글 그대로 찾는다
*/
private stripBlock(
record: ShareLinkRecord,
@@ -592,12 +593,19 @@ export class MailLargeFilesService implements OnModuleInit, OnModuleDestroy {
let text = textBody;
if (token !== null && text !== null) {
const { text: blockText } = buildLargeFileBlock({
// 지금 글 다음에 예전 글 — 글을 고치기 전에 붙잡아 둔 발송의 텍스트 덩이도 뗀다 (large-file-block.ts 의 LEGACY_BLOCK_WORDINGS)
const blockTexts = largeFileTextBlocks({
files: record.files,
expiresAt: record.link.expiresAt,
url: shareUrl(origin, token),
});
text = removeTextBlock(text, blockText) ?? text;
for (const blockText of blockTexts) {
const removed = removeTextBlock(text, blockText);
if (removed !== null) {
text = removed;
break;
}
}
}
return { rawHtmlBody: html, textBody: text };
}
@@ -51,15 +51,15 @@ describe('받은편지함 알림 메일의 글 (13단계 — 디자인 README 13
expect(notice.subject).toBe('관리자가 비밀번호를 재설정했습니다');
expect(notice.text).toBe(
[
'김아라 님, 관리자가 이 계정의 비밀번호를 임시 비밀번호로 바꿨습니다.',
'김아라 님, 관리자가 비밀번호를 임시 비밀번호로 바꿨습니다.',
'',
'시각: 2026년 10월 30일 (금) 오후 3:20',
'바뀐 것: 비밀번호 — 임시 비밀번호로 바뀌었고, 처음 로그인할 때 새 비밀번호를 정합니다',
'로그아웃: 모든 기기 · 브라우저의 웹메일 로그인을 끊었습니다',
'앱 비밀번호: 모두 지웠습니다 — 메일 프로그램은 새 앱 비밀번호로 다시 연결합니다',
'바뀐 것: 비밀번호가 임시 비밀번호로 바뀌었고, 처음 로그인할 때 새 비밀번호를 정하게 됩니다',
'로그아웃: 모든 기기와 브라우저의 웹메일에서 로그아웃되었습니다',
'앱 비밀번호: 모두 지워져서 메일 프로그램은 새 앱 비밀번호로 다시 연결해야 합니다',
'',
'할 일',
'아웃룩 · 휴대폰 메일 앱을 쓰면 메일 프로그램마다 앱 비밀번호가 필요합니다. 웹메일 › 설정 › 계정 › 앱 비밀번호에서 새로 만들어 주세요.',
'아웃룩이나 휴대폰 메일 앱을 쓰면 메일 프로그램마다 앱 비밀번호가 필요합니다. 웹메일의 [설정] → [앱 비밀번호]에서 새로 만들어 주세요.',
'',
'직접 요청한 일이 아니면 바로 IT 담당에게 알려 주세요.',
CONTACT,
@@ -69,7 +69,7 @@ describe('받은편지함 알림 메일의 글 (13단계 — 디자인 README 13
].join('\n'),
);
expect(notice.html).toContain(
'<strong>웹메일 › 설정 › 계정 › 앱 비밀번호</strong>에서 새로 만들어 주세요.',
'웹메일의 <strong>[설정] → [앱 비밀번호]</strong>에서 새로 만들어 주세요.',
);
});
@@ -77,13 +77,13 @@ describe('받은편지함 알림 메일의 글 (13단계 — 디자인 README 13
const notice = twoFactorResetNotice(ARA, AT);
expect(notice.subject).toBe('관리자가 2단계 인증을 껐습니다');
expect(notice.text).toContain(
'김아라 님, 휴대폰을 잃어버렸거나 인증 앱을 쓸 수 없다고 알려 주셔서 관리자가 이 계정의 2단계 인증을 껐습니다.',
'김아라 님, 휴대폰을 잃어버렸거나 인증 앱을 쓸 수 없다고 알려 주셔서 관리자가 2단계 인증을 껐습니다.',
);
expect(notice.text).toContain(
'그대로인 것: 비밀번호 · 앱 비밀번호 · 로그인된 브라우저',
'그대로인 것: 비밀번호, 앱 비밀번호, 로그인된 브라우저',
);
expect(notice.html).toContain(
'<strong>웹메일 › 설정 › 계정 › 2단계 인증</strong>에서 다시 켜 주세요.',
'웹메일의 <strong>[설정] → [2단계 인증]</strong>에서 다시 켜 주세요.',
);
});
@@ -96,10 +96,10 @@ describe('받은편지함 알림 메일의 글 (13단계 — 디자인 README 13
expect(notice.text).toContain('크기: 48 MB');
expect(notice.text).toContain('올린 때: 2026년 10월 30일 (금) 오후 3:58');
expect(notice.text).toContain(
'판정이 잘못되었다고 생각되면 IT 담당에게 알려 주세요.',
'검사 결과가 잘못되었다고 생각되면 IT 담당에게 알려 주세요.',
);
expect(notice.html).toContain(
'<strong>웹메일 › 설정 › 메일 › 대용량 첨부</strong>에서 볼 수 있습니다.',
'웹메일의 <strong>[설정] → [대용량 첨부]</strong>에서 볼 수 있습니다.',
);
});
@@ -114,6 +114,15 @@ describe('받은편지함 알림 메일의 글 (13단계 — 디자인 README 13
},
);
it.each(ALL)(
'%s — 사용자에게 말하는 말투: 줄표(—)로 잇지 않고 「」 를 쓰지 않는다 (디자인 README "화면 문구")',
(_label, notice) => {
for (const text of [notice.subject, notice.text]) {
expect(text).not.toMatch(/[—「」]/);
}
},
);
it.each(ALL)(
'%s — 보내는 메일 정화(허용 목록)를 거쳐도 지워지는 태그 · 스타일 · 글이 없다',
(_label, notice) => {
@@ -57,7 +57,7 @@ interface NoticeBody {
/** 맨 아래 한 줄 (세 통 같음) */
export const NOTICE_FOOTER =
'이 메일은 ARA SYSTEM 이 자동으로 보낸 알림입니다. 회사 보안 알림에는 링크가 없습니다 — 비슷한 메일의 링크는 누르지 마세요.';
'이 메일은 ARA SYSTEM이 자동으로 보낸 알림입니다. 회사 보안 알림에는 링크가 없으니, 비슷한 메일에 링크가 있다면 누르지 않도록 주의해 주세요.';
/** 확인 문장 — 비밀번호 재설정 · 2단계 해제 */
const CONFIRM_SELF = '직접 요청한 일이 아니면 바로 IT 담당에게 알려 주세요.';
@@ -151,24 +151,24 @@ export function passwordResetNotice(
{
kind: 'password-reset',
subject: '관리자가 비밀번호를 재설정했습니다',
greeting: `${recipient.name} 님, 관리자가 이 계정의 비밀번호를 임시 비밀번호로 바꿨습니다.`,
greeting: `${recipient.name} 님, 관리자가 비밀번호를 임시 비밀번호로 바꿨습니다.`,
rows: [
['시각', formatKstDateTime(at)],
[
'바뀐 것',
'비밀번호 — 임시 비밀번호로 바뀌었고, 처음 로그인할 때 새 비밀번호를 정합니다',
'비밀번호가 임시 비밀번호로 바뀌었고, 처음 로그인할 때 새 비밀번호를 정하게 됩니다',
],
['로그아웃', '모든 기기 · 브라우저의 웹메일 로그인을 끊었습니다'],
['로그아웃', '모든 기기와 브라우저의 웹메일에서 로그아웃되었습니다'],
[
'앱 비밀번호',
'모두 지웠습니다 — 메일 프로그램은 새 앱 비밀번호로 다시 연결합니다',
'모두 지워져서 메일 프로그램은 새 앱 비밀번호로 다시 연결해야 합니다',
],
],
todo: [
{
text: '아웃룩 · 휴대폰 메일 앱을 쓰면 메일 프로그램마다 앱 비밀번호가 필요합니다. ',
text: '아웃룩이나 휴대폰 메일 앱을 쓰면 메일 프로그램마다 앱 비밀번호가 필요합니다. 웹메일의 ',
},
{ strong: '웹메일 › 설정 › 계정 › 앱 비밀번호' },
{ strong: '[설정] → [앱 비밀번호]' },
{ text: '에서 새로 만들어 주세요.' },
],
confirm: CONFIRM_SELF,
@@ -186,17 +186,17 @@ export function twoFactorResetNotice(
{
kind: 'two-factor-reset',
subject: '관리자가 2단계 인증을 껐습니다',
greeting: `${recipient.name} 님, 휴대폰을 잃어버렸거나 인증 앱을 쓸 수 없다고 알려 주셔서 관리자가 이 계정의 2단계 인증을 껐습니다.`,
greeting: `${recipient.name} 님, 휴대폰을 잃어버렸거나 인증 앱을 쓸 수 없다고 알려 주셔서 관리자가 2단계 인증을 껐습니다.`,
rows: [
['시각', formatKstDateTime(at)],
['바뀐 것', '2단계 인증 꺼짐 — 지금은 비밀번호만으로 로그인합니다'],
['그대로인 것', '비밀번호 · 앱 비밀번호 · 로그인된 브라우저'],
['바뀐 것', '2단계 인증이 꺼져서 지금은 비밀번호만으로 로그인합니다'],
['그대로인 것', '비밀번호, 앱 비밀번호, 로그인된 브라우저'],
],
todo: [
{ text: '새 휴대폰에 인증 앱을 설치한 뒤 ' },
{ strong: '웹메일 › 설정 › 계정 › 2단계 인증' },
{ text: '새 휴대폰에 인증 앱을 설치한 뒤 웹메일의 ' },
{ strong: '[설정] → [2단계 인증]' },
{
text: '에서 다시 켜 주세요. 회사가 2단계 인증을 필수로 정했다면 다음 로그인 때 켜는 화면이 먼저 나옵니다.',
text: '에서 다시 켜 주세요. 회사가 2단계 인증을 필수로 정했다면 다음에 로그인할 때 켜는 화면이 먼저 나옵니다.',
},
],
confirm: CONFIRM_SELF,
@@ -221,17 +221,17 @@ export function dangerousFileNotice(
['올린 때', formatKstDateTime(file.uploadedAt)],
[
'조치',
'이 파일만 막았습니다 — 함께 올린 다른 파일은 그대로 보내고 받을 수 있습니다',
'이 파일만 막았고, 함께 올린 다른 파일은 그대로 보내고 받을 수 있습니다',
],
],
todo: [
{
text: '직접 만든 파일이 아니면 PC 를 백신으로 검사해 주세요. 다른 곳에서 받은 파일이면 보낸 사람에게도 알려 주세요. 막은 파일은 ',
text: '직접 만든 파일이 아니면 PC를 백신으로 검사해 주세요. 다른 곳에서 받은 파일이면 보낸 사람에게도 알려 주세요. 막은 파일은 웹메일의 ',
},
{ strong: '웹메일 › 설정 › 메일 › 대용량 첨부' },
{ strong: '[설정] → [대용량 첨부]' },
{ text: '에서 볼 수 있습니다.' },
],
confirm: '판정이 잘못되었다고 생각되면 IT 담당에게 알려 주세요.',
confirm: '검사 결과가 잘못되었다고 생각되면 IT 담당에게 알려 주세요.',
},
recipient,
);