generated from ttipo/project-base
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
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
계약 2절(서명 · 부재중 — 원본 판 3 과 되돌리기 계획) · 3절(가져오기 · 내보내기) · 5절(휴대폰 바꾸기), 디자인 README 16단계 절과 구현하며 정한 보완, nestjs · vue3 스킬 표, 로드맵의 추후 후보(16단계에서 미룬 것). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #24.
This commit is contained in:
@@ -337,7 +337,7 @@ API 와 서버 규칙은 계약 1절 "`PUT /api/auth/password`", 2절 "메일함
|
||||
|
||||
- [x] 부재중 자동 응답 사용(44). 끄면 아래 칸을 흐리게(쓴 글은 남긴다).
|
||||
- 기간: (•) 끌 때까지 계속 / ( ) 기간 정하기 → 시작 · 끝 날짜 칸(44). "시작하는 날 0시부터 끝나는 날 밤 12시까지 답합니다. 시각은 이 PC의 시간으로 셉니다."
|
||||
- 제목(44, 최대 폭 520) · 내용(여러 줄, 글자만 — "서식과 그림 없이 글자만 보내고, 서명은 붙지 않습니다.").
|
||||
- 제목(44, 최대 폭 520) · 내용(여러 줄, 글자만 — "서식과 그림 없이 글자만 보내고, 서명은 붙지 않습니다."). — **16단계부터 내용 칸은 서식 편집기**(아래 16단계 절).
|
||||
- [저장] → "저장했습니다. 10월 30일(금)부터 11월 3일(화)까지 자동으로 답합니다." / "끌 때까지 자동으로 답합니다." / 끄면 "자동 응답을 껐고, 써 둔 글은 그대로 남아 있습니다."
|
||||
- 미리 보기 "보낸 사람이 받는 답장"(보낸 사람 · 제목 · 내용) + "같은 사람에게는 매번 보내지 않습니다."
|
||||
- 켜져 있는 동안 메일 화면의 메일함 패널 아래(사용량 위)에 안내(바탕 #FFF7E6 · #7A5200, 높이 44 이상): "부재중 자동 응답 켜짐" / "11월 3일까지 · 설정" — 누르면 이 설정으로.
|
||||
@@ -750,7 +750,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **꺼져 있을 때** 상자 아래:
|
||||
- 갈색 경고 "켜면 메일 프로그램은 앱 비밀번호로 다시 연결해야 합니다" / "Outlook, 썬더버드, 휴대폰 메일 앱은 웹메일 비밀번호로 새로 연결할 수 없고, 이미 연결해 둔 프로그램도 다음에 다시 연결할 때 막힙니다. 켜기 전에 앱 비밀번호를 만들어 두면 편합니다."(앱 비밀번호 링크)
|
||||
- 「켜는 방법」 1 · 2 · 3(청록 굵은 숫자 — 동그라미 없음): 인증 앱 설치(Google · Microsoft Authenticator 처럼 6자리 코드를 만드는 앱) → [2단계 인증 켜기]를 눌러 QR 찍기 → 코드와 지금 비밀번호를 넣으면 켜진다.
|
||||
- **켜져 있을 때** 상자 아래: "휴대폰을 바꿀 때는 2단계 인증을 끈 뒤 새 휴대폰으로 다시 켜 주세요. 끌 때 지금 휴대폰의 코드가 필요합니다." · "휴대폰을 잃어버렸거나 인증 앱을 지웠으면 관리자에게 문의해 주세요." · "메일 프로그램은 앱 비밀번호로 연결합니다."
|
||||
- **켜져 있을 때** 상자 아래: "휴대폰을 바꿀 때는 2단계 인증을 끈 뒤 새 휴대폰으로 다시 켜 주세요. 끌 때 지금 휴대폰의 코드가 필요합니다."(**16단계부터 [휴대폰 바꾸기] — 첫 줄은 16단계 절의 글**) · "휴대폰을 잃어버렸거나 인증 앱을 지웠으면 관리자에게 문의해 주세요." · "메일 프로그램은 앱 비밀번호로 연결합니다."
|
||||
- **켜기 창**(모달 620 — `wide`, 필터 고치기 창과 같은 폭 · 처음 초점은 코드 칸)
|
||||
- ① "인증 앱으로 QR 코드를 찍어 주세요" / "인증 앱에서 [+] → [QR 코드 스캔]을 고른 뒤 이 그림을 찍어 주세요." + QR 그림(160 — 실제 otpauth 주소는 오류 정정 M 에서 49칸이라 보드의 128 보다 키웠다)
|
||||
- "찍을 수 없으면 이 키를 입력해 주세요" + 32자 키(4자씩 띄움, 고정폭) + [복사](36 — 「키를 복사했습니다.」) + "앱에는 ‘ARA SYSTEM (ara.kim@arasystem.it.kr)’로 저장됩니다."
|
||||
@@ -894,7 +894,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- 유예 중 · 꺼짐 — 머리 아래 갈색 상자(방패): **"11월 30일까지 2단계 인증을 켜 주세요"** / "회사 정책에 따라 그 뒤에는 2단계 인증을 켜야 로그인할 수 있습니다."
|
||||
그 아래 상태 상자 · 경고 · 켜는 방법 · 켜기 창은 12단계 그대로.
|
||||
- 켜짐 — 상태 상자의 [2단계 인증 끄기] 대신 회색 글 "회사 정책에 따라 끌 수 없습니다"(서버도 `VAL_001` 로 거절).
|
||||
아래 안내의 첫 줄은 "휴대폰을 바꿀 때는 관리자에게 2단계 인증을 풀어 달라고 요청해 주세요. 다음에 로그인할 때 새 휴대폰으로 다시 켜면 됩니다."(나머지 두 줄은 12단계 그대로).
|
||||
아래 안내의 첫 줄은 "휴대폰을 바꿀 때는 관리자에게 2단계 인증을 풀어 달라고 요청해 주세요. 다음에 로그인할 때 새 휴대폰으로 다시 켜면 됩니다."(**16단계부터 이 직원도 [휴대폰 바꾸기] — 첫 줄은 16단계 절의 글**)(나머지 두 줄은 12단계 그대로).
|
||||
- **휴대폰을 바꿀 때**(필수인 직원): 관리자에게 「2단계 해제」를 요청한다(운영 도구 — 받은편지함 알림 메일이 간다) → 다음 로그인 때 이 켜기 단계로 새 휴대폰을 등록한다.
|
||||
직원이 직접 하는 휴대폰 바꾸기는 미뤘다(아래).
|
||||
|
||||
@@ -1272,6 +1272,168 @@ PC 에서는 설정 › 계정 › 휴대폰 앱(A)이 QR 코드로 휴대폰에
|
||||
|
||||
메일함을 끌어 옮기기 · 순서 바꾸기(옮기기 창으로), 차단할 때 옮긴 메일의 한꺼번에 되돌리기, 예약됨 목록 줄의 단추, 옮기기 단축키, 휴대폰 앱의 [지금 보내기].
|
||||
|
||||
## 16단계 — 개인 설정 (2026-11-10 확정 — `Contacts16` · `Signature16` · `Settings16`)
|
||||
|
||||
직원이 혼자 할 수 있는 설정을 넓힌다 — 주소록 가져오기 · 내보내기, 여러 서명과 쓰기 화면 [서명 ▾], 부재중 응답의 서식 본문과 회사 밖 응답, 2단계 인증 휴대폰 바꾸기.
|
||||
서버 약속은 계약 2절("서명" · "부재중 자동 응답") · 3절(주소록) · 5절("2단계 인증")이다. 글의 원본은 화면 상수(`shared/constants/contacts.ts` · `mail-signature.ts` · `mail-vacation.ts` · `account-security.ts`)이고 아래는 옮겨 적은 것이다.
|
||||
새 크기 토큰 · 밀도 예외는 없다 — [사내 | 회사 밖] 미리 보기 전환 36 은 서명 미리 보기의 `.segmented`(`--size-segment-item-lg`)를 다시 쓴다. 새 아이콘은 upload 하나다. 문구는 위 「화면 문구 — 사용자에게 말하듯」을 따른다.
|
||||
|
||||
### 주소록 가져오기 · 내보내기 (`Contacts16`)
|
||||
|
||||
다른 메일이나 휴대폰에서 내보낸 연락처 파일을 주소록에 넣고, 내 주소록을 파일로 내려받는다. 서버 약속은 계약 3절("가져오기 · 내보내기").
|
||||
새 크기 토큰 · 밀도 예외는 없다(머리 52 · 단추 44 그대로). 새 아이콘은 올리기(`upload`) 하나다.
|
||||
|
||||
- **자리**: 「전체 연락처」 목록 머리 오른쪽에 [가져오기] · [내보내기](도구 단추 — 그룹 머리의 [구성원 넣기]와 같은 꼴). 그룹 · 사내 목록의 머리는 그대로다(그룹 하나만 내보낼 때는 내보내기 창에서 고른다).
|
||||
- 연락처가 하나도 없으면 [내보내기]를 끄고(`aria-disabled`) 까닭 "내보낼 연락처가 없습니다."(마우스를 올리면 뜨는 글 · 화면 낭독).
|
||||
- 연락처가 3,000개면 [가져오기]도 끄고 패널의 3,000개 안내를 까닭으로 쓴다. 가져오는 동안(되돌리기 포함)은 [가져오기]가 바쁜 표시(`aria-busy`)이고 눌러도 아무 일이 없다.
|
||||
- 빈 주소록 안내: [연락처 추가] 옆에 테두리 단추 [가져오기], 그 아래 "다른 메일이나 휴대폰에서 내보낸 연락처 파일이 있으면 [가져오기]로 한 번에 넣을 수 있습니다." + 지금 줄("메일을 읽다가 …").
|
||||
- **가져오기 창 ① 파일 고르기**(넓은 창 620): 제목 "연락처 가져오기" · "네이버, Google, Outlook 같은 메일이나 휴대폰에서 내보낸 연락처 파일을 그대로 넣을 수 있습니다."
|
||||
- 놓는 자리(168 · 회색 점선): 올리기 아이콘 · [파일 고르기](처음 초점 — .vcf · .csv · .xlsx 만 보이게 걸러 둔 파일 고르는 창) · "파일을 여기로 끌어다 놓아도 됩니다."
|
||||
- 창 어디에 끌어다 놓아도 받는다 — 끄는 동안 자리가 2px 청록 점선 · 옅은 청록 바탕, "놓으면 이 파일에서 연락처를 찾습니다". 창 밖 주소록 화면에 놓은 파일은 지금처럼 무시한다.
|
||||
- 읽는 동안: 도는 아이콘 · "파일을 읽는 중…" · 파일 이름. 그사이 [취소]는 읽기를 멈추고 창을 닫는다.
|
||||
화면 낭독기에는 창을 열 때부터 있는 상태 영역(`role=status` — 글만 바꾼다)이 같은 글을 읽는다(글과 함께 생기는 영역은 읽지 않기도 한다).
|
||||
읽다가 뜻밖에 멈추면(깨진 파일 등) "읽는 중"에 머물지 않고 "열지 못했습니다" 글로 돌아온다.
|
||||
- 아래 "vCard(.vcf), CSV(.csv), 엑셀(.xlsx) 파일을 넣을 수 있습니다. 파일 하나는 20MB까지입니다." · [취소].
|
||||
- 읽지 못하면 자리 아래 빨간 글로만(알림 없음) — 다른 파일을 고르거나 놓으면 지운다:
|
||||
- 넣을 수 없는 형식 "넣을 수 없는 파일입니다. vCard(.vcf), CSV(.csv), 엑셀(.xlsx) 파일을 골라 주세요."
|
||||
- 옛 엑셀 "옛 엑셀 파일(.xls)은 넣을 수 없습니다. 엑셀에서 [다른 이름으로 저장]을 눌러 .xlsx나 CSV 파일로 저장한 뒤 넣어 주세요."
|
||||
- 20MB 넘음 "파일이 너무 큽니다. 파일 하나는 20MB까지 넣을 수 있습니다."(읽지 않고 바로)
|
||||
- 연락처를 찾지 못함 "이 파일에서 연락처를 찾지 못했습니다. 이름이나 이메일이 든 열이 있는지 확인해 주세요."(vCard 는 앞 문장만)
|
||||
- 열지 못함 "이 파일을 열지 못했습니다. 암호를 건 엑셀 파일이면 암호를 푼 뒤 넣어 주세요."
|
||||
- 여러 파일 "파일은 한 번에 하나만 넣을 수 있습니다."
|
||||
- 형식은 이름 끝과 파일 앞부분을 함께 본다 — 이름이 .csv 여도 안이 엑셀이면 엑셀로, 안이 옛 엑셀(.xls)이면 .xls 글.
|
||||
- **가져오기 창 ② 미리 보기**: 파일 이름 + [다른 파일 고르기](파일 고르는 창을 바로 연다 — 고른 그룹과 체크 칸은 그대로) · "연락처 340명을 찾았습니다."(넣지 못하는 줄까지 센 수).
|
||||
- 세 묶음(테두리 상자 — 글과 숫자로만, 뱃지 없음):
|
||||
- "넣을 연락처 · 330명" — 앞 6명의 표(이름 64 · 이메일 · 전화 96 · 회사 · 그룹 76) + "그 밖에 324명".
|
||||
- "이미 있는 연락처는 건너뜁니다 · 8명" — 눌러 펼친다(`aria-expanded`). 줄마다 "4번째 줄 · 박지훈 jihoon.park@example.com · 주소록에 있음"(또는 "2번째 줄과 같음").
|
||||
- "넣지 못하는 줄 · 2개" — 눌러 펼친다. "57번째 줄 · 010-9034-6621 · 이름과 이메일이 모두 없습니다."(까닭은 갈색).
|
||||
- 펼친 목록은 6줄 높이(186)까지 늘고 넘으면 목록 안에서 넘긴다. 줄 번호는 엑셀에서 보이는 행 번호(머리 줄이 1번째 줄), vCard 는 "12번째 연락처"이고 묶음 이름이 "넣지 못하는 연락처"다.
|
||||
- 그룹: "파일에 적힌 그룹 4개 가운데 거래처, 세무는 이미 있는 그룹에 넣고 동호회, 학교는 새로 만듭니다."(이름이 많으면 앞 셋과 "외 N개", 없으면 "파일에 적힌 그룹이 없습니다.").
|
||||
- 「가져온 연락처를 모두 넣을 그룹」: 넣지 않음(처음) · 이미 있는 그룹(고르기 칸 280×44 — "거래처 · 4명") · 새 그룹(이름 칸 280×44 · 50자) + "파일에 적힌 그룹에 더해 넣습니다."
|
||||
새 그룹 이름이 이미 있는 그룹과 같으면 그 그룹에 넣는다. 이름을 비운 채 넣으려 하면 칸 아래 "그룹 이름을 넣어 주세요." 그룹이 100개면 「새 그룹」을 끄고 "그룹은 100개까지 만들 수 있습니다."
|
||||
- 체크 칸 「주소, 생일, 홈페이지, 별명처럼 주소록에 칸이 없는 정보는 메모 끝에 붙이기」(처음 켜짐) + "메모가 1,000자를 넘게 되면 붙이지 않습니다." —
|
||||
켜 두면 견본 "예: 강민지의 메모"(원래 메모는 흐리게 · 빈 줄 · "가져온 정보" · "회사 주소: 서울 중구 세종대로 110" · "생일: 1988년 3월 14일 (음력)" · "홈페이지: …" · "별명: …").
|
||||
- 남은 자리보다 많으면 노란 상자 "연락처는 3,000개까지 둘 수 있어 앞에서부터 188명만 넣습니다. 나머지 142명은 넣지 않습니다." — 단추의 수도 그만큼.
|
||||
- [연락처 330명 넣기](처음 초점) [취소]. 넣을 연락처가 없으면 [연락처 넣기]를 끄고(`aria-disabled`) 단추 왼쪽에 "넣을 연락처가 없습니다."
|
||||
- Esc · [취소]는 묻지 않고 닫는다(아직 아무것도 넣지 않았다). 창이 화면보다 길면 창 안이 스크롤하고 단추 줄은 고정하지 않는다(필터 창과 같다).
|
||||
- **넣는 동안 · 끝난 뒤**(알림 — 필터 적용 · 차단 정리와 같은 결): [연락처 N명 넣기]를 누르면 창을 닫고 진행 알림 "연락처를 넣는 중 · 120 / 330명"(넣은 수 / 넣을 수 — 아래 「요청 묶음」 하나가 끝날 때마다 고친다).
|
||||
- 끝: 같은 자리에서 "연락처 330명을 넣었습니다. 이미 있던 8명은 건너뛰었습니다." + [되돌리기] + ✕ — **✕로 닫을 때까지 남는다**(저절로 닫히지 않는다).
|
||||
건너뛴 수 · 한도에 걸린 수는 한 문장씩 덧붙인다 — "연락처는 3,000개까지 둘 수 있어 나머지 142명은 넣지 못했습니다." · "그룹은 100개, 그룹 하나에는 500명까지 둘 수 있어 24명은 그룹에 넣지 못했습니다."
|
||||
- [되돌리기]: "가져온 연락처를 지우는 중 · 100 / 330명" → "가져온 연락처 330명과 새로 만든 그룹 2개를 지웠습니다."(새 그룹이 없으면 "가져온 연락처 330명을 지웠습니다.").
|
||||
이번에 넣은 연락처와 이번에 새로 만든 그룹만 지운다 — 원래 있던 그룹에 넣은 것은 연락처를 지우며 함께 빠진다. 되돌리다 멈추면 "가져온 연락처 330명 가운데 100명을 지우고 멈췄습니다. 다시 시도해 주세요." + [다시 시도].
|
||||
- 멈춤: 흰 오류 알림 "연락처 330명 가운데 120명을 넣고 멈췄습니다. 다시 시도해 주세요." + [다시 시도](남은 연락처부터 — 줄마다 화면이 붙인 표시(uid)가 있어 두 번 들어가지 않는다),
|
||||
처음부터 넣지 못했으면 "연락처를 넣지 못했습니다. 다시 시도해 주세요." 멈춘 알림에는 [되돌리기]가 없다 — 다시 시도해 끝낸 뒤의 [되돌리기]가 이번에 넣은 연락처를 모두 지운다.
|
||||
알려진 한계: 멈춘 알림을 다시 시도하지 않고 닫으면 그때까지 넣은 연락처는 남는다(같은 파일을 다시 넣으면 이미 있는 연락처로 건너뛴다).
|
||||
- 넣는 동안 다른 화면으로 가도 이어 넣는다. 탭을 닫거나 로그아웃하면 멈추고 알림을 닫는다. 끝나거나 멈추면 연락처 수 · 그룹 목록 · 보고 있는 목록을 다시 받는다.
|
||||
실패 까닭(연결 · 서버)은 지금처럼 오류 사전의 글로 알리고 진행 알림은 결과로만 바뀐다.
|
||||
- **내보내기 창**(400 — 새 그룹 창과 같은 폭): 제목 "연락처 내보내기".
|
||||
- 「내보낼 연락처」 내 주소록 전체("연락처 342명" — 처음) · 그룹 하나("고른 그룹의 구성원만" — 고르면 그룹 고르기 칸 44, 첫 그룹이 골라진다, "거래처 · 24명") + "내 주소록의 연락처만 내보내고 사내 목록은 넣지 않습니다."
|
||||
- 「파일 형식」 vCard (.vcf) "다른 메일이나 휴대폰으로 옮길 때"(처음) · CSV (.csv) "엑셀에서 열어 볼 때". 고른 상자는 청록 테두리 · 옅은 청록 바탕.
|
||||
- "파일 이름 주소록-2026-11-10.vcf"(그룹이면 "주소록-거래처-2026-11-10.csv" — 날짜는 이 PC 의 날짜, 파일 이름에 쓸 수 없는 글자는 _).
|
||||
- 구성원이 없는 그룹을 고르면 [내보내기]를 끄고(`aria-disabled`) 그룹 칸 아래 "‘협력사 담당’ 그룹에는 아직 아무도 없어 내보낼 연락처가 없습니다." — 다른 그룹을 고르면 바로 돌아온다.
|
||||
- [내보내기] [취소] — 누르면 창을 닫고 바로 내려받는다(첨부 받기처럼 이동 직전에 로그인을 확인, 파일은 서버가 만든다). 따로 알림은 띄우지 않는다.
|
||||
- **받는 파일 · 칸 맞추기**: vCard 2.1 · 3.0 · 4.0(한 파일에 여러 명), CSV(네이버 · Google 새 · 옛 머리 · Outlook · 우리가 내보낸 꼴을 머리 줄로 알아보고, 모르는 꼴은 열 이름으로 짐작), 엑셀 .xlsx(첫 시트 · 첫 줄이 머리).
|
||||
CSV 글자는 BOM, UTF-8, 아니면 EUC-KR(한국어 엑셀의 CSV) 차례. 성 + 이름은 이름 하나로(한글끼리는 붙여 쓴다 — 강 + 민지 = 강민지), 이메일과 전화는 종류대로(팩스와 직접 입력한 번호는 기타),
|
||||
전화는 하이픈을 넣는다(010-1234-5678). 이미 있는 연락처는 같은 이메일(앞뒤 공백 · 대소문자 무시), 이메일이 없으면 이름과 전화번호가 모두 같을 때다.
|
||||
|
||||
### 설정 › 메일 › 서명 — 여러 서명 (`Signature16` 1~5)
|
||||
|
||||
서버 약속은 계약 2절 "서명"(16단계 — `/api/mail-signatures` · `/api/mail-signature-defaults`)이다. 글의 원본은 화면 상수(`shared/constants/mail-signature.ts`)이고 아래는 옮겨 적은 것이다.
|
||||
새 크기 토큰 · 밀도 예외는 없다 — 목록 줄 52 는 `--size-header-min`, 칸 · 단추 44 는 보통 크기다.
|
||||
|
||||
- **배치**: 머리(제목 "서명" · 설명) → 「메일에 넣을 서명」(서명이 있을 때만) → 왼쪽 「내 서명」 목록(208) · 오른쪽 고른 서명 고치기(이름 · 서명 칸 · 안내 · [저장] … [이 서명 지우기] · 가는 선 · 미리 보기). 콘텐츠 열은 다른 설정 화면과 같은 800.
|
||||
목록과 고치는 칸을 담은 칸이 640보다 좁으면 목록을 위로 올려 칸 폭을 쓰게 하고 고치는 칸을 그 아래에 둔다 — 창 폭이 아니라 그 칸의 폭을 본다(컨테이너 쿼리).
|
||||
설명은 "보내는 메일 끝에 넣는 글입니다. 여러 개 만들어 두면 메일을 쓸 때 [서명]에서 골라 바꿀 수 있고, 그 메일에서만 고치거나 지울 수도 있습니다."
|
||||
- **목록**: 만든 차례 · 줄 52 · 고른 줄은 메일함 패널의 지금 메일함처럼 청록 바탕 · 굵은 글자(aria-current). 이름 아래 회색 12 는 그 서명을 기본으로 넣는 메일("새 메일" · "답장과 전달" · "새 메일, 답장과 전달" — 뱃지 없음).
|
||||
목록 아래 [새 서명](글자 단추 44) · "서명 3개를 만들었고, 10개까지 만들 수 있습니다." — 10개면 [새 서명]은 누를 수 없고(aria-disabled — 초점은 받는다) 글이
|
||||
"서명은 10개까지 만들 수 있습니다. 쓰지 않는 서명을 지워 주세요."로 바뀐다(단추가 aria-describedby 로 가리킨다). 화면 낭독기는 목록을 「내 서명」으로, 고른 줄을 지금 항목으로 읽는다.
|
||||
- **처음 고른 서명**: 새 메일에 넣는 서명, 새 메일이 넣지 않음이면 첫 서명.
|
||||
- **메일에 넣을 서명**(지금의 두 체크를 대신한다): 칸 이름 84 · 고르기 240(서명 이름들 · 넣지 않음) · 아래 "메일을 쓰기 시작할 때 본문에 넣어 둡니다. 고르는 대로 바로 저장됩니다."
|
||||
고르는 대로 저장하고, 끝나면 고른 칸 오른쪽에 초록 ✓ "저장했습니다"(role=status) — 다시 고르면 내린다. 저장은 한 번에 하나(보내기 · 읽기 설정과 같은 결 — 보내는 동안 고른 것은 끝난 뒤 다르면 보낸다),
|
||||
끝내 저장하지 못하면 고르기 전 값으로 되돌리고 오류 알림. 고른 것은 나가기 확인의 까닭이 아니다.
|
||||
- **이름**: 칸 320(좁으면 칸 폭) · 30자에서 더 들어가지 않는다 · 아래 "메일을 쓸 때 [서명]에서 고르는 이름입니다. 30자까지 쓸 수 있습니다."
|
||||
[저장]할 때 비었으면(앞뒤 공백을 뺀다) "이름을 넣어 주세요.", 다른 서명과 같으면(영문 대소문자 무시) "같은 이름의 서명이 있습니다." — 안내 대신 빨간 글 · 칸 빨간 테두리(aria-invalid) · 이름 칸에 초점, 알림은 없다.
|
||||
이름을 고치면 안내로 돌아간다. 저장하면 이름은 앞뒤 공백을 뺀 글로 바뀐다.
|
||||
- **[저장] 뒤 글**: 기본으로 넣는 서명이면 지금 글 "저장했습니다. 다음에 메일을 쓸 때부터 들어갑니다.", 아니면 "저장했습니다." — 무엇이든 다시 바꾸면 내린다.
|
||||
- **편집기 · 그림 · 미리 보기**는 지금 서명 화면 그대로(그림 서명마다 3장 · 크기 줄 · 화면 어디에 놓은 그림은 지금 고른 서명에). 미리 보기는 고른 서명이 [새 메일 | 답장]에 들어갈 자리를 늘 보인다 —
|
||||
아래 글 "새 메일은 본문 끝에 서명을 넣습니다." · "답장과 전달에는 인용한 원문 위에 서명을 넣습니다."(그 메일에 넣을지는 「메일에 넣을 서명」이 정한다).
|
||||
- **서명이 없을 때**(2): 점선 상자 안 펜 아이콘 36 · "아직 서명이 없습니다" · "보내는 메일 끝에 넣을 이름, 소속, 연락처를 만들어 두면 메일을 쓸 때마다 본문에 넣어 둡니다. 사내용과 대외용처럼 여러 개를 만들어 골라 쓸 수도 있습니다."(최대 400) ·
|
||||
[새 서명](채운 단추). 「메일에 넣을 서명」 칸과 목록은 없다(고를 서명이 없다). 지금 서명을 쓰던 직원은 서버가 그 서명을 「기본 서명」으로 옮겨 두어 1의 모습으로 시작한다.
|
||||
- **새 서명**(3): 목록 끝에 "새 서명 / 저장하지 않음" 줄 · 빈 이름 칸(안 글 "예: 사내, 대외, 영문")에 초점 · 빈 서명 칸 · [저장] [취소]. 처음 [저장]에 만들고 그 뒤로는 같은 칸에서 고친다(쓰던 자리 · 실행 취소 기록이 남는다).
|
||||
첫 서명이면 두 곳의 기본이 그 서명이 된다(서버가 정한다). 그 밖의 새 서명은 기본을 바꾸지 않는다. [취소]는 새 줄을 빼고 전에 고른 서명을 연다(서명이 없었으면 2의 화면). 새 서명을 연 채 [새 서명]을 누르면 이름 칸으로 간다.
|
||||
- **지우기**(4): [이 서명 지우기](빨간 글자 단추 · [저장]과 떨어뜨려 오른쪽 끝) → 확인 창(폭 360 · 처음 초점 [취소] · Esc 도 [취소]) "‘대외’ 서명을 지울까요?" +
|
||||
기본이던 곳마다 다른 글(새 메일 · 답장과 전달 · 둘 다 · 기본 아님 "지운 서명은 되돌릴 수 없습니다.") · [지우기](빨강 채움) [취소].
|
||||
지우면 알림 "‘영문’ 서명을 지웠습니다." · 목록의 첫 서명을 연다(초점은 그 줄) — 마지막 서명이면 2의 화면(초점은 [새 서명]). 그 서명을 기본으로 넣던 곳은 넣지 않음이 되고 다른 서명이 저절로 기본이 되지 않는다.
|
||||
- **저장하지 않은 것이 있을 때**(5): 다른 서명을 고르면 "다른 서명을 고르면 ‘대외’ 서명에서 바꾼 내용이 사라집니다.", [새 서명]이면 "새 서명을 만들면 ‘대외’ 서명에서 바꾼 내용이 사라집니다."
|
||||
— 제목 "저장하지 않은 서명이 있습니다" · [계속 고치기](처음 초점 · Esc) [바꾼 내용 버리기]. 바꾼 것이 없으면 묻지 않는다.
|
||||
다른 화면으로 갈 때는 지금 나가기 확인 그대로("이 화면을 나가면 바꾼 서명이 사라집니다." · [계속 고치기] [나가기] — 저장하는 중 · 세션이 끝났을 때는 묻지 않고, 탭을 닫으면 브라우저가 묻는다).
|
||||
|
||||
### 쓰기 화면 [서명 ▾] (`Signature16` 6)
|
||||
|
||||
- **자리**: 아래 단추 줄 [보내기 ▾] [임시 저장] **[서명 ▾]** — 서명은 서식이 아니라 메일 전체에 붙는 것이라 편집기 툴바(좁은 폭에서 접는 차례)에 넣지 않았다. [임시 저장]과 같은 글자 단추(44)에 ▾ 12.
|
||||
화면 낭독은 툴바의 글꼴 단추처럼 지금 서명까지 — "서명: 대외" · "서명: 넣지 않음"(본문의 서명 자리가 어느 서명과도 같지 않으면 "서명"만). 편집기를 불러오는 중 · 보내는 중에는 누를 수 없다.
|
||||
- **메뉴**(편집기 메뉴 — 항목 40 · 폭 220, 긴 이름은 끝을 …): 서명(만든 차례 · 지금 본문에 든 서명에 ✓ · "기본" 표시 없음) · 넣지 않음 · 가는 선 · 서명 관리. 서명이 없으면 넣지 않음과 서명 관리만.
|
||||
키보드는 [보내기 ▾]와 같다 — Enter · Space · 아래 화살표로 열면 첫 항목에 초점(마우스로 열면 메뉴 자체), Esc 는 메뉴만 닫고 [서명 ▾]로. 아래 자리가 모자라면 위로 연다(아래 줄에서는 보통 위).
|
||||
- **고르면** 본문의 서명 자리만 그 서명으로 바뀐다 — 쓴 글 · 인용 · 첨부는 그대로이고, 서명 안을 고쳤다면 고친 것도 새 서명으로 바뀐다. 실행 취소(Ctrl+Z)로 되돌린다. 바꾸면 화면 낭독기에 "서명을 ‘사내’로 바꿨습니다."(알림 없음).
|
||||
넣지 않음은 서명 자리와 그 앞의 빈 줄을 뺀다. 서명 자리를 지운 뒤 서명을 고르면 처음 넣는 자리(새 메일은 쓴 글 끝, 답장과 전달은 인용 위)에 빈 줄 하나 뒤로 넣는다.
|
||||
- **처음 넣는 서명**: 새 메일은 「새 메일」 기본, 답장 · 전달은 「답장과 전달」 기본(넣지 않음이면 넣지 않는다). 이어 쓰는 임시 저장본에는 다시 넣지 않는다 — 서명 자리의 글이 저장한 서명과 같으면 그 서명에 ✓, 다르면 ✓ 없이.
|
||||
- **서명 관리**: 설정 › 메일 › 서명으로 — 지금 나가기 규칙대로 먼저 임시 저장하고 저장하지 못했을 때만 묻는다.
|
||||
- 받는 사람이 회사 밖이어도 저절로 바꾸지 않는다(처음 판 — 직원이 고르는 대로).
|
||||
|
||||
### 설정 › 메일 › 부재중 자동 응답 — 서식 본문 · 회사 밖 응답 (`Settings16` 1~4)
|
||||
|
||||
- 사용 · 기간 · 제목 칸 · [저장] · 저장한 뒤의 글은 4단계 그대로다.
|
||||
- **내용 칸은 서명 칸과 같은 편집기**(지연 로딩 — 못 받으면 서명 설정과 같은 사전 문구 + [다시 시도]): 툴바 52 · 굵게 · 기울임 · 밑줄 | 글자 색 · 링크(단추 40×44 — 그림 단추 없음, 붙여 넣은 그림 · 파일은 받지 않는다) ·
|
||||
본문 맑은 고딕 14 · 줄 간격 1.75 · 높이 150. 툴바의 화면 낭독 이름 "자동 응답 서식". 칸 아래 안내 "굵게, 기울임, 밑줄, 글자 색, 링크를 쓸 수 있습니다. 그림은 넣을 수 없고, 서명은 붙지 않습니다."
|
||||
- 글자 수는 칸 이름 오른쪽 "132 / 600"(12 · 회색 — 주소록 메모와 같은 자리). 서식을 뺀 보이는 글로 센다(받는 쪽의 글자 본문과 같은 글 — 링크는 "글 (주소)"). 켜 둔 채 600자를 넘으면 굵게 빨갛게.
|
||||
- 서식(굵게 · 기울임 · 밑줄 · 글자 색 · 링크)을 쓴 칸만 받는 사람에게 서식 본문과 글자 본문을 함께 보내고, 글만 쓴 칸은 글자 본문만 보낸다(서식 본문 null — 사내 · 회사 밖 칸 따로, 끄면서 하는 저장도 같다).
|
||||
서버는 서식 본문 · 회사 밖 응답을 쓴 직원만 새 원본 판으로 저장한다 — 글만 쓰는 직원은 옛 판 그대로라 되돌리기 계획의 전제가 지켜진다(16단계 리뷰).
|
||||
16단계 전에 저장한 글만 있는 응답은 줄마다 문단으로 열린다(빈 줄은 빈 문단).
|
||||
- **회사 밖에서 온 메일**(내용 칸과 [저장] 사이 · fieldset): "보낸 사람 주소가 회사 메일 주소(@arasystem.it.kr)가 아닌 메일입니다."(도메인은 내 주소의 것) + 고르기 셋 —
|
||||
보내기 · 읽기 설정의 외부 이미지 칸과 같은 모양(라디오 18 · 이름 굵게 · 아래 설명 12.5 · 줄 44):
|
||||
사내와 같은 응답("회사 안팎 모두 위의 제목과 내용으로 답합니다." — 처음 값) · 보내지 않기("회사 밖에서 온 메일에는 자동으로 답하지 않습니다.") · 따로 쓰기("회사 밖에서 온 메일에 보낼 제목과 내용을 따로 씁니다.").
|
||||
칸이 열리는 따로 쓰기가 맨 끝이다.
|
||||
- 따로 쓰기를 고르면 바로 아래(고르기 글자 선 28 에 맞춰 들여 씀 · 사이 18)에 "회사 밖에 보낼 제목"(최대 폭 520 · 150자) · "회사 밖에 보낼 내용"(같은 편집기 · 툴바 이름 "회사 밖 자동 응답 서식" · 같은 글자 수 · 아래 안내 없음).
|
||||
위의 제목 · 내용은 이때부터 사내에서 온 메일에만 간다.
|
||||
- 다른 고르기로 바꾸면 두 칸을 닫지만 써 둔 글은 남겨 두고 함께 저장한다 — 다시 따로 쓰기를 고르면 그대로 나온다.
|
||||
- 자동 응답을 끄면 이 묶음도 흐려진다(고르기 글 회색 · 칸은 다른 칸처럼).
|
||||
- **미리 보기**: 머리 오른쪽 전환 [사내 | 회사 밖](36 · 화면 낭독 이름 "미리 볼 답장" — 처음은 사내 쪽, 사내와 같은 응답일 때도 둔다). 칸을 고친 값을 받는 사람이 보는 서식 그대로 바로 그린다.
|
||||
아래 글: 사내와 같은 응답 — 사내 쪽 "받은 메일의 보낸 사람에게 위 글로 답합니다. 같은 사람에게는 매번 보내지 않습니다."(지금 글) · 회사 밖 쪽 "회사 밖에서 온 메일에도 같은 글로 답합니다. …" /
|
||||
보내지 않기 · 따로 쓰기 — 사내 쪽 "사내에서 온 메일에 위 글로 답합니다. …" · 따로 쓰기의 회사 밖 쪽 "회사 밖에서 온 메일에 위 글로 답합니다. …"(제목 · 내용은 회사 밖에 보낼 것) /
|
||||
보내지 않기의 회사 밖 쪽은 답장 대신 회색 상자(최소 높이 120) "회사 밖에서 온 메일에는 자동으로 답하지 않습니다." · 아래 글 없음 / 꺼 두었으면 두 쪽 모두 "꺼져 있어 자동으로 답하지 않습니다."
|
||||
- **켤 때 검사**(켠 채 [저장]일 때만 — 꺼 둔 채로는 빈 칸 · 긴 글도 저장한다): 보는 차례 기간 · 제목 · 내용 · 회사 밖에 보낼 제목 · 회사 밖에 보낼 내용, 첫 칸으로 초점. 알림은 띄우지 않는다.
|
||||
- 빈 칸은 사전 문구(`VAL_001`). 내용이 600자를 넘으면 칸 아래 "내용은 600자까지 쓸 수 있습니다." + 빨간 글자 수(회사 밖에 보낼 내용 칸도 같은 글).
|
||||
- 글은 600자 안인데 서식이 많아 서식 본문이 10,000자를 넘으면 사전 문구(`VAL_001` — 서명과 같은 한도).
|
||||
- 편집기 칸은 빨간 테두리와 aria-invalid(설명은 오류 → 안내 차례). 그 칸을 고치기 시작하면 아래 글이 사라진다.
|
||||
- 회사 밖 두 칸은 따로 쓰기일 때만 본다 — 사내와 같은 응답 · 보내지 않기면 비어 있어도 저장한다. 서버가 거절하면 지금처럼 [저장] 옆에 사전 문구.
|
||||
- 서버가 `MAIL_013`(필터 · 차단 · 부재중을 담는 메일 규칙이 한도를 넘음)으로 거절하면 같은 자리에 "자동 응답이 너무 깁니다. 서식이나 내용을 줄여 주세요."(알림 없음) —
|
||||
부재중은 서식 본문이 두 번 들어가 필터가 없어도 넘을 수 있어 사전 문구("필터나 차단 목록이 …") 대신 이 글이다(16단계 리뷰). 저장하는 사이 화면을 떠났으면 같은 글을 알림으로.
|
||||
|
||||
### 설정 › 계정 › 2단계 인증 — 휴대폰 바꾸기 (`Settings16` 5~8)
|
||||
|
||||
- **켜져 있을 때** 상태 상자의 [2단계 인증 끄기] 바로 앞에 [휴대폰 바꾸기](테두리 44 · 아이콘 없음). 회사 정책으로 끌 수 없는 직원도 "회사 정책에 따라 끌 수 없습니다" 앞 같은 자리에 있다. 꺼져 있을 때는 없다(켜기 창으로).
|
||||
아래 안내의 첫 줄은 두 직원 모두 "휴대폰을 바꿀 때는 [휴대폰 바꾸기]를 눌러 주세요. 새 휴대폰과 지금 휴대폰의 코드가 모두 필요합니다."(둘째 · 셋째 줄은 그대로).
|
||||
- **바꾸기 창**(모달 620 — 켜기 창과 같은 폭) "2단계 인증 휴대폰 바꾸기" · 제목 아래 차례 "새 휴대폰 등록 › 지금 휴대폰으로 확인"(12.5 · 글로만 — 지금 걸음 청록 굵게 · 마친 걸음 앞 초록 체크 · 남은 걸음 회색, 뱃지 · 동그라미 숫자 없음).
|
||||
- **① 새 휴대폰 등록**: "새 휴대폰을 등록하고 지금 휴대폰으로 확인하면 바뀝니다. 바꾸기를 마칠 때까지는 지금 휴대폰의 코드로 로그인합니다." →
|
||||
1 "새 휴대폰의 인증 앱으로 QR 코드를 찍어 주세요"(QR 160 · 키 4자씩 두 줄 · [복사] 36 — 켜기 창과 같은 부품) + "바꾸지 않고 닫으면 이 키는 더 쓸 수 없습니다. 새 휴대폰의 인증 앱에 먼저 추가했다면 앱에서 그 계정을 지워 주세요." /
|
||||
2 "새 휴대폰의 앱에 표시된 6자리 코드" → [다음] [취소]. 처음 초점은 코드 칸. 키는 창을 열 때마다 새로 만들고 QR · 키는 이 창 안에만 두었다가 닫으면 지운다.
|
||||
[다음]은 새 휴대폰의 코드부터 확인한다(확인하는 동안 "확인하는 중…") — 맞으면 ②. 그동안 2단계 인증은 지금 휴대폰으로 그대로 켜져 있다.
|
||||
- **② 지금 휴대폰으로 확인**: "새 휴대폰을 확인했습니다. 지금 휴대폰의 코드와 비밀번호를 넣으면 바뀌고, 그 뒤로는 새 휴대폰의 코드로만 로그인합니다." →
|
||||
"지금 휴대폰의 앱에 표시된 6자리 코드"(처음 초점) · "지금 비밀번호"(최대 320 · 보기 단추) → 위에 선을 둔 문의 줄 "지금 휴대폰을 쓸 수 없나요? 관리자에게 2단계 인증을 풀어 달라고 요청한 뒤, 새 휴대폰으로 다시 켜 주세요."
|
||||
(로그인 화면의 문의 줄과 같은 꼴 — 연락처가 있으면 다음 줄에) → [이전](왼쪽 · ‹) · [바꾸기] [취소]. 바꾸는 동안 "바꾸는 중…" · [바꾸기] · [이전] aria-disabled.
|
||||
[이전]은 ①로(QR · 키는 그대로 · 새 휴대폰의 코드 칸은 비운다 — 같은 코드는 2분 동안 다시 받지 않으니 앱의 다음 코드를 넣는다).
|
||||
- **틀렸을 때**(알림 없이 그 칸 아래 사전 문구 · 그 칸에 초점 · 틀린 코드는 지운다): ① 새 휴대폰의 코드 `AUTH_006` / ② 지금 비밀번호 `AUTH_005` · 지금 휴대폰의 코드 `AUTH_006`(서버가 비밀번호를 먼저 보고 한 칸만 알린다).
|
||||
보내기 전에 6자리가 아니면 `AUTH_007` 문구, 비밀번호가 비었으면 `VAL_001` 문구(칸 차례 — 코드 → 비밀번호). 시도가 너무 많으면 `REQ_001` 알림.
|
||||
- **10분이 지남 · 더 새 등록**(`AUTH_008` — ① · ② 어느 쪽이든): ①로 돌아와 창 위 빨간 상자에 사전 문구, 새 QR · 키로 다시 그리고 코드 칸을 비운다. 그사이 다른 곳에서 2단계 인증이 꺼졌으면 창을 닫고 사전 문구 알림(화면은 「꺼짐」).
|
||||
- 세션이 바뀌면(로그아웃 · 다른 기기의 [다른 곳 모두 로그아웃]) 키 · 칸을 지우고 창을 닫는다(켜기 창과 같다).
|
||||
- **바꾼 뒤**: 창을 닫고 단추 없는 알림 "2단계 인증 휴대폰을 바꿨습니다. 이제 새 휴대폰의 코드로 로그인합니다." · 초점은 [휴대폰 바꾸기]. 화면 · 켠 때(「2026년 10월 31일부터 켜짐」)는 그대로다.
|
||||
[취소] · Esc 로 닫아도 서버가 이미 바꿨으면 화면을 맞추고 같은 알림을 띄운다. 로그인된 기기는 끊지 않고 알림 메일도 보내지 않는다(켜기 · 끄기와 같다).
|
||||
|
||||
### 처음 판에서 미룬 것
|
||||
|
||||
주소록: 칸 늘리기(주소 · 생일 · 홈페이지 · 별명 · 사진), 겹친 연락처 합치기, 모르는 CSV 의 열을 손으로 맞추기, 옛 엑셀(.xls), 여러 연락처 고르기 · 한꺼번에 지우기, 사내 목록 내보내기.
|
||||
서명: 받는 사람에 따라 저절로 바꾸기, 휴대폰 앱에서 서명 고르기. 부재중: 「내 연락처에게만」, 그림 넣기. 2단계 인증: 복구 코드, 바꿀 때 알림 메일 · 계정 변경 기록.
|
||||
|
||||
## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다)
|
||||
|
||||
- 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다).
|
||||
@@ -1754,3 +1916,86 @@ PC 에서는 설정 › 계정 › 휴대폰 앱(A)이 QR 코드로 휴대폰에
|
||||
- 받는 사이 화면 밖으로 나갔으면 읽음으로 바꾸지 않고, 다시 들어올 때 읽음이다(읽음이 실패해도 드나들 때마다 다시 묻지 않는다).
|
||||
- 화면에 들어왔는지 알 수 없는 브라우저(IntersectionObserver 없음)는 펼친 줄이 생기면 들어온 것으로 본다 — 그래도 하나씩 받는다.
|
||||
- 펼칠 줄이 없으면(지금 메일만 받고 나머지를 받는 중) 단추가 없다. 그사이 지워진 메일(본문이 404)의 줄에는 머리 [답장]도 없다(아래 [답장]과 같다).
|
||||
|
||||
### 16단계 구현하며 정한 보완 — 주소록 (2026-11-10 — 캔버스에는 아직 없다)
|
||||
|
||||
- **파일은 이 브라우저에서 읽는다**(`frontend/src/shared/utils/contact-import/` — 순수 함수 · 새 의존성 없음): 글자 `text.ts` · CSV `csv.ts` · vCard `vcard.ts` · zip `zip.ts` · 엑셀 `xlsx.ts` · 머리 줄 → 칸 `table.ts` ·
|
||||
값 다듬기 `values.ts` · 넣을 · 이미 있는 · 넣지 못하는 `records.ts` · 요청 묶음 `batches.ts` · 형식 가르기 `read-file.ts`.
|
||||
모두 입력 길이 · 줄 수에 비례하게 읽는다(2026-11-10 리뷰 — 조각은 배열에 모아 한 번에 잇고, 큰 배열을 `push(...)` 로 펼쳐 넘기지 않고, 구분자 둘레 빈칸은 정규식이 아니라 나눈 뒤 trim).
|
||||
- 엑셀은 zip 가운데 목록 → `DecompressionStream('deflate-raw')` → `DOMParser`(공유 문자열 · 첫 시트 · 칸 안 문자열). 첫 시트는 workbook 의 첫 `<sheet>`(관계 파일로 시트 파일을 찾는다).
|
||||
푼 크기가 항목 하나 64MB · 모두 96MB 를 넘는 순간 멈춘다(압축 폭탄 — 목록에 적힌 크기는 믿지 않는다). DOCTYPE · ENTITY 가 든 XML · 암호를 건 항목 · ZIP64 는 "열지 못했습니다".
|
||||
열은 XFD(16,384번째)까지만 읽고 넘는 칸은 버린다. 늘어놓는 칸 수(사이의 빈 칸 포함)의 합이 400만을 넘으면 "열지 못했습니다" — 먼 열에 칸 하나씩 둔 줄이 많으면 줄마다 빈 칸 16,384개로 부풀기 때문이다.
|
||||
- 옛 오피스 파일(CFB)은 안의 목록 이름으로 가른다 — 암호를 건 .xlsx(EncryptedPackage)는 "열지 못했습니다", Workbook · Book 이 있으면 .xls 글, 그 밖은 "넣을 수 없는 파일".
|
||||
- 2.1 QUOTED-PRINTABLE 은 줄 끝 "=" 로 접힌 줄을 먼저 바이트로 이은 뒤 CHARSET 대로 푼다(삼성 안드로이드는 한글 한 글자를 두 줄에 나눈다). 아이폰의 묶음 라벨(`item1.X-ABLabel`)로 종류를 정한다.
|
||||
사진 · 로고 · 소리 · 열쇠(PHOTO · LOGO · SOUND · KEY)는 이어지는 줄까지 모으지 않고 건너뛴다(사진 든 vCard 는 이어지는 줄이 수만 개다).
|
||||
- 'cp949' 라벨은 쓰지 않는다(RangeError) — EUC-KR 라벨이 브라우저에서 CP949 전체다. Node(ICU)의 euc-kr 은 CP949 확장 글자(똠 등)를 모르므로 시험은 KS X 1001 글자만 쓴다.
|
||||
- **머리 줄 알아보기**: 머리 이름은 공백 · 기호를 빼고 소문자로 견준다("E-mail 1 - Value" → email1value, "회사·소속명" → 회사소속명).
|
||||
네이버 열 이름의 띄어쓰기 · 괄호가 조금 달라도 낱말(휴대폰 · 회사 · 집 · Fax · 직접입력 · 생일 · 양력음력 · 기념일 · 우편번호 · 주소 · 메신저 …)로 알아본다.
|
||||
"번호" 한 낱말 열은 전화로 보지 않는다(일련번호 열이 흔하다 — 네이버의 회사번호 · 집번호 · 기타번호는 따로 안다). Outlook 의 "Title"은 경칭(Mr. · Ms.)이라 쓰지 않고 "Job Title"이 직책이다.
|
||||
네이버 CSV 처럼 값 대부분이 앞에 '를 달았으면 모두 떼고, 그 밖은 엑셀 수식 막기로 붙인 '(뒤가 = + - @)만 뗀다.
|
||||
- **이름**: 성이 한글 · 한자 · 가나면 성 다음 이름(한글끼리는 붙여 쓴다), 그 밖은 이름 · 중간 이름 · 성 차례로 띄운다(영문 "John Smith").
|
||||
- **종류**: CSV 의 종류를 모르는 이메일 칸은 회사(연락처 추가 창의 처음 값과 같다), vCard 의 종류가 없는 EMAIL 은 기타(우리 내보내기가 기타를 INTERNET 만으로 쓰므로 다시 가져와도 같다).
|
||||
종류를 모르는 전화 칸(전화번호 · 연락처 · Google 의 빈 라벨)은 번호로 짐작한다 — 국내 휴대전화(01x)면 휴대전화, 아니면 기타. 엑셀이 숫자로 바꿔 앞의 0 이 빠진 휴대전화(1012345678)는 0 을 되살린다.
|
||||
- **넣지 못하는 줄의 까닭** 차례는 이메일 형식 → 이름과 이메일이 모두 없음 → 한 줄 칸 100자 → 메모 1,000자. 한 줄 칸은 어느 칸인지를 앞에 붙인다("이름은 100자까지 쓸 수 있습니다." —
|
||||
연락처 창은 칸 아래라 칸 이름이 없지만 목록에서는 가를 수 없어서). 이메일은 연락처 창과 같이 서버보다 좁게 본다. 한 연락처 안의 같은 이메일은 하나만, 이메일 · 전화는 20개까지.
|
||||
- **형식에 맞지 않는 전화번호**(내선 · 글자가 든 번호 — "02-555-0147 내선 3")는 버리지 않고 가져온 정보의 "전화: …" 줄로 둔다(체크 칸을 끄면 넣지 않는다). 전각 숫자 · 여러 가지 줄표는 보통 글자로 고친다.
|
||||
- **가져온 정보 줄**: "회사 주소 · 집 주소 · 기타 주소 · 주소(네이버 주소직접입력은 그 이름을 앞에) · 생일 · 기념일 · 홈페이지 · 별명 · 메신저 · 전화". 날짜는 "1988년 3월 14일"(해를 모르면 "3월 14일", 음력이면 뒤에 " (음력)"),
|
||||
한글 주소는 큰 곳부터 띄어 쓰고 영문은 작은 곳부터 쉼표로, 우편번호는 뒤에 괄호로, 우리나라 이름은 쓰지 않는다.
|
||||
값은 한 줄 글로 다듬는다(탭 · 줄바꿈 · 제어 문자는 띄어쓰기 — vCard 별명 · 형식이 틀린 전화 등), 붙인 메모는 연락처 창과 같은 메모 정리를 한 번 더 지난다 —
|
||||
서버는 메모의 줄바꿈 밖 제어 문자를 받지 않아 하나라도 남으면 그 묶음 전체가 400 이다.
|
||||
- **미리 보기**: 빈 묶음(이미 있는 연락처 0 · 넣지 못하는 줄 0)은 두지 않는다. 넣을 연락처가 없으면 표 · 그룹 · 메모 칸도 두지 않는다. 칸이 없는 정보가 든 연락처가 없으면 메모 체크 칸을 두지 않는다.
|
||||
그룹 글은 이미 있는 그룹만이면 "파일에 적힌 그룹 거래처, 세무는 이미 있는 그룹에 넣습니다.", 새로 만들 그룹만이면 "파일에 적힌 그룹 동호회, 학교는 새로 만듭니다."(보드는 섞인 꼴만).
|
||||
vCard 의 넣지 못하는 수는 "N명". 그룹이 하나도 없으면 「이미 있는 그룹」을 끈다(`aria-disabled`).
|
||||
- **겹침 열쇠는 서버와 같다**(짝: backend `modules/contacts/contact-import.ts` 의 `phoneDigitsKey` · `namePhonesKey`): 이메일이 없으면 이름(앞뒤 공백만 뺌) + 전화 숫자 모음 —
|
||||
번호마다 숫자만 남기고, 숫자가 없는 번호는 빼고, 겹침을 없앤 뒤 차례로 잇는다(같은 번호를 두 번 적은 줄도 한 번 적은 줄과 같은 사람이다).
|
||||
- **요청 묶음**: 확인 · 넣기 · 되돌리기 본문은 100명 · JSON 값 8,000개 · 700KB 가운데 먼저 닿는 데서 끊는다(`batches.ts` — 값은 서버 `json-shape.ts` 와 같은 셈, 바이트는 UTF-8 길이).
|
||||
칸을 한도까지 채운 연락처(이메일 · 전화 20개씩) 100명은 값 13,000개 남짓이라 61명 · 39명으로 나뉜다 — 서버 본문 한도(값 10,000 · 1MB)를 넘지 않게.
|
||||
- **이미 있는지 묻기**: 넣을 수 있는 연락처만 위 묶음으로 서버에 묻는다(`POST /contact-import-checks`). 남은 자리를 다 채우면 더 묻지 않는다(요청 수 한도를 아끼려고) —
|
||||
그래서 한도에 걸린 파일이면 묻지 않은 연락처 가운데 이미 있는 것도 "넣을 연락처"와 "나머지 N명"에 세진다(넣지 않으니 결과는 같다). 묻기가 실패하면 사전 문구 알림 뒤 파일 고르기로 돌아간다.
|
||||
- **넣기**: 새 그룹을 먼저 만든다 — 그사이 같은 이름이 생겼으면(`CONTACT_001`) 그 그룹을 쓰고, 100개면(`CONTACT_002`) 그 그룹 없이 넣는다(결과의 "그룹에 넣지 못했습니다"에 센다).
|
||||
만들기의 응답만 잃은 그룹(응답 없음 · 5xx — 서버에서는 만들어졌을 수 있다)이 [다시 시도]에서 `CONTACT_001` 이면 이번에 만든 그룹으로 센다([되돌리기]가 지운다).
|
||||
응답을 잃은 적 없이 `CONTACT_001` 인 그룹(다른 창 · 기기가 만든 그룹)은 넣기만 하고 [되돌리기]는 지우지 않는다.
|
||||
넣는 사이 주소록이 찼으면(409 `CONTACT_002`) 남은 자리를 다시 물어 그만큼만 넣고 나머지는 "넣지 못했습니다", 넣을 그룹이 그사이 지워졌으면(404 `RES_001`) 그룹을 다시 받아 그 그룹을 빼고 한 번 더 보낸다.
|
||||
메일 서버가 일부만 받으면 결과에 "N명은 넣지 못했습니다."를 덧붙인다(보드에 없는 글).
|
||||
- **멈춘 알림도 ✕까지 남는다**: 보드는 결과([되돌리기]) 알림만 적었지만, 멈춘 오류 알림의 [다시 시도]가 남은 연락처를 넣고 [되돌리기]를 받는 유일한 길이라 같이 남긴다(되돌리다 멈춘 알림도).
|
||||
알림은 넘쳐도 다른 알림부터 닫는다. 결과 알림 위에 다른 결과가 쌓이면 셋까지 보인다.
|
||||
- **차례**: 한 탭에서 가져오기 · 되돌리기는 하나씩 돈다. 도는 동안 누른 [되돌리기] · [다시 시도]는 버리지 않고 끝난 뒤에 한다 — 누른 뒤 세션이 바뀌었으면 하지 않는다(다른 직원의 주소록을 건드리지 않게).
|
||||
- **내보내기**: 처음 초점은 [내보내기](내 주소록 전체 · vCard 를 바로 받게), 그룹이 하나도 없으면 「그룹 하나」를 고를 수 없다(`disabled`). 화면의 파일 이름 날짜는 이 PC 의 날짜이고
|
||||
서버가 붙이는 이름은 한국 시간이다 — 자정 무렵 바깥 시간대의 PC 에서는 하루 다를 수 있다(알려진 한계).
|
||||
- **알림 부품**: ✕로 닫을 때까지 남는 알림은 `toast.store` 의 `sticky`(진행 알림의 `succeed` · `fail` 옵션 — `useToast`). 성공 알림에도 ✕ 를 단다(브랜드 바탕용 흐린 청록, 마우스를 올리면 흰 글자).
|
||||
지금 쓰는 알림의 시간 · 모양은 그대로다.
|
||||
|
||||
### 16단계 구현하며 정한 보완 — 서명 (2026-11-10 — 캔버스에는 아직 없다)
|
||||
|
||||
- **서명 자리**(`components/mail/editor/mail-signature-block.ts`): 편집기 안에서는 블록 노드 하나(`mailSignature` — 안은 서명의 문단)이고, 보낼 때 · 저장할 때 빈 문단 뒤의 **속성 없는 `<div>`** 로 나간다 —
|
||||
앱과 같은 경계(계약 2절 "앱이 만드는 모양")라 웹 · 앱 어느 쪽 저장본을 다시 열어도 그 자리를 찾는다. 안의 문단은 다른 문단처럼 `margin: 0` 이 붙는다.
|
||||
다시 열 때는 "맨 위 단계(문서 · 하나뿐인 감싸는 글꼴 div 바로 아래)에서 빈 문단 바로 뒤의 속성 없는 div 이고 글이나 그림이 있다"로 찾는다 — 원문 인용 안 · 꾸민 div · 빈 div(인용 경계 표시가 남은 것)는 서명 자리가 아니다.
|
||||
쓰기 화면이 처음 넣는 서명은 표시(`data-mail-signature` — 서버 정화가 지운다)로 안다.
|
||||
- **서명 앞의 빈 줄은 늘 둔다**(2026-11-10 리뷰 M3 — 빈 줄에 글을 쓰거나 지운 채 저장하면 다시 열 때 서명 자리를 잃었다): 그 빈 줄에 글을 쓰면 쓴 글 아래 · 서명 위에 새 빈 줄이 생기고,
|
||||
Backspace · Delete 로 빈 줄을 지우면 다시 생긴다(커서는 쓰던 자리 · 윗줄 끝, 실행 취소는 그 입력과 함께 — 쓴 글은 지우지 않는다).
|
||||
한글을 조합하는 중에는 미뤘다가 다음 입력에서 넣고, 보내고 저장하는 모양도 빠진 빈 줄을 한 번 더 넣는다.
|
||||
알려진 한계: 16단계 앞에 저장한 본문(서명이 문단으로만 들어 있다)은 서명 자리를 찾지 못한다 — 그 서명은 글로 남고, [서명 ▾]는 ✓ 없이 열리며 고르면 처음 넣는 자리에 새로 넣는다.
|
||||
- **서명 자리는 하나**: 서명 안에서 복사해 붙여 넣으면 편집기가 서명 자리째 붙이는데, 그때 처음부터 있던 것만 두고 나머지는 글로 푼다(쓰기 화면의 편집기에서만 — 붙여 넣기 · 끌어 놓기 모두).
|
||||
- **어느 서명이 들었나**: 서명 자리의 글(공백 · nbsp 를 하나로 줄여)과 그림 주소를 저장한 서명(편집기로 읽은 모양)과 견준다 — 만든 차례로 먼저 맞는 서명에 ✓. 본문이 바뀔 때마다 다시 잰다(서명 안을 고치면 ✓ 가 빠진다).
|
||||
- **글(보드에 없어 지었다)**: 넣지 않음을 고른 뒤 낭독은 같은 틀 "서명을 ‘넣지 않음’으로 바꿨습니다." — 방향 조사는 이름 끝소리로 고른다("‘영문’으로" · 영문 이름은 "(으)로").
|
||||
새 서명(저장하지 않은 줄)을 고치다 다른 서명을 고르면 "다른 서명을 고르면 새 서명에 쓴 내용이 사라집니다.", 쓴 것이 있을 때의 [취소]는 "취소하면 새 서명에 쓴 내용이 사라집니다."("‘새 서명’ 서명에서"가 되지 않게).
|
||||
- **서버 거절**: 저장이 400 `VAL_001` 이면 목록을 조용히 다시 받아 그사이 다른 창에서 같은 이름을 만들었으면 이름 칸의 빨간 글, 아니면 서명 칸 아래 사전 문구(본문 길이 · 그림 3장 등 — 지금과 같다).
|
||||
지우려던 서명이 이미 없으면(404) 지운 것으로 본다. 고치던 서명이 다시 받은 목록에서 사라졌으면(다른 창에서 지움) 쓰던 것은 두고 새 서명으로 본다([저장]하면 새로 만든다).
|
||||
한 사람이 보관하는 그림 20장을 넘는 올리기 거절(`VAL_001`)은 지금처럼 알림이다.
|
||||
- **쓰기 화면이 서명을 못 받았으면** 서명 없이 열고 알리지 않는다(지금과 같다). [서명 ▾]를 열 때 다시 받아 본다.
|
||||
- **공용 편집기**(`SignatureEditor` — 부재중 자동 응답의 내용 칸이 함께 쓴다): prop `bodyId`(편집 영역 id · 툴바 aria-controls) · `toolbarLabel`(툴바 이름) · `allowImages`(false 면 그림 단추 · 그림 노드가 없고 붙여 넣거나 끌어 놓은 파일은 받지 않는다) ·
|
||||
`invalid`(aria-invalid · 빨간 테두리) · `disabled`(읽기만 · 툴바 끔 · 흐리게). 편집 영역 최소 높이는 CSS 변수 `--signature-editor-min-height`(기본 132), 보이는 글은 `getText()`. 기본값은 서명 칸 그대로다.
|
||||
|
||||
### 16단계 구현하며 정한 보완 — 부재중 · 휴대폰 바꾸기 (2026-11-10 — 캔버스에는 아직 없다)
|
||||
|
||||
- **휴대폰을 바꿨는지 모를 때**(서버가 503 `MAIL_001` — 엔진이 새 비밀을 가려 다시 읽어도 가를 수 없다 · 시간 초과 · 연결 끊김 · 앞단 5xx 의 `SYS_001`): 바꿨다고도 못 바꿨다고도 하지 않는다(보드에 없는 새 글).
|
||||
사전 문구 알림은 끄고 창은 ② 그대로 · 지금 휴대폰의 코드 칸만 비운 채 창 위 빨간 상자에
|
||||
"휴대폰을 바꿨는지 확인하지 못했습니다. 지금 휴대폰에 다음 코드가 뜨면 넣고 [바꾸기]를 다시 눌러 주세요. 코드가 맞지 않다고 나오면 이미 바뀌었을 수 있으니, 다음에 로그인할 때 새 휴대폰의 코드를 넣어 주세요." —
|
||||
이미 바뀌었으면 지금 휴대폰의 코드는 막혀 [바꾸기]가 `AUTH_006` 으로 거절하므로, 그 상자를 남겨 둔 채 칸 아래 글이 함께 보인다.
|
||||
창을 닫은 뒤에 왔으면 알림 "휴대폰을 바꿨는지 확인하지 못했습니다. 다음에 로그인할 때 새 휴대폰의 코드가 맞지 않으면 지금 휴대폰의 코드를 넣어 주세요."
|
||||
- 닫은 창에 늦게 온 거절(지금 비밀번호 · 지금 코드)은 칸 대신 사전 문구 알림, 다시 연 창에는 넣지 않는다(켜기 · 끄기 창과 같다). ①의 늦은 확인 결과는 버린다(엔진을 바꾸지 않는다).
|
||||
- 켜기 창과 바꾸기 창이 한 화면에 함께 있어 키 이름표 id 가 겹치지 않게 QR · 키 부품(`TwoFactorEnrollmentScan`)이 머리 글 · 닫을 때의 안내 · 키 이름표 id 를 받는다.
|
||||
- 부재중 내용의 글자 수는 켜 둔 동안 600을 넘는 순간부터 빨갛게 보인다(보드는 [저장]을 누른 모습만 — 칸 아래 글은 지금처럼 [저장]을 눌렀을 때만).
|
||||
꺼 둔 채로는 넘어도 회색이다(켤 때만 센다).
|
||||
- 부재중 양식은 편집기와 함께 지연 로딩한다 — 청크를 받는 동안은 칸이 비어 있고, 못 받으면 서명 설정과 같은 사전 문구 + [다시 시도].
|
||||
- 회사 밖 칸을 쓴 적 없으면 제목 · 내용을 null 로 보낸다(빈 글을 남기지 않는다 — 서버도 빈 글을 null 로 저장한다).
|
||||
|
||||
+224
-27
@@ -415,8 +415,8 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙
|
||||
| `DELETE /api/mailboxes/{id}` | `withChildren=true`(선택 — 15단계 "하위 메일함까지 지우기") | 200 `MailboxDeletion` — 안의 메일을 휴지통으로 옮긴 뒤 지운다(하위까지면 나무 전체) | 400 `VAL_001`(기본 메일함 · 하위 메일함이 있는데 `withChildren=true` 가 아님 · 하위에 기본 메일함 · 다른 값), 404 `RES_001`, 409 `MAIL_015`(남은 메일이 붙잡힌 메일뿐 — 9단계 "보내기 취소 · 예약 발송"), 429 `REQ_001`(같은 메일함(하위까지면 나무의 메일함)의 처리가 진행 중), 503 `MAIL_001` |
|
||||
| `PATCH /api/mailboxes/{id}/messages` | `{ isRead: true }` — 그 메일함 전체를 읽음으로 | 200 `MailboxBulkResult` | 400 `VAL_001`, 404 `RES_001`, 429 `REQ_001`(같은 메일함의 처리가 진행 중), 503 `MAIL_001` |
|
||||
| `DELETE /api/mailboxes/{id}/messages` | — 휴지통 · 스팸메일함 비우기(영구 삭제) | 200 `MailboxBulkResult` | 400 `VAL_001`(휴지통 · 스팸메일함이 아님), 404 `RES_001`, 409 `MAIL_015`(남은 메일이 붙잡힌 메일뿐 — 9단계), 429 `REQ_001`(같은 메일함의 처리가 진행 중), 503 `MAIL_001` |
|
||||
| `GET /api/mail-vacation` | — | 200 `MailVacationState` (아래 "부재중 자동 응답" — `MailVacation` + 7단계의 `isActive`) | 503 `MAIL_001` |
|
||||
| `PUT /api/mail-vacation` | `MailVacation` (전체 교체 — `isActive` 는 보내지 않는다) | 200 `MailVacationState`(저장한 값) | 400 `VAL_001` · `MAIL_013`(필터와 함께 만든 엔진 규칙이 한도를 넘음 — 6단계), 503 `MAIL_001` |
|
||||
| `GET /api/mail-vacation` | — | 200 `MailVacationState` (아래 "부재중 자동 응답" — `MailVacation` + 7단계의 `isActive`. 16단계의 `htmlBody` · `external` 은 늘 담는다) | 503 `MAIL_001` |
|
||||
| `PUT /api/mail-vacation` | `MailVacation` (전체 교체 — `isActive` 는 보내지 않는다. 16단계의 `htmlBody` · `external` 은 보내지 않아도 된다 — 글만 · 사내와 같은 응답) | 200 `MailVacationState`(저장한 값) | 400 `VAL_001` · `MAIL_013`(필터와 함께 만든 엔진 규칙이 한도를 넘음 — 6단계), 503 `MAIL_001` |
|
||||
| `GET /api/mail-filters` | — | 200 `MailFilterSet` (아래 "메일 필터", 6단계) | 503 `MAIL_001` |
|
||||
| `PUT /api/mail-filters` | `{ rules, version }` (전체 교체) | 200 `MailFilterSet` | 400 `VAL_001` · `MAIL_013`(만든 규칙이 엔진 한도를 넘음), 409 `MAIL_012`(그사이 다른 곳에서 필터가 바뀜 — 저장 안 함), 503 `MAIL_001` |
|
||||
| `POST /api/mail-filter-runs` | `MailFilterRunRequest` — 받은편지함에 이미 있는 메일에 규칙 하나를 적용한다(아래 "이미 받은 메일에 적용", 7단계) | 200 `MailFilterRun` — 붙잡힌 메일(9단계)은 옮기지 않고 표시만 | 400 `VAL_001`(규칙 검사 · 할 일 없음 · 깨진 cursor), 429 `REQ_001`(같은 직원의 적용이 진행 중), 503 `MAIL_001` |
|
||||
@@ -440,9 +440,15 @@ HTML 은 정화하기 **전에** 길이(HTML 부분이 여럿이면 이어 붙
|
||||
| `GET /api/drafts/{id}` | — | 200 `Draft` (아래 "임시 저장") | 404 `RES_001`(없음·초안이 아님), 503 `MAIL_001` |
|
||||
| `POST /api/drafts` | 초안 본문 | 201 `DraftSaved` | 400 `VAL_001`·`MAIL_002`·`MAIL_003`·`MAIL_004`·`MAIL_006`·`MAIL_007`·`MAIL_016`(대용량 첨부 — 11단계, 4절)·`FILE_001`, 413 `FILE_002`, 503 `MAIL_001` |
|
||||
| `PUT /api/drafts/{id}` | 초안 본문 (전체 교체) | 200 `DraftSaved` — **새 id** | 위와 같음 + 404 `RES_001`(초안이 아닌 id — 지우지 않음) + 409 `MAIL_008`(다른 곳에서 이미 바뀜 — 저장 안 함) |
|
||||
| `GET /api/mail-signature` | — | 200 `MailSignature` (아래 "서명") | — |
|
||||
| `PUT /api/mail-signature` | `{ bodyHtml, body, isForNew, isForReply }` | 200 `MailSignature` | 400 `VAL_001` |
|
||||
| `POST /api/mail-signature/images` | multipart `file` 1개 | 201 `SignatureImage` | 400 `FILE_001`·`VAL_001`(올려 둔 그림 10장 초과), 413 `FILE_002`(300KB 초과) |
|
||||
| `GET /api/mail-signatures` | — | 200 `{ items: MailSignature[], pageInfo }` — 만든 차례, 10개까지 한 번에(hasNext=false · `total`) (아래 "서명", 16단계) | — |
|
||||
| `POST /api/mail-signatures` | `{ name, bodyHtml, body }` | 201 `MailSignature` — 서명이 하나도 없던 직원이면 「새 메일」 · 「답장 · 전달」 두 곳의 기본이 된다 | 400 `VAL_001`(이름 · 같은 이름 · 본문 한도 · 11번째) |
|
||||
| `PUT /api/mail-signatures/{id}` | `{ name, bodyHtml, body }` (전체 교체 — 기본 표시는 그대로) | 200 `MailSignature` | 400 `VAL_001`, 404 `RES_001`(없는 · 남의 · 형식이 틀린 id) |
|
||||
| `DELETE /api/mail-signatures/{id}` | — | 204 — 이 서명을 기본으로 쓰던 곳은 넣지 않음, 이 서명에만 쓰던 그림은 함께 지운다 | 404 `RES_001` |
|
||||
| `GET /api/mail-signature-defaults` | — | 200 `MailSignatureDefaults` (「메일에 넣을 서명」 둘 — 16단계) | — |
|
||||
| `PUT /api/mail-signature-defaults` | `{ newMessageId, replyId }` (두 키 모두 — null 은 넣지 않음) | 200 `MailSignatureDefaults` | 400 `VAL_001`, 404 `RES_001`(내 서명이 아닌 id — 바꾸지 않음) |
|
||||
| `GET /api/mail-signature` | — | 200 `LegacyMailSignature` — 「새 메일」 기본 서명을 15단계까지의 모양으로 (옛 경로 — 앱 첫 판이 읽는다) | — |
|
||||
| `PUT /api/mail-signature` | `{ bodyHtml, body, isForNew, isForReply }` | 200 `LegacyMailSignature` (옛 경로 — 16단계 전의 웹 탭) | 400 `VAL_001` |
|
||||
| `POST /api/mail-signature/images` | multipart `file` 1개 | 201 `SignatureImage` | 400 `FILE_001`·`VAL_001`(올려 둔 그림 20장 초과 — 16단계부터, 전에는 10장), 413 `FILE_002`(300KB 초과) |
|
||||
| `GET /api/mail-signature/images/{id}` | — | 그림 파일 (표준 래퍼 없음, 본인 그림만) | 404 `RES_001` |
|
||||
| `GET /api/recipient-suggestions` | `q`(선택, 1~100자) · `limit`(기본 20, 최대 100) · `cursor` | 200 `{ items: RecipientSuggestion[], pageInfo }` (아래 "받는 사람 추천") | 400 `VAL_001`, 503 `MAIL_001` |
|
||||
| `GET /api/scheduled-messages` | `cursor` · `limit`(기본 20, 최대 100) | 200 `{ items: MessageSummary[], pageInfo }` — 예약한 메일, 보낼 시각 차례, `total` 포함 (아래 "보내기 취소 · 예약 발송", 9단계) | 400 `VAL_001`, 503 `MAIL_001` |
|
||||
@@ -726,16 +732,30 @@ interface DraftSaved { id: string; attachments: Attachment[]; updatedAt: string
|
||||
- 저장 응답을 받지 못하면(시간 초과 등) 서버는 이미 새 저장본을 만들었을 수 있다 — 다음 PUT 은 `MAIL_008` 이 되어 클라이언트가 새로 남기므로
|
||||
임시보관함에 한 통이 더 남을 수 있다 (잃는 내용은 없다).
|
||||
|
||||
### 서명 (`/api/mail-signature`)
|
||||
### 서명 (`/api/mail-signatures` · `/api/mail-signature-defaults` · 옛 경로 `/api/mail-signature`) — 16단계부터 여러 개
|
||||
|
||||
```ts
|
||||
interface MailSignature {
|
||||
interface MailSignature { // 16단계 — 한 사람에 10개까지
|
||||
id: string // 서버가 만든 UUID
|
||||
name: string // 앞뒤 공백을 뺀 1~30자(JS 문자열 길이) — 제어 문자 없음, 내 다른 서명과 겹치지 않는다(영문 대소문자 무시)
|
||||
bodyHtml: string | null // 서명 서식 — 서식 본문 규칙 + 서명 그림. 정화 뒤 10,000자 이하
|
||||
body: string | null // 텍스트 서명 (텍스트 대체 본문에 넣는다) — 2,000자 이하
|
||||
isForNew: boolean // 새 메일에 넣는다 (기본 true)
|
||||
isForReply: boolean // 답장·전달에 넣는다 — 인용 위 (기본 true)
|
||||
images: SignatureImage[] // 서명이 쓰는 그림
|
||||
updatedAt: string | null // 저장한 적 없으면 null
|
||||
images: SignatureImage[] // 서명이 쓰는 그림 (HTML 에 나온 순서)
|
||||
updatedAt: string // 마지막으로 저장한 시각
|
||||
}
|
||||
|
||||
interface MailSignatureDefaults { // 「메일에 넣을 서명」 — null 은 넣지 않음 (15단계까지의 두 체크를 대신한다)
|
||||
newMessageId: string | null // 새 메일 (본문 끝)
|
||||
replyId: string | null // 답장 · 전달 (인용 위)
|
||||
}
|
||||
|
||||
interface LegacyMailSignature { // 옛 경로 GET · PUT /api/mail-signature — 15단계까지의 MailSignature 모양 (앱 첫 판이 읽는다)
|
||||
bodyHtml: string | null // 「새 메일」 기본 서명의 것 — 없으면 「답장 · 전달」 기본 서명의 것
|
||||
body: string | null
|
||||
isForNew: boolean // 「새 메일」 기본 서명이 있다
|
||||
isForReply: boolean // 「답장 · 전달」 기본이 이 서명이다
|
||||
images: SignatureImage[]
|
||||
updatedAt: string | null // 기본 서명이 둘 다 없으면 null
|
||||
}
|
||||
|
||||
interface SignatureImage {
|
||||
@@ -747,19 +767,42 @@ interface SignatureImage {
|
||||
}
|
||||
```
|
||||
|
||||
- 사용자마다 하나다. 저장한 적 없으면 기본값(`bodyHtml: null`·`body: null`·둘 다 `true`·`images: []`)을 돌려준다. 서버 DB 에 둔다(메일 엔진에 두지 않는다).
|
||||
- `PUT` 의 한도 — 넘으면 400 `VAL_001`: 텍스트 서명 2,000자, 서식 서명은 정화 전에 서식 본문과 같은 한도(500,000자·파서 비용)와 정화 뒤 10,000자,
|
||||
서명 그림 3장(서로 다른 그림 기준).
|
||||
- 서버 DB 에 둔다(메일 엔진에 두지 않는다). 목록은 **만든 차례**이고 10개까지 한 번에 준다(`hasNext: false` · `total` = 서명 수).
|
||||
- 만들기 · 고치기(`POST` · `PUT /api/mail-signatures/{id}` — 전체 교체)의 한도 — 넘으면 400 `VAL_001`(아무것도 저장하지 않는다):
|
||||
이름(위 — 같은 이름은 그사이 다른 창에서 만든 것도 서버가 막는다), 11번째 만들기, 텍스트 서명 2,000자,
|
||||
서식 서명은 정화 전에 서식 본문과 같은 한도(500,000자·파서 비용)와 정화 뒤 10,000자, 서명 그림 3장(서로 다른 그림 기준).
|
||||
없는 서명 · 남의 서명 · 형식이 틀린 id 는 404 `RES_001`. 같은 직원의 서명 저장 · 지우기 · 기본 바꾸기는 서버가 차례로 한다(사용자 잠금).
|
||||
- **「메일에 넣을 서명」**: 서명이 하나도 없던 직원의 첫 서명(`POST`)은 「새 메일」 · 「답장 · 전달」 두 곳의 기본이 된다(15단계까지 두 체크가 처음에 켜져 있던 것과 같다).
|
||||
그 밖의 만들기 · 고치기는 기본을 바꾸지 않는다. 지우면(`DELETE` → 204) 그 서명을 기본으로 쓰던 곳은 넣지 않음(null)이 된다 — 다른 서명이 저절로 기본이 되지 않는다.
|
||||
`PUT /api/mail-signature-defaults` 는 두 키를 모두 보내고(null = 넣지 않음, 두 곳이 같은 서명이어도 된다) 바꾼 뒤의 값을 돌려준다 —
|
||||
내 서명이 아닌 id 는 404 `RES_001`(아무것도 바꾸지 않는다). 기본은 한 곳에 하나다(표의 부분 유일 인덱스).
|
||||
- 이미 보냈거나 임시 저장한 메일에 들어간 서명 글은 서명을 고치거나 지워도 그대로다.
|
||||
- **옛 경로**(앱 첫 판 · 16단계 전의 웹 탭 — 그대로 둔다):
|
||||
- `GET /api/mail-signature` — 「새 메일」 기본 서명을 위 `LegacyMailSignature` 로. 「새 메일」 기본이 없으면 「답장 · 전달」 기본 서명을 `isForNew: false` · `isForReply: true` 로
|
||||
(옛 두 체크가 새 메일 끔 · 답장 켬이던 직원 — 옮기면 답장 기본만 있다 — 이 앱 첫 판의 답장에서 서명을 잃지 않게).
|
||||
둘 다 없으면 `{ bodyHtml: null, body: null, isForNew: false, isForReply: false, images: [], updatedAt: null }`
|
||||
(15단계까지는 저장한 적 없으면 두 표시가 `true` 였다 — 본문이 없으니 앱이 넣는 것은 같다). 「새 메일」 기본이 있을 때 `isForReply` 는 「답장 · 전달」 기본이 같은 서명일 때만 `true` —
|
||||
두 곳이 서로 다른 서명이면 앱 첫 판은 답장에 서명을 넣지 않는다(알려진 한계 — 앱에서 서명 고르기는 앱 다음 판 후보).
|
||||
- `PUT /api/mail-signature` — 옛 GET 이 보이는 서명(「새 메일」 기본, 없으면 「답장 · 전달」 기본, 둘 다 없으면 "기본 서명"이라는 이름의 서명 — 그것도 없으면 만든다)의 본문을 바꾸고 두 표시를 기본 둘로 옮긴다:
|
||||
`isForNew` → 「새 메일」 기본을 이 서명으로(false 면 넣지 않음), `isForReply` → 「답장 · 전달」 기본을 이 서명으로(false 면 이 서명이 기본이었을 때만 넣지 않음 — 다른 서명이 기본이면 그대로).
|
||||
한도 · 정화는 위와 같다(새로 만들어야 하는데 이미 10개면 400 `VAL_001`).
|
||||
- **옮기기**(마이그레이션 `MultipleMailSignatures` — 표 `mail_signatures` 를 서명마다 한 줄로): 지금 서명은 "기본 서명"이라는 이름의 서명 하나가 되고,
|
||||
두 체크(`isForNew` · `isForReply`)에서 켠 곳의 기본이 된다(끈 곳은 넣지 않음). **내용이 비어 있던 서명(서식 · 텍스트 모두 없거나 공백뿐)은 옮기지 않는다.**
|
||||
되돌리기(down)는 한 사람에 한 줄만 남긴다 — 「새 메일」 기본 → 「답장 · 전달」 기본 → 먼저 만든 서명 차례로 하나, 나머지 서명은 지운다(남긴 서명의 두 표시가 옛 두 체크가 된다).
|
||||
- **쓰기 화면의 서명 자리**: 앱과 같은 경계 — 빈 문단 하나 뒤의 **속성 없는 `<div>` 하나**(위 "서식 본문"의 "앱이 만드는 모양"). 보내는 정화 · 임시 저장본을 다시 열 때의 정화 ·
|
||||
받은 메일 정화를 지나도 그대로 남는다(서버 시험 — `sanitize-outgoing-html.spec.ts`). 다만 속성이 모두 지워지는 div(허용하지 않는 style 뿐 · class · data-*)도
|
||||
속성 없는 div 가 되므로, 경계는 자리(빈 문단 바로 뒤)로 알아본다.
|
||||
- **서명 그림**: `POST /api/mail-signature/images` (multipart `file`) — PNG·JPEG·GIF 만(확장자·MIME·파일 앞부분 모두 검사, WebP·SVG 는 받지 않는다),
|
||||
한 장 300KB 이하(넘으면 413 `FILE_002` — 받은 메일의 본문 그림 한도와 같아서, 보낸 메일을 우리 웹메일로 다시 읽어도 그림이 보인다),
|
||||
가로·세로 각각 4096px 이하(넘으면 400 `FILE_001` — 작은 파일 안의 아주 큰 그림이 받는 쪽을 무겁게 하지 않게).
|
||||
서명에는 3장까지 넣는다(편집기가 먼저 막는다). 발송 본문·임시 저장본도 서로 다른 서명 그림 3장까지다(넘으면 400 `VAL_001`).
|
||||
올려 두기만 한 그림을 포함해 한 사람 10장까지 둘 수 있다(넘으면 올리기가 400 `VAL_001`).
|
||||
올려 두기만 한 그림을 포함해 한 사람 20장까지 둘 수 있다(넘으면 올리기가 400 `VAL_001` — 여러 서명이 함께 쓰는 칸이라 16단계에 10 → 20).
|
||||
서명 HTML 은 그림을 `<img src="/api/mail-signature/images/{id}" width="144" alt="">` 로 가리킨다(`width` 1~600 — 작게 96 · 보통 144 · 원본은 원래 폭, 600 이하).
|
||||
- `GET /api/mail-signature/images/{id}` 는 그 사람의 그림만 내준다(남의 것은 404 `RES_001`) — 편집기·미리 보기용이고, 받는 사람은 이 주소를 쓰지 않는다.
|
||||
저장할 때 확인한 형식 그대로, `Content-Disposition: inline` · `X-Content-Type-Options: nosniff` · `Cache-Control: private, max-age=86400`
|
||||
(그림 id 의 내용은 바뀌지 않는다 — 새로 올리면 새 id).
|
||||
- `PUT` 은 서명 HTML 이 더는 가리키지 않는 그림을 지운다 — 다만 한 번도 저장하지 않은 그림 가운데 10분 안에 올린 것은 남긴다
|
||||
- 서명을 저장 · 지울 때(옛 경로 `PUT` 포함) **어느 서명의 HTML 도 더는 가리키지 않는 그림**(모든 서명의 합집합 밖)을 지운다 — 다른 서명이 쓰는 그림은 남고,
|
||||
지운 서명에만 쓰던 그림은 함께 지워진다. 다만 한 번도 저장하지 않은 그림 가운데 10분 안에 올린 것은 남긴다
|
||||
(그림을 넣자마자 저장하면 올리기와 저장이 엇갈릴 수 있다). 올리고 저장하지 않은 그림은 하루 뒤 지운다.
|
||||
- **보낼 때**: 쓰기 화면이 서명을 본문에 보통 글로 넣어 둔 그대로 보낸다 — 서명 그림은 위 "서식 본문"의 그림 예외로 메일 안에 함께 실린다
|
||||
(`multipart/related` 의 그림 부분 — `Content-Disposition: inline`, `Content-ID: <sig-{그림 id}@{보내는 사람 도메인}>`, 이름 `signature-1.png` …,
|
||||
@@ -1144,7 +1187,7 @@ interface MailboxBulkResult {
|
||||
다른 탭에서 먼저 끝냈으면 실패로 보이지 않는다 — 모두 읽음 · 비우기의 0통은 "…에 안 읽은 메일이 없습니다" · "…이 이미 비어 있습니다", 지우기의 404 `RES_001` 은 지운 것으로 본다.
|
||||
메일함 지우기의 진행은 `processed` 로 센다(`movedToTrash` 는 휴지통으로 간 것만이라 다른 메일함에도 든 메일만 남으면 늘지 않는다).
|
||||
|
||||
### 부재중 자동 응답 (`/api/mail-vacation`) — 4단계
|
||||
### 부재중 자동 응답 (`/api/mail-vacation`) — 4단계 · 16단계(서식 본문 · 회사 밖 응답)
|
||||
|
||||
```ts
|
||||
interface MailVacation {
|
||||
@@ -1152,7 +1195,15 @@ interface MailVacation {
|
||||
fromDate: string | null // ISO 8601 UTC — null 이면 켠 때부터
|
||||
toDate: string | null // null 이면 끌 때까지
|
||||
subject: string // 켤 때 150자까지(꺼 둔 채로는 200자까지 — 아래 "길이 한도의 근거") — 켤 때(isEnabled true)는 공백이 아닌 글자 1자 이상. 제어 문자(메일함 이름과 같은 범위 — 탭 · 줄바꿈 포함)는 쓰지 않는다(한 줄짜리 머리글)
|
||||
textBody: string // 켤 때 600자까지(꺼 둔 채로는 4,000자까지) — 켤 때는 공백이 아닌 글자 1자 이상. 글자만(서식 없음) — 제어 문자는 탭 · LF · CR 만. 서명은 붙지 않는다
|
||||
textBody: string // 보이는 글(서식을 뺀 글) — 켤 때 600자까지(꺼 둔 채로는 4,000자까지), 켤 때는 공백이 아닌 글자 1자 이상. 제어 문자는 탭 · LF · CR 만. 서명은 붙지 않는다
|
||||
htmlBody: string | null // 16단계 — 서식 본문(그림 없음). 정화해 저장한다 — 켤 때 정화 뒤 10,000자까지. null = 글만. PUT 에 없으면 null
|
||||
external: MailVacationExternal // 16단계 — 회사 밖에서 온 메일에 하는 응답. PUT 에 없거나 null 이면 사내와 같은 응답(글 칸 null)
|
||||
}
|
||||
interface MailVacationExternal { // 16단계 — PUT 에 담으면 네 키를 모두 보낸다(글 칸은 null 가능)
|
||||
mode: 'same' | 'none' | 'separate' // 사내와 같은 응답(처음 값) · 보내지 않기 · 따로 쓰기
|
||||
subject: string | null // 따로 쓰기의 제목 — 켜고 따로 쓰기면 1~150자(그 밖에는 200자까지 · 제어 문자 없음). 빈 글은 null 로 저장
|
||||
textBody: string | null // 따로 쓰기의 보이는 글 — 켜고 따로 쓰기면 1~600자(그 밖에는 4,000자까지 · 탭 · LF · CR 밖의 제어 문자 없음). 빈 글은 null
|
||||
htmlBody: string | null // 따로 쓰기의 서식 본문(그림 없음) — 정화해 저장, 켜고 따로 쓰기면 정화 뒤 10,000자까지
|
||||
}
|
||||
interface MailVacationState extends MailVacation { // GET · PUT 의 응답 (7단계)
|
||||
isActive: boolean // false = 엔진에서 우리 규칙(필터와 한 스크립트)이 꺼져 있어 켜 둔 자동 응답이 지금은 동작하지 않는다 — 저장하면 다시 켜진다.
|
||||
@@ -1166,11 +1217,12 @@ interface MailVacationState extends MailVacation { // GET · PUT 의 응답 (7
|
||||
이 한도는 **켤 때만** 본다 — 꺼 둔 채로는 4단계 한도(200자 · 4,000자)까지 받는다. 4단계에 엔진 한도 안으로 저장해 둔 긴 글(영문 내용 1,000자 등)이 켜져 있으면
|
||||
화면은 꺼 둘 때 칸이 흐려 고칠 수 없는데, 끄기는 늘 저장되어야 하기 때문이다. 다시 켤 때 150 · 600 에 걸리면 줄인 뒤 켠다.
|
||||
|
||||
- 한 번도 설정하지 않은 계정은 `{ isEnabled: false, fromDate: null, toDate: null, subject: "", textBody: "" }` 로 온다(엔진의 null 은 빈 문자열로). 끈 채로는 빈 제목 · 내용도 저장된다.
|
||||
- 한 번도 설정하지 않은 계정은 `{ isEnabled: false, fromDate: null, toDate: null, subject: "", textBody: "", htmlBody: null, external: { mode: "same", subject: null, textBody: null, htmlBody: null } }` 로 온다
|
||||
(엔진의 null 은 빈 문자열로 — 엔진 VacationResponse 의 서식 본문은 가져오지 않는다). 끈 채로는 빈 제목 · 내용도 저장된다.
|
||||
제목 · 내용은 쓴 그대로 저장한다 — "공백이 아닌 글자" 검사만 앞뒤 공백을 뺀 값(JS `trim()` — NBSP · U+3000 포함)으로 한다. 제어 문자 검사는 꺼 둔 채로도 한다.
|
||||
|
||||
- 저장 자리 — 6단계부터 메일 필터와 같은 엔진 규칙(Sieve 스크립트 `arasystem`)에 둔다(아래 "메일 필터"의 "저장 자리"). 4단계에는 엔진의 JMAP `VacationResponse`(singleton)였다.
|
||||
서식 본문은 없다(글자만). 저장은 전체 교체이고 응답은 저장한 값이다. 꺼도 쓴 제목 · 내용은 남겨 둔다.
|
||||
15단계까지 서식 본문은 없었다(글자만 — 16단계는 아래 "서식 본문 · 회사 밖 응답"). 저장은 전체 교체이고 응답은 저장한 값이다. 꺼도 쓴 제목 · 내용은 남겨 둔다.
|
||||
필터와 한 스크립트라서 부재중을 저장해도 스크립트를 다시 켠다(필터가 꺼져 있었으면 함께 다시 동작한다). 필터 규칙은 건드리지 않는다.
|
||||
- 6단계부터 바뀐 동작: 엔진이 스팸으로 분류한 메일에는 답하지 않는다(필터와 같은 스팸 막기 — "메일 필터"의 "만드는 Sieve").
|
||||
- 우리 스크립트가 엔진에서 꺼져 있으면(`isActive: false`) 부재중도 동작하지 않는다 — `isEnabled` 는 저장한 값이라 그때도 `true` 다.
|
||||
@@ -1181,6 +1233,45 @@ interface MailVacationState extends MailVacation { // GET · PUT 의 응답 (7
|
||||
둘 다 있으면 버린 값으로 `fromDate < toDate`(아니면 400 `VAL_001`). 화면은 날짜만 고르고, 브라우저 시간대의 시작일 0시 · 끝나는 날 다음 날 0시를 UTC 로 보낸다.
|
||||
- 같은 사람에게 매번 답하지는 않는다 — JMAP 규격(RFC 8621 8절)이 "보낸 사람마다 적당한 기간에 한 번" 을 요구하고 엔진이 따른다.
|
||||
6단계부터는 Sieve `vacation`(RFC 5230)이 같은 일을 한다 — 간격은 엔진 기본값이다.
|
||||
- **서식 본문 · 회사 밖 응답 (16단계)**:
|
||||
- **서식 본문**(`htmlBody`)은 서명과 같은 정화(쓰기 화면의 서식 본문 허용 목록 — **그림은 없다**)를 거쳐 저장한다. 정화 전 길이(500,000자) · 파서 비용은 켬 · 끔과 상관없이 보고,
|
||||
정화 뒤 10,000자(JS 문자열 길이)는 켤 때만 본다(회사 밖 서식은 따로 쓰기이고 켤 때만) — 넘으면 400 `VAL_001`. 정화해서 남는 것이 없으면 null(글만).
|
||||
보이는 글(`textBody`)은 화면이 서식을 뺀 글로 함께 보낸다 — 켤 때의 600자 한도는 이 글로 본다(지금 그대로).
|
||||
- 받는 사람에게는 서식이 있으면 **글 + 서식 두 벌**(`multipart/alternative` — text/plain · text/html, 각 base64)로 간다. 서식을 읽지 못하는 메일 프로그램은 글만 보인다.
|
||||
서식이 없으면 지금처럼 글만(text/plain)이다.
|
||||
- **회사 밖 응답**(`external`) — 사내 · 회사 밖은 **봉투 보낸 사람(return-path)의 도메인**으로 가른다: `MAIL_DOMAIN` 과 같으면 사내(하위 도메인은 회사 밖).
|
||||
엔진은 답장을 봉투 주소로만 보내므로 머리글 From 을 사내로 꾸며도 사내용 글은 그 봉투 주소(회사 밖)로 가지 않는다(2026-11-10 개발 엔진 — 실제 배달로 확인).
|
||||
`same` = 지금처럼 모두에게 같은 응답(처음 값), `none` = 회사 밖에는 답하지 않는다, `separate` = 회사 밖에는 따로 쓴 제목 · 내용으로.
|
||||
다른 고르기로 바꿔도 따로 써 둔 회사 밖 글은 그대로 저장한다(빈 글은 null). 회사 밖 칸은 따로 쓰기이고 켤 때만 사내와 같은 규칙(1자 이상 · 150 · 600자 · 서식 10,000자)으로 보고,
|
||||
사내와 같은 응답 · 보내지 않기면 비어 있어도 저장한다.
|
||||
- 같은 사람에게 다시 보내는 간격은 엔진 기본 그대로이고, 사내 · 회사 밖은 따로 센다 — 회사 밖 응답에 `:handle` 을 따로 둔다(응답 내용의 지문 — 글을 바꾸면 다시 보낸다,
|
||||
사내 응답은 15단계까지와 같은 모양이라 고르기를 바꿔도 같은 사람에게 다시 보내지 않는다).
|
||||
- 옛 요청(16단계 칸 없음)은 글만 · 사내와 같은 응답으로 저장한다 — 옛 화면이 보는 그대로. 응답(GET · PUT)에는 두 칸을 늘 담는다.
|
||||
- **만드는 Sieve**(아래 "메일 필터"의 "만드는 Sieve"에 더하는 것):
|
||||
```
|
||||
if not header :matches "X-Spam-Score" "spam*" {
|
||||
if currentdate … { ← 기간이 있으면 (지금 그대로)
|
||||
if envelope :domain :is "from" "<MAIL_DOMAIN>" { ← 회사 밖 응답이 사내와 같지 않을 때만
|
||||
vacation :mime :subject "<사내 제목>" "<MIME 개체>";
|
||||
} else { ← 따로 쓰기만 (보내지 않기면 else 가 없다)
|
||||
vacation :mime :handle "ara-external-<지문>" :subject "<회사 밖 제목>" "<MIME 개체>";
|
||||
}
|
||||
}
|
||||
필터 규칙 …
|
||||
}
|
||||
```
|
||||
require 에 `envelope`(가를 때) · `variables`(조각이 있을 때)를 더한다. MIME 개체가 Sieve 문자열 한 개 한도(4,096바이트)를 넘으면 `variables` 로 3,000바이트 이하 조각에 나눠 담고
|
||||
(`set "part0" "…";` — 조각은 줄 끝(CRLF) 뒤에서만 끊는다) `vacation :mime "${part0}${part1}…"` 로 잇는다 — 2026-11-10 개발 엔진: 이은 답장의 글 · 서식이 원래와 바이트까지 같다.
|
||||
사용자 글 · 서식은 base64 · encoded-word 안에만 있어 Sieve 글에 `${` 가 생기지 않는다(지금처럼). 스크립트 90,000바이트 한도(400 `MAIL_013`)는 그대로다 —
|
||||
서식 · 회사 밖 글 · 차단 목록이 함께 크면 걸릴 수 있다(알려진 한계).
|
||||
- **원본 판 3** — 머리 주석의 원본(JSON)은 **새 칸을 쓴 직원만** 판 3 으로 쓴다(서식 본문이 있거나 · 회사 밖 응답이 사내와 같지 않거나 · 회사 밖 글을 써 둔 경우 — 판 3 은 차단 목록 칸을 늘 담는다).
|
||||
새 칸을 쓰지 않은 직원은 판 1 · 2 그대로다. 판 3 의 서식 본문은 읽을 때 다시 정화한다(다른 메일 프로그램이 고친 원본의 HTML 이 화면에 그대로 가지 않게 — 정화할 수 없으면 깨진 원본).
|
||||
**전제 — 서식이 없는 글이면 화면은 `htmlBody: null` 을 보낸다**(웹 설정 화면 — 글만 쓴 직원은 판 1 · 2 에 머문다). 서버는 서식이 있는지 가리지 않고
|
||||
정화한 `htmlBody` 가 남으면(null · 빈 값이 아니면) 판 3 으로 쓴다 — 그래서 판 3 은 서식을 쓰거나 회사 밖 응답 칸을 쓴 직원뿐이고, 아래 되돌리기 계획이 막는 직원도 그들뿐이다.
|
||||
- **되돌리기 계획**: 판 3 을 모르는 서버(15단계까지)로 되돌리면 **새 칸을 쓴 직원만** 필터 · 부재중 · 차단 저장이 503 `MAIL_001` 이고 부재중 화면은 꺼짐으로 보인다 —
|
||||
엔진의 응답(Sieve)은 그대로 돈다(옛 서버가 새 판을 지우지 않게 — 6단계의 규칙). 그 직원들이 저장해야 하면 16단계 서버로 다시 올린다
|
||||
(판 3 을 읽기만 하는 서버를 먼저 배포할 수 있게 둔다 — 9단계 판 2 와 같은 길).
|
||||
- 로그: 켬 · 끔 · 서식 있음 · 회사 밖 고르기만 남긴다(제목 · 글 · 서식은 남기지 않는다).
|
||||
- **클라이언트**: 설정 › 메일 › 부재중 자동 응답. 켜져 있으면 메일 화면의 메일함 패널 아래(사용량 위)에 "부재중 자동 응답 켜짐 · 11월 3일까지"(누르면 설정)를 보인다 —
|
||||
메일 화면에 들어올 때 한 번 받고, 시간이 지나면 다시 계산한다(끝나는 시각이 지나면 숨기고, 시작 전이면 "10월 30일 ~ 11월 3일 · 설정").
|
||||
|
||||
@@ -1319,6 +1410,7 @@ interface MailFilterActions {
|
||||
- 4단계 `/api/mail-vacation` 은 계약 그대로 두고 저장 자리만 이 스크립트로 옮긴다 — 엔진의 VacationResponse 는 더 쓰지 않는다.
|
||||
스크립트가 아직 없는 계정은 처음 읽을 때 VacationResponse 의 값을 가져온다(개발 데이터 이어받기). 엔진의 `vacation` 스크립트는 고칠 수 없다(엔진이 `forbidden`).
|
||||
- 스크립트 머리의 주석에 규칙 · 부재중 원본(JSON — 판 번호 `v` 를 함께 둔다)을 두고, 읽을 때는 그것만 믿는다(엔진의 Sieve 를 다시 해석하지 않는다).
|
||||
판은 쓰는 칸에 따라 가장 낮은 것으로 쓴다 — 1: 규칙 · 부재중, 2: + 차단 목록(9단계 — 아래 "보낸 사람 차단"), 3: + 부재중의 서식 본문 · 회사 밖 응답(16단계 — 위 "부재중 자동 응답").
|
||||
- 스크립트가 아직 없으면 빈 규칙 + `isActive: true`, 부재중은 엔진의 VacationResponse 값(위 이어받기).
|
||||
- 스크립트는 있는데 원본이 없거나 깨졌으면 빈 규칙 + `isActive: false`, 부재중은 꺼짐 — 저장하면(필터든 부재중이든) 덮어쓴다.
|
||||
- 원본의 판이 이 서버가 아는 것보다 새로우면(새 서버를 배포했다 되돌린 경우) 읽기는 위와 같지만 **쓰기는 거절한다**(503 `MAIL_001`) — 옛 서버가 새 판의 규칙을 지우지 않게.
|
||||
@@ -1341,6 +1433,7 @@ interface MailFilterActions {
|
||||
(그 메일은 엔진 판정대로 들어간다). 엔진의 스팸 필터는 바깥에서 오는(인증 없는) 메일에만 돌아 사내 메일에는 이 머리글이 없다 — 규칙이 그대로 적용된다.
|
||||
- 부재중: 엔진이 하던 대로 `vacation :mime`(본문 base64) — 제목은 RFC 2047 로 인코딩해 넣는다. Stalwart 는 require 와 상관없이 문자열의 `${…}` 를 늘 변수로 펼쳐서
|
||||
그대로 넣으면 지워진다(같은 날 확인). 기간은 `currentdate :zone "+0000"`(엔진 시간대에 기대지 않게 — 같은 날 확인). 같은 사람에게 다시 답하는 간격은 엔진 기본값(지금과 같다).
|
||||
16단계의 서식 본문(글 + 서식 두 벌) · 회사 밖 응답(봉투 도메인으로 가름) · 긴 개체의 `variables` 조각은 위 "부재중 자동 응답"의 "만드는 Sieve".
|
||||
- 규칙 값에 `${` 는 받지 않는다(400 `VAL_001`) — 펼쳐져 빈 값이 되면 `:contains` 가 모든 메일에 맞는다. 문자열은 `"` · `\` 를 이스케이프한다.
|
||||
`%{…}`(Stalwart 의 식 모양)는 펼치지 않아 막지 않는다 — `%{"a"}` · `%{}` · `%{1+1}` · `%{subject}` 모두 글자 그대로 견주었다(같은 날 확인).
|
||||
- 크기: 엔진 한도는 스크립트 102,400바이트 · 문자열 4,096바이트다. 만든 스크립트가 90,000바이트를 넘으면 저장하지 않고 400 `MAIL_013`.
|
||||
@@ -1684,11 +1777,12 @@ interface MailSettings {
|
||||
### 이 단계에서 하지 않는 것
|
||||
|
||||
탭을 모두 닫아도 오는 알림(웹 푸시 — 엔진은 `webpush-vapid` 를 지원한다), 알림 소리(운영체제 설정을 따른다), 휴대폰 푸시, 본문 안 이미지(인라인 cid — 붙여 넣은 이미지는 첨부로, 서명 그림만 예외), 메일 안의 배경 그림 보이기 · 보낸 사람마다 고르는 "이미지 늘 보기" 목록(9단계는 설정 세 가지로),
|
||||
여러 개의 서명(자체 검색 색인 · 조건 검색 · 검색어 강조는 14단계에서 했다 — 위 "검색 — 14단계"),
|
||||
받는 사람에 따라 서명을 저절로 바꾸기 · 앱에서 서명 고르기(16단계 서명은 기본 둘과 쓰기 화면의 [서명 ▾]로 — 자체 검색 색인 · 조건 검색 · 검색어 강조는 14단계에서 했다),
|
||||
메일함을 끌어 옮기기 · 메일함 순서 바꾸기(15단계는 ⋯ 메뉴 「옮기기」 창으로 — 메일을 끌어다 메일함에 놓기는 15단계에서 했다, 서버는 지금 `PATCH /api/messages` 그대로), 옮기기 단축키,
|
||||
부재중 응답의 서식 본문 · 사내/외부 구분(앱 비밀번호 · 2단계 인증은 12단계에서 했다), 7일 넘는 예약(엔진 한도), 예약됨 목록 줄의 [지금 보내기](15단계는 읽기 화면의 예약 안내 줄에만),
|
||||
부재중 응답의 「내 연락처에게만」(엔진 Sieve 는 주소록을 보지 못한다) · 부재중 응답의 그림(앱 비밀번호 · 2단계 인증은 12단계에서 했다), 7일 넘는 예약(엔진 한도), 예약됨 목록 줄의 [지금 보내기](15단계는 읽기 화면의 예약 안내 줄에만),
|
||||
차단 정리의 되돌리기 · 받은편지함 밖 메일함의 정리(15단계는 받은편지함만 — 위 "보낸 사람 차단"의 "받은편지함 정리").
|
||||
15단계에 한 것: 메일함 옮기기 · 하위 메일함까지 지우기(위 "메일함 관리"), 차단할 때 받은편지함 정리(위 "보낸 사람 차단"), 예약한 메일 지금 보내기(위 "보내기 취소 · 예약 발송").
|
||||
16단계에 한 것: 여러 서명 · 「메일에 넣을 서명」 둘(위 "서명"), 부재중 응답의 서식 본문 · 회사 밖 응답(위 "부재중 자동 응답").
|
||||
|
||||
---
|
||||
|
||||
@@ -1790,6 +1884,10 @@ interface DirectoryEntry {
|
||||
| `POST /api/contact-groups/{id}/members` | `{ contactIds?, directoryIds? }` — 합쳐 1~100개 | 200 `ContactGroupMembersAdded` | 400 `VAL_001`, 404 `RES_001`(없는 그룹 · 연락처 · 사내 목록 id), 409 `CONTACT_002`, 503 `MAIL_001` |
|
||||
| `GET /api/directory-entries` | `q`(선택, 1~254자) · `kind`(선택 — `person` · `shared`) · `excludeGroupId`(선택 — 그 그룹 구성원의 주소를 뺀다. 구성원 넣기 창) · `cursor` · `limit`(기본 20, 최대 100) | 200 `{ items: DirectoryEntry[], pageInfo }` — 이름 차례, `total` 포함 | 400 `VAL_001`, 404 `RES_001`(없는 그룹), 503 `MAIL_001`(사내 목록을 읽지 못함 — 아래 "사내 목록") |
|
||||
| `GET /api/directory-entries/{id}` | — | 200 `DirectoryEntry` | 404 `RES_001`(없거나 빼는 계정), 503 `MAIL_001` |
|
||||
| `POST /api/contact-import-checks` — 16단계 | `{ contacts: ContactImportItem[] }` — 1~100명 | 200 `ContactImportCheck` — 사람마다 `new` · `exists`, 남은 자리(아래 "가져오기 · 내보내기") | 400 `VAL_001`, 503 `MAIL_001` |
|
||||
| `POST /api/contact-imports` — 16단계 | `{ contacts: ContactImportItem[] }` — 1~100명 | 201 `ContactImportResult` — 넣은 연락처(같은 uid 로 이미 있던 것 포함), 보낸 차례 | 400 `VAL_001`, 404 `RES_001`(없는 groupId), 409 `CONTACT_002`(연락처 3,000개 · 크기 — 묶음 전체로, 하나도 만들지 않음), 503 `MAIL_001` |
|
||||
| `POST /api/contact-deletions` — 16단계 | `{ ids }` — 1~500개, 겹치지 않게 | 200 `ContactsDeleted` — 영구 삭제(되돌리기 표 없음) | 400 `VAL_001`, 503 `MAIL_001` |
|
||||
| `GET /api/contact-exports` — 16단계 | `format`(`vcard` · `csv`) · `groupId`(선택 — 그 그룹만) | 200 파일 — `text/vcard` · `text/csv`(표준 래퍼 없음, `attachment`) | 400 `VAL_001`, 404 `RES_001`(없는 그룹), 503 `MAIL_001` |
|
||||
|
||||
- **에러 코드(8단계에 더함)**: `CONTACT_001` 409 — 같은 이름의 그룹이 있다(만들기 · 이름 바꾸기 — 화면은 칸 아래에, 알림 없이).
|
||||
`CONTACT_002` 409 — 주소록 한도(연락처 3,000개 · 그룹 100개 · 그룹 하나에 구성원 500명 · 아래 "크기")를 넘는다 — 아무것도 바꾸지 않는다
|
||||
@@ -1875,6 +1973,62 @@ interface DirectoryEntry {
|
||||
- **읽기 화면 ⋯**: 보낸 사람 주소가 내 주소록에 없으면 「보낸 사람을 주소록에 추가」(이름 · 주소를 채운 연락처 추가 창), 있으면 「주소록에서 보기」.
|
||||
판단은 메뉴를 열 때 `GET /api/contacts?q=<보낸 사람 주소>` 로 알림 없이 하고, 주소가 같은(대소문자 무시) 연락처가 있는지 본다.
|
||||
|
||||
### 가져오기 · 내보내기 — 16단계 (2026-11-10)
|
||||
|
||||
화면은 디자인 원본 `Contacts16`(디자인 README "16단계"). **파일은 화면(브라우저)이 읽는다** — vCard(2.1 · 3.0 · 4.0, 여러 명) · CSV(네이버 · Google · Outlook · 우리가 내보낸 꼴) · 엑셀 .xlsx 를
|
||||
화면이 풀어 미리 보기까지 하고, 넣기로 한 연락처만 100명씩 서버로 보낸다(파일은 서버로 가지 않는다 — 개인 정보 · 새 서버 파서 없음). 서버는 연락처 추가와 같은 검사 · 같은 한도로 한 요청에 엔진 요청 하나로 만든다.
|
||||
|
||||
```ts
|
||||
interface ContactImportItem extends ContactInput { // 연락처 추가 본문(ContactInput)의 칸 · 검사 그대로 +
|
||||
uid: string // 'urn:uuid:' + 소문자 UUID — 화면이 사람마다 만든다(엔진 카드의 uid 가 된다)
|
||||
}
|
||||
interface ContactImportCheck { // POST /api/contact-import-checks → 200
|
||||
items: { uid: string; status: 'new' | 'exists' }[] // 보낸 차례
|
||||
remaining: number // 3,000 − 지금 연락처 수 (0 아래로 내려가지 않는다)
|
||||
}
|
||||
interface ContactImportResult { // POST /api/contact-imports → 201
|
||||
items: { uid: string; id: string; skippedGroupIds: string[] }[] // 넣은 연락처만, 보낸 차례 — 빠진 uid 는 넣지 못한 것
|
||||
}
|
||||
interface ContactsDeleted { deletedIds: string[]; notFoundIds: string[] } // POST /api/contact-deletions → 200 (보낸 차례)
|
||||
```
|
||||
|
||||
- **확인**(`POST /api/contact-import-checks`): 만들지 않는다. `groupIds` 는 보지 않는다(새 그룹은 넣기 직전에 만든다). 사람마다 아래 가운데 하나면 `exists`, 아니면 `new`:
|
||||
- 이메일이 있는 사람 — 그 이메일 가운데 하나라도(앞뒤 공백을 빼고 소문자로 견준다) 내 주소록의 어느 연락처에 있다.
|
||||
- 이메일이 없는 사람 — 이름(앞뒤 공백만 뺀 글 — 가운데 공백 · 대소문자는 그대로)이 같고 **전화번호의 숫자 모음**이 같은 연락처가 있다.
|
||||
숫자 모음은 번호마다 숫자만 남긴 글의 모음이다(겹침 · 차례는 보지 않는다, 숫자가 없는 번호는 뺀다 — 번호 하나만 같으면 다른 사람이다). 주소록 쪽 연락처는 이메일이 있어도 된다.
|
||||
둘 다 전화가 없으면 이름만 같아도 `exists`.
|
||||
- 같은 uid 의 연락처가 이미 있다(다시 보낸 묶음).
|
||||
**같은 파일 안의 겹침은 화면이 같은 규칙으로 본다**(파일 앞쪽에 같은 사람이 있으면 건너뛴다) — 짝: backend `modules/contacts/contact-import.ts` · frontend `shared/utils/contact-import`(한쪽을 고치면 둘 다).
|
||||
- **넣기**(`POST /api/contact-imports`): 엔진 `ContactCard/set` 한 번(300명에 90ms — 2026-11-10 확인).
|
||||
- 사람마다 보낸 uid 로 만든다. 같은 uid 의 연락처가 이미 있으면(응답을 잃고 다시 보낸 묶음) 새로 만들지 않고 그 id 를 돌려준다 — 빠진 그룹만 마저 넣는다. 두 번 들어가지 않는다.
|
||||
- 연락처 3,000개 · 크기(800만 — 위 "크기") 한도는 **묶음 전체로** 미리 본다 — 넘으면 409 `CONTACT_002`, 하나도 만들지 않는다(화면은 `remaining` 으로 미리 줄인다).
|
||||
- 그룹 구성원 500명을 넘기는 넣기는 그 그룹만 빼고 만들고 `skippedGroupIds` 에 적는다 — 실패가 아니다(보낸 차례대로 넣다가 찬 그룹부터 뺀다). 없는 그룹은 404 `RES_001`(연락처 추가와 같다).
|
||||
- 검사(연락처 추가와 같은 칸 규칙 · 이름이나 이메일 · 같은 연락처 안의 같은 주소 · uid 모양 · 묶음 안의 같은 uid)에 걸리는 사람이 하나라도 있으면 400 `VAL_001` — 묶음 전체다(화면이 같은 규칙으로 미리 거른다). 겹침은 보지 않는다(확인의 몫).
|
||||
- 메일 서버가 일부만 받으면 받은 것만 `items` 에 — **보낸 uid 가 빠져 있으면 넣지 못한 것이다**(화면은 "넣지 못함"으로). 던지지 않는다. 그룹 카드가 이미 쓰는 uid 도 만들지 않는다(엔진이 같은 주소록의 같은 uid 를 받지 않는다).
|
||||
- **새 그룹**은 화면이 넣기 전에 지금 `POST /api/contact-groups` 로 만든다(같은 이름 409 `CONTACT_001` 이면 목록에서 그 그룹을 쓰고, 100개 한도 409 `CONTACT_002` 면 그 그룹 없이). 되돌릴 때 그 그룹은 `DELETE /api/contact-groups/{id}`.
|
||||
- **한꺼번에 지우기**(`POST /api/contact-deletions`): 가져오기 [되돌리기] 몫이다. 영구 삭제이고 되돌리기 표는 주지 않는다. 지운 연락처를 가리키는 그룹 구성원도 함께 뺀다(같은 uid 의 다른 카드가 남으면 그대로).
|
||||
내 주소록에 없는 id(이미 지움 · 그룹 id)는 `notFoundIds` — 실패가 아니다. 엔진 한 요청에 담는 카드 수(지울 연락처 + 고칠 그룹)가 엔진 상한(`maxObjectsInSet` 500)을 넘으면 서버가 앞에서부터 나눠 지운다 —
|
||||
중간에 503 이면 앞 몫은 지워진 채다(같은 목록을 다시 보내면 그 id 는 `notFoundIds`). 지울 카드는 id 만 읽는다(사진이 든 카드를 통째로 읽지 않는다).
|
||||
- **요청 수 한도**는 지금 기본(경로 · IP 1분 100회) — 100명씩이면 3,000명도 30번이다. 사무실이 IP 하나로 나가면 그동안 다른 직원의 같은 경로 요청과 나눠 쓴다.
|
||||
- **본문 한도**는 앱 공통(JSON 1MB · 값 10,000개). 보통 100명이 50KB 남짓이고, 칸이 많은 연락처(네이버 꼴 — 전화 20개 · 이메일 4개 · 그룹 4개)로 채워도 100명이 값 8,600개 남짓이다.
|
||||
칸을 한도까지 채운 연락처가 몰려 400 `VAL_001`(본문 모양) · 413 `FILE_002` 가 나면 화면은 묶음을 줄여 보낸다(권장).
|
||||
- **내보내기**(`GET /api/contact-exports`): 파일 응답(표준 래퍼 없음 — 실패는 표준 실패 JSON). 화면은 첨부 받기처럼 이동 직전에 세션을 확인하고 `<a href download>` 로 받는다.
|
||||
- 내 주소록 전체 또는 그룹 하나(`groupId`), 이름 차례. 사내 목록은 넣지 않는다(회사 직원 명단을 파일로 밖에 내보내는 길을 만들지 않는다). 우리 칸만 — 엔진 카드의 다른 칸(주소 · 생일 · 사진 등)은 넣지 않는다.
|
||||
- 머리글: `Content-Disposition: attachment` — 이름 `주소록-YYYY-MM-DD.vcf` · `주소록-<그룹 이름>-YYYY-MM-DD.csv`(날짜는 한국 날짜, 그룹 이름의 `\ / : * ? " < > |` 는 `_`, 앞뒤 공백 · 점은 떼고 50글자까지) ·
|
||||
`Cache-Control: private, no-store` · `X-Content-Type-Options: nosniff` · `Content-Security-Policy: default-src 'none'; sandbox`.
|
||||
- **vCard 3.0**(`text/vcard; charset=utf-8`): 줄 끝 CRLF · 한 줄 75바이트에서 접는다(UTF-8 글자를 가르지 않는다 — 이어지는 줄은 공백 하나로 시작) · 글 값의 `\` `;` `,` 줄바꿈은 이스케이프.
|
||||
`UID`(카드에 있으면) · `FN`(이름 — 없으면 첫 이메일, 그것도 없으면 첫 전화번호 — vCard 3.0 은 FN 을 꼭 담는다) · `N`(이름 전체를 성 칸에 — `N:김아라;;;;`, 이름이 없으면 `N:;;;;` — 다시 가져올 때 주소를 이름으로 읽지 않게) · `EMAIL;TYPE=INTERNET,WORK`(회사) / `INTERNET,HOME`(개인) / `INTERNET`(기타) ·
|
||||
`TEL;TYPE=CELL`(휴대전화) / `WORK` / `HOME` / `VOICE`(기타) · `ORG:회사;부서` · `TITLE` · `NOTE` · `CATEGORIES`(그룹 이름, 쉼표로). 한 파일에 여러 명이다.
|
||||
- **CSV**(`text/csv; charset=utf-8`): UTF-8 BOM · CRLF · 머리 `이름,이메일,휴대폰,회사 전화,집 전화,기타 전화,회사,부서,직함,메모,그룹` — 이메일은 종류 없이 모두, 같은 칸에 여럿이면 `; ` 로 잇는다.
|
||||
칸이 `= + - @` · 탭 · CR 로 시작하면 앞에 `'` 를 붙인다(엑셀 수식 막기 — OWASP CSV Injection, 다시 가져올 때 떼어 읽는다, 네이버 CSV 와 같은 꼴). 쉼표 · 따옴표 · 줄바꿈이 든 칸은 따옴표로 감싼다(RFC 4180 — 메모의 줄바꿈은 LF 그대로).
|
||||
우리 가져오기가 이 머리 줄을 알아본다 — 머리 이름 · 차례를 바꾸면 frontend `shared/utils/contact-import` 도 함께.
|
||||
- 없는 그룹 404 `RES_001`, 모르는 `format`(없음 포함) · 모르는 쿼리 400 `VAL_001`.
|
||||
- **로그**: 개수 · 형식 · 그룹 id 만 — 연락처 내용 · 파일 내용 · uid 는 남기지 않는다.
|
||||
- **알려진 한계**: 한 파일에 여러 명이 든 vCard 를 Outlook 클래식은 첫 사람만 읽는다. 그룹 이름에 `;` 가 있으면 CSV 의 그룹 칸이 둘로 읽힌다. 넣기의 일부를 엔진이 받지 않으면 그 사람의 uid 가 그룹 구성원에 남는다
|
||||
(그룹 구성원 수는 주소록에 있는 연락처만 세므로 보이지 않는다 — 같은 uid 로 다시 넣으면 그 그룹에 들어간다).
|
||||
- **이 단계에서 하지 않는 것**: 주소록 칸 늘리기(주소 · 생일 · 홈페이지 · 별명 · 사진 — 화면이 메모 끝 "가져온 정보" 줄로 붙인다), 겹친 연락처 합치기, 모르는 CSV 의 열을 손으로 맞추기, 옛 .xls,
|
||||
서버에서 파일 읽기, 사내 목록 내보내기, 여러 연락처를 골라 지우는 화면(서버 길은 한꺼번에 지우기로 있다), 앱의 주소록.
|
||||
|
||||
---
|
||||
|
||||
## 4. 대용량 첨부 — 11단계 (2026-10-30 확정)
|
||||
@@ -2650,7 +2804,7 @@ interface FileUploadCreate {
|
||||
// GET /api/two-factor → 200
|
||||
{ "isEnabled": true, "enabledAt": "2026-10-31T00:12:00.000Z" } // enabledAt — 웹메일에서 켠 때. 엔진 관리 화면에서 켰으면 null
|
||||
|
||||
// POST /api/two-factor/enrollments → 201 (꺼져 있을 때만, Cache-Control: no-store)
|
||||
// POST /api/two-factor/enrollments → 201 (꺼져 있으면 켜기용 · 16단계부터 켜져 있으면 휴대폰 바꾸기용 — 아래 "휴대폰 바꾸기", Cache-Control: no-store)
|
||||
{ "enrollmentId": "…", "secret": "JBSWY3DPEHPK3PXP…(32자)",
|
||||
"otpauthUrl": "otpauth://totp/ARA%20SYSTEM:ara.kim%40arasystem.it.kr?secret=…&issuer=ARA%20SYSTEM&algorithm=SHA1&digits=6&period=30",
|
||||
"expiresAt": "2026-10-31T00:22:00.000Z" }
|
||||
@@ -2672,20 +2826,62 @@ interface FileUploadCreate {
|
||||
3. 대리 접근으로 `currentSecret` + `otpAuth.otpUrl` 을 보낸다. 켠 코드는 2분 기억하고 등록의 비밀은 바로 지운다(`enabledAt` 기록이 실패해도 성공 — 엔진이 기준).
|
||||
- 끄기: 비밀번호를 본인 Basic 으로 먼저 확인(402) → 다시 쓴 코드인지 확인 → 대리 접근으로 `currentSecret` + `otpAuth: { otpCode, otpUrl: null }`. 이미 꺼져 있으면 200 이다(같은 결과). 엔진에서 꺼진 뒤 `enabledAt` 기록 지우기가 실패해도 200 이다(error 로그 — 엔진이 기준).
|
||||
- 켜고 끄는 일은 웹메일 세션을 끊지 않는다 — 로그인 기록 화면에 [다른 곳 모두 로그아웃]이 따로 있다. 이미 열린 IMAP 연결은 다시 연결할 때까지 남는다(알려진 한계).
|
||||
- 같은 직원의 2단계 인증 바꾸기(등록 · 켜기 · 끄기 · 2단계를 켠 직원의 비밀번호 바꾸기)는 한 번에 하나다 — 처리 중이면 429 `REQ_001`.
|
||||
- 휴대폰 바꾸기: 끈 뒤 다시 켠다(켜진 채 다시 등록하기는 처음 판에 없다).
|
||||
- 같은 직원의 2단계 인증 바꾸기(등록 · 켜기 · 끄기 · 2단계를 켠 직원의 비밀번호 바꾸기 · 16단계 휴대폰 바꾸기의 확인 · 바꾸기)는 한 번에 하나다 — 처리 중이면 429 `REQ_001`.
|
||||
- 휴대폰 바꾸기: 16단계부터 켠 채 바꾼다 — 아래 "휴대폰 바꾸기 (16단계)". 등록은 켜져 있어도 받는다(엔진에 묻지 않는다) — 켜진 채 받은 등록으로 켜기를 보내면 비밀번호 확인이 402 라 409 `AUTH_008` 이다.
|
||||
- 휴대폰을 잃었으면 운영자가 엔진 관리 화면에서 그 직원의 OTP 를 지운다(직원 비밀번호 없이 된다 — 절차는 mail/README).
|
||||
|
||||
| 실패 | 코드 |
|
||||
| --- | --- |
|
||||
| 코드 틀림 · 다시 쓴 코드 | 400 `AUTH_006` |
|
||||
| 지금 비밀번호 틀림 · 앱 비밀번호 모양 | 400 `AUTH_005` |
|
||||
| 등록이 없음 · 10분 지남 · 더 새 등록으로 바뀜 · 그사이 다른 곳에서 켜짐(등록 · 켜기) | 409 `AUTH_008` — 화면은 처음부터 다시 |
|
||||
| 등록이 없음 · 10분 지남 · 더 새 등록으로 바뀜 · 그사이 다른 곳에서 켜짐(켜기 — 16단계부터 등록은 켜져 있어도 201) | 409 `AUTH_008` — 화면은 처음부터 다시 |
|
||||
| 형식 오류 | 400 `VAL_001` |
|
||||
| 코드 · 비밀번호 실패 한도, 처리 중 | 429 `REQ_001` |
|
||||
| 웹메일로 다루지 않는 계정 | 401 `AUTH_001` |
|
||||
| 엔진 장애 · 결과를 모르는 실패 | 503 `MAIL_001` — 켜기 · 끄기의 결과를 모르면 엔진을 다시 읽어 맞춘다(바뀌었으면 성공으로) |
|
||||
|
||||
### 휴대폰 바꾸기 (16단계 — 2026-11-10)
|
||||
|
||||
2단계 인증을 **켠 채** 새 휴대폰으로 옮긴다 — 2단계가 꺼지는 틈이 없어 필수 정책인 직원도 바꾼다(화면은 [끄기]를 숨긴 직원에게도 [휴대폰 바꾸기]를 보인다). 화면은 디자인 원본 `Settings16`(창 하나 · 두 걸음).
|
||||
|
||||
```jsonc
|
||||
// 0) 등록 — 켜져 있어도 받는다 (응답은 켜기와 같은 꼴, Cache-Control: no-store)
|
||||
// POST /api/two-factor/enrollments → 201 { "enrollmentId": "…", "secret": "…", "otpauthUrl": "otpauth://…", "expiresAt": "…" }
|
||||
|
||||
// ① 새 휴대폰의 코드 — POST /api/two-factor/enrollments/{enrollmentId}/verification → 200
|
||||
{ "otpCode": "123456" }
|
||||
// → { "verified": true }
|
||||
|
||||
// ② 지금 휴대폰의 코드 + 지금 비밀번호 — POST /api/two-factor/replacements → 200 (GET /api/two-factor 와 같은 모양)
|
||||
{ "enrollmentId": "…", "currentOtpCode": "654321", "currentPassword": "…" }
|
||||
```
|
||||
|
||||
- **①** 새 휴대폰의 코드를 등록의 비밀로 확인하고(앞뒤 한 단계) 등록에 "확인됨"을 단다 — 엔진은 등록 때 코드를 보지 않으므로 서버가 먼저 본다. 엔진에는 아직 아무것도 바꾸지 않는다.
|
||||
코드 확인은 켜기 · 끄기와 같은 코드 가드다(코드 실패 한도 · 받은 코드는 2분 동안 다시 받지 않는다). 확인은 등록에 남는다(10분) — [이전]으로 돌아와도 다시 확인하지 않아도 된다(같은 코드로 다시 확인하면 2분 안에는 `AUTH_006`).
|
||||
- **②** 서버의 차례
|
||||
1. 등록이 "확인됨"이 아니면 409 `AUTH_008` — 그 등록은 버린다(처음부터).
|
||||
2. 앱 비밀번호 모양의 지금 비밀번호는 엔진에 묻지 않고 400 `AUTH_005`(세지 않는다). 지금 비밀번호를 본인 Basic 으로 본다 — 402(켜짐 · 비밀번호 맞음)여야 한다.
|
||||
401 이면 400 `AUTH_005`(로그인 실패 한도로 센다), 200 이면 그사이 다른 곳에서 꺼진 것이라 409 `AUTH_008`(등록을 버린다 — 화면은 상태를 다시 받는다).
|
||||
엔진은 비밀번호 틀림과 지금 코드 틀림을 가르지 않으므로(둘 다 `forbidden` "Current secret is incorrect.") 비밀번호를 먼저 본다.
|
||||
3. 다시 쓴 코드인지 보고 코드 실패 한 번을 차지한다(끄기와 같은 결) → 대리 접근 `x:AccountPassword/set` 에 `currentSecret` + `otpAuth: { otpCode: <지금 코드>, otpUrl: <새 otpauth 주소> }` 한 요청
|
||||
(2026-11-10 개발 엔진 — `updated`, 옛 비밀은 바로 죽고 거듭 바꿀 수 있다).
|
||||
4. `forbidden` 이면 지금 코드 틀림 400 `AUTH_006`(코드 실패로 센다 — 등록은 남아 맞는 코드로 다시 보낸다). 결과를 모르면(시간 초과 · 연결 끊김 · 5xx) 503 `MAIL_001` —
|
||||
엔진이 `otpUrl` 을 가려(`****`) 다시 읽어도 바뀌었는지 가를 수 없다. 그 코드는 쓰지 않은 것으로 두고 등록도 남긴다.
|
||||
5. 바뀌면 등록의 비밀을 지운다(새 비밀은 엔진에만). 두 코드는 2분 동안 다시 받지 않는다. 켠 때(`enabledAt`)는 그대로 — 로그인된 기기는 끊지 않고 알림 메일도 보내지 않는다(켜기 · 끄기와 같다).
|
||||
- 제한된 세션은 ① · ② 모두 403 `AUTH_010`(등록은 지금처럼 제한된 세션도 부른다). 같은 직원의 2단계 바꾸기와 함께 돌지 않는다(처리 중 429 `REQ_001`).
|
||||
- 지금 휴대폰을 잃었으면 지금처럼 운영자의 「2단계 해제」 뒤 다시 켠다(필수 정책이면 다음 로그인의 켜기 단계 — 화면은 창 ②에 문의 줄).
|
||||
- 로그 · 오류 context 에 코드 · 비밀번호 · otpauth 주소 · 엔진 설명 문장은 없다(가린 주소 · 사유만). 바꾸면 info "2단계 인증 휴대폰을 바꿨습니다: a***@…".
|
||||
|
||||
| 실패 | 코드 |
|
||||
| --- | --- |
|
||||
| 코드 · 등록 id 형식(경로의 id 가 UUID 가 아님 포함) | 400 `VAL_001` |
|
||||
| ① 새 휴대폰의 코드 틀림 · ② 지금 휴대폰의 코드 틀림 · 2분 안에 다시 쓴 코드 | 400 `AUTH_006` |
|
||||
| ② 지금 비밀번호 틀림 · 앱 비밀번호 모양 | 400 `AUTH_005` |
|
||||
| 등록이 없음 · 10분 지남 · 더 새 등록 · ②에서 확인(①)하지 않은 등록 · 그사이 다른 곳에서 꺼짐 | 409 `AUTH_008` — 화면은 처음부터(새 등록) |
|
||||
| 제한된 세션 | 403 `AUTH_010` |
|
||||
| 코드 · 비밀번호 실패 한도, 처리 중 | 429 `REQ_001` |
|
||||
| 엔진 장애 · ②의 결과를 모름 | 503 `MAIL_001` |
|
||||
|
||||
### 앱 비밀번호 (`/api/app-passwords`)
|
||||
|
||||
```jsonc
|
||||
@@ -2859,10 +3055,11 @@ interface FileUploadCreate {
|
||||
- 끊은 세션 `DELETE /api/auth/sessions/{id}` 의 모양이 틀린 id 는 404 `RES_001`(없는 것과 같다).
|
||||
- 엔진 관리 화면에서 켠 2단계 인증은 `enabledAt` 이 없다.
|
||||
- 복구 코드가 없다 — 휴대폰을 잃으면 운영자에게 문의한다.
|
||||
- 휴대폰 바꾸기(16단계)의 ②가 503 `MAIL_001`(결과를 모름)로 끝난 뒤 같은 등록으로 다시 보내면, 그사이 바뀌었다면 옛 휴대폰의 코드가 죽어 `AUTH_006` 이다 — 새 휴대폰의 코드로 로그인되는지로 안다.
|
||||
|
||||
### 이 단계에서 하지 않는 것
|
||||
|
||||
복구 코드, 이 브라우저 기억, 문자 · 메일 코드, 보안 키(WebAuthn) · 패스키, 관리자가 2단계를 강제하기(13단계), 켜진 채 휴대폰 바꾸기,
|
||||
복구 코드, 이 브라우저 기억, 문자 · 메일 코드, 보안 키(WebAuthn) · 패스키, 관리자가 2단계를 강제하기(13단계), 켜진 채 휴대폰 바꾸기(16단계에서 했다 — 위 "휴대폰 바꾸기"),
|
||||
앱 비밀번호 이름 바꾸기 · 만료 · 허용 IP, 새 기기 로그인 알림 메일, 계정 변경 기록(비밀번호 바꾸기 · 2단계 켜기 등), 메일 프로그램의 OAuth 로그인, 모바일 앱.
|
||||
|
||||
### 정한 것 (2026-10-31 추천대로)
|
||||
|
||||
+3
-1
@@ -60,9 +60,11 @@
|
||||
7일 넘는 예약(메일 엔진 한도라 어렵다).
|
||||
- **검색**(14단계에서 미룸): 강조한 곳 사이 다음 · 이전 이동, 검색어 줄임말(`from:` 꼴), 첨부 파일 안의 글 찾기, 끝이 바뀐 말 찾기(형태소 분석).
|
||||
- **메일함 정리**(15단계에서 미룸): 메일함을 끌어 옮기기 · 순서 바꾸기, 옮기기 단축키, 예약됨 목록 줄의 [지금 보내기], 차단할 때 옮긴 메일 한꺼번에 되돌리기 · 받은편지함 밖 메일함 정리.
|
||||
- **개인 설정**(16단계에서 미룸): 주소록 칸 늘리기(주소 · 생일 · 홈페이지 · 별명 · 사진), 겹친 연락처 합치기, 모르는 CSV 의 열을 손으로 맞추기, 옛 엑셀(.xls), 여러 연락처 고르기 · 한꺼번에 지우기,
|
||||
받는 사람에 따라 서명 저절로 바꾸기, 앱에서 서명 고르기, 부재중 응답을 내 연락처에게만 · 그림 넣기.
|
||||
- **대용량 첨부**(11단계에서 미룸 — 파일 저장소 때 함께): 보관 기간 고르기 · 연장, 다시 보내기, 받는 사람 확인 코드 · 비밀번호, 사내 직원만 받기, 받는 사람별 기록,
|
||||
처음 받음 · 만료 전 알림, 미리 보기, 모두 받기(zip), 폴더 올리기, 링크 → 메일에 담기 되돌리기.
|
||||
- **계정 · 보안**(12 · 13단계에서 미룸 — 공통 기반 때 함께): 복구 코드, 이 브라우저 기억, 패스키, 새 기기 로그인 알림 메일, 계정 변경 기록, 로그인 기록의 나라 · 지역,
|
||||
- **계정 · 보안**(12 · 13 · 16단계에서 미룸 — 공통 기반 때 함께): 복구 코드, 이 브라우저 기억, 패스키, 새 기기 로그인 알림 메일, 2단계 휴대폰을 바꿀 때 알림 메일, 계정 변경 기록, 로그인 기록의 나라 · 지역,
|
||||
앱 비밀번호 이름 바꾸기 · 만료, 비밀번호 스스로 재설정, 공용 주소로 보내기 · 공유 메일함, 웹메일 관리자 화면.
|
||||
- **앱**: 앱 안에서 받아 바로 설치, 최소 판이 아닌 새 판 알림, 아이폰, Google 개발자 인증(2027년 전 세계 — 그 전에 등록할지 정한다).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user