계약 2절 "서식 본문"에 앱이 만드는 모양(서명 경계 div · 글로 만든 원문
인용 · 다시 열기 조건), 앱 디자인 README 「쓰기」에 구현하며 정한 것 ·
알려진 한계 · 실기기 점검, flutter 스킬에 쓰기 화면 줄과 위젯 · 시험
함정을 적는다.
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>
앱 디자인 README 「읽기」에 짜임 · 두 번째 정화 · 그림 한도 · 첨부 형식 막기 ·
붙잡힌 메일 · 알려진 한계 · 실기기 점검을, 「쓰기」에 mailto 숨은참조를
펼쳐 보일 것을 적는다. flutter 스킬 골격 표에 읽기 화면 · 받은 파일 줄과
인터셉터의 실패 본문 규칙을 더한다.
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>
- 앱 디자인 README: 고르기(줄 누르기 · 바뀌는 단추 · 영구 삭제 · 읽음 뒤 끝내기 · 선택 풀기),
붙잡힌 메일, 알림 두 갈래, 서랍, 30일 줄 · [찾기] 준비 중, 새 메일 확인과 병합 규칙(웹과 다른 점),
크기 맞춤, 메일함 이름 차례
- flutter 스킬 골격 표: 메일 목록 · 앱 틀 줄(틀은 Scaffold 가 아니다 · ShellBarHidden · 알림 두 갈래 ·
생명주기 안 프로바이더 · 가짜 서버의 한 프레임 응답)
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>
- 앱 디자인 README: 단계마다 실패를 보이는 곳(표), 안내와 오류 함께, 세션 끝은 상자만,
[다른 계정으로 로그인]을 막는 때, 2단계 켜기의 처음 초점 · 켠 뒤 알림, 다른 곳에서 켠 경우,
주소 칸 자판, 글자 크기 맞춤, 새 글, 알려진 한계(인증 앱에 다녀오는 사이 앱이 정리되면 새 키 ·
최근 앱 미리 보기 · 클립보드 · iOS)
- flutter 스킬 골격 표에 로그인 화면 줄(노티파이어 제한 단계 API · 알림 끄는 묶음 · url_launcher
쓰는 규칙 · KeepAllText)
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>
- flutter 스킬: 기본 제공 골격을 이번 앱 기반으로(인증 인터셉터 · 토큰 보관 · 세션 노티파이어 ·
모듈 틀), 새 모듈 붙이는 법, 비밀이 든 요청의 실패 예외, 자동 다시 하기 끔, Flutter 판
- 보안 · 에러 코드 · 코딩 규칙: 앱은 리프레시 토큰을 본문으로 받아 보안 저장소에(kRefreshTokenKey),
세션 만료 승격은 갱신 뒤 · 스스로 로그아웃한 뒤의 401 은 승격하지 않는다(웹과 같다)
- mobile-dev 역할: 작업 순서 · 완료 기준(apk 빌드 포함)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
같은 마이너의 최신 패치 3.41.9(Dart 3.11.5 · 2026-04-30)로 개발 PC 와 CI 를 함께 올린다
(2026-11-06 사용자 확인 — 개발 PC 의 Flutter 는 다른 프로젝트도 함께 쓴다). CI 는
`flutter pub get --enforce-lockfile` 로 lock 과 어긋나면 실패한다. docker 스킬의 이미지 버전 표에
Flutter SDK 줄과 함께 고치는 곳을 둔다.
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>
캔버스에서 2026-11-05 확정한 앱 1단계 보드 다섯(로그인 네 단계 · 메일 목록 · 읽기 · 쓰기 ·
앱 틀과 설정 — 안드로이드 먼저)과 앱 디자인 README 다. 앱 틀은 웹의 전역 앱 레일을 아래 탭 막대로
옮긴 것이고, 파일 저장소 · 캘린더도 같은 모듈 틀에 들어간다. canvas.json 은 원래 꼴(보드마다 한 줄)에
보드 다섯 · 차례 · 노트 하나만 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 사용자가 정한 앱 전용 토큰 경로다(웹의 쿠키를 앱에 쓰지 않는다). 서버는 아직 없다 —
앱은 이 절대로 만들고 가짜 서버로 시험하며, 서버는 다음 차례에 이 절대로 만든다.
- POST /api/auth/app/login · refresh · logout — 토큰은 본문으로, Bearer 는 앱 표시(cli: app)
토큰만 · 쿠키는 웹 토큰만. 세션 규칙 · 실패 카운터 · 2단계 코드 가드 · 제한된 세션은 웹과 같다
- 유예 응답(tokens: null)은 세션 끝 — 액세스 토큰이 남았으면 Bearer 로 조용히 logout,
logout 본문의 refreshToken 은 선택
- 앱이 할 일: 토큰 보관 · 갱신 한 번에 하나 · AUTH_010 다시 확인 · 늦은 응답 버리기
- 5절(계정 보안): 앱 세션도 같은 목록에, 앱은 sid 로 지금 세션을 안다, 로그인 기록 method app,
기기 이름 "ARA SYSTEM 앱"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
사용자가 캔버스 초안을 확정했다(2026-11-05). 자체 색인의 부분 일치 · 조건 검색 · 검색어 강조, 읽기 화면의
같은 대화, 목록의 대화로 묶어 보기(사용자마다 · 기본 끔)를 README 14단계 절로 적고 보드 셋과 canvas.json 을
캔버스(v52)와 맞춘다. 엔진 스레드가 회신: · 답장: · 전달: 을 떼지 않고 검색이 어절 통째 일치뿐이라는
11-03 실측이 까닭이다. API 규칙은 구현 때 계약 2절에 더하고, 구현 · 배포는 전환 뒤 안정화 다음에 한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
화면 문구(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>
README 에 「화면 문구 — 사용자에게 말하듯」 절(규칙 여덟과 예시)을 두고, README 가 옮겨 적은 화면 문구를
새 글로 맞춘다. vue3 스킬 점검 목록에 문구 줄을 더한다. 보드(*.dc.html)의 글은 11-05 전 것으로 둔다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
설계 문서처럼 규칙을 적어 둔 문구가 많다는 사용자 지적(2026-11-05) — "메일 서버에 계정이 있는 직원입니다 —
이름과 주소만 보입니다." · "「@도메인」은 그 도메인에서 오는 메일 모두입니다." 같은 글. 화면 상수 · 템플릿 ·
문장을 만드는 유틸의 236곳을 무엇이 되는지 · 무엇을 하면 되는지로 다시 썼다: 줄표로 잇지 않고, 「」 대신
[단추 이름], 메일 서버 · 엔진 · 판정 같은 만든 쪽 말을 빼고, 존댓말은 그대로. 한도 · 날짜 · 개수는 빼지 않았다.
그 문구를 그대로 보던 spec 을 함께 맞췄다(lint · type-check · 단위 2,354 · TZ=UTC · build 통과).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vue 기본 favicon 이 남아 있었다. 5단계 알림 아이콘과 같은 모양(#0B3640 둥근 사각 +
흰 ARA 마크)을 원본 로고에서 크기마다 따로 줄였다 — 16 · 32 는 가는 획을 조금 굵게.
index.html 은 아이콘을 /src 경로로 걸어 빌드가 /assets/ 의 해시 파일로 바꾼다 —
배포 직후 새 아이콘이 보이고, 공개 내려받기 호스트도 /assets/ 는 연다(nginx 그대로).
public/favicon.ico(16 · 32 · 48)는 /favicon.ico 를 직접 묻는 클라이언트용으로 바꿔 둔다.
apple-touch-icon(180 불투명 정사각)과 theme-color #0B3640 도 더한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
사용자 요청(2026-11-04)으로 레일의 "준비 중" ERP 칸과 로그인 브랜드 패널 소개 글의 ERP 를 뺐다.
레일 테스트는 실제 목록(메일 · 캘린더 · 파일 · 주소록 · 설정)도 그대로 본다 — 다시 넣을 때 함께 고치게.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
문서만 바꾼 PR(#8, run 138)의 CI 에서 할당이 많은 성능 단언 셋이 넘쳤다(받는 사람 맞추기 3.29초 · JSON 모양 1.35초 · DTO 2.94초).
프로세스 user + system 시간에는 V8 의 GC · JIT 도우미 스레드와 페이지 폴트 몫이 들어가 기계 사정을 따라 크게 흔들린다.
같은 Linux 컨테이너에서 전체 테스트 16회: 프로세스 user + system 은 4회 넘침, 스레드 user 는 0회.
막으려는 옛 경로(제곱)는 거의 user 시간이라 그대로 잡힌다(받는 사람 맞추기 옛 방법 51~70초).
받는 사람 맞추기 한도는 CI 쪽 값(0.17~0.25초)의 10배쯤인 2.5초로 올렸다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
쓰기 화면의 두 테스트는 대용량 파일을 고른 뒤 양보(flushPromises) 두 번 만에 다 올렸다고
보았다. 조각의 SHA-256 은 jsdom 이 아니라 Node 의 Web Crypto(libuv 스레드 풀)라, CI 부하에서
그 안에 끝나지 않았다. 다 올리기 전에 누른 [보내기]는 막혀 보내기 요청이 끝내 나가지 않아
payload 가 undefined 였다(2026-11-03 CI — ComposePage.spec.ts 첨부 합계 테스트).
- 두 테스트(첨부 합계 · 링크로 보내기)는 대용량 줄이 ready 가 될 때까지 vi.waitFor 로 본다.
첨부 합계 테스트는 보내기 전에 막는 까닭이 없음도 단언한다.
- test-utils/timeouts.ts 에 WAIT_FOR_TIMEOUT_MS(15초 — 테스트 한도 20초보다 짧게)를 둔다.
- digest 를 늦춘 실험으로 고치기 전에는 깨지고 고친 뒤에는 통과함을 확인했다(실험은 되돌림).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
이 spec 은 fetch 를 spyOn 으로 감싸기만 해, 가짜 응답을 넣지 않은 호출이 진짜
http://mail:8080 으로 나갔다. 13단계부터 메일 계정이 없는 세션은 계정 정보를 한 번
더 묻는데, 그 요청이 CI 컨테이너에서 이름 풀기를 기다리다 테스트 5초를 넘겼다
(2026-11-03 CI — 개발 컨테이너에서는 개발 엔진이 실제로 받았다).
- 가짜 응답이 없는 fetch 는 곧바로 연결 실패로 거절하고 주소를 남긴다 — afterEach 가
남은 주소가 있으면 그 테스트를 실패시킨다. jest-mock 30 의 mockReset 은 구현까지
지우므로 resetFetchMock 이 기본을 다시 건다.
- 메일 계정이 없는 세션 · 실시간 연결 403 두 테스트에 계정 정보 응답을 넣고 요청을 단언.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
개발 엔진 실측(2026-11-03, 0.16.27): 큰 메일 한 통을 받으면 엔진 메모리가 그 크기의
5.5배쯤 는다(IMAP APPEND · SMTP 받기 같다). SMTP 받는 한도(x:MtaStageData 의
maxMessageSize, 기본 100MiB)는 x:Email 한도와 따로라, 기본값 · 1g 에서 바깥 104MB
한 통이 984/1024MiB 였다(운영은 106.4MB IMAP APPEND 로 OOM).
사용자 결정 — 메일 컨테이너 메모리는 1g 그대로 두고, setup 이 받는 한도를 50MiB 로
맞춘 뒤 리로드 뒤 EHLO 의 SIZE 로 실제로 걸렸는지 본다(저장 때 줄만으로는 리로드
실패를 못 거른다 — 리뷰). IMAP · 메일 크기 한도는 imapsync 이전 동안 운영자가 잠시
올리므로 setup 이 바꾸지 않고 기본보다 크면 [주의] 만 낸다.
README 에 잰 값 · 정책 · 이전 뒤 되돌리는 차례(세 값 → setup [확인] → 1g)와
문제 해결 줄, docker 스킬에 한 항목.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 운영 1 · 2차에서 겪은 것을 imapsync 절에 남긴다. 한도보다 큰 메일은
APPEND 에서 빠지고(114), 100MB 남짓 한 통은 엔진 1g 를 넘겨 OOM 으로 다시 떴다
(그동안 다른 계정은 Connection refused). docker update 2g 는 컨테이너를 다시
만들면 사라지고, 한도를 올린 채 두면 메모리 예산의 합이 바뀐다 — 이전 뒤
어떻게 둘지는 아직 정하지 않았다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 서버 B 에서 restic 저장소를 NCP Object Storage 에 만들고 첫 백업(14초 · 메일 엔진
멈춤 6초) · 복원 연습(계정 · 메일 · DB 행 수 · env 가 운영과 같음)을 마쳤다 — "NCP 로는 아직"을 거둔다.
init 의 "Stat: Access Denied" 는 HEAD 라 까닭이 안 보여, 같은 설정으로 목록을 조회해 S3 오류 코드를
보는 명령을 더했다(겪은 것은 SignatureDoesNotMatch — 콘솔 복사 버튼으로 다시 넣어 풀림).
journalctl 은 sudo 로, ops.env 의 ARASYSTEM_DIR 예시 값을 그대로 두지 않는다는 주석도 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
사용자가 서브 계정을 만들지 않고 메인 계정의 API 인증키로 가기로 했다(2026-11-03).
README · ops.env 예시에 만드는 곳(My Account › 계정 및 보안 관리 › 보안 관리 › 접근 관리 —
계정마다 2개, 기본 키와 섞지 않게 새로)과, 그 키가 계정의 모든 API 를 연다는 위험을 함께 적었다.
서브 계정 차례는 범위를 줄이는 길로 남겼다. 체크리스트(템플릿 공용)의 권장은 그대로 둔다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NCP 버킷에는 따로 키가 없다(사용자가 버킷을 만든 뒤 키를 찾지 못했다). 키는 서브 계정의
Access Key 탭에서 만들고 Secret Key 는 그때만 보인다 — 차례를 README 에 적었다.
"이 버킷만 쓰는 하위 계정"은 NCP 문서로 뒷받침되지 않는다(관리형 정책은 Object Storage 전체,
사용자 정의 정책에도 버킷을 고르는 칸이 문서에 없다) — README · ops.env 예시 · 체크리스트를
"Object Storage 만 쓰는 서브 계정 · 같은 계정에 다른 버킷을 두지 않는다"로 바로잡았다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NCP 버킷 생성 화면의 칸마다 무엇을 고르는지 적는다. 공개는 목록을 누구에게나 열고,
잠금(보존 기간)은 restic 의 prune 을 막고, KMS 암호화는 되돌릴 수 없고 restic 이 이미 암호화한다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
설치 2)는 서버에서 openssl 로 만들고 "비밀번호 관리자에도 둔다"고만 적어, 서버 밖에 두는
길이 빠져 있었다. 생성기로 먼저 만들어 저장한 뒤 read -rsp 로 붙여 넣게 바꿨다 — 서버 밖
사본이 백업보다 먼저 생긴다. 서버에서 만드는 길은 sudo cat 으로 한 번 옮기는 줄과 함께 남겼다.
회사가 꺼낼 수 있는 곳에 두라는 것(운영자 한 사람만이 아니라)과 재설정이 없다는 것을 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 서버 B(11단계)를 main 으로 올리며 확인한 것을 차례에 남긴다.
- DB 가 이미 고정할 digest 로 돌고 있으면 새 이미지가 없어 날짜를 기다리지 않아도 된다 — 0) 확인 명령
- 12단계 전 서버는 웹훅 키를 먼저 넣는다(키 없이 띄우면 백엔드가 env 검사에서 멈춘다)
- --recovery 가 Spamhaus ZEN 시간 초과로 종료 1 이어도 관리자 허용 IP 는 걸렸으니 보통 setup 으로 간다(ZEN 을 다시 켠다)
- 운영은 마이그레이션 로그가 없어 migrations 표로 본다
- 손 백업에 DB 덤프를 더하고, 떠 있는 alpine 대신 지금 도는 DB 이미지 · 읽기 권한만 · 0600 으로
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 mailcow 에서 확인(2026-11-03): 마스터 두 값은 빈 값(아직 아무도 고정하지 않음).
최근 7일 Dovecot 로그인 줄과 지금 연결이 모두 앞단 터널 주소 10.9.0.1(사설) — 공인 주소 0.
mailcow 의 Fail2ban 은 사설 주소를 막지 않으므로 서버 B 를 예외에 넣지 않는다.
남는 것은 Dovecot 의 늦추기(같은 주소의 바깥 직원 모두)라 실패를 되풀이하지 않는다.
이 mailcow 의 docker 로그(json-file · 회전 없음 · 428MB)는 --since 만 주면 0줄이고
--tail 은 된다 — 처음부터 읽다 중간의 읽지 못하는 줄에서 말없이 멈추는 것으로 보인다.
로그 명령을 --tail 로 바꾸고, 로그 없이 지금 연결을 보는 doveadm who 를 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
운영 mailcow.conf 에 DOVECOT_MASTER_USER · _PASS 가 주석 없이 있었는데, mailcow 의
generate_config.sh 가 두 줄을 빈 값으로 넣어 두므로 "주석이 아니면 정해 둔 것"으로 읽으면 틀린다.
값 대신 비었는지만 보는 확인으로 바꾸고, 끝낼 때는 줄을 지우지 않고 값을 비우게 했다.
mailcow 의 Fail2ban(netfilter)은 사설 · 루프백 주소를 막지 않는다(원본 main.py) — 예외가
필요한지는 mailcow 가 Contabo 앞단 · WSL 을 지난 서버 B 를 어떤 주소로 보느냐에 달렸다.
준비에 계정 없이 원본 로그인만 보는 --justlogin(두 쪽 모두 mailcow)과 mailcow 로그에서
그 주소 · 지난 사흘의 접속 주소를 보는 명령을 넣었다(개발 Dovecot 2.3.21.1 · mawk 1.3.4 로 시험).
운영 사실: 설치 폴더 /opt/mailcow-dockerized, 판 2026-05-12(32156a33).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mailcow.conf 와 mailcow 저장소가 root 소유라 sudo 없이는 grep 이 거절되고 git 이 dubious ownership 으로 멈췄다(2026-11-02 사용자 실행).
이전하는 동안 그 PC 가 절전 · 재부팅되면 imapsync 가 끊긴다는 줄도 더했다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
imapsync 절을 새 차례(계정 → 1차 → MX → 마지막 imapsync → 5분 뒤 이전 마치기 → 임시 비밀번호 나눠 주기)로 다시 썼다.
mailcow 고정 마스터 계정(사무실 PC 의 mailcow.conf), 서버 B 의 명령(고정 이미지 · 인증서 확인 · 비밀번호 파일),
개발에서 확인한 것(폴더 맞춤 · 수 · 날짜 · 표시 · 한글 폴더 · 차이분 · 만료 뒤 실패)을 적었다.
엔진 사실: 만료가 미래면 그때까지 IMAP 정상, 기본 만료의 찌꺼기는 끈 뒤에도 남는다.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전 직원 전환의 imapsync 는 새 엔진에 IMAP 으로 들어가야 하는데, 입사 · 재설정의 임시 비밀번호는
만료라 IMAP 이 끊긴다. 직원이 비밀번호를 바꾸면 마지막 차이분을 옮길 길도 없었다.
- 「입사」의 옮겨 올 메일 질문 → 이전용 비밀번호(엔진 만료 30일 뒤 · 실행 한 번 안에서 같은 값 · 메모리에만)
- 메뉴 「11. 메일 이전」 — 이전 현황 · 이전용 비밀번호 다시 만들기(세션을 먼저 끊는다) · 이전 마치기
(앱 비밀번호 → 세션 → imapsync 로그인 기록 지우기 → 임시 비밀번호 + 만료 → 세션, 알림 메일 없음, 끝에 표 한 번)
- 이전 중 = 엔진 만료가 미래(31일 안) + 실행 기록의 마지막 판정 기록이 이전용(hire-migration · migration-password)
— 엔진 기본 만료의 찌꺼기를 이전 중으로 읽지 않게
- 재설정 · 잠금 풀기 가드, 현황의 이전 중 표시, 엔진 기본 만료는 첫 화면 알림만
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
화면은 시각을 브라우저 시간대로 보이는데, 두 테스트의 고정 시각이 UTC 문자열이라
"오늘 오전 9:12" 같은 기대 글이 KST(개발 PC)에서만 맞았다 — CI(UTC)에서는 "오늘 오전 12:12" 로 세 건 실패.
고정 시각을 지역 시각(new Date(2026, …))으로 만들고, 같은 값을 다시 적던 기대값은 고정값을 가리키게 했다.
UTC · KST · America/Los_Angeles · Pacific/Kiritimati 에서 34건 통과, 전체 2,354건 UTC 통과.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 스킬 표: nestjs(제한된 세션 · 2단계 필수 정책 · 받은편지함 알림 · 운영 도구 · 15초 동기화 · 만료 계정의 엔진 사실), vue3(제한된 세션 화면 · 로그인 틀 단계 · 유예 줄 · 메일 프로그램 연결 · 30일 줄),
docker(이미지 표 — PostgreSQL · restic · shellcheck, 자동 설정 server, 호스트 systemd 백업 · 감시 절, setup 의 관리자 단단히 · 휴지통 30일, 로그 앵커 규칙, CI infra 잡).
- 규칙: security(제한된 세션 · 운영 도구 비밀 · 관리자 계정 · 알림 메일 링크 없음 · 퇴사 마무리 · 백업 비밀 · restic 의 root 예외), logging(컨테이너 로그 보존 · 실행 기록 · 자동 설정 쿼리),
env-structure(자동 설정 env · DB_IMAGE · ops.env 예외 · OPS_ALLOW_NO_TTY), error-codes(AUTH_006 ~ AUTH_010), checklist(13단계 운영 준비), release-ops 담당 범위(mail/ · ops/).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 계약 6절: 엔진 실측(만료 계정은 대리 접근 메서드도 막힘 · 2단계를 먼저 봄 · 잠긴 계정에도 402), 제한을 푸는 규칙(그 세션만 · 다음 제한으로 · 2단계를 켜면 다른 제한 세션은 끊음),
운영 도구의 차례(재설정 · 잠금 · 퇴사 마무리)와 결과를 모르는 실패, TTY, 만료 계정의 메일 API 401, 2단계를 켠 잠긴 계정의 로그인 AUTH_004.
- 디자인 README 13단계: 빈 칸 · 넘기지 못함 · 모르는 제한 상태.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 제한된 세션: restriction 이 있으면 보호 화면 대신 로그인 틀의 그 단계(새 비밀번호 → 2단계 켜기)로, AUTH_010 은 알림 없이 me 를 다시 받아 옮긴다.
제한된 동안은 나가기 확인을 묻지 않고(확인에서 이동을 취소하면 메일 화면에 갇혔다 — 리뷰) 실시간 알림을 열지 않는다.
- 로그인 때 넣은 비밀번호는 반응형이 아닌 클로저에 10분 · 세션이 끝나면 지운다. 2단계를 켠 직원의 새 비밀번호 단계는 다음 코드를 받는다.
- 유예 노란 줄(시작일 전날까지), 설정 › 2단계 인증의 필수 안내, 설정 › 메일 프로그램 연결(펼침 줄 · 새 Outlook 은 권하지 않음), 휴지통 · 스팸메일함 30일 줄, 로그인 · 코드 단계의 문의 줄.
검증: lint · type-check · 단위 2,354 · 실사용 시험 89/89.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
전 직원 전환 때 관리자가 계정을 다루고(입사 · 비밀번호 재설정 · 2단계 해제 · 잠금 · 퇴사 마무리 · 공용 주소) 회사가 2단계 인증을 필수로 할 수 있게 했다.
- 제한된 세션(AUTH_010): 임시 비밀번호(엔진 만료) · 2단계 필수인 직원은 로그인은 되지만 새 비밀번호 · 2단계 켜기만 지난다. 푸는 요청은 그 세션만 다음 제한으로 바꾸고,
2단계를 켜면 2단계를 거친 적 없는 다른 세션은 끊는다. 만료는 게이트웨이 대리 접근 세션의 권한(다섯)으로 로그인 · 갱신 때 알아챈다.
- 운영 도구 CLI(dist/cli/ops.js): 바뀔 것 → y/N → 실행 → ops_audit_log. 관리자 비밀번호는 TTY 에서만 · 입력 기록 0, 재설정은 앱 비밀번호 → 세션 → 알림 → 만료 차례(만료 뒤 대리 접근이 막힌다).
- 받은편지함 알림 메일(Email/import — 링크 없음): 재설정 · 2단계 해제 · 위험 파일 판정. 끊은 세션 기억은 15초마다 DB 에서 채운다.
- 2단계를 켠 잠긴 계정도 로그인은 AUTH_004 — 엔진이 잠긴 계정에도 2단계를 먼저 봐, 게이트웨이 자신의 권한으로 잠김과 설정 문제를 가른다(실사용 시험에서 찾음).
검증: lint 0 · 단위 3,138 · e2e 266 · 리뷰 두 번 · 실사용 시험(브라우저 + 운영 도구) 89/89.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>