docs: 앱 메일 쓰기 화면 1차의 본문 모양과 구현하며 정한 것을 적는다

계약 2절 "서식 본문"에 앱이 만드는 모양(서명 경계 div · 글로 만든 원문
인용 · 다시 열기 조건), 앱 디자인 README 「쓰기」에 구현하며 정한 것 ·
알려진 한계 · 실기기 점검, flutter 스킬에 쓰기 화면 줄과 위젯 · 시험
함정을 적는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 02:11:01 +09:00
parent 80e48d318d
commit f39f41ed4b
3 changed files with 54 additions and 0 deletions
+7
View File
@@ -27,6 +27,7 @@ description: Flutter 모바일 앱(flutter/) 개발 규칙 — Dart Sound Null S
| **메일 목록 (`App1MailList`)** · 앱 틀 | `lib/features/mail/` — data(models · `mail_repository.dart`) · domain(`mailbox_tree` · `mail_list_merge` · `mail_bulk` — 순수 함수) · presentation(`pages/mail_list_page.dart` · providers · widgets), 틀 `core/router/app_shell.dart` · `shell_bar.dart`, `core/utils/`(`app_foreground` · `accessible_navigation` · `korean_format`), `shared/widgets/confirm_dialog.dart`, 시험 도구 `test/support/mail_fakes.dart` · `mail_harness.dart` | **틀은 Scaffold 가 아니다**(Column + 탭 막대 — 탭 안 화면마다 자기 Scaffold 를 둔다, 없으면 스낵바 · Material 조상이 없다). 탭 막대는 `MediaQuery.removePadding(removeTop)` 으로 감싸고, 화면이 숨기려면 `ShellBarHidden`(모듈마다 센다 — 보이는지는 TickerMode). Scaffold 는 넓은 단추 자리에 안전 영역을 넘기지 않으니 Scaffold 밖에서 읽어 넘긴다. 목록 상태는 메일함마다(`mailListProvider(String?)` autoDispose), 세션이 바뀌면 `mailAccountProvider` 로 처음부터. 알림은 두 갈래 — 단추 있는 `mailToastProvider`(한 번에 하나 · 화면 낭독기 켠 동안 닫지 않음) · 단추 없는 `mailNoticeSinkProvider`(전역 스낵바). 생명주기 안에서는 프로바이더를 바꾸지 않는다(그린 뒤 · 마이크로태스크), 다른 노티파이어 인스턴스를 든 노티파이어는 세션이 바뀐 뒤 다시 읽는다. 가짜 서버는 응답이 한 프레임 안에 오므로 "받는 중이 끝나는 순간"을 기다리는 위젯을 만들지 않는다 |
| **메일 읽기 (`App1Reader`)** | `lib/features/mail/` — domain(`mail_html`(두 번째 정화 · 중첩 비용 · CSP 문서 · 중계 그림 바꿔 넣기) · `mail_images` · `mail_links` · `compose_request` · `mail_body_notices`) · `data/`(`models/message_detail.dart` · `mail_link_launcher.dart`) · presentation(`pages/message_reader_page.dart` · `providers/message_reader_controller.dart` · `attachment_downloads.dart` · widgets `mail_body_view` · `reader_*`), 시험 도구 `test/support/fake_webview.dart`(`WebViewPlatform.instance`) · `file_fakes.dart` | **WebView 에는 `buildMailDocument` 의 문서만 넣는다**(CSP 를 맨 앞 · JS 끔 · 처음 문서 말고는 탐색을 막음 · 안드로이드는 파일 · content 접근 · 위치 · 섞인 내용 끔, 아래 틀(iframe)의 탐색은 판정을 거치지 않으니 막는 것은 정화(내용째 버림)와 CSP 다). 받은 HTML 은 서버가 정화했어도 `sanitizeMailHtml` 을 한 번 더 지난다 — package:html 은 `parse().body`(`parseFragment` 는 맨 위 형제가 많으면 제곱), 그 전에 `htmlNestingCost`(선형 · 파서보다 깊게 센다)로 한도를 보고, 바로 할지 isolate 로 보낼지는 `mailBodyBuildPlaceOf` 하나로 고른다. 중계 그림은 Dio(Bearer)로 받아 `data:` 로 넣고 토큰은 WebView 에 가지 않는다 — 문서에 싣는 양은 **나온 횟수까지** 센다(`resolveProxiedImages`). 응답은 그 요청이 바꾼 칸만 지금 요약에 덮는다(`_applyResponse`). 메일 id 마다 autoDispose 프로바이더(라우트 `ValueKey(id)`) · 받기 세대 · 본문 세대 · 세션 세대로 늦은 응답을 버린다. 목록에는 `MailChange(updated, removedIds)` 로 알린다. 쓰기 화면을 여는 값은 웹과 같은 쿼리(`replyTo` · `all` · `forward` · `draft` — `composeIntentOf`), mailto 로 채울 값은 go_router `extra` 의 `ComposePrefill` |
| 받은 파일 · 형식 | `lib/core/files/` — `downloaded_files.dart`(`DownloadedFileStore` · `CacheDirFileStore` · `FileOpener`) · `file_names.dart`(`safeFileName`) · `file_types.dart`(`fileOpenTypeOf` · `isBlockedFileOpen`), 안드로이드 `MainActivity.kt`(채널 `kr.it.arasystem.app/files`) · `res/xml/downloaded_file_paths.xml` · 매니페스트의 FileProvider `${applicationId}.downloads` | 모듈 공통(메일 첨부 · 나중의 파일 모듈). 자리는 `cacheDir/downloads/<무작위 16자>/<정리한 이름>` 뿐 — **짝 셋**: Dart `kDownloadedFilesFolder` · `downloaded_file_paths.xml` 의 cache-path · `MainActivity.kt` 의 `DOWNLOADS_DIR`(채널 이름도 짝). Kotlin 은 canonical 경로로 그 폴더 안인지 다시 보고 읽기 권한을 한 번만 준다. 파일 여는 플러그인(open_filex 등)은 쓰지 않는다 — 저장소 권한 · 기기 전체 경로를 더한다. 열 형식은 확장자 먼저(`package:mime`), 보낸 쪽 형식은 확장자를 모를 때만 — 형식 글 모양 검사는 짝: Kotlin `MIME_PATTERN` · `GENERIC_TYPE`. 막는 형식(웹 문서 · svg · 설치 파일 · 인증서)은 받지도 않는다 |
| **메일 쓰기 (`App1Compose` — 1차)** | `lib/core/people/`(사람 고르기 — 주소 칩 · 추천 맞추기 · 그룹, 모듈 공통. 표본 표 `test/core/people/recipient_match_samples.dart` 는 웹 · 백엔드와 **세 쪽 짝**) · `features/mail/domain/`(`compose_body` 직렬화 · `compose_fit`(`buildInitialQuote`) · `mail_quote` · `compose_draft_body`(`buildDraftBody` · `draftBodyPlaceOf`) · `compose_reply` · `compose_send` · `compose_signature` · `html_nodes`) · `data/`(`compose_models` · `compose_payload` · `repositories/compose_repository.dart`) · presentation(`pages/message_compose_page.dart` · `providers/compose_controller.dart`(화면마다 하나 — `ComposeArgs.instance`, autoDispose) · `draft_autosave.dart` · `signature_image_provider.dart` · widgets `compose_*`) · `core/utils/utf16_length.dart` · 시험 도구 `test/support/compose_fakes.dart` | 본문 모양은 계약 2절 "앱이 만드는 모양". 웹 짝(`mail-address.ts` · `recipient-match.ts` · `MAIL_HTML_STYLES` · `docToText` · `fit-draft` · `DRAFT_*` · `shouldRenewSendKey`)은 각 파일 머리말에 적었다 — 한쪽을 고치면 둘 다. **package:html 의 `text` · `querySelectorAll` · `innerHtml` · `outerHtml` 은 깊이만큼 되풀이한다** — 쓰기 화면은 `html_nodes.dart`(되풀이 없는 글 모으기 · 찾기 · 직렬화)를 쓰고, 저장본 · 원문은 읽기 화면과 같은 한도 · 문턱(`mailBodyBuildPlaceOf`)으로 그 자리 · isolate · 읽지 않음을 고른다. **화면을 떠나도 끝까지 가야 하는 일(보내기)은 기다림을 시작하기 전에 `ref.keepAlive()` 를 잡고 try/finally 로 놓는다**(2026-11-07 리뷰 — 저장을 기다리는 사이 내려가 메일이 나가지 않았다). 임시 저장은 Timer 만 쓴다(시험은 `tester.pump` 로 시간을 넘긴다). 보내기 결과는 `mailToastProvider` 의 마감 알림(목록 · 읽기 화면이 그린다). 서버와 같은 한도를 거는 칸은 `Utf16LengthLimitFormatter`(UTF-16 단위) |
| HTTP 클라이언트 | `lib/core/api/dio_client.dart` · `dio_provider.dart` | Dio 둘: 화면용(인터셉터 줄 `ResponseInterceptor` → `AuthInterceptor` → `ErrorNotifyInterceptor`)과 갱신 · 다시 보내기용(언래핑만). baseUrl=dotenv `API_URL`(`api_config.dart`), User-Agent `ARASYSTEM-App/<판> (Android <판>; <기종>)`(`user_agent.dart`). `LogInterceptor` 는 넣지 않는다(비밀번호 · 토큰). 시험은 `httpClientAdapterProvider` 로 가짜 서버를 끼운다 |
| 언래핑 · 코드 정규화 | `lib/core/api/response_interceptor.dart` · `api_error_mapper.dart` | `{success,data}→data`(스트림 · 바이트는 그대로 — 그 **실패** 본문은 64KB 까지 읽어 `error.code` 를 꺼낸다(`decodeRawErrorBody`). 스트림을 받는 도중의 실패는 인터셉터를 거치지 않아 부르는 쪽이 `SYS_001` 로 알린다(`DownloadInterrupted`). 비동기 처리기는 무슨 일이 있어도 `handler.next` 로 끝낸다 — 던지면 요청이 매달린다), 200 + `success:false` 도 실패, 실패는 모두 `DioException.error` 에 `ApiException`. 상태 → 코드 폴백(웹 `resolveCodeByStatus` 와 같다) |
| **ApiException** | `lib/core/api/api_exception.dart` | `{code, message, traceId, statusCode}`. 화면은 `apiErrorCodeOf(e)` 또는 `e.error as ApiException` 의 **`code`** 로 가른다 |
@@ -290,6 +291,12 @@ await tokenStore.clear(byUser: true); // 로그아웃 (세션 끝은 byUser
Flutter 판은 위 "Flutter 판"(3.41.9) — CI 도 같은 판이다.
CI(`ci-flutter.yml`)는 **flutter/ 가 바뀐 커밋에서만** 자동 실행된다 (SDK 다운로드가 느려 분리).
다른 시점에 검사가 필요하면 Actions 탭의 "Run workflow" 로 수동 실행한다.
- [ ] **위젯 · 시험 함정**(쓰기 화면에서 찾음 — 2026-11-07):
`TextEditingController` 는 초점 · 선택만 바뀌어도 알린다 — 바꿈은 글을 견줘 가른다.
go_router 의 `context.pop()` 은 `NavigatorState.pop` 이라 PopScope 의 `canPop` 을 보지 않는다 — 화면이 스스로 닫을 때는 자기 표시로 가린다.
`ref` 는 `State.dispose` 에서 쓰지 못한다 — 노티파이어를 필드에 쥔다(`initState` 에서 `listenManual` + `read`).
되풀이 타이머(남은 시간 막대 250ms)는 `pumpAndSettle` 간격(100ms)보다 길게 둔다. isolate 를 쓰는 일(원문 인용)은 위젯 시험에서 프로바이더로 덮어쓴다(`composeQuoteBuilderProvider`).
소스에 보이지 않는 글자를 두지 않는다 — nbsp 는 `'\u00A0'` 이스케이프로 적는다.
- [ ] **린트로 강제되는 항목**: `dynamic` 호출·`print`·`const` 누락·await 누락·작은따옴표·import 정렬 (`analysis_options.yaml`).
- [ ] **env**: `flutter/.env.example` → `.env` 복사 후 `API_URL` 설정 (Android 에뮬레이터는 `10.0.2.2`).
`.env` 는 `pubspec.yaml` 의 asset 이라 **없으면 `flutter analyze` 가 경고**한다.
+37
View File
@@ -120,6 +120,43 @@
- 보낸 뒤에는 쓰기를 시작한 화면(읽기 · 목록)으로 돌아가고, 그 화면 아래에 "메일을 보냈습니다. [보내기 취소]"와 남은 시간 막대.
- 만들 때 지킬 것(2026-11-06 읽기 화면 리뷰): 읽기 화면의 mailto 링크로 채운 숨은참조가 있으면 숨은참조 칸을 펼쳐 보인다 — 링크가 숨은참조를 몰래 넣을 수 있다(RFC 6068 보안 고려).
- **구현하며 정한 것**(2026-11-07 — 보드에는 아직 없다. 1차 — 첨부 고르기 · 올리기는 2차)
- 본문: 글만 쓴다. 보낼 때 서식 본문 + 텍스트를 함께 만든다(계약 2절 "앱이 만드는 모양"). 원문 인용은 글로 만든 덩이라 원문의 표 · 색 · 링크 모양은 빠지고 글은 남는다(웹에서 쓴 답장은 원문 서식이 남는다 — 서식 편집을 넣는 다음 판에서 넓힌다).
처음 95% · 저장 · 보내기 직전 100% 로 맞추고, 내 글만으로 넘으면 본문 아래 띠(`MAIL_007`).
- 서명: 따로 된 칸(점선 위 · [서명 빼기] → 그 자리에 [서명 넣기] 한 줄). 새 메일은 isForNew, 답장 · 전달은 isForReply 일 때만 넣는다.
미리 보기는 글 줄과 서명 그림(Bearer · 300KB · HTML 폭을 화면 안으로)이고, 굵게 · 색 같은 꾸밈은 보낸 메일에만 남는다.
- 원본 메일: 처음 접혀 있다(요약 "보낸 사람 · 날짜" — 답장 · 전달은 "11월 5일 (목) …", 올해가 아니면 연도까지, 다시 연 저장본은 머리 표의 글 그대로). 펼치면 머리와 원문 줄, 원문 속 인용은 단마다 막대.
- 받는 사람
- 칩은 쉼표 · 세미콜론 · 줄바꿈(따옴표 · 꺾쇠 밖) · [다음] · 칸 벗어남 · 추천 고름에서 만든다. [다음]은 첫 추천을 넣고, 완전한 주소면 친 그대로, 빈 칸이면 다음 칸으로 간다.
- 글 자판(추천 켬 · 자동 고침 끔)이다 — 메일 주소 자판은 한글을 막는다(로그인 화면에서 본 문제).
- 추천은 열 때 100개를 받아 두고 모자라면 250ms 뒤 q 로 묻는다. 묶음 이름 줄(주소록 · 그룹 · 사내 · 최근 주고받은 주소 — 계약 2절의 "사내와 최근을 나눠 보인다")을 두고 8줄까지다.
- 틀린 주소는 빨간 칩, 받는 사람 오류(사전 검사 · `MAIL_002` · `MAIL_010`)는 칸 아래 띠다. 참조 · 숨은참조는 값이 있어도 접을 수 있고(보드대로) 오류가 나면 저절로 펼친다.
전체 답장 · 저장본 · mailto 에 값이 있으면 펼친 채 연다. 칩 ✕ 40 은 밀도 예외다.
- 제목: UTF-16 255자(서버 · 웹과 같은 단위 — 조합 중에는 막지 않고 끝나면 자른다). 저장본 · mailto 로 넘게 들어온 제목은 보낼 때 띠로 막고 제목에 초점을 옮긴다.
- 열 때 초점: 받는 사람 → 제목 → 본문 가운데 비어 있는 첫 칸(답장은 본문).
- 임시 저장: 웹 규칙(3초 멈춤 · 쉬지 않고 쓰면 30초 · 10초에 한 번 · 한 번에 하나 · `MAIL_008` · 404 면 새로 · 실패면 15 · 30 · 60초 뒤 다시)에 더해, 앱이 안 보이게 되면(홈 · 다른 앱 · 화면 끄기) 바로 저장한다.
쓰는 동안 저장 상태는 보이지 않고, 저장한 채 닫으면 "임시보관함에 저장했습니다." 옛 저장본 id → 새 id 를 세션 동안 기억한다(목록의 옛 줄 → 마지막 저장본).
- 저장본 읽기: 읽기 화면과 같은 한도 · 문턱(그 자리 · 다른 isolate · 읽지 않음). 한도를 넘은 저장본은 텍스트 본문으로 열고 원래 HTML 을 그대로 다시 보낸다. 읽다 실패하면 [다시 시도].
서식이나 그림이 있는 남의 저장본(웹의 정렬 · 들여쓰기 포함)은 "서식이나 그림이 있는 임시 저장본입니다. 앱에서 본문을 고치면 글자만 남으니, 서식을 지키려면 웹메일에서 이어 써 주세요."
- 보내기: 칩 확정 → 사전 검사 → 본문 길이 재기 → 진행 중 저장 기다림 → 발송(60초 · 같은 키 — 성공 · `MAIL_005` 에만 새 키).
보내는 동안은 칸 · 단추를 막는다(✕ · 뒤로는 그대로 — [보내기] 자리는 도는 표시, 낭독 "보내는 중입니다.").
[보내기]를 누른 뒤 떠나도 저장을 기다려 그대로 보내고 결과를 알린다(웹과 같다 — 2026-11-07 리뷰: 기다리는 사이 화면이 내려가면 나가지 않았다).
떠난 뒤 보내기 전에 멈추면(기다린 저장이 첨부를 찾지 못함 · 대용량 파일을 쓸 수 없음) `MAIL_004` · `MAIL_016` 의 사전 문구를 알리고, 저장할 수 있으면 쓰던 내용을 저장본으로 남긴다.
- 보낸 뒤: 시작한 화면(읽기 · 목록)으로 돌아가 "메일을 보냈습니다." + [보내기 취소] + 3px 남은 시간 막대(읽기 화면에도 아래 막대 위 12 에 알림 자리를 만들었다).
마감은 서버 시각이 있으면 sendAt − Date − 2초(Date 가 초 아래를 버리고 응답 시간을 빼지 않아 1초 더 남긴다), 없으면 min(설정 초 − 1초 — 모르면 30초, sendAt − 기기 시각), 상한 30초.
화면 낭독기를 켠 동안에도 마감에 닫는다. 바로 보냈으면(설정 0) [보낸편지함 보기]. [보내기 취소]는 쓰기 화면을 다시 열고(새 키 · 서명 칸 · 인용까지) "보내기를 취소했습니다. 쓰던 메일로 돌아왔습니다."
받는 사람 일부가 받지 못했으면 웹 문장 그대로("보냈지만 N명은 받지 못했습니다. …").
- 나가기: 잃을 것이 없으면 바로 닫는다(예측 뒤로 가기). 있으면 쓰다 만 주소를 확정하고 저장(5초)한다 — 저장했으면 "임시보관함에 저장했습니다."로 닫고,
못 했으면 "저장하지 못한 내용이 있습니다" / "이 화면을 나가면 마지막으로 바꾼 내용이 사라집니다." [계속 쓰기](주 단추 · 처음 초점) [나가기].
창을 연 뒤 저장이 끝나 잃을 것이 없어지면 저절로 닫고 떠난다. 떠나기 전 저장을 기다리는 동안은 [보내기]를 막는다. 보내는 중에는 묻지 않고, 세션이 끝나면 창만 닫고 알리지 않는다.
- 첨부(1차): 전달하는 원본의 첨부와 저장본에 있던 첨부 · 대용량 파일을 싣고 빼기만 한다. 문제가 있으면 줄 테두리가 빨갛고 줄 아래에 까닭을 적는다
(함께 보낼 수 없음 · 찾지 못함 `MAIL_004` · 올리다 멈춤 · 지워짐 · 위험한 파일). [파일 첨부]는 "앱에서 파일 첨부는 아직 준비 중입니다. 지금은 웹메일에서 첨부해 주세요." 첨부 머리의 한도 글은 2차다.
- 글자 크기는 테마 크기로 맞췄다(15.5 → 16 · 14와 14.5 → 15 · 13 → 13 · 12.5 → 12).
- **알려진 한계**: 앱이 안 보이게 될 때 칸에 쓰다 만 주소(칩이 되지 않은 글)는 저장에 들지 않고, 나가기 확인 창이 떠 있는 동안은 저장하지 않는다. 앱이 앞에 있는 채로 정리되면 마지막 몇 초를 잃는다.
첨부 합계 한도는 1차에서 25MB 대체값이다(`mail-quota` 는 2차). **푸시를 붙일 때**: 다른 화면으로 `go` 해서 쓰기 화면이 내려가면 detach 가 자동 저장을 멈춘다 — 내려가기 전에 마저 저장하게 바꾼다.
- **실기기에서 볼 것**: 삼성 · Gboard 자판의 칩 만들기(쉼표 · [다음] · 한글 조합 중 확정 · 여러 줄 붙여 넣기), 추천 목록이 자판에 가리는지, 예측 뒤로 가기, 홈 · 다른 앱으로 갔을 때 저장, 긴 원문 펼침 스크롤,
서명 그림 폭, TalkBack 차례(칩 · 추천 · 원본 펼침 · 띠), 제스처 내비게이션에서 읽기 화면 알림 자리, 웹 ↔ 앱으로 저장본을 서로 다시 열기, 앱 로그인 서버(feat/app-auth)와 함께 실제 보내기 · 저장 · 서명 그림.
### 새 메일 알림 (푸시)
- 알림 내용은 "새 메일 N통"만 — 보낸 사람 · 제목이 Google · Apple 서버를 거치지 않게. 누르면 메일 모듈의 받은편지함으로(알림에는 모듈 이름과 id 만 싣는다).
+10
View File
@@ -480,6 +480,16 @@ interface RejectedRecipient {
원문은 `htmlBody`(없으면 `textBody`)를 편집기로 옮기며, 편집기가 모르는 요소(이미지 등)는 빠진다.
- **본문 안 이미지는 이번 단계에서 넣지 않는다** — 붙여 넣거나 끌어다 놓은 이미지는 첨부 파일로 올린다 (첨부 한도 규칙 그대로).
서명 그림만 예외다 (아래 "서명").
- **앱이 만드는 모양**(2026-11-07 — 앱 쓰기 화면은 글만 쓴다. 웹이 다시 열 때 인용을 알아보는 모양이다):
- 감싸개는 위 기본 글꼴 div 이고, 내 글은 줄마다 `<p style="margin: 0">`(빈 줄은 `<p style="margin: 0"><br></p>`, 여러 칸 공백 · 줄 앞 공백 · 탭은 위 nbsp 규칙)이다.
- 서명이 있으면 빈 문단 하나 뒤의 **속성 없는 `<div>` 하나**가 서명 경계다 — 안은 서버가 준 서명 HTML 이고, margin 이 없는 `p` 에만 `margin: 0` 을 앞에 붙인다.
- 원문 인용은 웹과 같은 "원본 메일" 머리 문단 + 머리 표(답장은 보낸 사람 · 날짜 · 제목, 전달은 받는 사람 · 참조도 — 날짜는 "2026. 11. 5. (목) 오전 10:42")에
**글로 만든 원문 줄**(`<p style="margin: 0">` — 원문 속 인용은 안쪽 blockquote, 링크는 "글 (주소)")이다. 원문의 표 · 색 · 링크 모양은 빠지고 글은 남는다.
원문이 길면 처음 95% · 저장 · 보내기 직전 100% 로 맞추고, 줄였으면 끝에 "(원문이 길어 이하 생략)" 문단을 둔다. 인용 경계 표시는 대용량 파일이 있을 때만이다(4절).
- 쓴 글 · 서명 · 인용이 모두 없으면 `bodyHtml` 을 싣지 않는다(텍스트 메일). `body` 는 내 글 줄 + (서명이 있으면 빈 줄 + 텍스트 서명) + 인용 줄(웹 `docToText` 와 같은 꼴 — "> 원본 메일" · "> 항목: 값" · ">" · "> 원문" · "> > 안쪽")이다.
- **다시 열기**: 감싸는 div(속성 없음 또는 앱 글꼴 style) 아래가 "글 · br 만 든 문단(속성 없음 또는 style 이 `margin:0` 하나 — 공백 · 끝 `;` 를 빼고 견줌) → (속성 없는 div 하나 = 서명) → (빈 div) → (원본 메일 blockquote)"이면 칸마다 되살린다.
그 밖(웹의 정렬 · 들여쓰기 · 꾸밈 · 다른 프로그램의 저장본)은 인용 앞부분을 글로 바꿔 본문 칸에 넣고 안내 줄을 보이며, 본문을 고치지 않으면 원래 HTML · 텍스트를 그대로 다시 보낸다.
읽기 화면과 같은 한도(서식 본문 2,000,000자 · 중첩 비용 8,000,000)를 넘는 저장본은 읽지 않고 텍스트 본문으로 연다.
### 첨부 업로드 한도