feat: 메일 16단계 — 주소록 가져오기 · 내보내기 · 여러 서명 · 부재중 서식과 회사 밖 응답 · 2단계 인증 휴대폰 바꾸기 #24

Merged
ttipo merged 5 commits from feat/personal-settings into main 2026-11-10 12:40:35 +09:00
Owner

무엇을 · 왜

메일 16단계(개인 설정)입니다. 직원이 혼자 할 수 있는 설정을 넓힙니다. 화면은 캔버스 보드로 먼저 확정했습니다(v74 — Contacts16 · Signature16 · Settings16).

  • 주소록 가져오기 · 내보내기: 11-05 전환 때 주소록은 옮기지 않아 직원이 한 명씩 넣어야 했습니다.
    • 브라우저가 vCard(2.1 · 3.0 · 4.0) · CSV(네이버 · Google · Outlook · 우리 내보내기) · 엑셀(.xlsx)을 읽어 미리 보기를 보입니다. 파일은 서버로 가지 않고, 새 의존성 없이 DecompressionStream · DOMParser 로 읽습니다.
    • 넣을 연락처만 묶음(100명 · 값 8,000 · 700KB)으로 POST /api/contact-imports 에 보냅니다. 화면이 준 uid 라 다시 보내도 두 번 들어가지 않습니다. 결과 알림은 ✕로 닫을 때까지 남고 [되돌리기](POST /api/contact-deletions)가 붙습니다.
    • 칸이 없는 정보(주소 · 생일 · 홈페이지 · 별명)는 메모 끝 "가져온 정보" 줄로 붙입니다. 같은 이메일은 건너뜁니다(POST /api/contact-import-checks).
    • 내보내기 GET /api/contact-exports: vCard 3.0 · CSV(UTF-8 BOM · 한국어 머리 · 수식 막기). 사내 목록은 넣지 않습니다.
  • 여러 서명: 10개까지 이름을 붙여 만들고, 메일에 넣을 서명 둘(새 메일 · 답장과 전달)을 고릅니다. 쓰기 화면 [서명 ▾]로 바꾸면 본문의 서명 자리만 바뀝니다.
    • 서명 자리는 앱과 같은 경계(빈 문단 뒤 속성 없는 div)이고, 앞 빈 줄을 지켜 임시 저장 · 이어 쓰기에서도 다시 찾습니다.
    • 마이그레이션 MultipleMailSignatures: 한 사람 한 줄 → 서명마다 한 줄. 지금 서명은 "기본 서명"으로 옮기고 값은 그대로입니다. down 도 있습니다.
    • 옛 GET · PUT /api/mail-signature 는 앱 첫 판을 위해 그대로 둡니다.
  • 부재중 응답: 내용을 서식 편집기로 쓰고, 회사 밖에서 온 메일에는 사내와 같게 · 보내지 않기 · 따로 쓰기 가운데 고릅니다.
    • 엔진 Sieve 의 문자열 하나 4,096바이트는 variables 조각으로 넘깁니다.
    • 사내 · 회사 밖은 봉투 보낸 사람의 도메인으로 가릅니다. 답장도 그 주소로만 가므로 사내용 글이 밖으로 나가지 않습니다.
    • 원본 판 3 은 서식이나 회사 밖 응답을 쓴 직원만 씁니다(계약 2절에 되돌리기 계획).
  • 2단계 인증 휴대폰 바꾸기: 새 휴대폰의 코드를 확인한 뒤, 지금 휴대폰의 코드와 비밀번호로 엔진 요청 하나에 바꿉니다. 2단계가 꺼지는 틈이 없어 필수 정책인 직원도 혼자 바꿉니다.

엔진 실측(개발 Stalwart 0.16.27):

  • ContactCard/set 300명 한 요청 90ms
  • vacation :mime multipart 그대로, 변수 조각으로 펼친 값 60KB 까지 바이트 그대로
  • envelope 도메인 가름 · 답장은 봉투 주소로
  • 켠 채 otpAuth { 지금 코드, 새 주소 } 로 바뀌고 옛 코드는 바로 막힘

리뷰는 두 번 받았습니다. 1차는 중간 3 · 낮음 6 · 참고 6이었고, 모두 고쳤습니다(L6 는 알려진 한계로 계약에 적음).

  • vCard 줄 잇기가 줄 수의 제곱이던 것 → 선형
  • 판 3 이 거의 모든 직원에게 생기던 것 → 서식을 쓴 직원만
  • 서명 자리가 사라지던 것 → 앞 빈 줄 지키기
  • 엑셀 칸 상한 · 메모 다듬기 · 옛 서명 경로의 답장 기본 등

