읽기 화면은 머리(제목 · 보낸 사람 · 안내 · 첨부)가 남은 높이의 절반까지만 쓰고
나머지는 본문 WebView 였다. 본문이 비면 아래 절반이 빈 칸으로 남고 첨부가 많으면
위 절반 안에서 잘렸다(11-12 배포 뒤 사용자 보고).
정화한 본문에 보이는 것(글자 · 주소가 남은 그림 · hr · li · details · q)이 없으면
상태 bodyEmpty 로 본문 칸을 두지 않고 머리가 화면 전체를 한 스크롤로 쓴다.
판정은 정화와 같은 길에서 내고(큰 본문은 isolate), 아래 막대가 없는 예약 메일은
머리 스크롤에 시스템 아래 여백을 더한다. 본문이 있는 메일은 그대로다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
앱 버전 다음 줄에서 목록(제목 · 날짜, [더 보기])으로 들어가 줄을 누르면 글(날짜 · 제목 · 묶음, 다음 글 · 이전 글 줄)이 열린다.
없는 글 · 받지 못함 · 글 없음 상태를 두고, 날짜는 달력 날짜 글이라 시각으로 바꾸지 않는다(CalendarDate). 다른 글 줄은 지금 글을 바꿔 끼워 뒤로는 늘 목록이다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
스토어에 올리지 않으니 업데이트 화면의 Play 스토어 열기를 포털 /app 을 바깥 브라우저로 여는 것으로 바꾸고,
설정 › 앱 정보에도 같은 줄을 더했다. release 빌드는 우리 서명 키(key.properties) · google-services.json ·
운영 포털의 https API 주소가 갖춰지지 않으면 까닭을 알리고 멈춘다 — debug 키로 조용히 서명되거나
개발 주소가 든 판이 직원에게 나가지 않게(알리는 글에 주소 값은 싣지 않는다).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
이 PR 이 앱 서버 PR 뒤에 빨리 감기로 합쳐지고 CI 가 합친 모습 그대로를 시험하게 한다.
계약 1절의 앱 절은 앱 서버 쪽이 들고 있고 이 브랜치는 2절 줄만 더했다 — 충돌 없음.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
이 PR 이 문구 다듬기 PR 뒤에 빨리 감기로 합쳐지고 CI 가 합친 모습 그대로를 시험하게 한다.
충돌 넷은 모두 설정 › 로그인된 기기의 글이다 — 앱 쪽은 앱 세션이 함께 보여 "브라우저"를 "기기"로,
다듬기 쪽은 같은 안내 문장을 규칙 9 로("여기에 보이지 않습니다" → "이 목록에 없습니다") 바꿨다.
뜻은 앱 쪽 · 문장은 다듬은 글로 풀었고(account-security.ts · LoginSessionList.vue 주석 · 디자인 README),
시험은 두 쪽이 더한 상수를 모두 남기고 글자 그대로 견주던 줄을 상수로 바꿨다(AccountLoginHistoryPage.spec.ts).
합친 뒤 backend tsc · lint 0 · 단위 3,701 · e2e 333, frontend type-check · lint 0 · 단위 2,774 통과.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
서버는 N 을 seen_at 의 초부터 같은 초를 넣어 센다(받은 시각이 초 단위). 앱을 보는 중 새 메일 알림을 받자마자
봤음을 보내면 seen_at 이 그 메일과 같은 초가 되어, 목록으로 보고 열지 않은 그 메일이 다음 알림에 다시 세어졌다
(2026-11-08 실기기 — 한 통이 더 왔는데 "새 메일 2통"). 그 봤음만 받은 때 + 1.5초와 30초 간격 가운데 늦은 때에 보낸다.
앱으로 돌아올 때 · 기다리는 동안 뒤로 갈 때는 그대로다. 고친 뒤 실기기에서 봤음이 2초 뒤에 갔고 다음 알림은 "새 메일 1통"이었다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
앱 첫 판의 마지막 기능이다. 알림 글은 "새 메일 N통"뿐이고 화면은 확정 보드 App1Push · App1Shell 대로다.
- firebase_core 5.1.0 · firebase_messaging 17.0.1 — FCM 토큰 API 가 deprecated 라 FID 로 등록한다.
google-services.json 은 저장소에 두지 않고, 파일이 있을 때만 Gradle 플러그인을 건다(없으면 배포 판 빌드를 멈춘다).
- 받은편지함 위 권하기 줄과 설정 › 새 메일 알림 — 휴대폰의 허락 창은 [알림 켜기]를 누를 때만, 켬 · 끔은 이 휴대폰에 둔다.
- 등록 PUT · 끄기 DELETE · 봤음 unseen(앞에 있을 때만 · 30초 간격), 로그아웃과 세션 끝에는 FCM 등록을 푼다
(같은 휴대폰에 다른 직원이 로그인해도 앞 직원 알림이 남지 않게 — 풀기가 실패해도 다음 로그인은 다시 등록한다).
- 앱이 앞에 있으면 알림 대신 목록을 바로 맞추고, 앞으로 오면 알림창의 알림을 지운다. 알림을 누르면 받은편지함으로 가고 쓰던 메일은 저장한다.
- 앱 설정 GET /api/app-config 를 인증 줄 밖에서 먼저 묻고, 최소 판보다 낮으면 어느 화면에서든 업데이트 화면으로 간다(묻지 못하면 막지 않는다).
- 설정 탭을 한 화면으로(내 정보 · 새 메일 알림 · 앱 정보 · 문의 · 로그아웃 확인 창), 알림 채널 "새 메일"을 Kotlin 에서 만든다.
- 시험 932 → 1,056.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 확정한 보드 App1SearchConditions 대로 웹 14단계의 [조건] 패널을
바닥 시트로 옮겼다. 보낸 사람 · 받는 사람 · 제목 · 내용 · 기간 · 첨부로 찾고,
결과에는 "조건: …" 줄과 [조건 지우기]를 둔다. 조건 다듬기 · 합친 낱말 수 ·
기간 · 날짜의 UTC 시각 · 강조 쿼리 · "나"는 웹 mail-search.ts 와 짝이다.
[조건]은 위 막대 맨 오른쪽에 고정해 글을 써도 옆으로 밀리지 않게 했고,
시트에서 낱말 8개를 넘기면 시트를 연 채 알린다. 날짜는 오늘까지만 고른다.
바닥 시트 가림막을 앱 대화상자 색으로 맞춰 메일함 옮기기 시트도 같은 색이 된다.
리뷰에서 가로 화면에 자판이 오르면 시트 모양이 바뀌며 칸이 새로 만들어져
초점을 잃는 것을 찾았다. 시트를 한 트리로 두고 같은 레이아웃 안에서 머리 ·
단추 줄의 실제 높이로 모양 셋(위를 남긴 고정 · 화면 위까지 고정 · 한 스크롤)을
고르게 고쳤고, 모양이 바뀌거나 알림이 뜨면 초점 칸 · 알림을 보이게 한다.
시험 872 → 932.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
웹 조사 도우미(frontend shared/utils/korean.ts)와 짝으로 맞춘다. 초성 검색어(‘ㄱㅁ’)나
숫자로 끝나는 말이 "(으)로" · "이(가)" 꼴로 나오지 않게, 호환 자모 자음은 받침 있음
(ㄹ 은 "로"), 모음은 받침 없음, 숫자는 읽는 소리(영 · 일 · 이 …)로 고른다. 라틴 글자 ·
기호로 끝나면 지금처럼 괄호 꼴이다. 알려진 한계: 라틴 글자 뒤의 숫자도 한국어 소리로
센다(‘녹음.mp3’은).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 사용자가 "자연스러운 문구가 아니라 시스템, 설계 문구 형식"이라고 짚은
글을 고친다(규칙 9 · 10 — 화면 설명 · 번역투 빼기). "일시적인 시스템 오류가
발생했습니다" · "요청하신 정보나 페이지를 찾을 수 없습니다" · "허용 범위를 초과했습니다"
같은 안내문 말투를 무엇이 안 됐고 무엇을 하면 되는지로 바꾼다.
적용하며 셋을 더 다듬었다: REQ_001 은 가장 자주 보는 로그인 · 코드 실패 한도와 진행
중인 일에 함께 맞게, MAIL_003 은 용량이 휴지통도 세므로 휴지통 비우기까지, FILE_001
은 전달하는 원본 첨부에도 맞게. 코드 · 상태 · 뜻은 그대로이고, 웹 · 앱 사전은 sync 로
다시 만들었다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 사용자가 "자연스러운 문구가 아니라 시스템, 설계 문구 형식"이라고
짚은 글을 웹과 같은 기준으로 고친다. 화면이 하는 일을 설명하는 말("여기에
표시됩니다")과 번역투("맞는 메일")를 빼고, 빈 메일함 안내 · 읽기 화면 본문 안내 ·
검색 글 · 로그인 글을 내 메일이 어떻게 되는지와 할 일로 쓴다. 본문을 다 보이지
못할 때는 PC 웹메일에서 원문을 받는 길까지 적고, 사용자가 만든 메일함의 빈 안내는
앱의 [이동]이 아이콘이라 길게 눌러 고르는 말로 쓴다. 오류 사전은 이 브랜치를 main
위로 옮길 때 새 글로 다시 만든다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
목록 위 막대의 [찾기]가 탭 안 검색 화면(/mail/search)을 연다. 웹 14단계 검색과
같은 한도(UTF-16 200자 · 낱말 8개)를 먼저 보고, 찾을 곳은 시작한 메일함 | 전체
메일함이다. 결과 줄은 목록 줄 그대로 맞은 말을 노랑으로 칠하고(회색 미리 보기
안은 글자를 진하게), 결과에서 연 읽기 화면도 제목 · 첨부 이름 · 본문을 칠한다.
이어 찾기는 한 차례 5번 · 20통이고, 모자라면 [더 찾기]로 멈춘다. 결과는 새 메일
확인으로 다시 찾지 않고, 읽기 화면에서 한 일과 붙잡힌 메일의 풀림만 반영한다.
낱말 나누기 · 한 글자 판정은 웹 · 서버와 짝이고, 시험이 백엔드 표본 표를 그대로
읽는다. 시험 750 → 870.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nbsp(본문 코드 둘 · 시험 하나)와 폭 없는 공백 · BOM(시험 하나)이 소스에 그대로
있었다 — 눈에 보이지 않아 고치다 지우거나 잘못 옮기기 쉽다. flutter 스킬 규칙대로
\u 이스케이프로 바꾼다. 뜻은 같다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[파일 첨부]가 안드로이드 고르는 창(권한 없음)을 연다. 고른 파일은 앱 임시 폴더에
복사해 두고 그 복사본을 multipart 로 둘씩 올린다 — 올리기에 실패하거나 서버가 첨부를
찾지 못해도(MAIL_004) 다시 고르지 않고 다시 올리려고.
- 사전 검사 · 보내기 막기는 웹과 같은 차례, 한도는 GET /api/mail-quota
- 기기가 같은 차례로 미리 걸러 넣을 파일만 복사한다(content:// 만, 판정은 Dart 가 다시)
- 다 올리면 서버가 정리한 이름 · 크기로 싣는다(이름 255자 — 웹과 같다)
- MAIL_004 뒤 다시 올릴 수 없는 파일은 빼기 전까지 저장 · 보내기를 막는다
- 가져오는 · 올리는 동안 [보내기]를 막고(가져오기는 10분 시한), 올리는 중 나가기는 따로 묻는다
- 화면이 끝나거나 직원이 바뀌거나 앱을 띄울 때 복사본을 지운다
리뷰(M1~M4 · L 다섯 · 다시 본 리뷰의 L 하나)를 고쳤다. 시험 690 → 750.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
새 메일 · 답장 · 전체 답장 · 전달 · 이어 쓰기 · mailto 를 연다. 글만 쓰고,
보낼 때 웹과 같은 서식 본문(기본 글꼴 div · 서명 경계 div · "원본 메일"
인용)과 텍스트 본문을 함께 만든다 — 원문 인용은 글로 만든 덩이다.
- 임시 저장은 웹 규칙(3초 · 30초 · 10초 · 한 번에 하나)에 더해 앱이 안
보이게 되면 바로 저장한다
- 보내기는 같은 Idempotency-Key 규칙(성공 · MAIL_005 에만 새 키), 보낸 뒤
시작한 화면에 [보내기 취소] 마감 알림 · 남은 시간 막대
- 서명은 따로 된 칸([서명 빼기] · [서명 넣기]), 원본 메일은 접힌 채
- 리뷰 H1: 진행 중인 저장을 기다리는 사이 화면을 떠나면 메일이 나가지
않던 것 — keepAlive 를 기다림 앞에서 잡고 떠나도 그대로 보낸다
- 리뷰 M1 · M2 · M3: 저장본 읽기는 읽기 화면과 같은 한도 · isolate,
웹 저장본의 정렬 · 들여쓰기는 남의 저장본 길로, 확인 창의 거짓 "저장했습니다"
- 첨부 고르기 · 올리기는 2차(지금은 전달 원본 · 저장본의 첨부만 싣고 뺀다)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
쓰기 화면의 받는 사람 칸이 쓰고, 나중의 캘린더 · 파일 모듈도 함께 쓰게
core 에 둔다. 주소 검사는 웹 mail-address.ts, 추천 맞추기(초성 · 겹자음 ·
조합 중 글자)는 웹 recipient-match.ts 와 짝이고, 표본 표는 웹 · 백엔드와
같은 70행을 같은 차례로 옮겼다(세 쪽 짝 — 한쪽을 고치면 모두).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"오전 9:00 에 보낼 메일입니다." → "오전 9:00에 보낼 메일입니다." 웹 9단계 글을
그대로 옮긴 것이었는데, 웹 실사용 시험(2026-11-06)에서 띄어쓰기가 틀렸다고
나와 웹과 함께 고친다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
머리는 앱 화면, 본문은 스크립트를 끈 WebView 에 앱이 감싼 CSP 문서로
보인다. 받은 HTML 은 서버 정화 뒤 앱이 한 번 더 허용 목록으로 다시 쓰고,
외부 이미지는 [이미지 보기] 뒤 앱이 Dio 로 받아 data: 로 넣는다(토큰이
WebView 에 가지 않는다).
- 읽음 · 중요 · 이동 · 스팸 · 삭제 · 영구 삭제, 붙잡힌 메일의 보내기 취소 ·
예약 취소는 목록과 같은 규칙. 응답은 그 요청이 바꾼 칸만 지금 값에 덮는다
- 첨부는 앱 임시 폴더(cacheDir/downloads)에 받아 FileProvider + ACTION_VIEW
로 연다(파일 여는 플러그인 없이). 열 형식은 확장자 먼저, 웹 문서 · svg ·
설치 파일 · 인증서는 받지 않는다
- 응답 인터셉터가 바이트 · 스트림 실패 본문(64KB 까지)에서 코드를 읽고,
뜻밖의 실패에도 handler.next 로 끝낸다
- 리뷰 H1: 같은 그림을 수백 번 쓴 메일이 문서를 GB 단위로 키우던 것 —
문서에 싣는 그림 글을 나온 횟수까지 세어 10M 자에서 멈춘다
- 리뷰 M1: 짧고 깊은 본문을 화면 isolate 에서 정화하던 것 — 길이와 중첩
비용으로 고르고(6,144자 · 비용 5,000), 그 밖은 Isolate.run
- 새 의존성: webview_flutter 4.14.1 · webview_flutter_android 4.12.0 ·
html 0.15.7(+ csslib 1.0.2) — 배포 2주 넘음. path_provider · mime 은
lock 에 있던 판을 직접 의존으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
앱 틀(core/router):
- 틀을 Scaffold 가 아니라 Column + 탭 막대로 — 탭 안 화면마다 자기 Scaffold 를 두어 전역 알림이
[메일 쓰기] 위에 뜬다. 탭 막대는 위 안전 영역을 빼고(실기기에서 상태 표시줄만큼 두꺼워지지 않게),
화면이 ShellBarHidden 으로 숨길 수 있다(모듈마다 셈 · 보이는지는 TickerMode, 자판이 열리면 뺌)
메일 목록(features/mail):
- 메일함 서랍(바꿔 끼움 · 안 읽은 수는 회색 글), 줄(사진 · 보낸 사람 · 시각 / 제목 · 첨부 /
미리 보기 한 줄 · 안 읽음 굵게 · 중요 별), 커서로 이어 받기 · 당겨서 새로고침 · 빈 메일함 · 30일 줄
- 고르기 막대(읽음 · 중요 · 이동 · 삭제) — 먼저 바꾸고 failedIds 만 되돌리기, 이동 · 삭제는 응답
id 만, 100통씩, 휴지통으로 옮기고 [되돌리기], 휴지통에서는 확인 뒤 영구 삭제, 붙잡힌 메일은 고를 수 없음
- 새 메일 확인: 보이고 앱이 앞에 있을 때만 1분마다, 다시 받은 쪽이 끝까지면 지운 옛 메일을 뺀다
- [되돌리기] 알림은 [메일 쓰기]와 한 자리(화면 낭독기를 켠 동안은 닫지 않음), 단추 없는 알림은 전역 스낵바
- 시험 346건(새로 119)
리뷰 반영: 탭 막대 높이, 고르는 중 알림 줄 위치, 이어 받기 중 새로고침 실패, 붙잡힌 메일 다시 보기,
메일함 역할을 알기 전 그리지 않음, 선택 풀기 · 되살리기를 계약에 맞춤, 맞추기 한 번에 하나.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
주소 · 비밀번호 → 인증 앱 코드 → 새 비밀번호 정하기(임시 비밀번호) → 2단계 켜기(필수일 때)를
세션 노티파이어의 상태로 고른다. 첫 단계만 큰 브랜드 띠, 그다음은 짙은 띠에 [다른 계정으로 로그인].
- 2단계 켜기는 같은 휴대폰이라 QR 대신 [인증 앱에 추가](otpauth — url_launcher 6.3.3,
canLaunchUrl 은 주소를 로그에 남겨 쓰지 않는다) · 키 [복사] · 코드 칸
- 실패는 칸 아래 · 위쪽 상자 · 전역 알림으로 웹 로그인 틀과 같게(REQ_001 은 코드 · 제한 단계에서
상자), 칸의 안내와 오류를 함께, 세션 끝은 로그인 화면 상자만
- 비밀번호 · 코드 · 2단계 키는 화면 메모리와 노티파이어의 10분 기억에만(두 제한 단계에서만 쥔다),
비밀 요청의 실패 예외는 본문을 비운다 — 시험이 모든 프로바이더 값에 비밀이 없음을 본다
- 제한 단계를 보내는 동안 [다른 계정으로 로그인]을 막는다, 주소 칸만 자판 추천(@ 자판)
- 한글 낱말 단위 줄바꿈(KeepAllText), 시험 227건
리뷰 반영: 모르는 제한에서 비밀번호를 쥐지 않음, REQ_001 자리, 안내 · 오류 함께, 주소 칸 자판,
계약 테스트 목록(AUTH_005 · AUTH_008), AUTH_002 를 두 번 알리지 않음.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
앱 기반 3차 확인에서 나온 낮음 넷이다.
- 토큰은 실렸는데 모양이 틀린 로그인 응답은 응답을 뺀 실패로 던지고, 읽을 수 있는 토큰으로
조용히 logout 한다(비밀 요청의 실패 예외는 응답 본문도 비운다)
- 확인하지 못한(unknown) 다시 확인 결과는 처음 물은 때부터 5초만 다시 쓴다
- 로그아웃 · 다른 계정 · 세션 끝 때 기다리던 다시 맞추기에 stale 로 답한다 — 새 로그인의 오류가
앞 세션의 me 를 기다리지 않게
- postAppLogout 주석을 사실대로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
앱 1단계의 바탕이다(계약 1절 "앱 로그인" — 서버는 다음 차례, 지금은 가짜 서버로 시험한다).
- 토큰: 리프레시 토큰은 보안 저장소에만, 액세스 토큰은 메모리에만. 첫 실행 표시가 없으면 남은 토큰을
지우고, 안드로이드 백업을 끈다. 보안 저장소 일은 차례로 한다
- 인터셉터: 401 이면 갱신 한 번(동시 401 묶음) 뒤 다시 보내기, AUTH_010 은 me 를 다시 받아 그 단계로,
미리 갱신 30초 전 · 만료는 응답 Date 로, 세션 세대가 바뀐 뒤의 응답은 조용히 버린다,
비밀이 든 요청의 실패 예외는 본문 · Bearer 를 지운다
- 세션: 로그인 · 코드 · 새 비밀번호 · 2단계 켜기 단계의 상태와 늦은 결과 버리기(버린 로그인의 토큰은
조용히 logout), 로그아웃은 메모리부터 비운다, 유예 응답이면 세션 끝 + 조용한 logout
- 모듈 틀: go_router StatefulShellRoute 와 아래 탭 막대 — 모듈 목록은 웹 app-modules.ts 와 짝,
첫 판은 [메일 · 설정]. 모듈끼리 import 하지 않는다
- Riverpod 자동 다시 하기를 끈다, Pretendard 1.3.9(OFL), 확정 글자 크기, 시험 126건
- 판은 Flutter 3.41.9(Dart 3.11.5) — pubspec 의 sdk 하한과 lock 을 함께
리뷰 세 번 반영(AUTH_010 다시 보내기 · 늦은 응답 · 저장 실패 · 로그아웃 차례 · 비밀이 실린 객체 등).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
템플릿의 tma_app · com.example.tma_app 을 앱 이름 "ARA SYSTEM", 식별자 kr.it.arasystem.app
(스토어에 올린 뒤에는 바꿀 수 없다 — 2026-11-05 사용자 확정), 패키지 이름 arasystem_app 으로 바꾼다.
안드로이드 namespace · applicationId · MainActivity 자리, iOS · macOS 번들 id, 리눅스 · 윈도 · 웹의
이름을 함께 고치고, 템플릿의 카운터 위젯 시험은 지운다(앱 기반 커밋이 앱 시험을 둔다).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>