docs: 업데이트 내역을 계약 7절 · 디자인 README · 스킬에 적는다

계약 7절(API 둘 · 글 파일 규칙)과 머리말의 달력 날짜 예외, 웹 · 앱 디자인 README 의 업데이트 내역 절, 세 스택 스킬 행을 더했다.
보드의 목록 최대 개수를 구현(100)에 맞추고, 편집 도구가 보이지 않는 글자의 이스케이프를 실제 글자로 푼 일을 공통 규칙에 적는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-10 19:37:01 +09:00
parent def96e833b
commit 5ce614cc0c
8 changed files with 213 additions and 5 deletions
+32 -1
View File
@@ -20,7 +20,8 @@
- 탭마다 이동 기록을 따로 지킨다(메일에서 읽던 화면이 설정 탭에 다녀와도 남는다). 읽기 · 쓰기는 탭 막대 없는 전체 화면이다.
- 여러 통 고르기 중에는 탭 막대를 숨긴다(잠깐의 상태이고, 다른 탭으로 가면 고른 것을 잃는다). ✕ · 뒤로 가기로 나오면 다시 보인다.
- 화면 낭독기 이름은 "앱 메뉴".
- **설정 탭**: 내 정보(이름 · 주소) · 알림(새 메일 알림 켬/끔) · 앱 정보(앱 버전 · [새 버전 받기] · 오픈소스 라이선스) · 문의 연락처 · [로그아웃]. 비밀번호 · 2단계 인증 같은 계정 설정은 웹에서 한다.
- **설정 탭**: 내 정보(이름 · 주소) · 알림(새 메일 알림 켬/끔) · 앱 정보(앱 버전 · 업데이트 내역 · [새 버전 받기] · 오픈소스 라이선스) · 문의 연락처 · [로그아웃]. 비밀번호 · 2단계 인증 같은 계정 설정은 웹에서 한다.
업데이트 내역(2026-11-10 — 아래 「업데이트 내역」)은 앱 버전 다음 줄이고 오픈소스 라이선스 줄과 같은 꼴이다.
[새 버전 받기](2026-11-08 — `App1Push` ④ 앱 정보 절)는 끝 아이콘이 바깥으로 여는 아이콘이고, 업데이트 화면의 단추처럼 포털의 휴대폰 앱 받기 화면을 브라우저로 연다(같은 실패 알림).
[로그아웃]은 데이터를 지우는 일이 아니라 회색 테두리 단추이고, 누르면 확인 창(처음 초점 [취소]) — 로그아웃하면 이 휴대폰의 새 메일 알림도 오지 않는다고 알린다.
@@ -303,5 +304,35 @@
- 2026-11-08 갤럭시 S23(Android 16)에서 본 것: 등록 소식(자동 초기화를 끈 채) · 같은 알림 바꿔 쓰기와 앱으로 돌아올 때 지우기 · 보는 중에는 그리지 않음 · 뒤에 있을 때와 꺼져 있을 때 누르면 받은편지함 · 작은 아이콘(잘 보인다) · 홈 화면 배지 · 바꿔 쓴 알림도 다시 소리를 냄 · 로그아웃 때 FCM 풀기(서버 줄은 서버가 지운다). 서버 쪽은 계약 1절 "실기기 · 실제 키로 확인할 것".
- 알려진 한계(2026-11-08): FID 로 등록하는 길은 Google Play 서비스 26.12(versionCode 261200000) 이상에서만 탄다 — 그보다 옛 휴대폰은 SDK(firebase-messaging 26.0.1)가 옛 토큰 길로 돌아가 등록 소식(`onRegistered`)의 값이 FID 가 아닐 수 있고, 서버는 FID 모양만 받으므로 켜기가 실패할 수 있다(아직 고치지 않았다 — 등록에 드는 매니페스트 표시는 flutter/README "새 메일 알림").
### 업데이트 내역 (`ReleaseNotes` B — 2026-11-10 확정, 캔버스 v80)
직원이 무엇이 새로 생기거나 바뀌었는지 읽는 게시판(목록 → 글)이다. 웹과 같은 글을 보여 준다(서버가 글 파일을 읽어 준다 — 웹 · 앱을 거르지 않는다).
보드는 웹 · 앱을 한 장에 그린 `docs/design/webmail/ReleaseNotes.dc.html` 의 B(앱)이고, 웹 화면은 웹 디자인 README 「업데이트 내역」, 글 파일 · API 는 계약 7절이다.
- **자리**: 설정 탭 › 앱 정보 절의 앱 버전 다음 줄 "업데이트 내역"(52 · 끝에 › — 오픈소스 라이선스 줄과 같은 꼴). 누르면 목록 `/settings/release-notes`, 줄을 누르면 글 `/settings/release-notes/{id}` —
둘 다 탭 막대 없는 전체 화면이고 글은 목록 위에 쌓인다(주소로 바로 열어도 아래에 목록이 있다).
- **목록**(B2): 위 막대 64(뒤로 · "업데이트 내역" 18 · 아래 선). 줄 68(설정의 두 줄 칸과 같다) — 제목 16 · 날짜 13(회색 · 숫자 고른 폭) · ›, 줄 전체를 누른다. 최근 글부터 20개이고
더 있으면 목록 아래 [더 보기](폭 전체 48 · 회색 테두리 — 20개를 넘을 때만). 받는 동안은 그 단추만 바쁨(둥근 진행 표시 18 · 회색 글 · 다시 누르지 못한다),
더 받지 못하면 그 코드의 사전 문구로 알리고 받은 줄은 그대로다.
- **글**(B3 · B4): 위 막대는 뒤로만이고 늘 목록으로 간다. 날짜 13 · 제목 18 · 묶음(머리 15 · 점 목록 15 / 줄 간격 1.6 · 회색 점 5) 차례이고 길면 화면째 스크롤한다.
끝에 다른 글 줄 64 — "다음 글 · 날짜"(더 새 글)가 위, "이전 글 · 날짜"(더 옛 글)가 아래(목록과 같은 차례), 없는 쪽은 줄이 없다. [목록] 단추는 두지 않는다.
- **상태**: 목록 — 불러오는 중은 줄 자리의 막대(B7 — 줄 68 · 제목 막대 13 · 날짜 막대 104 × 10, 끝이 둥글다), 받지 못함은 그 자리에 사전 문구 · 문의 코드 · [다시 시도](B8 — 알림 없음),
글이 없으면 문서 그림 원과 "아직 알려 드릴 업데이트가 없습니다." 한 줄(B9). 글 — 없는 글(지워졌거나 주소가 틀림 — 404 `RES_001`)은 사전 문구 · 문의 코드 · [목록으로](B5 — 알림 없음),
그 밖의 실패는 [다시 시도](B6). 받지 못함 · 없는 글의 제목은 긴 문장이라 15, 글이 없음의 제목은 빈 메일함과 같은 크기(18)다.
- **글 규칙**: 서버가 다듬은 글 그대로 그린다(마크다운 해석 · 링크 · 그림 없음 — 대괄호 글도 꾸미지 않는다). 날짜는 달력 날짜 그대로 "2026년 11월 18일"이다(시각으로 바꾸지 않는다 — 시간대와 상관없이 그날).
새 글 표시(점 · 개수 · NEW) · 번호 · 작성자 · 조회 수는 두지 않는다.
- **구현하며 정한 것**(2026-11-10)
- 받지 못함 · 가운데 상태의 틀과 불러오는 중 막대는 메일 목록에 있던 것을 `shared/widgets/load_states.dart`(`StateLayout` · `LoadErrorView` · `LoadingSkeleton` · `SkeletonBar`)로 옮겨 메일과 함께 쓴다 —
메일 화면의 모양 · 동작은 그대로다. 목록의 막대 줄은 B7 대로 따로 그렸다(메일 목록의 막대 줄에는 사진 원이 있다).
- 글을 받는 동안의 막대(보드에 없음): 날짜 · 제목 · 머리 · 줄 넷 자리를 목록 막대와 같은 결(끝이 둥근 막대)로 잡아 둔다. 화면 낭독기는 "글 불러오는 중"(웹과 같은 글).
- 다른 글 줄을 누르면 지금 글을 그 글로 바꿔 끼운다(쌓지 않는다 — 뒤로는 늘 목록). 새 화면이 들어오는 넘김이고 스크롤은 맨 위부터다.
- 글이 없을 때의 그림은 Material `description_outlined`(접힌 모서리와 글 줄 — 웹 `file-text` 와 같은 꼴, 보드의 "article_outlined 쯤").
- [더 보기]는 받는 동안에도 단추를 그대로 두고 누름만 받지 않는다(초점이 옮겨 가지 않게 — 메일 검색의 [더 찾기]와 같다). 응답의 모양이 틀린 [더 보기](서버 쪽 문제)도 `SYS_001` 로 알린다.
- 화면 낭독기: 목록 줄은 "제목, 날짜", 다른 글 줄은 "다음 글, 날짜, 제목"(가운뎃점 대신 쉼표로 쉰다)을 단추 하나로 읽는다. 받는 동안의 [더 보기]는 꺼진 단추 "더 보기, 불러오는 중"이다
(앱에서 새로 쓴 글 — 보드는 바쁨 표시만). 위 막대의 뒤로는 "뒤로"다.
- 세션이 바뀌면(로그아웃 · 다른 직원) 목록 · 글을 처음부터 받고 앞 세션의 늦은 응답은 버린다. 목록은 화면을 열 때마다 새로 받는다(닫으면 버린다).
- **실기기에서 볼 것**: 큰 글자에서 줄 높이 · 점 위치 · [더 보기], TalkBack 차례(위 막대 → 날짜 → 제목 → 머리 → 줄 → 다른 글 줄)와 [더 보기]를 받는 동안 읽기,
다른 글로 옮길 때의 넘김 · 뒤로 가기 몸짓, 업데이트 내역 서버(feat/release-notes)와 함께 실제 글(첫 글의 날짜는 배포 날로 고친다).
### 처음 판에서 미룬 것
서식 편집 · 대용량 첨부 · 필터 · 주소록 화면 · 서명 고치기 · 계정 설정(웹에서) · 오프라인 저장 · iOS. 검색은 결과에서 여러 통 고르기를 미뤘다(읽기 화면의 같은 대화 · 목록 대화로 묶기도 — 보드 먼저).
+89
View File
@@ -1436,6 +1436,70 @@ PC 에서는 설정 › 계정 › 휴대폰 앱(A)이 QR 코드로 휴대폰에
주소록: 칸 늘리기(주소 · 생일 · 홈페이지 · 별명 · 사진), 겹친 연락처 합치기, 모르는 CSV 의 열을 손으로 맞추기, 옛 엑셀(.xls), 여러 연락처 고르기 · 한꺼번에 지우기, 사내 목록 내보내기.
서명: 받는 사람에 따라 저절로 바꾸기, 휴대폰 앱에서 서명 고르기. 부재중: 「내 연락처에게만」, 그림 넣기. 2단계 인증: 복구 코드, 바꿀 때 알림 메일 · 계정 변경 기록.
## 업데이트 내역 (2026-11-10 확정 — `ReleaseNotes`)
직원이 웹메일과 휴대폰 앱에 무엇이 새로 생기거나 바뀌었는지 읽는 게시판이다. 업데이트할 때마다 글을 하나씩 더하고, 직원은 목록에서 글을 골라 들어가 읽은 뒤 [목록]이나 뒤로 가기로 돌아온다.
글은 운영자가 저장소의 `backend/release-notes/` 에 글마다 파일 하나(`2026-11-18.md` — 이름이 날짜이자 글 주소)로 쓰고, 서버가 웹 · 앱에 같은 글을 준다. 올리는 화면 · API 는 없다(쓰는 법은 보드 C 와 그 폴더의 `README.md`).
서버 약속은 계약 7절("업데이트 내역")이다 — 목록 `GET /api/release-notes?cursor=&limit=` → `{ items: [{ id, date, title }], pageInfo }`(최근 글부터 · 20개씩),
글 `GET /api/release-notes/{id}` → `{ id, date, title, sections: [{ heading, items }], previous, next }`(없는 글 · 모양이 틀린 id 는 404 `RES_001`). 로그인해야 보이고 제한된 세션은 막힌다.
글의 원본은 화면 상수 `frontend/src/shared/constants/release-notes.ts` 이고 아래는 옮겨 적은 것이다. 글 내용(제목 · 묶음 · 줄)은 서버가 다듬은 글 그대로 그린다 — 마크다운 해석 · v-html 없음, 대괄호 글([설정] → [휴대폰 앱])도 꾸미지 않는다.
새 크기 토큰 · 밀도 예외 · 새 아이콘은 없다(메뉴 아이콘은 이미 있는 문서 그림 `file-text`). 새 글 표시(점 · 개수 · NEW)와 번호 · 작성자 · 조회 수 칸은 두지 않는다. 앱 화면(보드 B)은 앱 디자인 README 가 적는다.
### 자리 · 주소
- 설정 왼쪽 메뉴의 셋째 묶음 「정보」(계정 · 메일 다음) › 「업데이트 내역」. 주소는 목록 `/settings/info/release-notes`, 글 `/settings/info/release-notes/{id}`(id 는 글 파일 이름 — `2026-11-18`, 같은 날 둘째 글은 `2026-11-18-2`).
- 글을 보는 동안에도 메뉴의 「업데이트 내역」이 고른 상태(청록 바탕)다 — 메일함 패널의 지금 메일함과 같은 결. 화면 낭독은 목록이면 `aria-current="page"`,
글이면 링크가 가리키는 곳(목록)과 지금 쪽이 달라 `aria-current="true"` 다. 탭 제목은 목록 · 글 모두 "업데이트 내역 · ARA SYSTEM".
- `/settings/info` 는 목록으로 바꿔 끼운다 — 주소창에 글 주소 뒤 `..`(`%2E%2E` 도)를 치면 브라우저가 `/settings/info/` 로 다듬는다. 다른 묶음 주소(`/settings/account` · `/settings/mail`)는 그대로다.
- 날짜는 쓴 날짜 그대로 "2026년 11월 18일"이다. 시각이 아니라 달력 날짜라 브라우저 시간대로 바꾸지 않는다(`formatCalendarDate`).
### 목록 (`ReleaseNotes` A1)
- 머리 "업데이트 내역" + "웹메일과 휴대폰 앱에 새로 생기거나 바뀐 것을 알려 드립니다." 콘텐츠 열은 다른 설정 화면과 같은 800.
- 표 꼴 상자(로그인 기록 표와 같은 테두리 · 모서리 10): 머리 줄(32 · #FBFBFC · 12 회색 굵게) "제목" · "날짜"(136). 머리 줄은 보이는 꼴만이고, 화면 낭독은 줄 링크가 제목과 날짜를 함께 읽는다.
글마다 한 줄(48 이상 · 위아래 13): 제목(14 · 500) · 날짜(12.5 회색 · 숫자 고정폭), 최근 글부터. 제목이 길면 줄을 바꾸고 날짜는 오른쪽 칸에 둔다.
- 줄 전체가 그 글로 가는 링크다 — 마우스를 올리면 옅은 청록 바탕(#F3F8F8), 키보드 초점은 줄 안쪽 2px 청록 테두리.
- 처음 20개를 받고, 더 있으면 상자 아래(사이 16) [더 보기](44 · 흰 바탕 테두리)로 20개씩 더 받는다. 받는 동안 그 단추만 바쁨(`aria-busy` · `aria-disabled` · 흐리게)이고,
더 받지 못하면(응답의 모양이 틀려도 — `SYS_001`) 알림(사전 문구)으로 알리고 받은 줄 · [더 보기]는 그대로 둔다.
- 들어올 때마다 새로 받고, 받은 뒤 목록의 맨 위가 보이게 스크롤을 맞춘다(글에서 돌아와도 — 아래 보완 절 "스크롤 자리"). [더 보기] 뒤에는 맞추지 않는다.
### 글 (`ReleaseNotes` A2)
- 위에 [← 목록으로] 줄(52 · 아래 선 — 목록 보기에서 연 메일의 [목록으로]와 같은 꼴, 청록 굵은 글 · 화살표 15). 어떤 상태에서도 위에 남는다.
- 글을 열거나 다른 글로 옮기면 받은 뒤 글의 맨 위(날짜 · 제목)가 보이게 스크롤을 맞춘다(아래 보완 절 "스크롤 자리").
- 날짜(12.5 · 500 회색) · 제목(h1 18 · 줄 간격 1.4), 사이 4. 그 아래(사이 22) 묶음(묶음 사이 18): 머리(h2 14 굵게) + 점 목록(13 · #344054 · 줄 간격 1.6 · 줄 사이 4 · 회색 점 4).
머리가 없는 묶음은 점 목록만이다. 묶음 글은 680 까지.
- 글 끝(사이 22)의 다른 글 줄 — 위아래 선 · 680 까지. 줄(48 이상)은 이름(64 · 12.5 회색 굵게) · 제목(14 · 500) · 날짜(12.5 회색)이고 목록 줄처럼 줄 전체를 누른다.
「다음 글」(바로 더 새 글)이 위, 「이전 글」(바로 더 옛 글)이 아래(목록과 같은 차례)다. 없는 쪽의 줄은 두지 않는다(글이 하나뿐이면 칸째 없다).
- 맨 아래 [목록](44 · 흰 바탕 테두리 · 안쪽 18). 설명 한 줄은 목록 화면에만 둔다.
### 상태 (`ReleaseNotes` A1 · A2)
- 목록 · 불러오는 중: 머리 줄은 두고 줄 자리를 막대 넷(제목 막대 54 · 38 · 62 · 46%, 날짜 막대 96)으로 잡아 둔다 — 메일 목록을 불러올 때와 같은 막대. 화면 낭독 "업데이트 내역 불러오는 중".
- 목록 · 받지 못함: 상자 자리에 사전 문구 · [다시 시도] · 문의 코드(다른 설정 화면의 `MailLoadError`). 알림은 띄우지 않는다.
- 목록 · 글이 없을 때: 상자와 [더 보기] 없이 문서 그림 36 · "아직 알려 드릴 업데이트가 없습니다." 한 줄(`StateMessage`).
- 글 · 받는 동안: 글 자리를 막대로 잡아 둔다(날짜 · 제목 · 묶음 자리 — 보드에는 그리지 않았다). 화면 낭독 "글 불러오는 중".
- 글 · 없는 글(지워졌거나 주소가 틀림 — 404 `RES_001`): 알림 없이 사전 문구("찾을 수 없습니다. 이미 지워졌거나 옮겨졌을 수 있습니다.") · [목록으로] · 문의 코드.
파일 이름(날짜)을 고치면 글 주소가 바뀌어, 그전 주소로 들어오면 이 화면이다.
- 글 · 받지 못함(그 밖의 실패): 사전 문구 · [다시 시도] · 문의 코드. 알림은 띄우지 않는다.
### 좁은 폭 (`ReleaseNotes` A3)
- 화면 폭이 840보다 좁으면(레일 72 · 메뉴 208 · 글 칸 560 이 한 줄에 들지 않으면) 메뉴가 위로 올라가고 목록 · 글은 그 아래로 내려간다 — 지금 다른 설정 화면과 같다(휴대폰 브라우저에서는 메뉴 아래로 내려야 글이 나온다).
- **목록 칸이 480보다 좁으면**(화면 폭이 아니라 그 칸의 폭) 머리 줄을 감추고 날짜를 제목 아래로 내린다(사이 2 · 줄 안쪽 12 16) — 이것만 새로 정한 것이다.
글 화면은 넓은 화면과 같은 차례 · 글자 크기로 한 칸에 흐른다(좌우 여백 32 그대로 — 폭 360 에서 글 폭 224).
### 글 규칙 (보드 C 요약 — 쓰는 법의 원본은 `backend/release-notes/README.md`)
- 글 하나가 파일 하나다(`YYYY-MM-DD.md`, 같은 날 둘째부터 `YYYY-MM-DD-2.md`). 첫 줄 `# 제목`(꼭) · `## 묶음 머리` · `- 한 줄`, 빈칸 두 개로 시작한 줄은 앞 줄에 띄어쓰기 하나로 이어 붙고,
`<!-- 메모 -->` 는 화면에 나오지 않는다. 머리 없이 바로 `- ` 를 쓰면 머리 없는 묶음이다.
- 길이는 제목 100자 · 묶음 머리 40자 · 한 줄 500자. 굵게 · 링크 · 그림은 없다 — 단추 · 메뉴 이름은 화면 글처럼 `[설정] → [휴대폰 앱]`.
- 글도 위 「화면 문구 — 사용자에게 말하듯」을 따른다(직원이 읽는 글이다 — 정중하고 부드럽게, 만든 쪽 말 없이). 웹과 앱이 같은 글을 보여 주니 어디 이야기인지는 묶음 머리로 가른다.
### 처음 판에서 미룬 것
새 글 알림(안 읽은 표시 · 한 번 뜨는 안내 줄 — 뱃지를 쓰지 않는 원칙이라 배포 때 직원 공지 메일에 [설정] → [업데이트 내역]을 적는다), 글 안의 링크 · 그림 · 굵게, 웹 · 앱 거르기, 검색.
## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다)
- 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다).
@@ -2002,3 +2066,28 @@ PC 에서는 설정 › 계정 › 휴대폰 앱(A)이 QR 코드로 휴대폰에
꺼 둔 채로는 넘어도 회색이다(켤 때만 센다).
- 부재중 양식은 편집기와 함께 지연 로딩한다 — 청크를 받는 동안은 칸이 비어 있고, 못 받으면 서명 설정과 같은 사전 문구 + [다시 시도].
- 회사 밖 칸을 쓴 적 없으면 제목 · 내용을 null 로 보낸다(빈 글을 남기지 않는다 — 서버도 빈 글을 null 로 저장한다).
### 업데이트 내역 구현하며 정한 보완 (2026-11-10 — 캔버스에는 아직 없다)
데이터 · 경로는 위 "업데이트 내역" 머리말(계약 7절)과 같다.
- **기록**: 다른 글 줄은 기록을 바꿔 끼운다 — 몇 글을 넘겨 봐도 뒤로 가기는 목록으로 간다(메일 읽기 화면의 이전 · 다음과 같다).
[← 목록으로] · [목록] · 없는 글의 [목록으로]는 바로 앞 기록이 목록이면 뒤로 가기, 아니면 목록으로 바꿔 끼운다(메일의 [목록으로]와 같다). 링크라 새 탭으로도 열 수 있다.
- **[← 목록으로] 줄은 스크롤해도 위에 남는다**(읽기 화면의 도구 줄과 같다 — 긴 글을 읽다가도 바로 돌아간다).
그 줄 밑에 초점이 가리지 않게(WCAG 2.4.11) 글 칸의 제목 · 링크 · 단추는 줄 높이 + 8(61)만큼 띄워 스크롤한다(`scroll-margin-top`).
- **스크롤 자리**(2026-11-10 실사용 시험): 같은 문서 안의 이동이라 앞 화면의 스크롤 자리가 남아, 목록을 내린 채 글을 열면 날짜 · 제목이 위로 지나가 빈 글처럼 보였고
글에서 목록으로 돌아오면 목록의 맨 아래 근처가 보였다. 글을 열거나 다른 글로 옮기면 받은 뒤 글의 맨 위가, 목록은 받은 뒤 목록의 맨 위가 보이게 맞춘다
(`composables/useSettingsPageScroll.ts` — 라우터 전역 `scrollBehavior` 는 메일 등 다른 화면의 뒤로 가기까지 바꿔 쓰지 않는다). 스크롤하는 것이 폭마다 달라 둘 다 맞춘다:
넓은 화면(1120 이상)은 글 칸을 맨 위로, 좁은 화면은 문서를 — 맨 위로 올려도 제목이 화면 안이면 맨 위로(메뉴가 옆에 있는 폭), 아니면(위에 메뉴가 긴 휴대폰 · 낮은 창) 글 칸의 위 끝을 화면 위 끝에.
문서가 맨 위에 있으면(처음 연 화면) 그대로 둔다 — 다른 설정 화면처럼 휴대폰에서는 메뉴부터 보인다. 없는 글 · 받지 못함도 그 안내가 보이게 같은 규칙으로 맞춘다.
- **메뉴가 고른 상태**: 목록 · 글 라우트를 화면 없는 상위 라우트로 묶었다(`router/routes.ts`). 설정 메뉴는 그 항목을 고른 상태면 청록 바탕을 그리고,
`aria-current` 는 정확히 그 화면이면 `page` · 아래에 묶은 화면이면 `true` 다(RouterLink 의 `isExactActive` · `isActive`). 다른 메뉴 항목은 아래에 묶은 화면이 없어 그대로다.
- **초점**: 다른 글 줄을 누르면 그 줄이 사라져 초점을 잃는다 — 새 글을 받아 맨 위를 보이게 맞춘 뒤 제목(h1)으로 옮긴다(스크롤 없이 · 키보드로 왔을 때만 고리를 그린다).
[더 보기]로 마지막 쪽을 받아 단추가 사라졌으면 새로 붙은 첫 줄로 옮긴다(단추가 남으면 초점도 그대로).
- **주소의 글 id 가 글 id 꼴이 아니면**(`2026-11-18` · `2026-11-18-2` 꼴이 아님 · 32자 넘음) 묻지 않고 없는 글 화면이다(문의 코드 없음).
"." · ".." 같은 경로 조각은 브라우저가 주소를 다듬으며 다른 경로(목록 API 등)로 보내서 꼴부터 거른다.
- **목록을 받지 못했는데 404 여도**(그 경로가 없는 옛 서버) 빠져나갈 곳이 없어 [다시 시도]다.
- **응답의 모양**: 계약의 모양 그대로일 때만 읽는다 — 줄 · 묶음 · 다른 글 하나라도 틀리면(날짜가 실제 있는 달력 날짜가 아님 포함) 받지 못함(`SYS_001` 사전 문구)으로 본다(앱과 같은 규칙).
처음 목록 · 글은 칸 자리에 그리고(알림 없음), [더 보기]는 다른 실패와 같은 길로 알린다(받은 줄 · [더 보기]는 그대로).
- **다른 글 칸이 480보다 좁으면** 날짜를 제목 아래로 내린다(보드 A3 에는 그리지 않았다 — 목록 줄과 같은 기준). 이름 칸 64 는 그대로다.
- 목록 · 글은 들어올 때마다 새로 받는다(배포하며 글을 더하거나 고친다). 세션이 바뀌면(로그아웃 · 만료 · 다른 계정) 늦게 온 응답을 버리고 받아 둔 목록 · 글을 지운다.
+1 -1
View File
@@ -1040,7 +1040,7 @@ a{color:#0E7C86}a:hover{color:#0A5F67}
<li>웹은 설정 메뉴의 셋째 묶음 정보 아래 업데이트 내역입니다(계정, 메일 다음). 아이콘은 이미 있는 문서 그림(file-text)입니다.</li>
<li>웹 주소는 목록 /settings/info/release-notes, 글 /settings/info/release-notes/{id}입니다. id는 파일 이름(예 2026-11-18)이고, 글을 보는 동안에도 메뉴의 업데이트 내역이 고른 상태입니다. 탭 제목은 둘 다 업데이트 내역입니다(다른 화면처럼 라우트마다 정한 이름).</li>
<li>앱은 설정 탭 앱 정보 절의 앱 버전 다음 줄입니다. 목록 /settings/release-notes, 글 /settings/release-notes/{id}이고 둘 다 탭 막대 없는 전체 화면입니다.</li>
<li>목록 GET /api/release-notes?cursor=&limit= 는 { items: [{ id, date, title }], pageInfo }입니다. 최근 글부터, 기본 20, 최대 50.</li>
<li>목록 GET /api/release-notes?cursor=&limit= 는 { items: [{ id, date, title }], pageInfo }입니다. 최근 글부터, 기본 20, 최대 100.</li>
<li>글 GET /api/release-notes/{id} 는 { id, date, title, sections: [{ heading, items }], previous, next }입니다. previous(더 옛 글)와 next(더 새 글)는 { id, date, title } 또는 null이고, 없는 글은 404 RES_001입니다. 올리는 API는 없습니다.</li>
<li>웹과 앱이 같은 목록이고 거르지 않습니다. 새 글 표시(점, 개수, NEW)와 번호, 작성자, 조회 수 칸은 두지 않습니다.</li>
</ul>
+81
View File
@@ -8,6 +8,7 @@
- 성공 `{ "success": true, "data": … }` / 실패 `{ "success": false, "error": { "code", "message", "traceId" } }`
- 날짜·시각은 ISO 8601 UTC 문자열, 목록은 `{ items, pageInfo }`
(예외: 7절 업데이트 내역의 `date` 는 시각이 아닌 달력 날짜 글 `YYYY-MM-DD` 다 — `new Date()` 로 읽지 않고 시간대를 바꾸지 않은 채 그대로 쓴다)
- 이 단계에서 쓰는 에러 코드: `AUTH_001` `AUTH_002`(클라이언트 전용) `AUTH_003` `AUTH_004` `AUTH_005` `VAL_001` `RES_001` `REQ_001` `FILE_001` `FILE_002` `MAIL_001` `MAIL_002` `MAIL_003` `MAIL_004` `MAIL_005` `MAIL_006` `MAIL_007` `MAIL_008` `MAIL_009` `MAIL_010` `MAIL_011` `MAIL_012` `MAIL_013` `MAIL_014` `MAIL_015` `CONTACT_001` `CONTACT_002` `SYS_001`
(11단계 — 4절 "대용량 첨부"가 `FILE_003` ~ `FILE_008` · `MAIL_016` 을 더한다. 12단계 — 5절 "계정 보안"이 `AUTH_006` ~ `AUTH_009` 를, 13단계 — 6절 "전환 준비"가 `AUTH_010` 을 더한다)
- **적용 범위: 웹(브라우저)은 HttpOnly 쿠키뿐이고 토큰을 본문으로 주지 않는다.**
@@ -3288,3 +3289,83 @@ interface FileUploadCreate {
웹메일 관리자 화면(공통 기반 때), 비밀번호 스스로 재설정(복구 연락처 필요), 직원이 켜진 채 새 휴대폰으로 바꾸기(관리자의 「2단계 해제」로 한다),
공용 주소로 보내기 · 공유 메일함(Group), 새 기기 로그인 알림 메일, 관리자 열람 기능, 주소 바꾸기, 모바일 앱.
---
## 7. 업데이트 내역 (2026-11-10 정함 · 서버)
> 직원이 웹메일과 휴대폰 앱에서 무엇이 새로 생기거나 바뀌었는지 읽는 게시판이다. 업데이트할 때마다 글을 하나씩 더한다(목록에서 글을 골라 들어가 읽는다).
> 글은 운영자가 저장소의 `backend/release-notes/` 에 **글마다 파일 하나**로 쓴다 — 올리고 고치는 API 는 없다(파일을 더해 커밋 → 배포). 웹과 앱이 같은 글을 보고 거르지 않는다(어디 이야기인지는 묶음 머리가 말한다).
> 보드 `docs/design/webmail/ReleaseNotes.dc.html`(웹 A · 앱 B · 쓰는 법 C), 서버는 `modules/release-notes`. 새 에러 코드 · DB 표 · env 는 없다.
### 타입
```ts
interface ReleaseNoteSummary {
id: string // 글 파일 이름에서 .md 를 뗀 것 — "2026-11-18", 같은 날 두 번째 글부터 "2026-11-18-2" …
date: string // 달력 날짜 "YYYY-MM-DD"(파일 이름에서) — 시각이 아니다
title: string // 첫 줄의 # 제목 — 100자까지
}
interface ReleaseNoteSection {
heading: string | null // ## 머리 — 40자까지. 머리 없이 쓴 묶음은 null(글의 맨 앞에만 온다)
items: string[] // - 한 줄 — 500자까지, 하나 이상
}
interface ReleaseNote extends ReleaseNoteSummary {
sections: ReleaseNoteSection[] // 파일에 쓴 차례 — 하나 이상
previous: ReleaseNoteSummary | null // 바로 더 옛 글(목록에서 이 글 아래) — 없으면 null
next: ReleaseNoteSummary | null // 바로 더 새 글(목록에서 이 글 위) — 없으면 null
}
```
### 엔드포인트
| 메서드 · 경로 | 요청 | 성공 | 주요 실패 |
| --- | --- | --- | --- |
| `GET /api/release-notes` | `cursor` · `limit`(기본 20, 최대 100) | 200 `{ items: ReleaseNoteSummary[], pageInfo }` — 최근 글부터, `total` 은 글 수 | 400 `VAL_001`(틀린 커서 · limit 범위 밖 · 모르는 쿼리) |
| `GET /api/release-notes/{id}` | — | 200 `ReleaseNote` | 404 `RES_001` — 없는 글 · 모양이 틀린 id |
- 두 경로 모두 로그인 필요(웹 쿠키 · 앱 Bearer — `JwtAuthGuard`), 없으면 401 `AUTH_001`. 제한된 세션(6절)은 403 `AUTH_010`(허용 목록에 넣지 않는다 — 그 단계를 마치면 본다).
요청 수는 기본 한도(경로마다 IP 당 1분 100회 — 넘으면 429 `REQ_001`). 응답은 `Cache-Control: private, no-store`(글을 고친 배포가 바로 보이게).
- **차례**: 날짜 내림차순, 같은 날은 뒤 번호가 먼저(`2026-11-18-2` → `2026-11-18`). `previous` · `next` 는 이 차례의 아래 · 위다(목록 화면과 같은 차례).
- `cursor` 는 불투명 문자열(위치)이다. 차례 사이에 글을 더하면 다음 쪽에서 한두 개가 겹치거나 빠질 수 있다(3절과 같다).
- **id**: 서버는 모양(`YYYY-MM-DD` 또는 `YYYY-MM-DD-N` — N 은 2~99, 32자까지)만 보고 읽어 둔 목록에서 찾는다 — 파일 경로로 쓰지 않는다.
모양이 틀린 id 도 400 이 아니라 404 `RES_001` 이다(손으로 고친 주소도 "없는 글"). 파일 이름(날짜)을 고치면 글 주소도 바뀌어 옛 주소는 404 다.
- **글은 서버가 다듬은 그대로다** — 앞뒤 빈칸을 지우고, 안의 빈칸 · 탭 여럿은 하나로, 이어 쓴 줄은 띄어쓰기 하나로 붙이고, NFC 로 모은다.
마크다운(굵게 · 링크 꼴)은 해석하지 않고 쓴 글 그대로다 — 클라이언트도 글로만 그린다(v-html · 마크다운 해석 없음). 단추 · 메뉴 이름은 글에 `[설정] → [휴대폰 앱]` 처럼 쓴다.
- **날짜(`date`)는 달력 날짜 글이다** — 화면은 시간대를 바꾸지 않고 "2026년 11월 18일"로 쓴다. `new Date('2026-11-18')` 로 읽지 않는다(UTC 자정이라 음수 시간대에서 하루 앞이 된다).
### 글 파일 (`backend/release-notes/`)
- 글 하나 = 파일 하나. 이름 `YYYY-MM-DD.md`(글의 날짜 — 있는 날짜여야 한다: `2026-02-30` 은 틀림), 같은 날 둘째부터 `YYYY-MM-DD-2.md` … `-99`(`-1` 은 없다 — 첫 글은 번호 없이). 이름이 글 id 다.
- 폴더에는 `README.md`(쓰는 사람용 안내 — 서버는 읽지 않는다)와 글 파일만 둔다. 그 밖의 것(다른 이름 · 폴더 · 링크)은 글이 아니다.
- 파일 안 — 마크다운의 아주 작은 일부(글만). UTF-8, 256KB 까지:
| 줄 | 뜻 |
| --- | --- |
| `# 제목` | 첫 글 줄(앞의 빈 줄 · 메모는 된다) — 꼭 있어야 하고 한 번만. 100자까지 |
| `## 머리` | 묶음 시작 — 40자까지. 아래에 줄이 하나는 있어야 한다 |
| `- 한 줄` | 그 묶음의 한 줄 — 500자까지(이어 붙인 뒤). 머리 없이 바로 쓰면 머리 없는 묶음(`heading: null` — 제목 바로 아래에만) |
| 빈칸 두 개 이상(또는 탭)으로 시작한 줄 | 앞 `- ` 줄에 띄어쓰기 하나로 이어 붙는다(빈 줄 · 메모를 사이에 두어도). 들여 쓴 `- `(하위 목록)는 틀림 |
| `<!-- … -->` | 메모 — 화면에 나오지 않는다. 줄 앞에서 시작한다(앞 빈칸은 괜찮다 · 글 뒤에 붙이지 않는다). 여러 줄도 된다. 닫은 뒤 같은 줄에 글을 쓰지 않는다 |
| 빈 줄 | 어디든 |
그 밖의 줄(맨 글 · `###` · `* ` · `1. ` · 표 · 인용 · 한 칸 들여 쓴 줄 등), 글 안의 `<!--`, 보이지 않는 글자(제어 문자 · 줄 구분 문자)는 틀림. 글 하나에 줄이 하나는 있어야 한다.
글자 수는 앞뒤 빈칸을 지운 뒤의 JS 문자열 길이(UTF-16 — 웹의 `.length` 와 같다). 줄 끝의 CR(CRLF) · 앞의 BOM 은 지운다.
- **읽기**: 서버는 기동 때 한 번, 그 뒤로는 요청 때 폴더를 본다 — 파일 이름 · 크기 · 바뀐 시각이 앞과 같으면 앞서 읽은 결과를 쓴다(개발 서버는 다시 띄우지 않아도 새 글이 보인다).
운영은 이미지 안의 폴더(`/app/release-notes` — `backend/Dockerfile` 이 넣는다)라 배포해야 바뀐다. 함께 온 요청은 한 번 읽은 결과를 나눈다.
글 파일은 한 번 열어(링크를 따라가지 않는다) 그 열린 파일로 일반 파일인지 · 크기를 보고 256KB + 1 바이트까지만 읽는다.
- **틀린 글**: 시험(CI — `release-notes.content.spec.ts` 가 실제 폴더를 엄격하게 읽는다)이 실패한다. 그래도 배포되면 서버는 그 파일만 건너뛰고
바뀔 때마다 error 로그를 한 번 남긴다(파일 이름 · 줄 번호 · 까닭 — 줄 차례로 앞의 20개, 글 내용은 싣지 않는다). 기동 때 읽으므로 배포 직후의 로그에 보인다.
기동은 막지 않는다(기동 때 읽지 못해도 — 요청 때 다시 읽는다). 폴더가 없으면 빈 목록 + warn 한 번.
### 클라이언트
- **웹**: 설정 메뉴의 셋째 묶음 "정보" › "업데이트 내역" — 목록 `/settings/info/release-notes`, 글 `/settings/info/release-notes/{id}`(글을 보는 동안에도 메뉴 항목이 고른 상태). 404 `RES_001` 은 알림 없이 "없는 글" 화면.
- **앱**: 설정 탭 › 앱 정보 › "업데이트 내역" — 목록 `/settings/release-notes`, 글 `/settings/release-notes/{id}`(둘 다 탭 막대 없는 전체 화면). 앱은 옛 판이어도 서버의 새 글을 본다(새 APK 를 내지 않아도 된다).
- 화면 · 상태(불러오는 중 · 받지 못함 · 글 없음 · 없는 글)는 디자인 README(웹 · 앱)의 "업데이트 내역" 절.
### 이 단계에서 하지 않는 것
올리고 고치는 API · 화면, 새 글 표시(점 · 개수 · NEW — 뱃지를 쓰지 않는다), 웹 · 앱 거르기, 글 안의 링크 · 그림 · 굵게, 검색, 번호 · 작성자 · 조회 수.