memi-board 0.52.0 → 0.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -759,7 +759,6 @@ async function translateAllLocales() {
759
759
  aiRunning.value = true
760
760
  try {
761
761
  const translations = await translatePost({
762
- source: locale.value,
763
762
  targets: translationTargets.value,
764
763
  title: storedTitle,
765
764
  summary,
@@ -1061,7 +1060,6 @@ async function handleSubmit() {
1061
1060
  if (!props.postId && translationEnabled.value && getBoard(resolvedBoardId.value)?.autoTranslatePosts === true && !translations) {
1062
1061
  submitHint.value = t('editor.hint.translating', { count: translationTargets.value.length })
1063
1062
  translations = await translatePost({
1064
- source: locale.value,
1065
1063
  targets: translationTargets.value,
1066
1064
  title: storedTitle,
1067
1065
  summary: buildPostPreview(bodyContent, imageBoard ? [] : attachments.value).summary,
@@ -1,37 +1,37 @@
1
- # Firebase 프로젝트 준비 (처음 설치하는 경우)
1
+ # Firebase project setup (first-time install)
2
2
 
3
- 이 모듈을 처음 붙이는 프로젝트, 특히 Firebase를 아직 안 써본 프로젝트라면 README의 "설치" 섹션만으로는 부족하다. 여기 순서대로 하면 된다.
3
+ If this is the first time you are adding this module to a project, especially one that has never used Firebase, the README's "Installation" section is not enough. Follow the steps below in order.
4
4
 
5
- [호스트 설치 가이드](host-integration.md) · [국제화·번역 설정](internationalization.md) · [역할 마이그레이션](roles-migration.md)
5
+ [Host installation guide](host-integration.md) · [Internationalization and translation](internationalization.md) · [Role migration](roles-migration.md)
6
6
 
7
- ## 1. 사전 준비
7
+ ## 1. Prerequisites
8
8
 
9
- - Node.js + pnpm (또는 npm/yarn)
10
- - Nuxt **4.0 이상**, `@nuxt/ui` **4.0 이상**
11
- - Firebase CLI: `npm i -g firebase-tools` 후 `firebase login`
9
+ - Node.js + pnpm (or npm/yarn)
10
+ - Nuxt **4.0 or later**, `@nuxt/ui` **4.0 or later**
11
+ - Firebase CLI: `npm i -g firebase-tools`, then `firebase login`
12
12
 
13
- ## 2. Firebase 프로젝트 만들기 (아직 없다면)
13
+ ## 2. Create a Firebase project (if you don't have one)
14
14
 
15
- 1. [Firebase 콘솔](https://console.firebase.google.com)에서 새 프로젝트를 만든다.
16
- 2. **Firestore Database**를 사용 설정한다 (Native mode).
17
- 3. **Storage**를 사용 설정한다.
18
- 4. **Authentication**을 사용 설정하고, Sign-in method에서 쓸 provider(Google/Apple/이메일-비밀번호)를 켠다.
15
+ 1. Create a new project in the [Firebase console](https://console.firebase.google.com).
16
+ 2. Enable **Firestore Database** (Native mode).
17
+ 3. Enable **Storage**.
18
+ 4. Enable **Authentication**, and turn on the providers you need (Google/Apple/Email/Password) under Sign-in method.
19
19
 
20
- 이미 있는 Firebase 프로젝트에 붙이는 거라면 이 단계는 건너뛰고, 위 세 서비스가 켜져 있는지만 확인한다.
20
+ If you are adding the board to an existing Firebase project, skip this step and just confirm that the three services above are enabled.
21
21
 
22
- ## 3. `firebaseConfig` 값 가져오기
22
+ ## 3. Get the `firebaseConfig` values
23
23
 
24
- Firebase 콘솔 → 프로젝트 설정(톱니바퀴) → **일반** 탭 → "내 앱" 섹션 → 웹 앱이 없으면 추가 → "SDK 설정 및 구성"에서 `apiKey`/`authDomain`/`projectId`/`storageBucket`/`messagingSenderId`/`appId` 값을 그대로 복사해 `nuxt.config.ts`의 `vuefire.config`에 붙여넣는다(memi-board가 아니라 호스트의 nuxt-vuefire 설정이다 — README [설치](../README.md#설치) 참고).
24
+ Firebase console → Project settings (gear icon) → **General** tab → "Your apps" section → add a web app if there is none → under "SDK setup and configuration", copy the `apiKey`/`authDomain`/`projectId`/`storageBucket`/`messagingSenderId`/`appId` values as-is into `vuefire.config` in `nuxt.config.ts` (this is the host app's nuxt-vuefire config, not memi-board's — see README [Installation](../README.md#installation)).
25
25
 
26
- ## 4. 호스트 프로젝트를 Firebase CLI에 연결
26
+ ## 4. Connect the host project to the Firebase CLI
27
27
 
28
- 프로젝트 루트에 아직 `firebase.json`/`.firebaserc`가 없다면:
28
+ If the project root does not have `firebase.json`/`.firebaserc` yet:
29
29
 
30
30
  ```bash
31
31
  firebase init firestore storage
32
32
  ```
33
33
 
34
- 또는 수동으로 만든다:
34
+ Or create them manually:
35
35
 
36
36
  ```json
37
37
  // .firebaserc
@@ -43,22 +43,22 @@ firebase init firestore storage
43
43
  {
44
44
  "firestore": { "rules": "firestore.rules", "indexes": "firestore.indexes.json" },
45
45
  "storage": {
46
- "bucket": "<vuefire.config.storageBucket 과 동일한 값>",
46
+ "bucket": "<same value as vuefire.config.storageBucket>",
47
47
  "rules": "storage.rules"
48
48
  }
49
49
  }
50
50
  ```
51
51
 
52
- `storage.bucket`은 호스트 `vuefire.config.storageBucket`과 **같아야** 한다. 예: `my-app.appspot.com`.
52
+ `storage.bucket` **must match** the host's `vuefire.config.storageBucket`. Example: `my-app.appspot.com`.
53
53
 
54
- 이미 Firebase CLI를 쓰고 있는 프로젝트라면, `firebase.json`에 `firestore.rules`/`storage.rules` 경로가 있는지만 보지 말고 **버킷 이름**도 확인한다.
54
+ If the project already uses the Firebase CLI, don't just check that `firebase.json` has the `firestore.rules`/`storage.rules` paths — check the **bucket name** too.
55
55
 
56
- 구 프로젝트는 버킷이 두 개일 수 있다.
56
+ Older projects may have two buckets:
57
57
 
58
- - `*.appspot.com` — 예전 웹 앱 SDK snippet 의 `storageBucket`
59
- - `*.firebasestorage.app` — 콘솔/CLI 기본 버킷
58
+ - `*.appspot.com` — the `storageBucket` from the older web app SDK snippet
59
+ - `*.firebasestorage.app` — the console/CLI default bucket
60
60
 
61
- `firebase deploy --only storage`에 `bucket`이 없으면 **기본 버킷**에만 규칙이 올라간다. 앱은 `appspot.com`에 올리는데 규칙은 `firebasestorage.app`에만 있으면, 에디터 이미지에서 `storage/unauthorized`가 난다. 둘 다 쓰면 배열로 같은 규칙을 올린다.
61
+ If `firebase deploy --only storage` has no `bucket`, the rules are deployed only to the **default bucket**. If the app uploads to `appspot.com` but the rules exist only on `firebasestorage.app`, editor images fail with `storage/unauthorized`. If you use both, deploy the same rules as an array:
62
62
 
63
63
  ```json
64
64
  "storage": [
@@ -67,111 +67,111 @@ firebase init firestore storage
67
67
  ]
68
68
  ```
69
69
 
70
- 글 삭제가 `storage/unauthorized` (`access 'memiBoardPosts/{postId}'`) 로 실패하면 버킷이 아니라 **list 경로**다. `listAll` 접두사는 `memiBoardPosts/{postId}/` 이라 `match /memiBoardPosts/{postId} { allow list }` 만으로는 안 된다. `docs/storage.rules.example`의 `{postId}/{allPaths=**}` 블록을 그대로 넣고 `firebase deploy --only storage` 한다.
70
+ If deleting a post fails with `storage/unauthorized` (`access 'memiBoardPosts/{postId}'`), the problem is the **list path**, not the bucket. The `listAll` prefix is `memiBoardPosts/{postId}/`, so `match /memiBoardPosts/{postId} { allow list }` alone is not enough. Copy the `{postId}/{allPaths=**}` block from `docs/storage.rules.example` as-is and run `firebase deploy --only storage`.
71
71
 
72
- ## 5. 게시판 Security Rules 병합 + 배포
72
+ ## 5. Merge and deploy the board Security Rules
73
73
 
74
- README의 [Security Model](../README.md#security-model) 섹션대로 `docs/firestore.rules.example`/`docs/storage.rules.example`을 프로젝트 규칙 파일에 병합한 뒤:
74
+ Merge `docs/firestore.rules.example`/`docs/storage.rules.example` into the project's rules files as described in the README's [Security Model](../README.md#security-model) section, then:
75
75
 
76
76
  ```bash
77
77
  firebase deploy --only firestore:rules,storage
78
78
  ```
79
79
 
80
- **병합만 하고 배포를 안 하면 글쓰기·댓글에서 `permission-denied`가 난다** — 가장 흔한 설치 실수다.
80
+ **If you merge but don't deploy, writing posts and comments fails with `permission-denied`** — this is the most common installation mistake.
81
81
 
82
- ### 0.45.1 규칙 변경과 기존 호스트 전환
82
+ ### 0.45.1 rule changes and migrating existing hosts
83
83
 
84
- 0.46 이상에서는 아래 파일 관리의 전역 역할이 **admin/owner**다. staff/manager는 담당 콘텐츠를 관리하지만 타인 파일의 수정·삭제는 기본 규칙으로 허용하지 않는다. 신규 설치의 최초 역할은 `owner`로 지정하고, 기존 admin·staff 계정은 [역할 마이그레이션](roles-migration.md) 절차로 이전한다.
84
+ From 0.46 onward, the global roles for the file management described below are **admin/owner**. staff/manager manage their assigned content, but the default rules do not let them modify or delete other users' files. For a new install, set the first role to `owner`; migrate existing admin and staff accounts using the [role migration](roles-migration.md) procedure.
85
85
 
86
86
 
87
- 0.45.1은 클라이언트와 Firestore·Storage 규칙을 함께 갱신해야 한다. 사이트 자동 배포는 Firebase 규칙 배포를 대신하지 않는다.
87
+ 0.45.1 requires updating the client and the Firestore and Storage rules together. Automatic site deployment does not replace deploying the Firebase rules.
88
88
 
89
- | 대상 | 변경 내용 |
89
+ | Target | Change |
90
90
  |------|-----------|
91
- | 글 수정 | `boardId`·`authorUid` 변경을 금지하고 `listed` 변경을 공개 범위 전환으로 제한한다. |
92
- | 글·본문 동시 생성 | 본문 쓰기에서 `getAfter()`로 batch 적용 후 부모 글의 소유권을 확인한다. |
93
- | 공개 범위 전환 | 보드 설정의 `visibilityTransition` 동안 새 글·댓글 생성과 일반 글 수정을 잠근다. 담당 관리자의 `listed` 동기화는 허용한다. |
94
- | 삭제·감사 기록 | 콘텐츠 작성자·담당 관리자가 연관 좋아요를 정리할 수 있다. 글을 지워도 신고 기록은 남긴다. |
95
- | 이미지·첨부 업로드 | 기존 글은 작성자 또는 전역 admin·owner만 쓴다. 글 저장 전에는 업로드 파일의 `customMetadata.ownerUid`가 로그인 UID와 같아야 한다. 덮어쓰기는 기존 파일의 `ownerUid`도 확인한다. |
96
- | 파일 삭제 | 글 문서가 있으면 작성자 또는 전역 admin·owner, 없으면 파일에 기록된 `ownerUid` 본인만 삭제한다. |
91
+ | Post updates | Changing `boardId` or `authorUid` is forbidden, and changing `listed` is limited to visibility transitions. |
92
+ | Creating a post and its body together | The body write uses `getAfter()` to check ownership of the parent post after the batch is applied. |
93
+ | Visibility transitions | While the board settings have `visibilityTransition`, creating new posts and comments and regular post edits are locked. Assigned managers may still sync `listed`. |
94
+ | Deletion and audit records | Content authors and assigned managers can clean up related likes. Report records remain even when the post is deleted. |
95
+ | Image and attachment uploads | For existing posts, only the author or a global admin/owner can write. Before the post is saved, the uploaded file's `customMetadata.ownerUid` must equal the signed-in UID. Overwrites also check the existing file's `ownerUid`. |
96
+ | File deletion | If the post document exists, only the author or a global admin/owner can delete; otherwise only the user recorded in the file's `ownerUid`. |
97
97
 
98
- 적용 순서:
98
+ Rollout order:
99
99
 
100
- 1. 호스트에서 `memi-board@^0.45.1` 이상을 설치하고 lockfile을 갱신한다. 직접 만든 업로드 코드도 이미지·썸네일·첨부파일 모두에 `customMetadata: { ownerUid: user.uid }`를 넣는다.
101
- 2. 현재 설치한 버전 예제의 함수와 컬렉션·경로별 규칙 전체를 호스트에 병합한다. 컬렉션 이름을 바꿨다면 함수 내부 Firestore 경로도 맞춘다. 예전의 `!postExists(postId)`만으로 파일 쓰기·삭제를 허용하는 조건은 제거한다. Rules의 겹치는 `match` 중 하나라도 허용하면 접근이 가능하므로, 넓은 `allow write`를 남겨두지 않는다.
102
- 3. 에뮬레이터에서 두 계정으로 새 글·본문 동시 저장, 이미지·첨부 업로드, 타인 파일 덮어쓰기·삭제 거절, 댓글 좋아요 정리, 숨김 전환 재시도를 확인한다.
103
- 4. 새 클라이언트와 규칙 배포를 같은 배포 작업으로 진행하고 앱이 사용하는 모든 버킷에 적용한다. 새 규칙이 적용되면 기존 브라우저의 구버전 클라이언트는 저장 전 업로드에 `ownerUid`가 없어 거절될 수 있으므로 새 버전을 불러온 뒤 재시도한다. 아래 교차 서비스 IAM 설정도 확인한다.
100
+ 1. In the host app, install `memi-board@^0.45.1` or later and update the lockfile. Any custom upload code must also set `customMetadata: { ownerUid: user.uid }` on images, thumbnails, and attachments.
101
+ 2. Merge all functions and per-collection/per-path rules from the example of the version you installed into the host. If you renamed collections, update the Firestore paths inside the functions too. Remove old conditions that allow file writes or deletes based only on `!postExists(postId)`. Access is granted if any overlapping `match` in the rules allows it, so don't leave a broad `allow write` in place.
102
+ 3. In the emulator, with two accounts, verify: saving a new post and body together, image and attachment uploads, rejection of overwriting or deleting another user's files, comment like cleanup, and retrying a hide transition.
103
+ 4. Deploy the new client and the rules in the same deployment, and apply them to every bucket the app uses. Once the new rules are live, old clients in existing browsers may be rejected because their pre-save uploads lack `ownerUid`; reload the new version and retry. Also check the cross-service IAM setup below.
104
104
 
105
- 기존 글의 파일은 `ownerUid`가 없어도 부모 글의 작성자·역할로 판단하므로 메타데이터 일괄 수정이 필요하지 않다. 반면 부모 글 문서가 없고 `ownerUid`도 없는 예전 고아 파일은 클라이언트에서 수정·삭제할 수 없다. 필요한 정리는 신뢰할 수 있는 Admin SDK 또는 콘솔에서 수행한다.
105
+ Files on existing posts are authorized by the parent post's author and roles even without `ownerUid`, so no bulk metadata update is needed. However, old orphaned files that have neither a parent post document nor an `ownerUid` cannot be modified or deleted from the client. Do any necessary cleanup from a trusted Admin SDK or the console.
106
106
 
107
- 공개 범위 전환 중 실패하면 `visibilityTransition`을 임의로 지우거나 반대 범위로 바꾸지 말고, 설정 화면에서 **같은 목표 범위로 다시 저장**해 동기화를 완료한다. 숨김 전환은 콘텐츠의 `listed`를 먼저 내리고 마지막에 `visibility: 'hidden'`을 확정한다. 공개 복귀는 보드 공개를 먼저 허용한 뒤 콘텐츠를 동기화하고 전환 표시를 비운다.
107
+ If a visibility transition fails, don't manually clear `visibilityTransition` or switch it to the opposite visibility; instead, **save again with the same target visibility** in the settings screen to complete the sync. A hide transition first lowers the content's `listed`, then finalizes `visibility: 'hidden'` last. Returning to public first allows the board to be public, then syncs the content, then clears the transition marker.
108
108
 
109
- Storage의 Firestore 조회 제한은 서로 다른 문서 최대 2개이므로 예제는 글 문서와 사용자 역할 문서만 읽는다. 따라서 Storage의 admin·owner는 보드 담당 여부와 무관한 전역 파일 관리 권한을 유지한다. Firestore의 담당 스태프 범위와 혼동하지 않는다. [공식 조회 제한](https://firebase.google.com/docs/storage/security/rules-conditions#enhance_with_firestore)을 참고한다.
109
+ Storage rules can read at most 2 distinct Firestore documents, so the example reads only the post document and the user role document. As a result, admin/owner in Storage keep global file management rights regardless of board assignment. Don't confuse this with the assigned-staff scope in Firestore. See the [official access limits](https://firebase.google.com/docs/storage/security/rules-conditions#enhance_with_firestore).
110
110
 
111
- 예제의 파일 읽기와 목록 조회는 계속 공개다. 숨김 보드·비공개 초안 파일까지 비공개로 저장해야 하는 서비스는 별도의 Storage 읽기 정책을 설계해야 한다.
111
+ File reads and listing in the example remain public. Services that need files of hidden boards or private drafts to be stored privately must design a separate Storage read policy.
112
112
 
113
- ### Storage 규칙의 Firestore 조회 권한 (부모/호스트 프로젝트 필수 설정)
113
+ ### Firestore read access for Storage rules (required host app setup)
114
114
 
115
- Storage 예시 규칙은 이미지·첨부 업로드와 파일 삭제 시 `firestore.exists()` / `firestore.get()`으로 글의 존재 여부, `authorUid`, 사용자 `role`을 조회한다. 따라서 **호스트 Firebase 프로젝트의 Storage 서비스 계정**에 Firestore 조회 역할이 필요하다. 패키지 설치나 Firestore Rules 배포만으로 이 IAM 설정이 완료되지는 않는다.
115
+ On image/attachment uploads and file deletion, the example Storage rules use `firestore.exists()` / `firestore.get()` to look up whether the post exists, its `authorUid`, and the user's `role`. The **Storage service account of the host app's Firebase project** therefore needs a Firestore read role. Installing the package or deploying the Firestore rules does not complete this IAM setup.
116
116
 
117
- 호스트 프로젝트 관리자가 다음을 설정한다:
117
+ An administrator of the host project sets this up as follows:
118
118
 
119
- 1. 호스트 저장소에서 위 배포 명령을 대화형으로 실행한다. Firebase CLI가 교차 서비스 규칙용 IAM 역할 추가를 물으면 승인한다. Firebase 콘솔에서 Storage Rules를 저장할 때 표시되는 연결 권한 안내로도 설정할 수 있다.
120
- 2. Google Cloud Console → **IAM 및 관리자 → IAM**에서 같은 Firebase 프로젝트를 선택하고 **Google 제공 역할 부여 포함(Include Google-provided role grants)**을 켠다.
121
- 3. `service-<PROJECT_NUMBER>@gcp-sa-firebasestorage.iam.gserviceaccount.com` 계정을 찾아 **Firebase Rules Firestore Service Agent** (`roles/firebaserules.firestoreServiceAgent`) 역할이 있는지 확인한다. `<PROJECT_NUMBER>`는 프로젝트 ID가 아니라 Firebase 프로젝트 설정의 숫자 프로젝트 번호다. 역할이 없으면 IAM 변경 권한이 있는 호스트 관리자가 이 서비스 계정에 추가한다.
122
- 4. 권한 반영 후 로그인 상태에서 새 글에 20MiB 미만의 작은 첨부파일을 올려 확인한다. 기존 글에서는 작성자·스태프의 첨부 작업도 확인한다.
119
+ 1. Run the deploy command above interactively from the host repository. When the Firebase CLI asks whether to add the IAM role for cross-service rules, approve it. You can also grant it from the connection permission prompt shown when saving Storage Rules in the Firebase console.
120
+ 2. In the Google Cloud Console → **IAM & Admin → IAM**, select the same Firebase project and turn on **Include Google-provided role grants**.
121
+ 3. Find the `service-<PROJECT_NUMBER>@gcp-sa-firebasestorage.iam.gserviceaccount.com` account and confirm it has the **Firebase Rules Firestore Service Agent** (`roles/firebaserules.firestoreServiceAgent`) role. `<PROJECT_NUMBER>` is the numeric project number from the Firebase Project settings, not the project ID. If the role is missing, a host administrator with permission to change IAM adds it to this service account.
122
+ 4. After the permission takes effect, verify by uploading a small attachment (under 20 MiB) to a new post while signed in. On existing posts, also verify attachment operations by the author and staff.
123
123
 
124
- 이 역할은 프로젝트의 Firestore 문서 조회(`datastore.entities.get`)를 허용한다. 일반 사용자 계정에 부여하는 역할이 아니며, Firestore 쓰기·삭제 권한은 포함하지 않는다. Storage 규칙의 소유권·역할 검사는 그대로 유지한다.
124
+ This role allows reading Firestore documents in the project (`datastore.entities.get`). It is not a role for regular user accounts, and it does not include Firestore write or delete permissions. The ownership and role checks in the Storage rules remain in place.
125
125
 
126
- **CI의 `--non-interactive` 배포 성공만으로 IAM 설정까지 완료됐다고 판단하지 않는다.** 최초 설정은 호스트 관리자가 대화형 배포 또는 IAM 콘솔에서 수행하고, 위 서비스 계정의 역할을 확인한다.
126
+ **Do not assume IAM setup is complete just because a `--non-interactive` deploy in CI succeeded.** The host administrator must do the initial setup via an interactive deploy or the IAM console, and confirm the role on the service account above.
127
127
 
128
- 2MB ZIP처럼 용량 조건을 충족해도 이 권한이 없으면 `memiBoardPosts/{postId}/attachments/...`에서 `storage/unauthorized`가 날 수 있다. 새 글 문서가 아직 없어도 `firestore.exists()` 조회 자체에는 권한이 필요하다. 0.45.1의 본문 이미지 업로드도 같은 Firestore 조회 권한이 필요하다.
128
+ Even when the size limit is met, as with a 2 MB ZIP, uploads to `memiBoardPosts/{postId}/attachments/...` can fail with `storage/unauthorized` without this permission. Even if the new post document doesn't exist yet, the `firestore.exists()` lookup itself requires the permission. Body image uploads in 0.45.1 need the same Firestore read permission.
129
129
 
130
- 공식 근거: [교차 서비스 Storage Rules 권한 설정](https://firebase.google.com/docs/rules/manage-deploy#manage_permissions_for_cross-service_cloud_storage_security_rules), [역할의 포함 권한](https://docs.cloud.google.com/iam/docs/roles-permissions/firebaserules#firebaserules.firestoreServiceAgent).
130
+ Official references: [Managing permissions for cross-service Storage Rules](https://firebase.google.com/docs/rules/manage-deploy#manage_permissions_for_cross-service_cloud_storage_security_rules), [Permissions included in the role](https://docs.cloud.google.com/iam/docs/roles-permissions/firebaserules#firebaserules.firestoreServiceAgent).
131
131
 
132
- ## 6. 로그인 공급자 설정
132
+ ## 6. Sign-in provider setup
133
133
 
134
134
  ### Google
135
135
 
136
- Firebase 콘솔 → Authentication → Sign-in method에서 Google을 활성화하기만 하면 된다. 추가 설정 없음.
136
+ Just enable Google in Firebase console → Authentication → Sign-in method. No further setup.
137
137
 
138
138
  ### Apple
139
139
 
140
- Google보다 손이 더 간다:
140
+ This takes more work than Google:
141
141
 
142
- 1. Firebase 콘솔에서 Apple 로그인을 활성화한다.
143
- 2. [Apple Developer](https://developer.apple.com/account) 계정에서 **Services ID**를 새로 만든다.
144
- 3. 그 Services ID의 "Web Authentication Configuration"에 콜백 도메인을 등록한다 — Domains에 `{authDomain}`(Firebase 프로젝트의 authDomain, 보통 `<project-id>.firebaseapp.com`), Return URL에 `https://{authDomain}/__/auth/handler`.
145
- 4. Apple Developer의 **Keys**에서 "Sign in with Apple" 키를 새로 만든다 (기존에 다른 용도 키가 있어도 별도로 새로 만들어야 한다).
146
- 5. Team ID, 방금 만든 키의 Key ID, 그 키의 개인 키(`.p8` 파일 내용)를 Firebase 콘솔의 Apple 로그인 설정 화면에 입력한다.
142
+ 1. Enable Apple sign-in in the Firebase console.
143
+ 2. Create a new **Services ID** in your [Apple Developer](https://developer.apple.com/account) account.
144
+ 3. Register the callback domain in that Services ID's "Web Authentication Configuration" — under Domains, `{authDomain}` (the Firebase project's authDomain, usually `<project-id>.firebaseapp.com`); under Return URL, `https://{authDomain}/__/auth/handler`.
145
+ 4. Create a new "Sign in with Apple" key under **Keys** in Apple Developer (even if you already have keys for other purposes, you must create a separate new one).
146
+ 5. Enter the Team ID, the Key ID of the key you just created, and its private key (the contents of the `.p8` file) in the Apple sign-in settings in the Firebase console.
147
147
 
148
- 자세한 건 [Firebase 공식 가이드](https://firebase.google.com/docs/auth/web/apple)를 따른다. 이 설정 없이 Apple 로그인 버튼을 누르면 에러가 난다.
148
+ For details, follow the [official Firebase guide](https://firebase.google.com/docs/auth/web/apple). Without this setup, clicking the Apple sign-in button results in an error.
149
149
 
150
- ## 7. Firebase AI Logic(Gemini) 활성화 — AI 검열용
150
+ ## 7. Enable Firebase AI Logic (Gemini) for AI moderation
151
151
 
152
- 1. Firebase 콘솔 → **AI Logic** 에서 활성화하고, **Gemini Developer API** 를 사용하도록 설정한다.
153
- 2. **요금제**: Spark에서도 되는 경우가 있으나 제한·쿼터가 있을 수 있다. 부족하면 Blaze.
154
- 3. 모델: 호스트 `nuxt.config.ts`의 `memiBoard.moderation.model`에 `gemini-3.5-flash-lite` 권장.
155
- `gemini-2.5-flash` 는 **신규 프로젝트에서 "no longer available to new users"** 로 404 날 수 있다.
156
- 4. `moderation.enabled` 기본이 `true` 이므로 활성화 후 글쓰기 시 자동 호출된다. 끄려면 `enabled: false`.
152
+ 1. Enable it in Firebase console → **AI Logic**, and configure it to use the **Gemini Developer API**.
153
+ 2. **Pricing plan**: it may work on Spark, but there may be limits and quotas. If they are insufficient, use Blaze.
154
+ 3. Model: `gemini-3.5-flash-lite` is recommended for `memiBoard.moderation.model` in the host's `nuxt.config.ts`.
155
+ `gemini-2.5-flash` may return a 404 with **"no longer available to new users" on new projects**.
156
+ 4. `moderation.enabled` defaults to `true`, so once enabled it is called automatically when posts are written. To turn it off, set `enabled: false`.
157
157
 
158
- ## 8. App Check 설정 (AI Logic Enforce 시 사실상 필수)
158
+ ## 8. App Check setup (effectively required when AI Logic is enforced)
159
159
 
160
- AI Logic / `firebaseml` 이 **Enforce** 이면 App Check 토큰 없이 검열 API가 실패한다. memi-board는 App Check를 초기화하지 않는다. **호스트 책임**이다. 기본값은 한 번만 쓰는 토큰(`useLimitedUseAppCheckTokens: true`)이라 reCAPTCHA v3 공급자로는 맞지 않는다. 플러그인 예시는 [README App Check 절](../README.md#app-check-ai-enforce-시-사실상-필수)을 따른다.
160
+ If AI Logic / `firebaseml` is set to **Enforce**, the moderation API fails without an App Check token. memi-board does not initialize App Check. **That is the host app's responsibility.** The default uses limited-use tokens (`useLimitedUseAppCheckTokens: true`), which don't fit the reCAPTCHA v3 provider. For a plugin example, follow the [README App Check section](../README.md#app-check-effectively-required-with-ai-enforce).
161
161
 
162
- 1. reCAPTCHA Enterprise **점수형** 웹 키를 만들고, Firebase 콘솔 App Check의 웹 앱에 그 사이트 키를 등록한다. 등록하는 웹 앱의 **appId** 가 호스트 `vuefire.config.appId` 와 같아야 한다. 시크릿 키는 클라이언트에 넣지 않는다.
163
- 2. 브라우저 API 키 제한에 **Firebase App Check API** 를 포함한다. 이미 허용한 API는 유지한다.
164
- 3. SPA는 `app:mounted` 이후에 `enterprise.execute` 로 토큰을 만들어 `exchangeRecaptchaEnterpriseToken` 에 `limited_use: true` 로 교환한다. `ReCaptchaEnterpriseProvider` 는 마운트 전 빈 `document.body` 에 숨은 위젯을 그리다 `appCheck/recaptcha-error` 가 난다.
165
- 4. **로컬:** `self.FIREBASE_APPCHECK_DEBUG_TOKEN = true`(콘솔에 UUID 출력 후 App Check → 디버그 토큰 등록) 또는 등록된 고정 UUID.
166
- 5. **프로덕션:** 디버그 토큰 사용 금지. reCAPTCHA 도메인에 실제 호스트 도메인 등록.
167
- 6. 적용은 **AI Logic만** 켠다. Cloud Storage와 Cloud Firestore는 모니터링으로 둔다. 한 번용 토큰은 AI Logic만 받는다. Storage나 Firestore에 적용하면 이미지 업로드와 글 쓰기가 거절된다.
162
+ 1. Create a reCAPTCHA Enterprise **score-based** web key and register that site key for the web app in App Check in the Firebase console. The **appId** of the registered web app must match the host's `vuefire.config.appId`. Never put the secret key in the client.
163
+ 2. Include the **Firebase App Check API** in the browser API key restrictions. Keep the APIs that are already allowed.
164
+ 3. In an SPA, after `app:mounted`, create a token with `enterprise.execute` and exchange it via `exchangeRecaptchaEnterpriseToken` with `limited_use: true`. `ReCaptchaEnterpriseProvider` renders a hidden widget into an empty `document.body` before mounting and fails with `appCheck/recaptcha-error`.
165
+ 4. **Local:** `self.FIREBASE_APPCHECK_DEBUG_TOKEN = true` (prints a UUID to the console; register it under App Check → debug tokens) or a registered fixed UUID.
166
+ 5. **Production:** never use debug tokens. Register the real host domain in the reCAPTCHA domains.
167
+ 6. Enforce **only AI Logic**. Leave Cloud Storage and Cloud Firestore on monitoring. Only AI Logic accepts limited-use tokens. If you enforce Storage or Firestore, image uploads and post writes are rejected.
168
168
 
169
- ## 9. Firestore 복합 인덱스
169
+ ## 9. Firestore composite indexes
170
170
 
171
- `memiBoardPosts`/`memiBoardComments`/`memiBoardLikes` 는 모두 최상위 flat 컬렉션이고, 보드/글 단위 스코핑은 `boardId`/`postId` 필드에 대한 `where` 동등 필터로 한다. [docs/firestore.indexes.json.example](firestore.indexes.json.example) 를 호스트 `firestore.indexes.json` 에 병합 후:
171
+ `memiBoardPosts`/`memiBoardComments`/`memiBoardLikes` are all top-level flat collections, and board/post scoping is done with `where` equality filters on the `boardId`/`postId` fields. Merge [docs/firestore.indexes.json.example](firestore.indexes.json.example) into the host's `firestore.indexes.json`, then:
172
172
 
173
173
  ```bash
174
174
  firebase deploy --only firestore:indexes
175
175
  ```
176
176
 
177
- 콘솔 에러에 인덱스 생성 링크가 뜨면 그걸 따라도 된다 — 위 예시 파일은 패키지가 실제로 실행하는 쿼리 기준으로 미리 정리해 둔 것이다. 컬렉션 이름을 커스터마이즈했다면(`memiBoard.postsCollection` 등) `collectionGroup` 값도 맞춰 바꾼다.
177
+ If a console error shows an index creation link, you can follow it too — the example file above is prepared in advance based on the queries the package actually runs. If you customized collection names (`memiBoard.postsCollection`, etc.), update the `collectionGroup` values to match.