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
+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 — 뱃지를 쓰지 않는다), 웹 · 앱 거르기, 글 안의 링크 · 그림 · 굵게, 검색, 번호 · 작성자 · 조회 수.