generated from ttipo/project-base
docs: 휴대폰 앱 사내 배포를 계약 · 운영 · 앱 · 디자인 문서와 스킬에 적는다
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 12m41s
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 22m13s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m13s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m13s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m5s
CI (Flutter) / Flutter (analyze · test) (push) Successful in 21m49s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 13m13s
CI / Frontend (lint · type-check · test · build) (push) Successful in 12m54s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 1m47s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 10m26s
CI / Frontend (lint · type-check · test · build) (pull_request) Successful in 12m41s
CI (Flutter) / Flutter (analyze · test) (pull_request) Successful in 22m13s
CI / Backend (lint · type-check · test · e2e · build) (pull_request) Successful in 13m13s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (pull_request) Successful in 2m13s
CI / 에러 코드 SSOT 동기화 검사 (pull_request) Successful in 11m5s
CI (Flutter) / Flutter (analyze · test) (push) Successful in 21m49s
CI / Backend (lint · type-check · test · e2e · build) (push) Successful in 13m13s
CI / Frontend (lint · type-check · test · build) (push) Successful in 12m54s
CI / Infra (compose · Dockerfile · nginx · 운영 스크립트) (push) Successful in 1m47s
CI / 에러 코드 SSOT 동기화 검사 (push) Successful in 10m26s
계약 1절 "휴대폰 앱 받기"와 6절 운영 도구 명령, 서버 B 에 APK 올리는 차례와 직원 안내(링크 없이), 앱의 서명 키 · release 빌드 · 올리기 전 확인, 웹 화면 절, 스킬 표, App1Shell 보드의 앱 정보 줄. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #22.
This commit is contained in:
@@ -289,6 +289,41 @@
|
||||
누르면 늘 메일 탭 받은편지함이다 — `kind` 로 가르지 않는다(`session-ended` 를 눌렀으면 받은편지함의 첫 요청이 세션 끝을 알아 로그인 화면으로 간다).
|
||||
작은 아이콘 · 색은 서버가 싣지 않는다 — 앱 매니페스트의 기본값(`ic_stat_ara` · #0E7C86)이고, 채널 `new_mail`(이름 "새 메일" · 중요도 높음)은 앱이 만든다.
|
||||
|
||||
### 휴대폰 앱 받기 — 사내 배포 (2026-11-08 정함 · 서버)
|
||||
|
||||
> 2026-11-08 사용자가 정했다 — 앱은 스토어(Play 내부 테스트) 없이 **직접 빌드해 서명한 APK 를 포털에 로그인한 직원에게만** 나눠 준다.
|
||||
> 받는 곳은 웹의 설정 › 계정 › 「휴대폰 앱」과 휴대폰 받기 화면 `/app`(보드 `docs/design/webmail/SettingsMobileApp.dc.html`), 앱의 업데이트 화면 · 설정 › 앱 정보 「새 버전 받기」는 바깥 브라우저로 `<포털>/app` 을 연다.
|
||||
> 위 app-config 계약은 그대로다(받는 곳 주소를 싣지 않는다 — 앱이 API_URL 에서 `/api` 를 뗀 출처로 만든다). 서버는 `modules/app-releases` 가 이 절대로 만들었다.
|
||||
|
||||
| 경로 | 응답 |
|
||||
| --- | --- |
|
||||
| `GET /api/app-releases/android/latest` | 200 `{ version: "1.0.0", sizeBytes: 32505856, releasedAt: "2026-11-18T01:23:45.000Z" }` · `Cache-Control: private, no-store` / 404 `RES_001` — 올린 판이 없다 |
|
||||
| `GET /api/app-releases/android/latest/file` (HEAD 도) | 200 APK 바이트 (Range 206 · 416) / 404 `RES_001` — 올린 판이 없다 · 그 파일이 없다 / 429 `REQ_001` — 앞단 동시 연결 상한 |
|
||||
|
||||
- 두 경로 모두 로그인 필요(웹 쿠키 · 앱 Bearer — `JwtAuthGuard`), 없으면 401 `AUTH_001`. 제한된 세션(6절)은 403 `AUTH_010`(허용 목록에 넣지 않는다). 요청 수는 기본 한도(경로마다 IP 당 1분 100회). 새 오류 코드는 없다.
|
||||
- **최신** = 가장 늦게 올린 판(올린 시각 차례 — 버전 번호가 아니다). 옛 APK 를 다시 올리면 그것이 최신이 된다(되돌리기 길 — 단 더 높은 판을 이미 깐 휴대폰은 안드로이드가 낮은 판으로 덮어 깔지 않는다).
|
||||
`releasedAt` 은 올린 때(ISO 8601 UTC), `sizeBytes` 는 파일 바이트 수다.
|
||||
- **파일 응답**은 대용량 첨부 받기(4절 "파일 응답")와 같은 머리글이고 형식만 APK 다 — 서버 `shared/utils/download-headers.ts` 한 곳:
|
||||
`Content-Type: application/vnd.android.package-archive` · `Content-Disposition: attachment; filename="ARA-SYSTEM-<버전>.apk"` · `X-Content-Type-Options: nosniff` ·
|
||||
`Content-Security-Policy: default-src 'none'; sandbox` · `Cache-Control: private, no-store` · `Referrer-Policy: no-referrer` · `X-Robots-Tag: noindex` · `Accept-Ranges: bytes` ·
|
||||
강한 `ETag: "<판 id>"` · `Last-Modified`(올린 때) · `X-Accel-Buffering: no`. Range 하나면 206, 여럿 · 형식 오류면 200 전체, 처음이 크기 이상이면 416(본문 없음 — `Content-Range: bytes */<크기>`),
|
||||
`If-Range` 가 ETag · Last-Modified 와 다르면 200 전체. HEAD 는 머리글만. 실패는 머리글을 쓰기 전에 표준 실패 형식(JSON)이다.
|
||||
파일 이름 · 경로는 서버가 만든 값뿐이다 — 내려받는 이름은 검사한 버전으로 만들고, 디스크의 이름은 UUID 다.
|
||||
- 표에는 판이 있는데 볼륨에 파일이 없거나 크기가 표와 다르면 파일은 404 `RES_001`(정보는 200 그대로)이고 서버가 error 로그를 남긴다 — 운영자가 다시 올린다(운영 도구 「휴대폰 앱 목록」이 "파일 없음"을 보인다).
|
||||
"올린 판이 없음"의 404 는 화면이 설정을 열 때마다 묻는 예상된 상태라 서버 로그는 debug 다.
|
||||
- **동시 연결 상한(앞단 nginx)**: 파일 `…/file`(HEAD 포함)은 대용량 첨부의 앱 안 받기와 같은 그릇으로 센다 — 클라이언트 주소(`X-Forwarded-For` 의 마지막 칸)마다 30 · 서버 전체 200,
|
||||
처리 중인 요청만(4절 "운영 확인"). 넘으면 백엔드에 닿기 전에 429 `REQ_001`(nginx 오류 JSON — `message` 는 빈 값, `traceId` 는 nginx 요청 ID). 정보 경로는 세지 않는다.
|
||||
앞단은 이 응답만 버퍼링 · 압축을 끄고 `X-Accel-Buffering: no` 를 외부 프록시로 넘긴다(느린 휴대폰마다 APK 사본을 임시 파일에 쌓지 않게). 경로는 대소문자 · `%2F` · 겹 `/` 를 가리지 않고 같은 설정을 탄다.
|
||||
- **올리기 — API 가 없다**(서버 B 의 운영 도구 「휴대폰 앱 올리기」, 6절 "운영 도구" — 차례는 mail/README "휴대폰 앱 올리기"):
|
||||
운영자가 `sudo docker cp` 로 백엔드 컨테이너 안에 넣은 APK → 검사(일반 파일 · 이름 `.apk` · zip 머리 `PK\x03\x04` · 1KB~200MB) · SHA-256 → 버전(`숫자.숫자.숫자` — 위 앱 알림 등록의 `appVersion` 과 같은 규칙,
|
||||
app-config 의 최소 판보다 낮으면 올리지 않는다) → 업로드 볼륨(`UPLOAD_DIR`) 아래 `app-releases/<UUID>.apk`(임시 이름에 쓰고 디스크에 내려쓴 뒤 이름을 바꾼다 — 쓰면서 다시 센 SHA-256 이 다르면 올리지 않는다)
|
||||
→ 표 `app_releases` 에 한 줄 → 실행 기록(`ops_audit_log` — `app-release-upload`). 파일은 매일 백업(업로드 볼륨)에 든다.
|
||||
- 표 `app_releases`(마이그레이션 `AddAppReleases`): id(UUID — ETag) · platform(지금 `android`) · version · size_bytes · sha256(소문자 16진수) · file_key(UUID — 디스크 이름) · uploaded_by(운영자 이름) · created_at(올린 때).
|
||||
지우는 명령은 없다 — 옛 판은 표와 볼륨에 남는다. 직원 주소로 나뉜 표가 아니라 퇴사 마무리와 상관없다.
|
||||
- **클라이언트 — 웹**: 404 `RES_001` 은 "아직 받을 수 있는 앱이 없습니다" 칸으로(알림 끔). 내려받기는 첨부처럼 이동 전에 세션을 확인한다(`confirmSession()`).
|
||||
- **클라이언트 — 앱**: 이 API 를 부르지 않는다 — 바깥 브라우저로 `<포털>/app` 을 연다.
|
||||
- **이 단계에서 하지 않는 것**: 앱 안에서 받아 바로 설치(REQUEST_INSTALL_PACKAGES), 최소 판이 아닌 새 판 알림, 아이폰(Ad Hoc · TestFlight), 올린 판 지우기.
|
||||
|
||||
---
|
||||
|
||||
## 2. 메일 (모두 로그인 필요 — 없으면 401 `AUTH_001`)
|
||||
@@ -2355,11 +2390,12 @@ interface FileUploadCreate {
|
||||
|
||||
### 운영 확인 (이 계약이 기대하는 것 — 고치는 일은 release-ops)
|
||||
|
||||
- 앞단 nginx: `/api/file-uploads` 본문 17MB · 읽기 90초. `/api/public-links/` · `/api/messages/*/large-files/` 는 응답 버퍼링을 끄고 `X-Accel-Buffering` 을 외부로 넘긴다
|
||||
- 앞단 nginx: `/api/file-uploads` 본문 17MB · 읽기 90초. `/api/public-links/` · `/api/messages/*/large-files/` · 휴대폰 앱 받기 `/api/app-releases/*/file`(1절 "휴대폰 앱 받기")은 응답 버퍼링을 끄고 `X-Accel-Buffering` 을 외부로 넘긴다
|
||||
(기본값이면 느린 받는 사람마다 임시 파일을 1GB 까지 만든다 — 실시간 알림과 같은 방법). `/s/` 는 `index.html` + `Cache-Control: no-cache` · `X-Robots-Tag: noindex, nofollow` · `Referrer-Policy: no-referrer`.
|
||||
접근 로그의 경로 토큰 가리기, 공개 호스트의 server(위 "공개 호스트").
|
||||
**내려받기 동시 연결 상한**(2026-10-30 리뷰 — 버퍼링을 끈 느린 받기는 연결 하나가 nginx 연결 둘과 백엔드 fd 를 잡아, 토큰 하나로 프론트 nginx 를 바닥낼 수 있다): `limit_conn` 으로
|
||||
공개 받기 IP 마다 10 · 앱 안 받기 IP 마다 30(사무실 NAT) · 서버 전체 공개 받기 400 · 앱 안 받기 200, 넘으면 429 `REQ_001`(nginx 오류 JSON). `worker_connections` 는 이 수와 SSE · 포털을 함께 버티게.
|
||||
공개 받기 IP 마다 10 · 앱 안 받기 IP 마다 30(사무실 NAT) · 서버 전체 공개 받기 400 · 앱 안 받기 200, 넘으면 429 `REQ_001`(nginx 오류 JSON).
|
||||
휴대폰 앱 받기(`/api/app-releases/*/file` — 1절 "휴대폰 앱 받기")는 앱 안 받기와 같은 그릇으로 센다(둘 다 로그인한 직원의 받기 — 합쳐 IP 마다 30 · 전체 200). `worker_connections` 는 이 수와 SSE · 포털을 함께 버티게.
|
||||
전체 상한을 경로마다 따로 둔다(2026-10-30 리뷰 — 처음 판은 두 경로가 500 을 함께 써, 바깥 IP 50개(IPv6 면 /64 하나)로 직원의 앱 안 받기가 모두 429 가 됐다).
|
||||
경로의 대소문자 변형(`/FILES/` · `/api/Messages/…`)도 같은 상한 · 같은 흘려보내기를 탄다(2026-10-30 리뷰 — 처음 판은 글자 하나로 상한 · `X-Accel-Buffering` 넘김을 비켜 갔다).
|
||||
구현(2026-10-30): 클라이언트 주소는 `X-Forwarded-For` 의 마지막 칸(외부 프록시가 이어 붙인 값 — 없으면 접속 주소). 워커마다 `worker_connections` 4096 · 열린 파일 8192.
|
||||
@@ -2873,7 +2909,7 @@ interface FileUploadCreate {
|
||||
- 실행 기록 표(`ops_audit_log`): 언제 · 누가 · 명령 · 대상 · 결과. 비밀은 남기지 않는다.
|
||||
- 표준 입력이 터미널(TTY)이 아니면 운영자 이름 · 비밀번호를 묻기 전에 멈춘다(`docker exec -it` 로 실행). 입력 기록(↑)에 비밀번호를 남기지 않는다.
|
||||
자동 시험만 `OPS_ALLOW_NO_TTY=1` 로 파이프 입력을 받는다 — 사람은 쓰지 않는다.
|
||||
- 명령
|
||||
- 명령 (열세 개 — 메뉴 차례 그대로. 12 · 13 은 2026-11-08 사내 배포 때 더했다)
|
||||
|
||||
| 명령 | 하는 일 |
|
||||
| --- | --- |
|
||||
@@ -2888,6 +2924,8 @@ interface FileUploadCreate {
|
||||
| 현황 | 직원마다 잠금 · 2단계 · 앱 비밀번호 수 · 마지막 로그인(웹메일 · 앱 — 2026-11-06 부터 앱도 센다) · 메일함 사용량 · 이전 중(끝나는 날) |
|
||||
| 정책 · 설정 | 2단계 필수(켜기 · 시작일 · 예외), 문의 연락처 |
|
||||
| 메일 이전 (2026-11-02 더함) | 이전 현황(이전 중인 계정 · 끝나는 때 · 메일함 사용량) · 이전용 비밀번호 다시 만들기(한 사람 또는 이전 중인 계정 모두 — 잃었거나 30일을 늘릴 때) · 이전 마치기(한 사람 또는 이전 중인 계정 모두 → 직원용 임시 비밀번호) |
|
||||
| 휴대폰 앱 올리기 (2026-11-08 더함) | 운영자가 `docker cp` 로 백엔드 컨테이너에 넣은 APK 경로 → 검사(일반 파일 · `.apk` · zip 머리 · 1KB~200MB) · SHA-256 → 버전(파일 이름의 `x.y.z` 가 기본값 · app-config 의 최소 판보다 낮으면 하지 않음 · 지금 최신보다 낮거나 같으면 y/N) → 바뀔 것(버전 · 크기 · SHA-256) y/N → 업로드 볼륨 `app-releases/` 에 임시 이름 → 이름 바꾸기 → 표 → 실행 기록. 끝에 SHA-256 을 한 번 더 보인다(빌드한 파일과 견준다). 실패하면 반쯤 쓴 파일을 남기지 않는다 — 1절 "휴대폰 앱 받기" |
|
||||
| 휴대폰 앱 목록 (2026-11-08 더함) | 최근 판 10개 — 버전 · 크기 · 올린 때 · 올린 이 · SHA-256 앞부분, 맨 위가 직원이 받는 판, 볼륨에 파일이 없으면 "파일 없음"(읽기만 — 기록하지 않는다) |
|
||||
|
||||
- 차례가 있는 명령
|
||||
- 비밀번호 재설정: 앱 비밀번호 지우기(대리 접근) → 웹메일 · 앱 세션 끊기(DB) → 알림 메일 넣기(대리 접근) → 엔진에 임시 비밀번호 + 만료 → 세션 한 번 더 끊기.
|
||||
|
||||
Reference in New Issue
Block a user