앱 버전 다음 줄에서 목록(제목 · 날짜, [더 보기])으로 들어가 줄을 누르면 글(날짜 · 제목 · 묶음, 다음 글 · 이전 글 줄)이 열린다. 없는 글 · 받지 못함 · 글 없음 상태를 두고, 날짜는 달력 날짜 글이라 시각으로 바꾸지 않는다(CalendarDate). 다른 글 줄은 지금 글을 바꿔 끼워 뒤로는 늘 목록이다. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ARA SYSTEM 앱 (Flutter)
ARA SYSTEM 그룹웨어의 모바일 앱이다. 식별자 kr.it.arasystem.app · 이름 "ARA SYSTEM", 안드로이드 먼저.
스토어에 올리지 않고 우리 키로 서명한 APK 를 사내에만 나눠 준다(포털의 휴대폰 앱 받기 화면 — 아래 "사내 배포", 2026-11-08).
- 구조 · 구현 규칙:
.claude/skills/flutter/SKILL.md - 앱 로그인(앱 전용 토큰 경로): 계약
docs/mail/api-contract.md1절 "앱 로그인" - 화면 기준:
docs/design/app/README.md(보드 사본docs/design/webmail/App1*.dc.html)
이 문서는 개발 PC · 배포에서 알아 둘 사실만 적는다.
개발 실행
cd flutter
cp .env.example .env # 없으면 analyze 가 asset 경고를 낸다
flutter pub get
flutter analyze # 0건이어야 한다
flutter test
flutter run # 에뮬레이터 · USB 실기기
- API 주소는
.env의API_URL하나다(루트 env 를 따르지 않는다)..env는 앱 안에 그대로 들어가므로 비밀은 넣지 않는다.- Android 에뮬레이터:
http://10.0.2.2:3000/api— 에뮬레이터의 10.0.2.2 가 PC 의 127.0.0.1 이다. - USB 실기기:
adb reverse tcp:3000 tcp:3000뒤http://localhost:3000/api. 개발 백엔드는127.0.0.1에만 열려 있어 같은 와이파이의 휴대폰은 닿지 않는다(docker 스킬 "호스트 포트"). - 받기 화면까지 볼 때(업데이트 화면 · 설정 › 앱 정보의 [새 버전 받기] — 아래 "사내 배포"): vite 개발 서버(5173 —
/api를 백엔드로 넘긴다)를 가리킨다. 에뮬레이터http://10.0.2.2:5173/api, USB 실기기adb reverse tcp:5173 tcp:5173뒤http://localhost:5173/api. [새 버전 받기]는API_URL의 출처에/app을 붙여 열기 때문에, 백엔드(3000)를 바로 가리키면 백엔드의 404 가 열린다. 받기 화면은 휴대폰 브라우저에서 웹메일에 로그인해야 보인다.
- Android 에뮬레이터:
- 개발 PC 의 안드로이드 도구: Android Studio 의 JDK 21 · SDK 36 · NDK 28.2(Flutter 기본)로 빌드된다. SDK 라이선스 동의(
flutter doctor --android-licenses)는 사람이 직접 한다.
Flutter 판
3.41.9 stable (Dart 3.11.5) 으로 고정이다 — 개발 PC 와 CI 가 같은 판이어야 한다. 판을 바꿀 때 함께 고치는 곳:
.github/workflows/ci-flutter.yml의flutter-version(CI 는flutter pub get --enforce-lockfile— lock 그대로)pubspec.yaml의environment.sdk,intl(flutter_localizations가 SDK 마다 정확한 판으로 고정한다 — 새 SDK 의packages/flutter_localizations/pubspec.yaml값으로).claude/skills/docker/SKILL.md이미지 버전 표의 "Flutter SDK" 줄 · flutter 스킬 "Flutter 판" · 이 문서- 개발 PC 의 Flutter 는 다른 프로젝트도 함께 쓴다 — 올리기는 사용자가 정한다.
빌드
flutter build apk --debug # 2~3분 · 약 160MB (build/ 는 git 이 무시한다) — 개발 · 시험용, debug 키로 서명한다
flutter build apk --release # 직원에게 나눠 주는 판 — 우리 서명 키 · google-services.json · 운영 API_URL(https://<포털>/api)이 먼저다(아래 "사내 배포")
- release 매니페스트(
android/app/src/main/AndroidManifest.xml)에 INTERNET 권한이 있어야 네트워크가 된다(flutter create 기본은 debug · profile 에만 둔다). - release 빌드는 아래 셋 가운데 하나라도 갖추지 못하면 멈추고, 빠진 것을 한 번에 알린다(
android/app/build.gradle.kts맨 아래). debug · profile 빌드는 셋 없이도 된다.android/app/google-services.json(아래 "새 메일 알림") — 배포 판이 새 메일 알림 없이 나가지 않게.- 우리 서명 키 —
android/key.properties와 그것이 가리키는 keystore(아래 "사내 배포"). 파일이 없거나 · 칸이 비었거나 · keystore 파일이 없으면 멈춘다. flutter create 기본처럼 debug 키로 조용히 서명하지 않는다 — debug 키로 서명한 판을 깐 휴대폰에는 우리 키의 다음 판이 덮어 설치되지 않는다.flutter run --release도 키가 든다. - 운영 API 주소 — 앱에 들어가는
flutter/.env의API_URL이 운영 포털의 API 주소(https://<포털>/api, 끝의/하나까지)여야 한다:https://· 개발 주소(localhost ·.localhost·.test로 끝나는 이름 · 127.0.0.0/8 ·::1· 0.0.0.0 · 에뮬레이터의 10.0.2.2)가 아님 · 경로/api· 사용자 정보 · 쿼리 · 조각 없음(APK 안의.env는 누구나 풀어 볼 수 있다) · 앞뒤 빈칸 없음(앱은 읽은 값을 그대로 쓴다 — 따옴표 안의 빈칸은 떼지 않는다)..env가 없거나 ·API_URL이 없거나 비었거나 · 읽지 못해도 멈춘다(값이 없으면 앱은 개발 주소로 돈다). 앱과 같은 규칙(flutter_dotenv 5.2.1 의 Parser — 주석 ·export· 따옴표 ·$이름)으로 읽고, 알리는 글에는 주소 값을 싣지 않는다(무엇이 틀렸는지만). - Gradle 이 실행할 작업 그래프를 본다 —
flutter build apk --release뿐 아니라./gradlew assemble·bundle·build(모든 변형) · 줄여 쓴 이름(aR)도 멈춘다. - debug · release 를 다른 Firebase 프로젝트로 나누려면(
app/src/debug·app/src/release에 따로)build.gradle.kts의 조건도 함께 바꾼다(지금은app/google-services.json하나만 본다).
- CI(
ci-flutter.yml)는 analyze · test 만 돌고 안드로이드를 빌드하지 않는다 — 서명 키가 없어도 CI 는 그대로다.
새 메일 알림 (Firebase Cloud Messaging)
- 의존성은
firebase_core·firebase_messaging둘뿐이다(Analytics · 로컬 알림 · 권한 플러그인은 넣지 않는다). 알림은 FCM 이 그리는 알림 메시지이고, 앱은 채널 · 지우기 · 휴대폰 설정 열기만 한다(MainActivity.kt의NewMailNotifications.kt— 채널kr.it.arasystem.app/notifications). google-services.json: Firebase 콘솔에서 안드로이드 앱(패키지kr.it.arasystem.app)을 만들고 받은 파일을flutter/android/app/google-services.json에 둔다. 커밋하지 않는다(android/.gitignore). 키가 들어 있지만 비밀은 아니다(앱 안에 그대로 들어간다) — 그래도 저장소 밖에서 나눈다. 파일이 있을 때만 Gradle 이com.google.gms.google-services(4.4.4)를 건다.- 파일 없이 만든 debug 판은 Firebase 를 띄우지 못해 설정 › 새 메일 알림이 "이 휴대폰에서는 … 받을 수 없습니다"로 보인다(나머지는 그대로 된다). 서버 쪽 발송 키(서비스 계정)는 백엔드 설정이다.
- 매니페스트: 알림 권한(
POST_NOTIFICATIONS), 기본 채널new_mail(이름 · 설명은res/values/strings.xml), 작은 아이콘@drawable/ic_stat_ara, 강조색@color/notification_accent, FCM 자동 초기화 끔(firebase_messaging_auto_init_enabled— 로그인한 뒤에만 등록한다), 광고 ID 권한은 빼 둔다(tools:node="remove"— 어느 라이브러리도 넣지 않지만 들어오지 않게). FID 등록 켬(firebase_messaging_installation_id_enabled=true) — 없으면register()·unregister()가 "API disabled" 로 곧바로 실패해 켜기가SYS_001이 된다(firebase-messaging 26.0.1 에서 확인 — 플러그인 README · CHANGELOG 에는 없고 예제 매니페스트에만 있다). 켜면 거꾸로 옛getToken()·deleteToken()이 거절된다 — 앱은register()·unregister()만 쓴다. - 등록은 FID 로 한다(
FirebaseMessaging.register()·onRegistered) — 토큰 · FID 를 로그 · 디버그 출력에 남기지 않는다. 서버 경로 · 규칙은 계약 1절 "앱 새 메일 알림 · 앱 설정".
앱 아이콘 · 알림 아이콘
보드 App1Push ⑨ · ⑪(2026-11-07 확정). 그림은 docs/brand/logo_arasystem_white.png 의 ARA 마크만 잘라 만들었다(만드는 스크립트는 저장소에 두지 않는다 — 의존성을 늘리지 않으려고). 다시 만들 때의 치수:
- 앱 아이콘(적응형 — 안드로이드 8 이상):
res/mipmap-anydpi-v26/ic_launcher.xml·ic_launcher_round.xml= 배경색@color/ic_launcher_background(#0B3640) + 앞면@mipmap/ic_launcher_foreground+ monochrome(안드로이드 13 테마 아이콘 — 같은 앞면 그림). 앞면은 108dp 칸(투명)에 흰 마크 폭 46.5dp(보이는 72dp 의 0.646 — 잘리지 않는 가운데 66dp 안)를 가운데에. mdpi 108px · hdpi 162 · xhdpi 216 · xxhdpi 324 · xxxhdpi 432. - 옛 아이콘(안드로이드 7 — minSdk 24 · 25):
mipmap-*/ic_launcher.png(48dp 칸에 44dp 둥근 네모 · 모서리 30%) ·ic_launcher_round.png(동그라미) — 같은 남색 · 마크 비율. 48 · 72 · 96 · 144 · 192px. - 알림 작은 아이콘
ic_stat_ara:res/drawable-*/ic_stat_ara.png— 24dp 칸(투명)에 흰 마크 폭 22dp, 알파만 쓴다(휴대폰이 색을 칠한다). 24 · 36 · 48 · 72 · 96px, mdpi · hdpi 는 획을 반 픽셀 굵게. 봉투로 바꾸려면 이 다섯 장만 같은 이름으로 바꾼다 — 매니페스트의 기본 아이콘(default_notification_icon)이 이 이름 하나를 가리킨다. 서버 메시지는 아이콘 · 색을 싣지 않는다 — 휴대폰이 매니페스트의 기본값(아이콘 ·default_notification_color· 채널new_mail)으로 그린다.
사내 배포 (안드로이드 APK — 2026-11-08)
앱은 스토어에 올리지 않는다. 우리 키로 서명한 release APK 를 서버에 올리면, 웹메일에 로그인한 직원이 포털의 휴대폰 앱 받기 화면(<포털>/app)에서 받아 설치한다
(PC 의 웹 설정 › 계정 › [휴대폰 앱]의 QR 코드가 그 주소다). 앱의 업데이트 화면과 설정 › 앱 정보의 [새 버전 받기]도 같은 화면을 휴대폰 브라우저로 연다 —
주소는 .env 의 API_URL 에서 /api 를 뗀 출처 + /app 이다(같은 주소 배포 — lib/core/app_download/, 짝: 웹 MOBILE_APP_PATH).
1. 서명 키 만들기 (처음 한 번 — 사람이 한다)
keytool 은 Android Studio 에 든 JDK 의 것을 쓴다(jbr\bin). 키는 저장소 밖에 만든다. keytool 이 keystore 비밀번호를 두 번 묻는다.
New-Item -ItemType Directory -Force "$env:USERPROFILE\arasystem-signing" | Out-Null
& "C:\Program Files\Android\Android Studio\jbr\bin\keytool.exe" -genkeypair -v -keystore "$env:USERPROFILE\arasystem-signing\arasystem-release.jks" -alias arasystem -keyalg RSA -keysize 4096 -validity 10000 -dname "CN=ARA SYSTEM, C=KR"
-dname은 APK 의 인증서에 그대로 보이는 이름이다 — 회사 이름을 넣으려면O=<회사 이름>을 더한다. 빼면 keytool 이 이름 · 조직 같은 칸을 차례로 묻는다.- 지금 keytool 의 기본 형식(PKCS12)은 키 비밀번호를 따로 묻지 않는다 — 아래
keyPassword에는storePassword와 같은 값을 적는다. 비밀번호는 영문 · 숫자로 길게 만든다(key.properties는\를 이스케이프로 읽는다). - 유효 기간은 10000일(27년 남짓)이다 — 이 키로 앱을 계속 올리므로 짧게 잡지 않는다.
- keystore 파일과 비밀번호는 비밀번호 관리자에 함께 두고(파일은 첨부로) 오프라인 사본을 하나 더 둔다. 저장소 · 메일 · 메신저에는 올리지 않는다 — 이 키를 가진 사람은 우리 앱처럼 보이는 판을 만들 수 있다.
- 이 키는 바꿀 수 없다. 잃거나 새로 만들면 그 뒤의 판은 직원 휴대폰의 앱 위에 설치되지 않는다 — 직원 모두가 앱을 지우고 새로 설치해야 한다(로그인 · 새 메일 알림 설정도 처음부터).
2. key.properties
flutter/android/key.properties 를 이 꼴로 만든다 — 커밋하지 않는다(android/.gitignore 가 key.properties · *.jks · *.keystore 를 무시한다).
storeFile=C:/Users/<이름>/arasystem-signing/arasystem-release.jks
storePassword=<keystore 비밀번호>
keyAlias=arasystem
keyPassword=<storePassword 와 같은 값>
- 경로는 절대 경로로,
\대신/로 적는다(상대 경로면android/기준). - 파일이 없거나 · 칸이 비었거나 · keystore 파일이 없으면 release 빌드가 그 까닭을 알리고 멈춘다(위 "빌드"). 비밀번호 · 별칭이 틀리면 Gradle 의 서명 단계가 실패한다.
3. 버전 올리기
pubspec.yaml의version: <이름>+<숫자>를 판마다 올린다 — 예1.0.0+1→1.0.1+2.- 이름(
1.0.1— versionName): 직원에게 보이는 판(설정 › 앱 정보 · 받기 화면) · User-Agent · 서버의 최소 판과 견주는 값이다. 숫자 셋까지 · 칸마다 4자리까지(서버의 판 칸과 같다). - 숫자(
+2— versionCode): 판마다 1 이상 올린다. 숫자가 낮은 판은 깔린 앱 위에 설치되지 않는다.
- 이름(
- 꼭 필요한 업데이트(옛 앱으로는 쓸 수 없게 서버를 바꾸는 배포)면 서버
backend/src/modules/app-config/app-config.constants.ts의MIN_SUPPORTED_APP_VERSION.android를 새 판 이름으로 올린다 — 그 서버가 배포되면 옛 앱은 어느 화면에서든 업데이트 화면이 된다. 새 APK 를 먼저 서버에 올린 뒤 그 서버를 배포한다(업데이트 화면의 [새 버전 받기]가 받을 판이 있게).
4. release 빌드
.env는 앱 안에 그대로 들어간다 — release 를 만들 때는flutter/.env의API_URL을 운영 주소(https://portal.arasystem.it.kr/api꼴 — 포털 출처 +/api)로 둔 채 만들고, 다 만든 뒤 개발 주소로 되돌린다.API_URL의 출처가 곧 [새 버전 받기]가 여는 받기 화면의 출처다. 개발 주소 · http 주소인 채로 release 를 부르면 Gradle 이 멈춘다(위 "빌드").- release 확인(
android/app/build.gradle.kts— 서명 키 ·google-services.json· API 주소 판정)을 고쳤으면 key.properties 없이(또는.env가 개발 주소인 채로)flutter build apk --release를 불러 본다 — 빠진 것이 그 글로 한 번에 나오며 멈추는지, 글에 주소 값이 없는지 본다(판정 함수는 Gradle 스크립트라 저장소의 시험이 없다).
cd flutter
flutter build apk --release
Copy-Item build\app\outputs\flutter-apk\app-release.apk "$env:USERPROFILE\Desktop\ARA-SYSTEM-1.0.1.apk" # 이름은 ARA-SYSTEM-<버전>.apk
- 나오는 파일은
build/app/outputs/flutter-apk/app-release.apk하나다(모든 ABI — arm64-v8a · armeabi-v7a · x86_64). 저장소 밖으로 옮기며 이름을ARA-SYSTEM-<버전>.apk로 바꾼다(받기 화면이 주는 파일 이름과 같은 꼴). - 올리기 전에 본다:
- 우리 키로 서명했는지 —
& "$env:LOCALAPPDATA\Android\Sdk\build-tools\36.1.0\apksigner.bat" verify --print-certs <파일>의certificate DN이 keytool 에 넣은 이름이다(CN=Android Debug면 debug 키다). - 파일 안의 판이 파일 이름과 같은지 —
& "$env:LOCALAPPDATA\Android\Sdk\build-tools\36.1.0\aapt2.exe" dump badging <파일> | Select-String "^package:"의versionName이 파일 이름의 버전,versionCode가pubspec.yaml의+뒤 숫자다(예versionCode='2' versionName='1.0.1'). - 운영 서버를 가리키는지 · 판이 맞는지 — 휴대폰 하나에 설치해 로그인해 보고, 설정 › 앱 정보의 앱 버전이 파일 이름의 버전과 같은지 본다. 운영 도구는 APK 안의 versionName 을 보지 않고 파일 이름 · 운영자가 넣은 버전을 믿는다 — 판이 어긋나면 받기 화면에 틀린 버전이 보이고, 최소 판을 올린 뒤에도 업데이트 화면이 풀리지 않을 수 있다.
- 파일 지문 —
Get-FileHash <파일> -Algorithm SHA256를 적어 두고, 서버에 올린 뒤 운영 도구가 보이는 값과 견준다.
- 우리 키로 서명했는지 —
5. 서버에 올리기
파일을 서버 B 로 옮긴 뒤 sudo docker cp ARA-SYSTEM-<버전>.apk <PROJECT_NAME>-backend:/tmp/ → 운영 도구(sudo docker exec -it <PROJECT_NAME>-backend node dist/cli/ops.js)의 「휴대폰 앱 올리기」. 자세한 차례는 mail/README(운영).
6. 설치 · 업데이트
- 직원은 휴대폰 브라우저로 받기 화면에 들어가 받고 설치한다(설치 차례 글은 그 화면에 있다 — 웹
frontend/src/shared/constants/mobile-app.ts). 새 판도 같은 곳에서 받아 쓰던 앱 위에 설치하면 로그인 · 설정이 그대로다. - 서명이 다른 시험판이 깔린 휴대폰은 먼저 그 앱을 지우고 설치한다 —
flutter run· debug APK(debug 키)나 다른 키로 만든 판 위에는 설치되지 않는다. 지우면 그 휴대폰의 로그인 · 알림 설정은 처음부터다. - 받기 · 설치 때 뜨는 창의 글(받기 경고 · 알 수 없는 앱 설치 허용 · Play 프로텍트 경고)은 브라우저 · 제조사마다 다르다 — 실기기로 한 번씩 거쳐 본다(웹 보드
SettingsMobileApp"구현 때 맞출 것").
7. Google 개발자 인증 (정할 것)
Google 이 인증된 개발자의 앱만 설치되게 바꾼다 — 스토어 밖에서 받은 APK 도 그렇다. 일정은 2026-09-30 첫 네 나라, 2027 년 전 세계다(한국은 전 세계로 넓힐 때). 그 전에 개발자 인증(신원 확인 · 앱 등록)을 받을지 정한다 — 받지 않으면 그때부터 직원 휴대폰에 새로 설치되지 않을 수 있다.
개인정보 메모
새 메일 알림 때문에 Firebase 설치 ID(FID)를 서버에 보낸다(전송 중 암호화 · 다른 곳에 주지 않는다). 메일 내용 · 주소는 알림에 싣지 않는다(글은 "새 메일 N통"뿐).
광고 ID 는 쓰지 않는다(매니페스트에서 뺐다 — tools:node="remove").
iOS
맥 · Apple 개발자 계정이 먼저다 — 빌드 · 서명 · 시뮬레이터가 모두 맥(Xcode)에서만 되고, 배포 · 푸시(APNs 키)는 Apple Developer Program(연 99달러, 법인은 D-U-N-S)이 든다. 내부 배포는 TestFlight 내부 테스터(빌드 90일 만료) 또는 Ad Hoc.
맥에서 처음 빌드할 때 ios/Runner/Info.plist 에 CFBundleLocalizations(ko)가 있는지 확인한다 — 없으면 복사 · 붙여 넣기 같은 시스템 글이 한국어로 나오지 않는다. bundle id 는 kr.it.arasystem.app 으로 맞춰 두었다(맥에서 확인 전).
보안 메모
- 토큰: 리프레시 토큰만
flutter_secure_storage(Android Keystore · iOS Keychain), 액세스 토큰은 메모리. 비밀번호 · 토큰을 로그에 남기지 않는다(LogInterceptor를 넣지 않는다). - Android 백업을 껐다(
android:allowBackup="false"·res/xml/data_extraction_rules.xml) — 보안 저장소의 값은 이 기기의 Keystore 키로 암호화되어 다른 기기로 복원되면 읽지 못하고, 첫 실행 표시까지 복원되면 남은 토큰 지우기가 돌지 않는다. - iOS Keychain 은 앱을 지워도 남는다 — 일반 저장소(
shared_preferences)의 첫 실행 표시가 없으면 앱을 띄우기 전에 보안 저장소를 모두 지운다(core/storage/token_store.dart의prepare). - 만료는 기기 시계로 보지 않는다 — 응답의
Date(서버 시각)로 남은 시간을 잰다. - 받은 메일 본문(WebView): 앱이 한 번 더 정화한 문서(
features/mail/domain/mail_html.dart— 허용 목록 · CSPdefault-src 'none'; img-src data:)만 넣고, 스크립트 · 파일 · content 주소 접근을 끄고, 주 틀의 탐색은 모두 막는다(링크는 바깥 브라우저 · mailto 는 쓰기 화면). 아래 틀(iframe)의 탐색은 안드로이드가 판정에 넘기지 않는다 — 아래 틀은 정화(iframe 을 내용째 버림)와 CSP(default-src 'none'— frame-src)가 막는다. 외부 이미지는 앱이 Dio(Bearer)로 받아data:로 넣는다 — 토큰이 WebView 에 가지 않는다. 문서에 싣는 그림 글은 같은 그림이 여러 번 나오면 나온 만큼 세어 10MB 에서 멈춘다. - 받은 첨부: 앱 임시 폴더(
cacheDir)의downloads/에 받고, 다른 앱으로 열 때는 FileProvider(${applicationId}.downloads— 그 폴더만)의 주소와 읽기 권한 1회로ACTION_VIEW한다(MainActivity.kt의 파일 채널). 앱을 띄울 때(지난 실행이 남긴 것) · 메일을 보는 직원이 바뀔 때 모두 지운다. 파일 이름은 경로 · 제어 · 방향 제어 글자를 지우고 줄인다(core/files/file_names.dart). 열 형식은 정리한 이름의 확장자에서 먼저 정하고(보낸 쪽 형식은 확장자를 모를 때만), 웹 문서 · SVG · 설치 파일 · 인증서 · 키 묶음은 받지 않고 알린다(core/files/file_types.dart). 고르는 창(createChooser)으로 열지 않는다 — 직원이 고른 브라우저가 스크립트를 돌린다.
나중에 넣을 때 주의
- build_runner(freezed · json_serializable · riverpod_generator): 같은 폴더의
lib/core/api/error_lexicon.g.dart(백엔드 레지스트리에서 만든 사전 — 직접 고치지 않는다)를 build_runner 가 자기 출력으로 보고 지울 수 있다. 넣을 때는build.yaml의generate_for를 모델 폴더로 좁히고, 처음 한 번 돌린 뒤 사전이 그대로인지(git diff) 확인한다. - 첨부 받기(스트림 · 바이트 응답): 실패 응답의 본문은
ResponseInterceptor.onError가 64KB 까지 읽어 UTF-8 · JSON 으로 풀고 표준 실패의error.code를 꺼낸다 (decodeRawErrorBody— 그 전에는 HTTP 상태로만 정해FILE_007·FILE_008같은 코드를 잃었다). 성공 본문은 풀지 않고 그대로 넘긴다(시험이 지킨다). 받는 도중 끊기거나 조각 사이 받기 한도(receiveTimeout— Dio 가 스트림에 싣는다)를 넘기면 응답이 이미 인터셉터를 지나 아무도 알리지 않으므로 부르는 쪽이SYS_001로 알린다(DownloadInterrupted).
라이선스
글꼴 Pretendard 1.3.9(SIL OFL 1.1 — assets/licenses/pretendard-ofl.txt)는 앱의 [설정] → [앱 정보] → [오픈소스 라이선스]에 보인다.