Commit Graph

210 Commits

Author SHA1 Message Date
ttipo 20cde01454 fix(flutter): 읽기 화면 예약 안내의 조사를 붙여 쓴다
"오전 9:00 에 보낼 메일입니다." → "오전 9:00에 보낼 메일입니다." 웹 9단계 글을
그대로 옮긴 것이었는데, 웹 실사용 시험(2026-11-06)에서 띄어쓰기가 틀렸다고
나와 웹과 함께 고친다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 02:10:59 +09:00
ttipo 08aafdef5c docs: 14단계 실사용 시험 · 사용자 관점 점검의 고침을 계약 · 디자인 README · 스킬에 적는다
계약 2절에 본문 강조 · 미리 보기 조각 고르기 · 같은 대화 줄 차례 · 설정
다시 받기, 디자인 README 14단계 보완 절에 바뀐 글 · 크기 · 알림과 알려진
한계, nestjs 스킬에 찾기 GIN 하나 규칙, vue3 스킬에 새 도우미를 적는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:15:44 +09:00
ttipo 9ba4bc1a41 fix(frontend): 14단계 화면을 실사용 시험 · 사용자 관점 점검대로 고친다
- 분할 보기에서 대화 줄을 고르면 도구 줄이 두 줄로 넘치던 것, 목록 머리의
  안 읽은 수가 1440 에서도 늘 잘리던 것
- 묶은 줄에 한 일의 알림은 "{메일함}에 있는 메일 N통을 …"(줄의 통 수는
  대화 전체라서), 되돌리기 알림은 되돌린 메일함을 말한다
- 같은 대화 칸: 펼친 줄의 첨부 수 안내, 우리 원문 인용 머리부터 뺀 미리
  보기, 보내기를 기다리는 메일은 줄 끝에
- 검색 준비 중 · 결과 없음 · 하루짜리 기간 · 내 이름 조건의 글, 예약 시각의
  띄어쓰기, 이름 맞추기(대화로 묶기 · 내용에 든 말 · 표 머리 참여자)
- 다른 탭 · PC 에서 바꾼 설정을 탭으로 돌아올 때 다시 받는다(60초 지났을 때)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:15:43 +09:00
ttipo 0090a161ec fix(backend): 메일함 안 검색을 찾기 GIN 하나로 거르고 미리 보기 · 본문 강조를 고친다
14단계 실사용 시험(2026-11-06)에서 나온 것을 고친다.

- 1만 통 메일함 안 검색이 1.1초였다. 계획기가 메일함만 담은 GIN(메일함 id
  가 계정마다 같은 꼴이라 posting 이 회사 전체)으로 그 메일함 줄을 다 읽고
  묶음 배열을 줄마다 풀었다. 직원 · 묶음 · 메일함을 한 색인에 담은 GIN
  하나로 바꾸고 옛 GIN 둘을 지운다(마이그레이션 SearchIndexUserGramsMailboxes
  — 큰 메일함 15ms · 작은 메일함 1ms · 전체 15ms)
- 한 글자 낱말뿐이면 본문을 찾지 않는데 본문 글자를 강조했다 — 본문 강조는
  본문을 찾은 두 글자 이상 낱말만
- 답장의 결과 미리 보기가 원문 인용 조각이었다 — 인용 줄 밖의 맞은 곳을
  먼저, 인용 안에서만 맞고 다른 칸에서도 맞았으면 본문 첫머리, 인용 조각은
  줄 앞 `>` 를 뗀다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:15:43 +09:00
ttipo f4b1d0a206 docs: 앱 메일 읽기 화면의 구현하며 정한 것을 앱 디자인 README · flutter 스킬에 적는다
앱 디자인 README 「읽기」에 짜임 · 두 번째 정화 · 그림 한도 · 첨부 형식 막기 ·
붙잡힌 메일 · 알려진 한계 · 실기기 점검을, 「쓰기」에 mailto 숨은참조를
펼쳐 보일 것을 적는다. flutter 스킬 골격 표에 읽기 화면 · 받은 파일 줄과
인터셉터의 실패 본문 규칙을 더한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 22:26:09 +09:00
ttipo a37045a553 feat(flutter): 메일 읽기 화면을 만든다 (App1Reader)
머리는 앱 화면, 본문은 스크립트를 끈 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>
2026-11-06 22:26:09 +09:00
ttipo 5cece8ebf6 docs: 14단계 목록 대화로 묶어 보기의 규칙과 구현하며 정한 보완을 적는다
계약 2절 "대화 — 14단계"에 묶은 목록의 클라이언트 줄(첫 진입 짐작 · 맞추기 ·
할 일 메일 · 줄의 별 · 옮기기 뒤 다시 받기 · 알려진 한계), 디자인 README 에
"14단계 구현하며 정한 보완 — 목록 대화로 묶어 보기", vue3 스킬에 골격 줄 ·
밀도 예외 · 줄 기준 도우미 규칙을 적는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:56:14 +09:00
ttipo 726f9f854f feat(frontend): 목록을 대화로 묶어 보게 한다 (14단계 화면 3차)
메일함 목록을 대화 한 줄로 묶어 보는 [대화로 묶기]와 설정 › 보내기 · 읽기의
한 줄을 더한다. 켜면 목록 출처가 GET /api/conversations 이고, 줄에 하는 일
(고르기 · 읽음 · 중요 · 이동 · 삭제)은 그 줄의 messageIds 에서 붙잡힌 메일을
뺀 메일을 지금의 여러 통 API 로 보낸다 — 붙잡힌 메일은 취소로만 보낸편지함을
떠난다(9단계).