확인 방법

  • backend(컨테이너): lint 0, 단위 3,940, e2e 386(16단계 새 경로마다 401 · 400 · 404 · 409 · 429 · 성공 꼴), tsc 컴파일
  • 실엔진(개발 엔진 · 개발 계정):
    • 가져오기 100명 · 같은 묶음 다시 보내기(같은 id) · 지우기 · 내보내기(vCard 를 엔진이 다시 읽어도 같음)
    • 휴대폰 바꾸기(옛 코드 거절 · 새 코드로 로그인)
    • 서식 부재중(사내 · 회사 밖 따로 쓰기 · 보내지 않기 · 같은 응답 · 큰 서식)
    • 시험 데이터는 모두 처음 상태로 돌렸습니다.
  • frontend: lint 0, type-check, test:unit 3,194(KST — 바꾼 묶음은 UTC 로도 403)
  • 네이버 주소록 내보내기 실물(엑셀 · CSV)로 칸 맞추기를 확인했습니다(값은 가려서 — 파일은 커밋하지 않음).
  • 아직 못 한 것: 브라우저로 직접 띄운 실사용 시험 — 이 PR 의 CI 와 함께 봅니다.

영향 범위

  • backend
  • frontend
  • flutter
  • docker / 인프라
  • 규칙 문서(.claude/)

공유 계약

  • 새 경로: POST /api/contact-import-checks · POST /api/contact-imports · POST /api/contact-deletions · GET /api/contact-exports ·
    /api/mail-signatures(GET · POST · PUT · DELETE) · GET · PUT /api/mail-signature-defaults · POST /api/two-factor/enrollments/{id}/verification · POST /api/two-factor/replacements
  • 넓힌 경로: GET · PUT /api/mail-vacation 에 htmlBody · external(없으면 지금과 같음), POST /api/two-factor/enrollments 가 켜진 계정에도 201(웹 켜기 창 · 앱 켜기 단계는 활성화의 AUTH_008 로 그대로 처리)
  • 마이그레이션 MultipleMailSignatures(운영 배포 때 migrationsRun 으로 적용)
  • 새 에러 코드는 없습니다.
  • 앱 첫 판이 읽는 GET /api/mail-signature 네 칸은 그대로입니다(새 메일 기본이 없으면 답장 기본을 줌).

체크리스트

  • 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과
  • 주석 한국어, 매직 넘버·하드코딩 문자열 없음
  • 사용자 노출 실패는 모두 에러 코드와 함께 처리 (BusinessException / error.code 분기)
  • 에러 코드를 바꿨다면 npm run sync:error-codes 실행 후 생성 파일까지 커밋 — 바꾸지 않음
  • 민감정보 미커밋 (*.example 값은 placeholder) — 실제 연락처 파일은 커밋하지 않음
  • 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음 — 연락처 · 서명 · 부재중 글 · 코드 · otpauth 주소 없음

🤖 Generated with Claude Code

