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:
@@ -69,6 +69,7 @@ description: Vue 3 프론트엔드(frontend/) 개발 규칙 — Composition API
|
||||
| 대용량 첨부 (11단계) | 올리기 엔진 `src/composables/useLargeFileUploads.ts` · 쓰기 화면 잇기 `useComposeLargeFiles.ts` · 첨부 칸 `components/mail/ComposeAttachmentGroups.vue` · `ComposeLargeFileItem.vue` · 읽기 `MessageAttachments.vue`(+ `MessageLargeFileItem` · `MessageLargeFileLinkBar` · `LargeFileRevokeDialog`) · 검사 다시 묻기 `useLargeFileScanRefresh.ts` · 위험 알림 `useLargeFileAlerts.ts`(실시간 `large-file`) · 설정 `pages/settings/MailLargeFilesPage.vue` + `components/settings/LargeFile*.vue` · `stores/large-files.store.ts` · API `useLargeFiles.ts` · 공개 화면 `pages/share/SharePage.vue` + `usePublicShare.ts` · `shared/utils/large-file.ts` · `large-file-status.ts` · `kst-date.ts` · `share-links.ts` · `shared/constants/large-file.ts` | 20MB 넘는 파일 · 합계 25MB 를 넘기는 파일 · 메일에 못 담는 형식은 저절로 링크로(묻는 창 없음). **올리기**: 고르자마자 서버에 모두 만들고(임시 저장본에 바로 실림) 조각(16MiB · `Content-Digest`)은 파일 3개까지만 함께, 끊기면 2 · 5 · 15 · 30초로 다시, 1분 넘으면 멈춤 + [다시 시도], 404 는 저절로 다시 올리지 않는다, 새로고침 뒤 같은 파일(이름 · 크기 · 고친 시각 + 맨 앞 조각 SHA-256)을 다시 고르면 잇는다. 다 올릴 때까지 [보내기] · ▾ 는 `aria-disabled` + 까닭(누르면 role=status). **본문**: 대용량 파일이 있을 때만 첫 원문 인용 앞에 `<div data-ara-quote-start="">` 와 `bodyQuoteStart`(텍스트 인용 줄의 처음)를 보낸다 — 서버가 그 자리에 덩이를 넣는다. 다시 연 저장본의 원문 인용은 `mail-extensions.ts` 의 `isMailQuoteBlock`("원본 메일" 머리 문단 + 머리 표)으로 알아본다. **읽기**: 첨부 칸의 대용량 항목은 서버의 `largeFiles` 에서만 오고, 본문 덩이는 서버만 붙일 수 있는 `data-ara-large-files` 요소만 `<template>` 안에서 접는다(`mail-html.ts`). **공개 화면** `/s/:token` 은 `meta.public`(세션 복원을 건너뛴다 — 이 라우트 하나뿐), 요청은 `credentials: 'omit'` · `no-store` · `no-referrer`, v-html 없음. 서버 거절은 사전 문구(`getErrorMessage`), 화면 상수 글은 서버에 묻기 전 검사뿐 — 계약 4절, 디자인 README "11단계" |
|
||||
| 계정 보안 (12단계) | 로그인 코드 단계 `src/components/auth/LoginCodeStep.vue` · `composables/useLoginChallenge.ts` · 설정 `pages/settings/AccountTwoFactorPage.vue` · `AccountAppPasswordsPage.vue` · `AccountLoginHistoryPage.vue`(+ 비밀번호 화면의 코드 칸 `PasswordChangeForm`) · `components/settings/`(`OtpCodeField` · `TwoFactorEnableDialog` + `TwoFactorEnrollmentScan` · `TwoFactorDisableDialog` · `AppPassword*` · `MailClientSettings` · `LoginSessionList` · `LoginEventTable` · `EndOtherSessionsDialog`) · QR `components/QrCodeImage.vue`(`uqr` 0.1.3 — SVG `<rect>` v-for, v-html 없음) · API `composables/useAccountSecurity.ts` · 스토어 `two-factor` · `app-passwords` · `login-history` · 상수 `shared/constants/account-security.ts`(상한 5 · 이름 40자 · 10분 · `MAIL_CLIENT_SETTINGS`) | **비밀은 화면 메모리에만** — 코드 단계의 비밀번호는 반응형이 아닌 클로저(`useLoginChallenge` — 성공 · [다른 계정으로 로그인] · 떠날 때 · 10분(타이머와 시계 `startedAt` 둘 다)에 지운다, 화면이 사라진 뒤의 늦은 `twoFactorRequired` 로는 시작하지 않는다), 2단계 키 · otpauth 주소 · 앱 비밀번호 값은 창 안에만 두고 닫을 때 지운다. **비밀이 드는 요청은 Pinia 액션에서 부르지 않는다** — devtools 타임라인이 액션의 인자 · 결과를 기록한다(창이 `useAccountSecurity` 로 보내고 스토어에는 비밀 없는 결과만 — `apply(status)` · `add(값을 뺀 줄)`). 세션 세대(`authStore.generation`)가 바뀌면 비밀이 든 창을 모두 닫는다(12단계부터 다른 기기의 로그아웃이 이 세션을 바로 끊는다). 창은 "몇 번째로 연 창인가"로 늦은 응답을 거른다 — 단 서버가 이미 켰거나 만든 결과는 반영한다. `login()` 은 `{ status }` 를 돌려준다. 코드로 나눈다: `AUTH_006` 칸 아래 · `AUTH_007` 코드 칸 보이기(6자리가 아닐 때도 이 문구) · `AUTH_008` 처음부터(새 키) · `AUTH_009` 상한. 지금 브라우저 줄이 없으면 [다른 곳 모두 로그아웃]은 "이 브라우저를 포함해"로 묻는다. **로그인된 기기**(2026-11-06 — 앱 세션이 함께 보인다): 웹 · 앱은 세션의 `client` · 로그인 기록의 `method` 로만 가른다(User-Agent 이름으로 짐작하지 않는다 — `shared/utils/account-security.ts` 의 `sessionCounts` · `isMobileSession`), 앱 이름은 `MOBILE_APP_NAME`(짝: backend `APP_BROWSER_NAME`) — 계약 5절, 디자인 README "12단계" |
|
||||
| 전환 준비 (13단계) | 제한된 세션 `stores/auth.store.ts`(`restriction` · `twoFactorDeadline` · `restrictionChecks` · `canUseApp`) · `shared/utils/auth-session.ts` · `router/index.ts` 가드 + `composables/useSessionRestriction.ts`(App) · `useApi.ts` 의 `recheckRestriction` · 로그인 틀 단계 `components/auth/LoginPasswordChangeStep.vue` · `LoginTwoFactorSetupStep.vue` · `LoginTwoFactorScan.vue`(QR 148 — `QrCodeImage` 의 `compact`) · `LoginAccountRow.vue` · `LoginSupportLine.vue` · `composables/useLoginSecret.ts` · `shared/constants/session-restriction.ts` · 문의 연락처 `stores/support-contact.store.ts` · `shared/utils/support-contact.ts` · 유예 줄 `components/mail/TwoFactorGraceNotice.vue`(AppShell 의 `notice` 슬롯 — MailLayout 에만) · 메일 프로그램 연결 `pages/settings/AccountMailClientsPage.vue` + `components/settings/MailClientGuideList.vue` · `shared/constants/mail-clients.ts` · 30일 줄 `MailListFrame`(`MAILBOX_RETENTION_NOTICES` · `MAILBOX_EMPTY_HINTS`) | **제한된 세션**(계약 6절): login · me · refresh 의 `restriction` 이 있으면 보호 화면 대신 로그인 틀의 그 단계(새 비밀번호 → 2단계 켜기)로 — 403 `AUTH_010` 은 알림 없이 me 를 다시 받고(제한 없음이면 refresh 후 원래 요청 한 번만), 서버가 제한을 확인할 때마다(`restrictionChecks`) 다시 옮긴다. 제한된 동안은 `canUseApp` 이 false 라 나가기 확인을 묻지 않고(세션이 끝난 것처럼 — 2026-11-02 리뷰: 확인에서 이동을 취소하면 메일 화면에 갇혔다) 실시간 알림 · 새 메일 확인을 열지 않는다. 모르는 제한 이름은 `unknown` — 갱신 · 다시 보내기 없이 사전 문구 + [다른 계정으로 로그인]만. **비밀**: 로그인 때 넣은 비밀번호는 반응형이 아닌 클로저에 10분(서버가 마지막으로 확인한 때부터) — 성공 · 다른 계정 · 떠남 · `isLoggedIn` false 때 지운다, 거절(`AUTH_005`)되면 지우고 칸을 보인다. 성공한 로그인 코드도 지운다(`resetCodeStep`). 단계를 마친 뒤 refresh 가 장애면 me 를 먼저 묻는다(제한이 있으면 그 단계 · 없으면 이어 감 · me 도 실패면 위쪽 상자 + [다시 시도]). 2단계를 켠 직원의 새 비밀번호 단계는 다음 코드를 받는다(로그인 코드는 2분 다시 못 쓴다). **유예 줄**: 높이 40 · [지금 켜기] 36(안내 줄 밀도 예외 재사용), 날짜는 `twoFactorDeadline` 의 1ms 앞(올해가 아니면 해), 닫기 없음, 마감이 있으면 `GET /two-factor` 를 한 번 물어 이미 켠 직원에게는 숨긴다. 설정 › 2단계 인증은 `isRequired` · `deadline` — 필수 · 켜짐이면 끄기 대신 글. **메일 프로그램 연결**: 펼침 줄 52(`--size-header-min` 재사용 — 예외 아님) · 한 번에 하나 · 처음 모두 접힘(h3 > button · `aria-expanded` · region), 글은 상수가 원본(디자인 README 13단계), 앱 비밀번호 화면에 바로가기 줄, 아이콘 `monitor-smartphone`. `PasswordField` · `OtpCodeField` 의 `variant="login"`(로그인 틀 칸 모양), `PasswordField` 의 `readonly`. 12단계 코드 단계의 도움말은 문의 줄로 바뀌었다(`LOGIN_TWO_FACTOR_TEXT.help` 삭제) — 계약 6절, 디자인 README "13단계" |
|
||||
| 휴대폰 앱 받기 (2026-11-08) | 설정 › 계정 › 휴대폰 앱 `src/pages/settings/AccountMobileAppPage.vue` · 앱 틀 밖 받기 화면 `pages/mobile-app/MobileAppPage.vue`(`/app` — 인쇄 화면처럼 `meta.requiresAuth` 만) · `components/mobile-app/`(`MobileAppDownloadLink` · `MobileAppIcon` · `MobileAppSteps` · `MobileAppIphoneNote`) · `stores/app-release.store.ts` · API `composables/useAppReleases.ts` · `shared/utils/app-release.ts` · `shared/types/app-release.ts` · 글 `shared/constants/mobile-app.ts`(`MOBILE_APP_PATH` · `MOBILE_APP_ROUTE` — 글은 이 상수가 원본) | 안드로이드 APK 를 로그인한 직원만 받는다(스토어 없음). 판 정보 `GET /api/app-releases/android/latest` 는 들어올 때마다 `silent` 로 받는다 — 404 `RES_001` 은 "받을 앱 없음"(알림 없음 · 안내와 문의 줄만), 그 밖의 실패는 칸 자리에 `MailLoadError`(사전 문구 + [다시 시도]), 세션 세대(`authStore.generation`)가 바뀌면 늦은 응답을 버리고 값을 지운다. [앱 내려받기]는 첨부처럼 `<a href download>` + 이동 직전 `confirmSession()`(`…/latest/file`). QR(`QrCodeImage`)은 `location.origin + '/app'` 만 담는다(비밀 아님) — `/app` 은 앱의 [새 버전 받기]도 여는 약속이라 경로를 바꾸면 앱도 함께. 문의 줄은 `LoginSupportLine`(`/app` 은 `size="base"` 13). 올린 날은 브라우저 시간대(`formatLongDate`). 설정은 좁은 폭(480 이하)에서 QR 줄을 빼고 [앱 내려받기]를 전체 폭으로, `/app` 은 휴대폰 폭이 먼저 · 480 초과는 띠 전체 폭 + 가운데 칸 480 · [앱 내려받기] 48(밀도 예외 아님). 글자 17 토큰 `--font-size-title-md` — 디자인 README "휴대폰 앱 받기" |
|
||||
| 시계 주입 | `src/composables/useClock.ts` · `src/test-utils/manual-clock.ts` | 타이머를 쓰는 로직(자동 저장 등)은 시계를 주입받는다 — 테스트는 손으로 돌리는 시계로 시간을 넘긴다(고정 대기 없이, [[testing]]) |
|
||||
| 나가기 전 확인 | `src/composables/useLeaveConfirm.ts` · `src/shared/utils/compose-changes.ts` | 화면을 연 뒤(또는 마지막으로 저장한 뒤) 바꾼 것이 있으면 앱 안 이동(라우트 떠남·바뀜)은 확인 대화상자, 탭 닫기·새로고침은 `beforeunload`. 쓰기 화면은 떠나기 전에 먼저 저장하고 저장하지 못한 것이 있을 때만 묻는다(서명 설정은 저장하지 않은 서명이 있으면 묻는다). 가드가 도는 중은 `guarding`. 바꿈 여부는 연 직후(저장한 뒤에는 마지막으로 저장한 판)의 스냅숏과 비교한다(편집기는 `isChanged()`). 떠나기 전 저장은 5초까지만 기다린다. 보낸 뒤·보내는 중·세션이 끝났을 때는 묻지 않는다 — 보내는 중에 떠나면 결과는 토스트로만 알리고 떠난 양식·새 양식을 건드리지 않는다(양식 세대로 늦게 온 결과를 버린다, 디자인 README "나가기 전 확인"). **고치던 모달 창**(필터 고치기 창 — 7단계)은 `composables/useDialogLeaveConfirm.ts` — 화면의 확인에 "창 닫기" 길을 더한다: [취소] · Esc 는 `asking: 'close'`([계속 고치기]면 창을 그대로 — 브라우저가 닫았으면 `reshow()`), 다른 화면은 `'route'`, 탭 닫기 · 새로고침은 `beforeunload`. 보내는 중 · 세션이 끝났을 때는 묻지 않는다 |
|
||||
| 읽기 화면 본문 안내 | `src/shared/utils/mail-body-notices.ts` · `shared/constants/mail.ts` 의 `MAIL_BODY_NOTICES` | 서버의 `isHtmlBodyOmitted`·`isBodyTruncated`(계약 2절 "읽기 한도")를 외부 이미지 차단과 같은 안내 줄로 보인다(`.reader__notice[data-notice]`) |
|
||||
|
||||
Reference in New Issue
Block a user