- 대화 목록에서는 messages 가 비므로 읽기 화면 · 삭제 · 예약 취소가 줄 기준
  도우미(rowCount · rowIndexOfMessage · isListedMessage 등)로 같은 규칙을 탄다
- 첫 진입은 이 브라우저가 그 직원에게서 마지막으로 안 값으로 열고, 설정을
  받지 못해도 [대화로 묶기]를 쓸 수 있다
- 리뷰 M1: 로그아웃 · 세션 만료 직후 설정 reset 이 watcher 를 깨워 떠나는
  화면의 목록을 다시 부르던 회귀 — canUseApp 이 false 면 부르지 않고 reset 은
  보이는 값을 뒤집지 않는다
- 리뷰 L7: 버린 목록 요청은 AbortController 로 끊는다(엔진 방식은 한 요청이
  15초까지)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:56:13 +09:00
ttipo ef23afc94c docs: 앱 메일 목록의 구현하며 정한 것을 앱 디자인 README · flutter 스킬에 적는다
- 앱 디자인 README: 고르기(줄 누르기 · 바뀌는 단추 · 영구 삭제 · 읽음 뒤 끝내기 · 선택 풀기),
  붙잡힌 메일, 알림 두 갈래, 서랍, 30일 줄 · [찾기] 준비 중, 새 메일 확인과 병합 규칙(웹과 다른 점),
  크기 맞춤, 메일함 이름 차례
- flutter 스킬 골격 표: 메일 목록 · 앱 틀 줄(틀은 Scaffold 가 아니다 · ShellBarHidden · 알림 두 갈래 ·
  생명주기 안 프로바이더 · 가짜 서버의 한 프레임 응답)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 19:36:31 +09:00