## 무엇을 · 왜 메일 16단계(개인 설정)입니다. 직원이 혼자 할 수 있는 설정을 넓힙니다. 화면은 캔버스 보드로 먼저 확정했습니다(v74 — `Contacts16` · `Signature16` · `Settings16`). - **주소록 가져오기 · 내보내기**: 11-05 전환 때 주소록은 옮기지 않아 직원이 한 명씩 넣어야 했습니다. - 브라우저가 vCard(2.1 · 3.0 · 4.0) · CSV(네이버 · Google · Outlook · 우리 내보내기) · 엑셀(.xlsx)을 읽어 미리 보기를 보입니다. 파일은 서버로 가지 않고, 새 의존성 없이 `DecompressionStream` · `DOMParser` 로 읽습니다. - 넣을 연락처만 묶음(100명 · 값 8,000 · 700KB)으로 `POST /api/contact-imports` 에 보냅니다. 화면이 준 uid 라 다시 보내도 두 번 들어가지 않습니다. 결과 알림은 ✕로 닫을 때까지 남고 [되돌리기](`POST /api/contact-deletions`)가 붙습니다. - 칸이 없는 정보(주소 · 생일 · 홈페이지 · 별명)는 메모 끝 "가져온 정보" 줄로 붙입니다. 같은 이메일은 건너뜁니다(`POST /api/contact-import-checks`). - 내보내기 `GET /api/contact-exports`: vCard 3.0 · CSV(UTF-8 BOM · 한국어 머리 · 수식 막기). 사내 목록은 넣지 않습니다. - **여러 서명**: 10개까지 이름을 붙여 만들고, 메일에 넣을 서명 둘(새 메일 · 답장과 전달)을 고릅니다. 쓰기 화면 [서명 ▾]로 바꾸면 본문의 서명 자리만 바뀝니다. - 서명 자리는 앱과 같은 경계(빈 문단 뒤 속성 없는 div)이고, 앞 빈 줄을 지켜 임시 저장 · 이어 쓰기에서도 다시 찾습니다. - 마이그레이션 `MultipleMailSignatures`: 한 사람 한 줄 → 서명마다 한 줄. 지금 서명은 "기본 서명"으로 옮기고 값은 그대로입니다. down 도 있습니다. - 옛 `GET · PUT /api/mail-signature` 는 앱 첫 판을 위해 그대로 둡니다. - **부재중 응답**: 내용을 서식 편집기로 쓰고, 회사 밖에서 온 메일에는 사내와 같게 · 보내지 않기 · 따로 쓰기 가운데 고릅니다. - 엔진 Sieve 의 문자열 하나 4,096바이트는 `variables` 조각으로 넘깁니다. - 사내 · 회사 밖은 봉투 보낸 사람의 도메인으로 가릅니다. 답장도 그 주소로만 가므로 사내용 글이 밖으로 나가지 않습니다. - 원본 판 3 은 서식이나 회사 밖 응답을 쓴 직원만 씁니다(계약 2절에 되돌리기 계획). - **2단계 인증 휴대폰 바꾸기**: 새 휴대폰의 코드를 확인한 뒤, 지금 휴대폰의 코드와 비밀번호로 엔진 요청 하나에 바꿉니다. 2단계가 꺼지는 틈이 없어 필수 정책인 직원도 혼자 바꿉니다. 엔진 실측(개발 Stalwart 0.16.27): - `ContactCard/set` 300명 한 요청 90ms - `vacation :mime` multipart 그대로, 변수 조각으로 펼친 값 60KB 까지 바이트 그대로 - envelope 도메인 가름 · 답장은 봉투 주소로 - 켠 채 `otpAuth { 지금 코드, 새 주소 }` 로 바뀌고 옛 코드는 바로 막힘 리뷰는 두 번 받았습니다. 1차는 중간 3 · 낮음 6 · 참고 6이었고, 모두 고쳤습니다(L6 는 알려진 한계로 계약에 적음). - vCard 줄 잇기가 줄 수의 제곱이던 것 → 선형 - 판 3 이 거의 모든 직원에게 생기던 것 → 서식을 쓴 직원만 - 서명 자리가 사라지던 것 → 앞 빈 줄 지키기 - 엑셀 칸 상한 · 메모 다듬기 · 옛 서명 경로의 답장 기본 등 ## 확인 방법 - backend(컨테이너): lint 0, 단위 3,940, e2e 386(16단계 새 경로마다 401 · 400 · 404 · 409 · 429 · 성공 꼴), tsc 컴파일 - 실엔진(개발 엔진 · 개발 계정): - 가져오기 100명 · 같은 묶음 다시 보내기(같은 id) · 지우기 · 내보내기(vCard 를 엔진이 다시 읽어도 같음) - 휴대폰 바꾸기(옛 코드 거절 · 새 코드로 로그인) - 서식 부재중(사내 · 회사 밖 따로 쓰기 · 보내지 않기 · 같은 응답 · 큰 서식) - 시험 데이터는 모두 처음 상태로 돌렸습니다. - frontend: lint 0, type-check, test:unit 3,194(KST — 바꾼 묶음은 UTC 로도 403) - 네이버 주소록 내보내기 실물(엑셀 · CSV)로 칸 맞추기를 확인했습니다(값은 가려서 — 파일은 커밋하지 않음). - **아직 못 한 것**: 브라우저로 직접 띄운 실사용 시험 — 이 PR 의 CI 와 함께 봅니다. ## 영향 범위 - [x] backend - [x] frontend - [ ] flutter - [ ] docker / 인프라 - [x] 규칙 문서(.claude/) > **공유 계약** > - 새 경로: `POST /api/contact-import-checks` · `POST /api/contact-imports` · `POST /api/contact-deletions` · `GET /api/contact-exports` · > `/api/mail-signatures`(GET · POST · PUT · DELETE) · `GET · PUT /api/mail-signature-defaults` · `POST /api/two-factor/enrollments/{id}/verification` · `POST /api/two-factor/replacements` > - 넓힌 경로: `GET · PUT /api/mail-vacation` 에 `htmlBody` · `external`(없으면 지금과 같음), `POST /api/two-factor/enrollments` 가 켜진 계정에도 201(웹 켜기 창 · 앱 켜기 단계는 활성화의 `AUTH_008` 로 그대로 처리) > - 마이그레이션 `MultipleMailSignatures`(운영 배포 때 `migrationsRun` 으로 적용) > - 새 에러 코드는 없습니다. > - 앱 첫 판이 읽는 `GET /api/mail-signature` 네 칸은 그대로입니다(새 메일 기본이 없으면 답장 기본을 줌). ## 체크리스트 - [x] 린트/분석 통과 (Warning 0) · 변경 영역 테스트 통과 - [x] 주석 한국어, 매직 넘버·하드코딩 문자열 없음 - [x] 사용자 노출 실패는 모두 에러 코드와 함께 처리 (`BusinessException` / `error.code` 분기) - [x] 에러 코드를 바꿨다면 `npm run sync:error-codes` 실행 후 생성 파일까지 커밋 — 바꾸지 않음 - [x] 민감정보 미커밋 (`*.example` 값은 placeholder) — 실제 연락처 파일은 커밋하지 않음 - [x] 로그에 비밀번호·토큰·요청 본문 전체가 남지 않음 — 연락처 · 서명 · 부재중 글 · 코드 · otpauth 주소 없음 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ttipo added 5 commits 2026-11-10 11:54:21 +09:00
15단계(메일 · 메일함 다루기)는 main 에 합쳐 14단계와 함께 배포를 기다리고,
16단계(개인 설정)를 개발하기 시작했다. 16단계 가져오기는 엑셀(.xlsx)도 받는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
주소록 가져오기 · 내보내기(Contacts16), 여러 서명과 쓰기 화면 서명 바꾸기(Signature16),
부재중 서식 · 회사 밖 응답과 2단계 인증 휴대폰 바꾸기(Settings16). 캔버스 v74 에서 확정했다 — 글은 화면 상수가 기준이다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
메일 16단계(개인 설정)의 서버다.
- 주소록: POST /api/contact-import-checks · contact-imports · contact-deletions 로 여러 명을 한 번에
  확인 · 만들기 · 지우기(화면이 준 uid 로 다시 보내도 두 번 들어가지 않는다), GET /api/contact-exports 로
  vCard 3.0 · CSV(BOM · 한국어 머리 · 수식 막기) 내보내기 — 하나씩 만들면 사무실 요청 수 한도에 걸린다.
- 여러 서명: /api/mail-signatures · /api/mail-signature-defaults, 서명마다 한 줄로 바꾸는 마이그레이션
  (지금 서명은 "기본 서명"으로 — 값 보존), 그림 정리는 모든 서명의 합집합. 옛 /api/mail-signature 는 앱 첫 판을 위해 그대로.
- 부재중: 서식 본문(multipart — 문자열 4,096바이트는 variables 조각으로)과 회사 밖 응답(봉투 도메인으로 가름 —
  답장도 그 주소로만), 원본 판 3 은 새 칸을 쓴 직원만.
- 2단계 인증: 켠 채 휴대폰 바꾸기(새 코드 확인 → 지금 코드 · 비밀번호로 엔진 요청 하나 — 꺼지는 틈 없음).
새 에러 코드 · 새 의존성 없음.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
메일 16단계(개인 설정)의 화면이다(보드 Contacts16 · Signature16 · Settings16).
- 주소록: 브라우저가 vCard · CSV · 엑셀(.xlsx)을 읽어 미리 보기(파일은 서버로 가지 않는다 — 새 의존성 없이
  DecompressionStream · DOMParser), 네이버 · Google · Outlook 머리를 알아보고 칸 없는 정보는 메모 끝에,
  묶음(100명 · 값 8,000 · 700KB)으로 넣고 ✕까지 남는 결과 알림의 [되돌리기], 내보내기 창.
- 서명: 왼쪽 목록 · 오른쪽 편집 · 메일에 넣을 서명 둘, 쓰기 화면 [서명 ▾]로 서명 자리만 바꾸기
  (앱과 같은 경계 — 앞 빈 줄을 지켜 임시 저장 · 이어 쓰기에서도 찾는다).
- 부재중: 내용 편집기(서식이 있을 때만 서식 본문) · 회사 밖에서 온 메일 응답 셋 · 미리 보기 전환.
- 2단계 인증: [휴대폰 바꾸기] 두 걸음 창(비밀은 창 안에만).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: 메일 16단계(개인 설정)를 계약 · 디자인 README · 스킬 · 로드맵에 적는다
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 15m17s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 14m10s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m13s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m54s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 14m55s
CI / Frontend (lint · type-check · test · build) (push) Successful in 13m43s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 2m2s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 11m39s
19dbb2babb
계약 2절(서명 · 부재중 — 원본 판 3 과 되돌리기 계획) · 3절(가져오기 · 내보내기) · 5절(휴대폰 바꾸기),
디자인 README 16단계 절과 구현하며 정한 보완, nestjs · vue3 스킬 표, 로드맵의 추후 후보(16단계에서 미룬 것).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ttipo merged commit 19dbb2babb into main 2026-11-10 12:40:35 +09:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: ttipo/arasystem#24