ttipo ad8b126abb feat(flutter): 메일 목록 화면을 만들고 앱 틀이 화면마다 탭 막대를 숨길 수 있게 한다 (App1MailList)
앱 틀(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>
2026-11-06 19:36:31 +09:00
ttipo 67f63f05fc docs(rules): nestjs · vue3 스킬에 앱 토큰 경로와 로그인된 기기를 적는다
- nestjs: Swagger 는 쿠키와 Bearer 둘 다, 인증 가드가 Bearer(앱 토큰만)와 쿠키를 읽는 차례,
  직원 인증 · 계정 보안 줄에 앱 경로 · client 칸 · 앱 세션의 유예, JWT 절을 사실대로(12단계부터
  끊은 세션은 sid 로 바로 막고 13단계부터 rst 토큰만 세션 표를 읽는다)
- vue3: 계정 보안 줄에 로그인된 기기 — 웹 · 앱은 client · method 로만, 앱 이름 MOBILE_APP_NAME
  (짝: backend APP_BROWSER_NAME)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:41:49 +09:00
ttipo 39623ae3a8 docs: 계약 1 · 5 · 6절과 디자인 README 를 앱 로그인 서버에 맞춘다
- 계약 1절 "앱 로그인": 서버 구현, 갱신 · 로그아웃의 400 / 401 경계(빈 · 너무 긴 토큰),
  private, no-store, Bearer 는 cli 와 sid 가 있는 앱 토큰만, 웹 · 앱 로그인 요청 수 한 칸,
  앱 세션의 갱신 유예는 세션 끝(서버도 끊는다), 앱 이름은 진짜 클라이언트로만, 알려진 한계
- 5절: "로그인된 기기"(세션 목록 client 칸 · 앱 기기 이름 규칙), 로그인 기록 method app ·
  device 가 앱 기록에도
- 6절: 운영 도구 현황의 마지막 로그인(웹메일 · 앱), 재설정 · 이전 마치기의 세션 끊기가 앱도
- 디자인 README: 12단계 로그인된 기기 · 최근 로그인 · 비밀번호 화면 글, 13단계 직원 알림 메일의
  로그아웃 줄, 2단계 켜기에서 다른 곳이 먼저 켠 경우(서버가 이 세션을 끊는다 — 계약 6절)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:41:49 +09:00
ttipo a111cd2fd4 feat(frontend): 로그인된 기기 — 앱 세션 · 앱 로그인 기록을 보인다
앱 로그인 서버가 생겨 계정 보안 화면에 앱 세션 · 앱 로그인이 함께 들어온다.

- "로그인된 브라우저" → "로그인된 기기"(계약 5절), 개수는 세션의 client 로 "웹메일 2곳 ·
  ARA SYSTEM 앱 1곳"(0곳인 쪽은 쓰지 않음), 앱 세션은 늘 휴대폰 아이콘
- 로그인 기록 방식 "ARA SYSTEM 앱", 앱 줄의 기기 칸은 운영체제만(방식 칸이 이미 앱 이름)
- [다른 곳 모두 로그아웃] · 비밀번호 바꾸기 · 안내 글이 웹메일만이 아니라 앱까지 끊는다고 말한다
- 웹 · 앱은 client · method 로만 가른다(User-Agent 이름으로 짐작하지 않는다)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:40:54 +09:00
ttipo 5d3a018708 test(backend): 2단계 인증 e2e 가 30초 경계에서 흔들리지 않게 끄기 코드를 고른다
"등록 → 켜기 → … → 끄기" 시나리오는 끄기에 "그때 단계 −1" 코드를 썼다. 켜기와 끄기 사이에
30초 경계를 넘으면 그 코드가 켤 때 쓴 코드와 같아져, 서버의 다시 쓴 코드 막기가 AUTH_006 으로
거절해 가끔 실패했다(코드는 맞게 거절한 것이다). 이미 쓴 코드가 아닌 것을 0 → +1 → −1 단계에서
고르고(0 · +1 은 요청이 경계를 넘어도 서버의 ±1 창 안), −1 만 남았는데 경계가 1초 안이면
경계를 기다린 뒤 다시 고른다. 따로 다섯 번 돌려 모두 통과했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:40:53 +09:00
ttipo 6ee6c71ace feat(backend): 앱 전용 토큰 로그인 · 갱신 · 로그아웃 경로를 만든다
계약 1절 "앱 로그인"이다(웹의 쿠키를 앱에 쓰지 않는다). 세션 규칙은 웹과 같고 토큰을 나르는
길만 다르다.

- POST /api/auth/app/login · refresh · logout — 같은 AuthService.login 을 지나 실패 카운터 ·
  2단계 코드 가드 · 앱 비밀번호 모양 거절 · 관리자 차단이 웹과 같고, 로그인 요청 수 한도는
  웹 · 앱이 한 칸을 함께 쓴다. 토큰 응답은 private, no-store
- Bearer 는 앱 표시(cli: app)와 UUID sid 가 있는 액세스 토큰만, 쿠키는 웹 토큰만 — 리프레시 토큰 ·
  세션 표의 client 도 맞아야 하고 다르면 401(재사용으로 보지 않음)
- 앱 세션의 갱신 유예는 그 세션을 끝낸다(app-refresh-grace — 응답을 잃었거나 토큰을 훔친 것이다)
- 지금 세션은 앱이면 액세스 토큰의 sid(비밀번호 바꾸기 · 로그인된 기기 · 앱 logout), 로그아웃은
  리프레시 토큰이 틀리면 sid 로 끝낸다
- 세션 표 client 칸(마이그레이션 AddAppSessionClient — 기본 web), 세션 목록 항목에 client,
  앱 이름 "ARA SYSTEM 앱"은 client · method 로만(꾸민 User-Agent 로 앱처럼 보이지 않게),
  로그인 기록 method app
- 운영 도구: 현황의 마지막 로그인이 앱도 세고, 세션 끊기 글 · 직원 알림 메일이 앱을 말한다
- Swagger 에 Bearer 를 등록하고 쿠키로 보호하는 작업마다 "또는"으로 붙인다

리뷰 반영: 유예에서 앱 세션 끊기, 앱 이름을 진짜 클라이언트로만, sid 없는 앱 토큰 거절,
빈 · 너무 긴 리프레시 토큰은 401(로그아웃은 sid 로), 시험(웹 쿠키 + 앱 logout 등).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:38:17 +09:00
ttipo 4e779dedd2 docs: 앱 로그인 네 단계의 구현하며 정한 것을 앱 디자인 README · flutter 스킬에 적는다
- 앱 디자인 README: 단계마다 실패를 보이는 곳(표), 안내와 오류 함께, 세션 끝은 상자만,
  [다른 계정으로 로그인]을 막는 때, 2단계 켜기의 처음 초점 · 켠 뒤 알림, 다른 곳에서 켠 경우,
  주소 칸 자판, 글자 크기 맞춤, 새 글, 알려진 한계(인증 앱에 다녀오는 사이 앱이 정리되면 새 키 ·
  최근 앱 미리 보기 · 클립보드 · iOS)
- flutter 스킬 골격 표에 로그인 화면 줄(노티파이어 제한 단계 API · 알림 끄는 묶음 · url_launcher
  쓰는 규칙 · KeepAllText)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:57:39 +09:00
ttipo c14580e3a4 feat(flutter): 로그인 네 단계 화면을 만든다 (앱 1단계 — App1Login)
주소 · 비밀번호 → 인증 앱 코드 → 새 비밀번호 정하기(임시 비밀번호) → 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>
2026-11-06 15:57:39 +09:00
ttipo dd02b9621a fix(flutter): 모양이 틀린 로그인 응답의 토큰 · 다시 확인 재사용 창 · 기다리던 다시 맞추기를 고친다
앱 기반 3차 확인에서 나온 낮음 넷이다.

- 토큰은 실렸는데 모양이 틀린 로그인 응답은 응답을 뺀 실패로 던지고, 읽을 수 있는 토큰으로
  조용히 logout 한다(비밀 요청의 실패 예외는 응답 본문도 비운다)
- 확인하지 못한(unknown) 다시 확인 결과는 처음 물은 때부터 5초만 다시 쓴다
- 로그아웃 · 다른 계정 · 세션 끝 때 기다리던 다시 맞추기에 stale 로 답한다 — 새 로그인의 오류가
  앞 세션의 me 를 기다리지 않게
- postAppLogout 주석을 사실대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:05:46 +09:00
ttipo 2bb7fa4cdb docs(rules): 앱 토큰 경로 · 모듈 틀을 flutter 스킬과 공통 규칙에 적는다
- flutter 스킬: 기본 제공 골격을 이번 앱 기반으로(인증 인터셉터 · 토큰 보관 · 세션 노티파이어 ·
  모듈 틀), 새 모듈 붙이는 법, 비밀이 든 요청의 실패 예외, 자동 다시 하기 끔, Flutter 판
- 보안 · 에러 코드 · 코딩 규칙: 앱은 리프레시 토큰을 본문으로 받아 보안 저장소에(kRefreshTokenKey),
  세션 만료 승격은 갱신 뒤 · 스스로 로그아웃한 뒤의 401 은 승격하지 않는다(웹과 같다)
- mobile-dev 역할: 작업 순서 · 완료 기준(apk 빌드 포함)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:11:29 +09:00
ttipo dc0fb81387 chore(docker): Flutter 판을 3.41.9 로 고정하고 CI 는 lock 그대로 받는다
같은 마이너의 최신 패치 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>
2026-11-06 11:11:29 +09:00
ttipo 35fcf5e4ef feat(flutter): 앱 로그인 · 토큰 갱신 · 모듈 라우터 기반을 만든다
앱 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>
2026-11-06 11:11:28 +09:00
ttipo 5823f600aa chore(flutter): 앱 이름 · 식별자를 ARA SYSTEM · kr.it.arasystem.app 으로 바꾼다
템플릿의 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-06 11:09:30 +09:00
ttipo bb560d39f9 docs: 앱 1단계 화면을 확정해 디자인 문서에 옮긴다
캔버스에서 2026-11-05 확정한 앱 1단계 보드 다섯(로그인 네 단계 · 메일 목록 · 읽기 · 쓰기 ·
앱 틀과 설정 — 안드로이드 먼저)과 앱 디자인 README 다. 앱 틀은 웹의 전역 앱 레일을 아래 탭 막대로
옮긴 것이고, 파일 저장소 · 캘린더도 같은 모듈 틀에 들어간다. canvas.json 은 원래 꼴(보드마다 한 줄)에
보드 다섯 · 차례 · 노트 하나만 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 11:06:01 +09:00
ttipo fe355f3943 docs: 계약 1절에 앱 로그인(앱 전용 토큰 경로)을 적고 5절을 앱과 맞춘다
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-06 11:06:01 +09:00
ttipo 1a4f1d3b56 docs: 14단계 같은 대화 칸의 규칙과 구현하며 정한 보완을 적는다
- 계약 2절 "대화 — 14단계"에 읽기 화면 칸의 클라이언트 줄: 100통 · 이어 묻기 5번 · [더 보기],
  통 수, 알림 없는 실패와 404 면 칸 없음, 다시 받는 때와 10초 간격, 펼친 줄의 읽음
- 디자인 README "14단계 구현하며 정한 보완 — 같은 대화": 크기, 보드와 다르게 한 것
  (기존 바탕 토큰 · h3 · 본문 14px · 펼친 줄의 안내), 보드에 없던 것, 갈 곳 · 초점 · 다시 받기
- vue3 스킬에 같은 대화 줄 · 스토어 구성 · 밀도 예외 재사용을 더한다
- docker 스킬의 요청 수 한도 안내에 메일을 열 때마다 부르는 같은 대화 경로를 더한다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:15:45 +09:00
ttipo 8f02814a4f feat(frontend): 읽기 화면에 같은 대화 칸을 더한다 (14단계 화면 2차)
- 본문 · 첨부 아래 "같은 대화 N통" — 오래된 차례의 줄(48), 지금 보는 메일 줄은 누를 수 없고,
  다른 줄은 그 자리에서 펼쳐 본문(MailBodyFrame — 강조 쿼리 없이)과 [답장] · [이 메일만 열기]
- 100통씩 받고 쪽이 모자라면 5번까지 이어 묻기 + [더 보기], 통 수를 모르면 수 없이
- 실패는 칸 안 안내 + [다시 시도](알림 없음), 이 경로가 없는 옛 서버(404)면 칸이 없다
- 다시 받기는 메일함 통 수가 바뀔 때 · 쓰기 화면에서 돌아올 때 · 붙잡힌 메일이 풀릴 때 —
  읽음 표시만 바뀐 소식으로는 받지 않고, 끝난 때부터 10초 간격
- 펼친 안 읽은 메일은 읽음으로(읽기 화면과 같은 길), 펼친 본문 안 Ctrl+P 는 그 메일 인쇄

리뷰 반영: 누른 단추가 사라질 때 초점 옮기기, 읽음 소식으로 요청이 두 배가 되던 것,
받는 중 온 소식 · 시계 뒤로 · [더 보기] 뒤 붙잡힌 메일, 받아 둔 본문 10통까지,
본문 안내 줄 · 지워진 메일, 목록 보기에서는 [이 메일만 열기]가 기록을 바꿔 끼운다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 10:15:44 +09:00
ttipo 37d689e113 docs: 14단계 검색 화면의 주소 규칙과 구현하며 정한 보완을 적는다
- 계약 2절 "검색 — 14단계"에 클라이언트 줄: 화면 주소(조건 이름 · since · until · attachment),
  손으로 고친 주소의 검사, 읽기 화면에 넘기는 다섯 칸, 이어 찾기 5번, 준비 상태를 모를 때,
  보낸 사람이 쓴 <mark> 의 한계
- 디자인 README "14단계 구현하며 정한 보완 — 검색": [조건] 32 밀도 예외 재사용, 패널 · 조건 줄 ·
  결과 없음 · [더 찾기] · 낱말이 너무 많은 주소의 글, 목록 보기는 행 38 에 제목만 강조,
  결과 줄의 별 · 고르기 칸(3단계 여러 통 처리), 강조 색 토큰
- vue3 스킬에 14단계 검색 줄과 밀도 예외 재사용을 더한다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 07:55:27 +09:00
ttipo ab16379836 feat(frontend): 검색에 조건 · 강조 · 준비 중 안내를 더한다 (14단계 화면 1차)
- [조건] 패널(보낸 사람 · 받는 사람 · 제목 · 내용 · 기간 · 첨부)과 결과 머리의 조건 줄 —
  조건도 화면 주소에 싣는다(since · until 은 브라우저 시간대 날짜 · 양 끝 포함)
- 결과 줄 세 줄(분할 보기)과 맞은 말 노랑 강조 — HighlightedText 가 글 조각으로 그린다(v-html 없음),
  읽기 화면은 서버가 넣은 <mark> · 제목 · 첨부 이름 강조. 강조가 든 응답은 보이기만 한다
  (답장 · 전달 · 인쇄 · 예약은 강조 없이 받는다)
- "검색 준비 중" 줄 · 한 글자 안내 · 빈 쪽이면 이어 찾기(한 번에 5번까지) + [더 찾기],
  total 이 null 이면 통 수를 숨긴다
- search-match.ts — backend search-query · search-text · subject-base 와 짝(같은 표본 표)

리뷰 반영: 이어 받는 동안 [다음] · [메일 더 보기] 기다림, 새로 고치는 동안 이어 받지 않음,
손으로 고친 주소도 패널과 같은 검사(8낱말 · 날짜 범위 · 거꾸로 된 기간), 패널 컴포넌트 나눔,
시험 정리(unmount · 보이지 않는 글자 이스케이프).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 07:53:56 +09:00
ttipo 59375f1347 docs: 계약 2절에 14단계 대화(같은 대화 · 묶은 목록) 규칙과 API 표를 적는다
- "대화 — 14단계" 절: 기준(basis — 색인 · 엔진), 대화에 드는 메일, 같은 대화 ·
  대화로 묶어 보기의 쿼리 · 응답(ConversationSummary — messageIds 최신 500 ·
  pendingSendIds), 엔진 방식의 상한과 어림이 되는 줄, 빈 쪽에도 커서가 오는 때, 알려진 한계
- 보내기 · 읽기 설정에 groupByConversation(처음 false — 서버는 저장만)
- API 표에 검색 조건 · 강조 쿼리, 같은 대화 · 묶은 목록 · 검색 색인 경로를 더한다
- nestjs 스킬의 메일 검색 색인 줄에 3차(대화 키 · 보낸 사람 칸 · 엔진 상한)를 적는다

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 07:46:50 +09:00
ttipo 425cbc0e29 feat(backend): 대화 묶기를 더한다 — 대화 키 · 같은 대화 · 대화로 묶은 목록 · 설정
14단계 3차다. 같은 대화(답장 · 전달로 이어진 메일)를 읽기 화면에 보이고, 목록을 대화로
묶어 볼 수 있게 한다. 색인이 준비된 직원은 우리 대화 키로, 채우는 중인 직원은 엔진의
스레드로 묶는다(응답의 basis — 첫 쪽에서 정하고 커서가 잇는다).

- 대화 키: 색인 쓰기 때 넣은 메일의 부모 · 자식(References · In-Reply-To) 가운데 말머리
  (회신 · 답장 · 전달 포함)를 뗀 제목이 같은 것을 가장 작은 키로 합친다. 늦게 온 원본도
  잇고, 다시 넣어도 키는 풀리지 않는다. 색인 판 4
- GET /api/messages/{id}/conversation — 오래된 차례, 휴지통 · 스팸메일함에만 든 메일과
  발송 중인 내부 초안은 빼고 지금 메일은 늘 넣는다
- GET /api/conversations?mailboxId= — 그 메일함의 대화 한 줄씩(참여자 10명 · 통 수 ·
  messageIds 최신 500 · pendingSendIds). 직원 주소 btree 로 먼저 좁힌다(10명 흉내 큰 메일함
  첫 쪽 81ms)
- 설정 groupByConversation(처음 끔) — 서버는 저장만 하고 묶을지는 화면이 정한다
- 마이그레이션 AddMailSearchSender — 참여자는 따로 둔 첫 보낸 사람 칸에서만 읽는다

리뷰 반영: 엔진 방식에 개수 · 시간 상한(같은 대화 한 요청 1,000통 · 묶은 목록 스레드마다
500 · 한 요청 2,000 · 15초, 요약 속성은 내보낼 메일만), Mailbox/get 장애는 503,
꾸민 커서는 엔진 · DB 보다 먼저 400, 같은 대화 한 문장, 이름 안의 <주소>로 참여자를
꾸미지 못하게, 로그 추적 ID. 검색 표본 표 머리 주석(frontend 로 옮겼다)도 함께 고친다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 05:39:10 +09:00
ttipo 53a8d7176d docs: 계약 2절에 14단계 검색(색인 · 조건 · 강조) 규칙을 적는다
쿼리 일곱 · 합친 낱말 8개 · 찾는 규칙(부분 일치 · 구절 · 한 글자) · 목록과 읽기 화면의
searchHighlight · hasNext 인데 빈 쪽 · total null 이면 화면이 통 수를 숨기는 것 · 강조 응답은
보이기만(인용 · 인쇄에 쓰지 않음)을 적고, 3단계의 알려진 한계는 색인을 채우는 동안에만
남는다고 고쳤다. nestjs 스킬의 mail-search 줄도 2차에 맞췄다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:18:42 +09:00
ttipo f9fbf8a20f feat(backend): 메일 검색을 자체 색인으로 바꾸고 조건 검색 · 강조를 더한다
14단계 2차다. 색인이 준비된 직원은 서버의 자체 색인으로, 채우는 중인 직원은 지금처럼
엔진으로 찾는다(방식은 첫 쪽에서 정하고 커서가 잇는다).

- 조건 from · to(참조 포함) · subject · body · after · before · hasAttachment — q 와 합친 낱말 8개까지
- 부분 일치(대소문자 · 전각 · NFC), 구절은 줄바꿈 · NBSP 로 갈린 글에도, 한 글자 낱말뿐이면
  제목 · 사람 · 첨부 이름에서만(2026-11-05 사용자 확정)
- 찾기: GIN(lower(user_email), grams) 후보 → 기본 키로 확인, 한 요청 확인 3,000줄 상한 +
  이어 찾기 커서, total 은 첫 쪽에서 1,000 까지(못 세면 null) — 1만 통에서 가장 무거운 검색 0.8초
- 목록 항목 searchHighlight(제목 · 맞은 말 둘레 미리 보기), 읽기 화면은 정화한 HTML 의 글
  조각에 <mark> · 제목 · 첨부 이름 · 글 본문 위치
- 마이그레이션 SearchIndexUserGrams(btree_gin), 색인 판 2(공백 접기) — 판이 바뀌면 그 직원의
  색인을 지우고 밤에 다시 채운다

리뷰 반영: 옛 판 줄 지우기, 꾸민 커서 · NUL 은 VAL_001 · U+FFFD, 한 글자 길 한 트랜잭션,
다른 직원 줄 격리 e2e.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:18:41 +09:00
ttipo d57e6bfe6c chore(docker): DB 메모리 한도를 1g · 공유 버퍼를 256MB 로 올린다(14단계 검색 색인)
1만 통 측정에서 색인 찾기는 표 · 색인이 메모리에 있을 때만 1초 안이었고 캐시가 비면
3~8배 느렸다(사용자 결정 2026-11-05). 8GB 서버의 한도 합은 6.65GB · 백업 때 7.65GB 다.
복원 연습 때는 8.15GB 로 8GB 를 넘는다 — 사실만 적고 다룰 방법은 따로 정한다.
mail/README 에 운영에 올리는 차례(db 만 먼저 up · 한도 · shared_buffers 확인)를 적었다.
docker 스킬의 연습 주기를 기준 문서(README — 한 달에 한 번)에 맞췄다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:00:44 +09:00
ttipo 0fc5735959 docs: 검색 색인 준비 상태와 개발 쿼리 로그 규칙을 계약 · 규칙 문서에 적는다
계약 2절에 "검색 색인 (GET /api/search-index)" — 색인에 드는 것 · 만드는 때 · 밤 채우기 ·
맞추기 · 조회지만 상태 줄을 만드는 동작(멱등)을 적었다. 로그 규칙에 개발 환경도 DB 쿼리
매개변수를 남기지 않는다는 줄, nestjs 스킬에 mail-search 모듈 줄과 Redis 로 옮길 상태를 더했다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:51:15 +09:00
ttipo 9887035bcd feat(backend): 메일 검색 색인의 바탕(표 · 맞추기 · 지난 메일 채우기 · 준비 상태)을 더한다
14단계 자체 검색 색인의 1차다. 검색 API 를 색인으로 바꾸는 일과 대화 키 잇기는 다음 차례다.

- 마이그레이션 AddMailSearchIndex 하나: 표 mail_search_messages · mail_search_states 와
  mail_settings.group_by_conversation 열(3차의 대화로 묶어 보기 설정 — 같은 배포에서 한 번에 만든다)
- 접기 · 글자 두 개 묶음 · 맞은 곳 · 말머리 떼기 순수 함수와 표본 표(나중에 frontend 와 짝)
- 엔진 포트 MAIL_INDEX_SOURCE + JMAP 어댑터: Email/changes 맞추기(실시간 · 5분 · 검색 전,
  서버 전체 동시 4), 밤 창(KST 22:00~02:45 · 05:00~07:30) 지난 메일 채우기, 전체 대조
- GET /api/search-index, 로그인 · 새 비밀번호 때 상태 줄, 퇴사 마무리에 표 둘
- 로그 가림에 조건 검색 이름 넷, 개발 환경 DB 쿼리 로그 끔(메일 글이 DB 에 든다)

리뷰 반영: 시간 끝은 실패가 아니라 이어 하기, 본문 읽기 묶음의 메일 크기 상한(32MiB ·
16MiB 넘으면 혼자), 겹친 id 빼기, 사내 도메인 주소만 색인(개발 서버 cron 이 e2e 줄을 건드리지 않게).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:51:15 +09:00
ttipo f49dc2a186 docs(rules): 의존성 유예 표를 11-05 점검으로 고친다
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 13m48s
CI / Frontend (lint · type-check · test · build) (push) Successful in 12m42s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 1m44s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m20s
올린 넷을 표에서 빼고, 남은 운영 권고(swagger 아래 js-yaml · typeorm 아래 glob · minimatch)와
개발 전용 묶음의 다시 볼 시점을 적는다. multer override 는 platform-express 를 v12.3 이상으로
올릴 때 지운다(nestjs 스킬도 같이). npm audit fix 를 통째로 돌리지 않는 까닭을 함께 적는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:28:33 +09:00
ttipo c476a4ecf4 chore(backend): 미뤄 둔 의존성 넷을 배포 2주 지난 판으로 올린다
security.md 유예 표에서 11-05 를 기다리던 항목 — body-parser 2.4.0(lock 만, qs 6.16.1 ·
iconv-lite 0.7.3 이 따라옴) · multer override ^2.5.0(platform-express 12.3.0 이 2.5.0 을
선언한다 — 11.x 최신도 2.4.0 고정이라 override 를 남긴다) · sanitize-html 2.18.1 · fast-uri 3.1.9.
모두 도달 경로가 없던 권고라 2주 규칙대로 골랐다(--before 로 그보다 새 판은 받지 않았다).
audit 29 → 23(운영 11 → 5, high 3 → 0). js-yaml 은 swagger 11.4.8(11-04)이 2주가 안 돼 그대로 둔다.

npm 11.6.2 컨테이너에서 바꿨다. 임시 복사본에서 npm ci, 인자 없는 npm install 이 lock 을 바이트
그대로 두는지, npm ci --omit=dev 뒤 require, lint · type-check · 단위 3,203 · e2e 266 · build 통과.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:28:33 +09:00
ttipo ba80956d7e docs: 14단계 화면(검색 · 대화 묶기)을 확정해 디자인 문서에 옮긴다
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m45s
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 20m40s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 12m31s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m45s
CI (Flutter) / Flutter (analyze · test) (push) Successful in 20m0s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m9s
CI / Backend (lint · type-check · test · e2e · build) (push) Has been cancelled
CI / Frontend (lint · type-check · test · build) (push) Has been cancelled
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Has been cancelled
CI / 에러 코드 SSOT 동기화 검사 (push) Has been cancelled
사용자가 캔버스 초안을 확정했다(2026-11-05). 자체 색인의 부분 일치 · 조건 검색 · 검색어 강조, 읽기 화면의
같은 대화, 목록의 대화로 묶어 보기(사용자마다 · 기본 끔)를 README 14단계 절로 적고 보드 셋과 canvas.json 을
캔버스(v52)와 맞춘다. 엔진 스레드가 회신: · 답장: · 전달: 을 떼지 않고 검색이 어절 통째 일치뿐이라는
11-03 실측이 까닭이다. API 규칙은 구현 때 계약 2절에 더하고, 구현 · 배포는 전환 뒤 안정화 다음에 한다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:05:01 +09:00
ttipo 1171593724 docs: 서버 글의 새 말투를 README · 계약 · 오류 코드 규칙에 맞춘다
CI (Flutter) / Flutter (analyze · test) (pull_request) Failing after 0s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 15m4s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 14m25s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m22s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m50s
README 11 · 13단계의 덩이 · 알림 메일 글(이 절이 원본), 계약의 덩이 글 · FILE_003 · FILE_004 · AUTH_006 ·
알림 메일 메뉴 경로, 오류 코드 규칙 표의 AUTH_006 · VAL_001 을 새 글과 같게 둔다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:04:45 +09:00
ttipo 0021c0a1fc 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>
2026-11-05 14:04:45 +09:00
ttipo cdebcb85f9 docs: 계약이 옮겨 적은 화면 문구를 새 말투로 맞춘다
일부 거절 알림 · 부재중과 필터의 노란 줄 · 필터 적용 진행과 끝 알림 · 이어 올리기 줄 — 화면 상수가
원본인 글을 ddfb2d1 의 새 글과 같게 둔다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:50:18 +09:00
ttipo cda4093ffd docs: 화면 문구 말투 규칙을 디자인 README · vue3 스킬에 적는다
README 에 「화면 문구 — 사용자에게 말하듯」 절(규칙 여덟과 예시)을 두고, README 가 옮겨 적은 화면 문구를
새 글로 맞춘다. vue3 스킬 점검 목록에 문구 줄을 더한다. 보드(*.dc.html)의 글은 11-05 전 것으로 둔다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:44:43 +09:00
ttipo ddfb2d14ab fix(frontend): 화면 문구를 사용자에게 말하는 말투로 고친다
설계 문서처럼 규칙을 적어 둔 문구가 많다는 사용자 지적(2026-11-05) — "메일 서버에 계정이 있는 직원입니다 —
이름과 주소만 보입니다." · "「@도메인」은 그 도메인에서 오는 메일 모두입니다." 같은 글. 화면 상수 · 템플릿 ·
문장을 만드는 유틸의 236곳을 무엇이 되는지 · 무엇을 하면 되는지로 다시 썼다: 줄표로 잇지 않고, 「」 대신
[단추 이름], 메일 서버 · 엔진 · 판정 같은 만든 쪽 말을 빼고, 존댓말은 그대로. 한도 · 날짜 · 개수는 빼지 않았다.
그 문구를 그대로 보던 spec 을 함께 맞췄다(lint · type-check · 단위 2,354 · TZ=UTC · build 통과).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 12:44:43 +09:00
ttipo 7d0d5f34c0 docs: 탭 · 홈 화면 아이콘 규칙을 디자인 README · vue3 스킬에 적는다
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 14m34s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 12m11s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 1m40s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m8s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 13m30s
CI / Frontend (lint · type-check · test · build) (push) Successful in 13m4s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 2m0s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m15s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:41:05 +09:00
ttipo 9c8728138e feat(frontend): 탭 · 홈 화면 아이콘을 ARA 마크로 바꾼다
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 22:41:05 +09:00
ttipo 8abcaed98e docs: 앱 레일에서 ERP 를 뺀 것을 디자인 README · vue3 스킬에 반영
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 12m27s
CI / Frontend (lint · type-check · test · build) (push) Successful in 14m18s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 2m2s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 13m7s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m56s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 13m6s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m1s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m20s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:32:38 +09:00
ttipo bdd42a3cb2 feat(frontend): 앱 레일과 로그인 소개 글에서 ERP 를 뺀다
사용자 요청(2026-11-04)으로 레일의 "준비 중" ERP 칸과 로그인 브랜드 패널 소개 글의 ERP 를 뺐다.
레일 테스트는 실제 목록(메일 · 캘린더 · 파일 · 주소록 · 설정)도 그대로 본다 — 다시 넣을 때 함께 고치게.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 09:32:38 +09:00
ttipo bc22f5f3ba test(backend): CPU 시간 단언을 지금 스레드의 user 시간으로 — CI 흔들림 고침
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m21s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 12m37s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m18s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m10s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 14m41s
CI / Frontend (lint · type-check · test · build) (push) Successful in 14m29s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 2m26s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 12m27s
문서만 바꾼 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>
2026-11-04 00:02:14 +09:00
ttipo 349c312989 test(frontend): 대용량 파일을 다 올린 뒤 보낸다 — 조각 SHA-256 이 스레드 풀이라 부하에서 늦다
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Failing after 10m31s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 14m21s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m18s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m30s
쓰기 화면의 두 테스트는 대용량 파일을 고른 뒤 양보(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>
2026-11-03 23:26:28 +09:00
ttipo 2cd2764da8 test(backend): jmap.client spec — 가짜 응답 없는 fetch 는 연결 실패로 막고 빠진 응답을 잡는다
이 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 23:10:44 +09:00