memi-board 0.4.6 → 0.4.12
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.
- package/README.md +221 -73
- package/dist/composables/useMemiBoardAuth.d.ts +15 -3
- package/dist/composables/useMemiBoardModeration.d.ts +3 -1
- package/dist/composables/useMemiBoardStorage.d.ts +5 -1
- package/dist/config.d.ts +7 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +458 -138
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CommentForm.vue +13 -2
- package/dist/runtime/components/Detail.vue +19 -5
- package/dist/runtime/components/Editor.vue +215 -27
- package/dist/types.d.ts +13 -1
- package/dist/utils/compressImage.d.ts +7 -0
- package/dist/utils/extractEditorImageUrls.d.ts +5 -0
- package/dist/utils/moderation-prompt.d.ts +10 -1
- package/dist/utils/moderation-strike.d.ts +14 -0
- package/dist/utils/renderMarkdown.d.ts +10 -0
- package/docs/firebase-setup.md +106 -0
- package/docs/firestore.rules.example +97 -0
- package/docs/storage.rules.example +49 -0
- package/package.json +11 -2
package/README.md
CHANGED
|
@@ -1,32 +1,57 @@
|
|
|
1
1
|
# memi-board
|
|
2
2
|
|
|
3
|
-
Nuxt 4 + Nuxt UI
|
|
3
|
+
Nuxt 4 + Nuxt UI + Firebase 프로젝트에 **클라이언트 전용 게시판**을 붙이는 npm 패키지.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- 게시글 CRUD · 댓글 · Storage 첨부 · 카테고리
|
|
6
|
+
- 작성자/관리자 권한 (Firestore Rules)
|
|
7
|
+
- 로컬 비속어 + Firebase AI Logic(Gemini) 검열 (선택)
|
|
8
|
+
|
|
9
|
+
**서버 / Firebase Admin 불필요.** 권한은 Rules, 검열은 브라우저(우회 가능 — 아래 한계 참고).
|
|
10
|
+
|
|
11
|
+
같은 스택(Nuxt 4 · `@nuxt/ui` · `nuxt-vuefire` · Firebase)이면 **다른 호스트 앱에도 그대로 재사용** 가능하다. Firebase 프로젝트는 호스트마다 달라도 되고, 같은 프로젝트를 공유해도 `collectionPrefix`로 컬렉션만 나누면 된다.
|
|
12
|
+
|
|
13
|
+
## 플랫폼 요구사항
|
|
14
|
+
|
|
15
|
+
| 항목 | 요구 |
|
|
16
|
+
|------|------|
|
|
17
|
+
| 런타임 | Nuxt **4+** (3.x peer 는 허용하나 playground/검증은 4 기준) |
|
|
18
|
+
| UI | `@nuxt/ui` **4+** (컴포넌트가 `UButton` 등 auto-import 전제) |
|
|
19
|
+
| 데이터 | Firebase **Firestore + Auth + Storage** |
|
|
20
|
+
| Vue | `vue` 3.5+, `vuefire` 3.2+, `firebase` 12+ |
|
|
21
|
+
| 모듈 | `nuxt-vuefire` (호스트가 설치·설정) |
|
|
22
|
+
|
|
23
|
+
**의도적으로 하지 않는 일:** Firebase 초기화, 라우트/미들웨어 등록, App Check 초기화, 서버 검열.
|
|
6
24
|
|
|
7
25
|
## 구조 (0.4+)
|
|
8
26
|
|
|
9
27
|
| 엔트리 | 역할 |
|
|
10
28
|
|--------|------|
|
|
11
|
-
| `memi-board` | **core** — `configureMemiBoard`, composables, types |
|
|
12
|
-
| `memi-board/nuxt` | **thin Nuxt 모듈** —
|
|
29
|
+
| `memi-board` | **core** — `configureMemiBoard`, composables, types (**Vue 컴포넌트 export 없음**) |
|
|
30
|
+
| `memi-board/nuxt` | **thin Nuxt 모듈** — SFC를 호스트 Vite에 등록 → `MemiBoardList` 등 auto-import |
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
Firebase·인증 설정 지점은 항상 **호스트 하나**. UI 는 호스트가 SFC 를 컴파일해야 Nuxt UI auto-import(`UButton` 등)가 동작한다 — 그래서 prebundle 하지 않고 thin 모듈로 올린다.
|
|
32
|
+
런타임 설정은 모듈 options가 아니라 호스트 플러그인의 `configureMemiBoard()` 한곳.
|
|
16
33
|
|
|
17
|
-
|
|
34
|
+
```
|
|
35
|
+
호스트 Nuxt 앱
|
|
36
|
+
├── nuxt-vuefire + firebaseConfig ← 인증·Firestore
|
|
37
|
+
├── (선택) App Check 플러그인 ← AI Logic Enforce 시 필수에 가까움
|
|
38
|
+
├── memi-board/nuxt ← UI 등록
|
|
39
|
+
├── plugins/memi-board.ts ← configureMemiBoard
|
|
40
|
+
├── pages/board/* ← 라우트는 호스트 소유
|
|
41
|
+
└── firestore.rules + storage.rules ← docs 예시 병합 후 배포
|
|
42
|
+
```
|
|
18
43
|
|
|
19
44
|
## 설치
|
|
20
45
|
|
|
21
|
-
**사전
|
|
46
|
+
**사전 준비:** Node + pnpm, Firebase 프로젝트(Firestore·Storage·Auth). 처음부터면 → [docs/firebase-setup.md](docs/firebase-setup.md).
|
|
22
47
|
|
|
23
48
|
```bash
|
|
24
49
|
pnpm add memi-board @nuxt/ui nuxt-vuefire vuefire firebase
|
|
25
50
|
```
|
|
26
51
|
|
|
27
|
-
`vuefire
|
|
52
|
+
`vuefire` / `firebase` / `nuxt-vuefire` 는 **호스트에 직접 설치** — peer 이중 인스턴스가 나면 인증·Firestore가 깨진다.
|
|
28
53
|
|
|
29
|
-
### 1. modules + vuefire
|
|
54
|
+
### 1. modules + vuefire
|
|
30
55
|
|
|
31
56
|
```ts
|
|
32
57
|
// nuxt.config.ts
|
|
@@ -34,9 +59,11 @@ export default defineNuxtConfig({
|
|
|
34
59
|
modules: [
|
|
35
60
|
'@nuxt/ui',
|
|
36
61
|
'nuxt-vuefire',
|
|
37
|
-
'memi-board/nuxt',
|
|
62
|
+
'memi-board/nuxt',
|
|
38
63
|
],
|
|
39
64
|
vuefire: {
|
|
65
|
+
// 콘솔 → 프로젝트 설정 → 내 앱 → 웹 앱 SDK snippet 과 동일해야 함
|
|
66
|
+
// (appId 가 다른 웹 앱이면 App Check / AI Logic 이 실패한다)
|
|
40
67
|
config: {
|
|
41
68
|
apiKey: '...',
|
|
42
69
|
authDomain: '...',
|
|
@@ -50,7 +77,7 @@ export default defineNuxtConfig({
|
|
|
50
77
|
})
|
|
51
78
|
```
|
|
52
79
|
|
|
53
|
-
### 2. configureMemiBoard
|
|
80
|
+
### 2. configureMemiBoard
|
|
54
81
|
|
|
55
82
|
```ts
|
|
56
83
|
// app/plugins/memi-board.ts
|
|
@@ -58,14 +85,24 @@ import { configureMemiBoard } from 'memi-board'
|
|
|
58
85
|
|
|
59
86
|
export default defineNuxtPlugin(() => {
|
|
60
87
|
configureMemiBoard({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
// Firestore: {prefix}Posts, {prefix}Users, {prefix}Settings
|
|
89
|
+
collectionPrefix: 'board',
|
|
90
|
+
auth: { providers: ['google', 'apple'] }, // + 'emailPassword'
|
|
91
|
+
moderation: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
model: 'gemini-3.5-flash-lite', // 권장. 2.5-flash 는 신규 프로젝트에서 미제공일 수 있음
|
|
94
|
+
onError: 'block', // 'allow' | 'block' — AI 실패 시 저장 허용/거부 (기본 allow)
|
|
95
|
+
moderateImages: false,
|
|
96
|
+
useLimitedUseAppCheckTokens: false, // App Check 세션 토큰으로도 가능
|
|
97
|
+
// localBlocklist: ['금칙어'],
|
|
98
|
+
},
|
|
64
99
|
})
|
|
65
100
|
})
|
|
66
101
|
```
|
|
67
102
|
|
|
68
|
-
### 3. Tailwind `@source`
|
|
103
|
+
### 3. Tailwind `@source` (필수)
|
|
104
|
+
|
|
105
|
+
패키지 SFC 안의 클래스가 purge되지 않도록:
|
|
69
106
|
|
|
70
107
|
```css
|
|
71
108
|
/* app/assets/css/main.css */
|
|
@@ -74,11 +111,44 @@ export default defineNuxtPlugin(() => {
|
|
|
74
111
|
@source "../../../node_modules/memi-board/dist/runtime/components";
|
|
75
112
|
```
|
|
76
113
|
|
|
77
|
-
|
|
114
|
+
### 4. Security Rules + 인덱스 배포
|
|
115
|
+
|
|
116
|
+
1. [docs/firestore.rules.example](docs/firestore.rules.example) → 호스트 `firestore.rules`에 병합
|
|
117
|
+
(`memiBoard*` 이름을 쓰는 `collectionPrefix`에 맞게 치환)
|
|
118
|
+
2. [docs/storage.rules.example](docs/storage.rules.example) → `storage.rules` 병합
|
|
119
|
+
3. 카테고리 필터 목록을 쓰면 복합 인덱스 필요 (아래)
|
|
120
|
+
4. 배포:
|
|
121
|
+
```bash
|
|
122
|
+
firebase deploy --only firestore:rules,storage,firestore:indexes
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Rules를 파일에만 넣고 배포 안 하면** 글쓰기에서 `permission-denied` — 가장 흔한 설치 실수.
|
|
126
|
+
|
|
127
|
+
#### 카테고리 목록용 인덱스 예시
|
|
128
|
+
|
|
129
|
+
`firestore.indexes.json` (컬렉션 ID는 prefix에 맞게):
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"indexes": [
|
|
134
|
+
{
|
|
135
|
+
"collectionGroup": "boardPosts",
|
|
136
|
+
"queryScope": "COLLECTION",
|
|
137
|
+
"fields": [
|
|
138
|
+
{ "fieldPath": "category", "order": "ASCENDING" },
|
|
139
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> ⚠️ npm 패키지 tarball에도 `docs/` 가 포함된다(0.4.7+). 구버전이면 GitHub의 `docs/`를 보면 된다.
|
|
78
147
|
|
|
79
148
|
## 사용법
|
|
80
149
|
|
|
81
|
-
|
|
150
|
+
라우팅·미들웨어는 **호스트 소유**. 컴포넌트는 모듈 auto-import (`MemiBoardList` 등).
|
|
151
|
+
`import { MemiBoardEditor } from 'memi-board'` 는 **안 된다** — core 엔트리는 composable/types 만 export.
|
|
82
152
|
|
|
83
153
|
```vue
|
|
84
154
|
<!-- app/pages/board/index.vue -->
|
|
@@ -90,7 +160,15 @@ const { isSignedIn } = useMemiBoardAuth()
|
|
|
90
160
|
<template>
|
|
91
161
|
<div>
|
|
92
162
|
<UButton v-if="isSignedIn" to="/board/new" label="새 글쓰기" />
|
|
93
|
-
|
|
163
|
+
<!-- 단순: 상세 /board/{id} -->
|
|
164
|
+
<MemiBoardList post-link-base="/board" />
|
|
165
|
+
<!-- 카테고리 path: get-post-link 권장 -->
|
|
166
|
+
<!--
|
|
167
|
+
<MemiBoardList
|
|
168
|
+
:category="route.params.category as string"
|
|
169
|
+
:get-post-link="(p) => `/board/${p.category}/${p.id}`"
|
|
170
|
+
/>
|
|
171
|
+
-->
|
|
94
172
|
<MemiBoardSignIn v-if="!isSignedIn" />
|
|
95
173
|
</div>
|
|
96
174
|
</template>
|
|
@@ -112,10 +190,8 @@ const router = useRouter()
|
|
|
112
190
|
</template>
|
|
113
191
|
```
|
|
114
192
|
|
|
115
|
-
글쓰기(`/board/new`)·수정 라우트 가드도 호스트가 직접 (`vuefire` `getCurrentUser`):
|
|
116
|
-
|
|
117
193
|
```ts
|
|
118
|
-
// app/middleware/board-auth.ts
|
|
194
|
+
// app/middleware/board-auth.ts — 글쓰기 가드 (호스트)
|
|
119
195
|
import { getCurrentUser } from 'vuefire'
|
|
120
196
|
|
|
121
197
|
export default defineNuxtRouteMiddleware(async () => {
|
|
@@ -127,100 +203,172 @@ export default defineNuxtRouteMiddleware(async () => {
|
|
|
127
203
|
```vue
|
|
128
204
|
<!-- app/pages/board/new.vue -->
|
|
129
205
|
<script setup lang="ts">
|
|
130
|
-
import { MemiBoardEditor } from 'memi-board'
|
|
131
206
|
definePageMeta({ middleware: 'board-auth' })
|
|
132
207
|
const router = useRouter()
|
|
208
|
+
// MemiBoardEditor 는 auto-import (import from 'memi-board' 불필요)
|
|
133
209
|
</script>
|
|
134
210
|
|
|
135
211
|
<template>
|
|
136
|
-
<MemiBoardEditor
|
|
212
|
+
<MemiBoardEditor
|
|
213
|
+
@saved="(id) => router.push(`/board/${id}`)"
|
|
214
|
+
@cancel="router.push('/board')"
|
|
215
|
+
/>
|
|
216
|
+
<!-- path 로 카테고리 고정 시: fixed-category="free" -->
|
|
137
217
|
</template>
|
|
138
218
|
```
|
|
139
219
|
|
|
140
|
-
`playground/app/pages/board
|
|
220
|
+
전체 페이지 예시는 `playground/app/pages/board/` 참고.
|
|
141
221
|
|
|
142
|
-
제공하는
|
|
143
|
-
제공하는 composable: `useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`.
|
|
222
|
+
### 제공하는 API
|
|
144
223
|
|
|
145
|
-
|
|
224
|
+
**컴포넌트 (auto-import):**
|
|
225
|
+
`MemiBoardList`, `MemiBoardDetail`, `MemiBoardEditor`, `MemiBoardCommentList`, `MemiBoardCommentForm`, `MemiBoardAttachments`, `MemiBoardSignIn`, `MemiBoardVersionHistory`
|
|
226
|
+
|
|
227
|
+
**List props:** `pageSize`, `category`, `postLinkBase`, `getPostLink(post)`, `linkBase`(deprecated)
|
|
228
|
+
|
|
229
|
+
**Editor props:** `postId`(수정), `fixedCategory`(path 고정 시 선택 UI 숨김)
|
|
230
|
+
본문은 Nuxt UI **`UEditor`**(TipTap, `content-type="markdown"`). 호스트 `@nuxt/ui` 4.x 에 Editor 포함 필요.
|
|
146
231
|
|
|
147
|
-
|
|
232
|
+
**에디터 이미지:** 툴바 / 붙여넣기 / 드롭 → Firebase Storage
|
|
233
|
+
`{prefix}/posts/{postId}/images/*` + `.../images/thumbnails/*` (원본+썸네일).
|
|
234
|
+
본문 markdown 에는 원본 URL. 수정 중 버려진 파일은 호스트 스케줄러로 정리 (`extractEditorImageUrls` 로 본문 참조 비교).
|
|
148
235
|
|
|
149
|
-
|
|
236
|
+
**composables:**
|
|
237
|
+
`useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`
|
|
238
|
+
|
|
239
|
+
### 카테고리
|
|
240
|
+
|
|
241
|
+
- 문서: `{prefix}Settings/config` → `categories: { id, label }[]`
|
|
242
|
+
- 문서 없으면 UI는 기본(자유/공지/질문)으로 동작
|
|
243
|
+
- **로그인 사용자**가 글쓰기 폼에서 카테고리 추가 가능 (`ensureSettings`로 문서 시드)
|
|
244
|
+
- 관리자 전용 설정 UI는 없음 — 콘솔에서 배열 수정 가능
|
|
150
245
|
|
|
151
246
|
### 로그인 공급자
|
|
152
247
|
|
|
153
|
-
`
|
|
248
|
+
`auth.providers` 기본 `['google', 'apple']`, `'emailPassword'` 추가 가능.
|
|
154
249
|
|
|
155
|
-
- **Google
|
|
156
|
-
- **Apple
|
|
250
|
+
- **Google:** 콘솔 Sign-in method 활성화
|
|
251
|
+
- **Apple:** Services ID·키 등 추가 설정 → [docs/firebase-setup.md#apple](docs/firebase-setup.md#apple)
|
|
157
252
|
|
|
158
|
-
### 버전 히스토리
|
|
253
|
+
### 버전 히스토리
|
|
159
254
|
|
|
160
|
-
`<MemiBoardVersionHistory />`
|
|
255
|
+
`<MemiBoardVersionHistory />` 를 페이지에 두면 패키지 기능 변경을 방문자에게 보여줄 수 있다.
|
|
161
256
|
|
|
162
|
-
## Security Model
|
|
257
|
+
## Security Model
|
|
163
258
|
|
|
164
|
-
|
|
259
|
+
서버가 없으므로 **Rules가 전부**다.
|
|
165
260
|
|
|
166
|
-
|
|
167
|
-
2. [`docs/storage.rules.example`](docs/storage.rules.example) — 호스트 프로젝트의 `storage.rules`에 병합
|
|
168
|
-
3. **배포까지 실행**:
|
|
169
|
-
```bash
|
|
170
|
-
firebase deploy --only firestore:rules,storage
|
|
171
|
-
```
|
|
172
|
-
(호스트 프로젝트에 이미 `firestore.rules`/`storage.rules`가 배포 파이프라인에 연결돼 있다면, 평소 쓰던 배포 명령에 이 파일들이 포함돼 있는지 확인한다. Firebase CLI를 처음 연결하는 프로젝트라면 [docs/firebase-setup.md](docs/firebase-setup.md#4-호스트-프로젝트를-firebase-cli에-연결)부터 본다.)
|
|
261
|
+
권한 요약:
|
|
173
262
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
263
|
+
| 동작 | 조건 |
|
|
264
|
+
|------|------|
|
|
265
|
+
| 글/댓글 읽기 | 공개 |
|
|
266
|
+
| 글/댓글 작성 | 로그인 + `moderationStatus == 'approved'` (클라가 항상 approved 로 씀 — 검열 통과 후에만 write) |
|
|
267
|
+
| 글 수정·삭제 | 작성자 또는 `{prefix}Users/{uid}.role == 'admin'` |
|
|
268
|
+
| 댓글 삭제 | 댓글 작성자 · 글 작성자 · admin |
|
|
269
|
+
| 관리자 승격 | **패키지가 하지 않음** — 콘솔에서 `role: 'admin'` 수동 설정 |
|
|
178
270
|
|
|
179
|
-
**알려진
|
|
271
|
+
**알려진 한계:** AI 검열은 클라이언트 전용. SDK로 직접 write 하면 우회 가능. 중요 서비스면 Cloud Functions 검증을 호스트가 추가(패키지 범위 밖).
|
|
180
272
|
|
|
181
|
-
## Firebase AI Logic (
|
|
273
|
+
## Firebase AI Logic (검열)
|
|
182
274
|
|
|
183
|
-
|
|
275
|
+
흐름: **로컬 금칙어 → Gemini 구조화 JSON** → 걸리면 저장 안 함.
|
|
184
276
|
|
|
185
|
-
|
|
277
|
+
| 옵션 | 기본 | 설명 |
|
|
278
|
+
|------|------|------|
|
|
279
|
+
| `enabled` | `true` | 끄면 로컬만 / 또는 전부 통과에 가깝게 |
|
|
280
|
+
| `model` | `gemini-3.5-flash-lite` | Firebase AI Logic 지원 모델명 |
|
|
281
|
+
| `onError` | `'allow'` | AI 실패 시 `'block'` 이면 저장 거부 |
|
|
282
|
+
| `localBlocklist` | `[]` | 기본 목록에 문자열 추가 |
|
|
283
|
+
| `useLimitedUseAppCheckTokens` | `true` | `false`면 일반 App Check 세션 토큰 |
|
|
284
|
+
| `blockBanThreshold` | `3` | 콘텐츠 차단(local/ai) 누적 이상이면 글·댓글 제한. `0` = off |
|
|
285
|
+
| `blockBanDecayMs` | `86400000` | 경고 1회 차감 간격(기본 24h) |
|
|
186
286
|
|
|
187
|
-
|
|
287
|
+
콘텐츠 차단이 누적되면 `{prefix}Users/{uid}` 에 `moderationBlockCount` / `moderationBlockAt` 이 기록된다.
|
|
288
|
+
3회면 약 하루 동안 작성 불가(24h마다 1 차감). Rules 예시에서 본인은 **+1만** 가능하고 감소·리셋은 admin.
|
|
188
289
|
|
|
189
|
-
|
|
290
|
+
콘솔에서 **AI Logic(Gemini Developer API)** 활성화. 신규 프로젝트는 구형 `gemini-2.5-flash` 가 404 날 수 있음 → 3.x Flash / Flash-Lite 사용.
|
|
190
291
|
|
|
191
|
-
|
|
292
|
+
### App Check (AI Enforce 시 사실상 필수)
|
|
192
293
|
|
|
193
|
-
|
|
294
|
+
`firebaseml` / AI Logic 이 **Enforce** 되어 있으면 App Check 토큰 없이 `generateContent` 가 실패한다.
|
|
194
295
|
|
|
195
|
-
|
|
296
|
+
1. 콘솔 App Check → 웹 앱에 **reCAPTCHA v3** 등록 (사이트 키·시크릿)
|
|
297
|
+
2. 호스트에서 **`initializeAppCheck`** (memi-board 밖). 예:
|
|
196
298
|
|
|
197
|
-
|
|
299
|
+
```ts
|
|
300
|
+
// app/plugins/app-check.client.ts
|
|
301
|
+
import { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check'
|
|
302
|
+
import { useFirebaseApp } from 'vuefire'
|
|
198
303
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
304
|
+
export default defineNuxtPlugin(() => {
|
|
305
|
+
if (!import.meta.client) return
|
|
306
|
+
// 로컬: self.FIREBASE_APPCHECK_DEBUG_TOKEN = true | '<등록된-uuid>'
|
|
307
|
+
initializeAppCheck(useFirebaseApp(), {
|
|
308
|
+
provider: new ReCaptchaV3Provider('<RECAPTCHA_SITE_KEY>'),
|
|
309
|
+
isTokenAutoRefreshEnabled: true,
|
|
310
|
+
})
|
|
311
|
+
})
|
|
204
312
|
```
|
|
205
313
|
|
|
206
|
-
|
|
314
|
+
3. 로컬: 디버그 토큰을 콘솔에 등록 (또는 고정 UUID)
|
|
315
|
+
4. `firebaseConfig.appId` 가 App Check에 등록한 **웹 앱과 동일**한지 확인
|
|
316
|
+
5. (선택) Firestore/Storage 에도 Enforce — 켜면 토큰 없는 요청 거부
|
|
317
|
+
|
|
318
|
+
> `nuxt-vuefire` 의 `vuefire.appCheck` 옵션만으로 AI Logic까지 충분한지는 SDK/버전에 따라 다를 수 있다. 문제가 있으면 위처럼 명시적 `initializeAppCheck` 를 권장한다.
|
|
319
|
+
|
|
320
|
+
## 다른 프로젝트에 붙일 때 체크리스트
|
|
321
|
+
|
|
322
|
+
- [ ] 동일 스택: Nuxt 4 + `@nuxt/ui` 4 + `nuxt-vuefire` + Firebase
|
|
323
|
+
- [ ] `pnpm add memi-board` 및 peer 직접 설치
|
|
324
|
+
- [ ] `modules`에 `memi-board/nuxt`
|
|
325
|
+
- [ ] `configureMemiBoard` 플러그인 (`collectionPrefix` 충돌 없게)
|
|
326
|
+
- [ ] CSS `@source` …/memi-board/dist/runtime/components
|
|
327
|
+
- [ ] Rules 예시 병합 + **deploy**
|
|
328
|
+
- [ ] 카테고리 필터 사용 시 indexes deploy
|
|
329
|
+
- [ ] 페이지·미들웨어 직접 작성 (또는 playground 복사)
|
|
330
|
+
- [ ] (검열) AI Logic 활성화 + 권장 모델 + App Check
|
|
331
|
+
- [ ] `pnpm why vuefire` 로 이중 인스턴스 없는지 확인
|
|
332
|
+
|
|
333
|
+
같은 Firebase 프로젝트에 호스트 앱 유저 컬렉션이 있어도 무방 — 게시판 역할은 **`{prefix}Users` 전용**이다.
|
|
334
|
+
|
|
335
|
+
## 문제 해결
|
|
336
|
+
|
|
337
|
+
### `useCurrentUser() called before the VueFireAuth module...`
|
|
338
|
+
|
|
339
|
+
`vuefire.auth.enabled` 미설정, 또는 `vuefire`/`firebase` 이중 설치. `pnpm why vuefire` → 하나로. 모듈이 `vite.resolve.dedupe`에 vue/vuefire/firebase를 넣지만, **호스트 peer 설치**가 우선이다.
|
|
340
|
+
|
|
341
|
+
### 카테고리 필터 쿼리 에러 / 인덱스 링크
|
|
342
|
+
|
|
343
|
+
콘솔이 안내하는 복합 인덱스 생성, 또는 위 `firestore.indexes.json` 배포.
|
|
344
|
+
|
|
345
|
+
### AI 검열 실패 / 글 안 올라감 (`onError: 'block'`)
|
|
346
|
+
|
|
347
|
+
App Check 토큰, `appId` 일치, 모델명(3.x), AI Logic 활성화, 네트워크. 브라우저 콘솔의 `moderation AI failed` warn 확인.
|
|
348
|
+
|
|
349
|
+
### monorepo `link:` / `file:` 로 개발할 때
|
|
350
|
+
|
|
351
|
+
패키지 쪽 `node_modules`의 vuefire가 잡히면 이중 인스턴스. 호스트에서 `vite.resolve.dedupe` + 필요 시 alias/optimizeDeps. 배포 전에는 **npm 버전**으로 설치하는 것을 권장.
|
|
352
|
+
|
|
353
|
+
## 로컬 개발 (이 리포)
|
|
207
354
|
|
|
208
355
|
```bash
|
|
209
|
-
pnpm
|
|
356
|
+
pnpm install
|
|
357
|
+
pnpm build
|
|
358
|
+
pnpm dev:prepare
|
|
359
|
+
pnpm dev # playground
|
|
360
|
+
pnpm build:watch # src 변경 시 dist 갱신
|
|
210
361
|
```
|
|
211
362
|
|
|
212
|
-
|
|
363
|
+
에뮬레이터:
|
|
213
364
|
|
|
214
365
|
```bash
|
|
215
366
|
firebase emulators:start --project demo-memi-board
|
|
216
367
|
NUXT_PUBLIC_USE_EMULATORS=1 pnpm dev
|
|
217
368
|
```
|
|
218
369
|
|
|
219
|
-
## 버전
|
|
220
|
-
|
|
221
|
-
- 사이트에 설치된 상태에서 보기: `<MemiBoardVersionHistory />` 컴포넌트를 원하는 페이지에 배치
|
|
222
|
-
- 코드 수준의 자세한 변경 내역: [CHANGELOG.md](CHANGELOG.md)
|
|
223
|
-
|
|
224
|
-
## 라이선스
|
|
370
|
+
## 버전 · 라이선스
|
|
225
371
|
|
|
226
|
-
|
|
372
|
+
- UI: `<MemiBoardVersionHistory />`
|
|
373
|
+
- 상세: [CHANGELOG.md](CHANGELOG.md)
|
|
374
|
+
- **MIT**
|
|
@@ -7,6 +7,12 @@ export interface UseMemiBoardAuthReturn {
|
|
|
7
7
|
isSignedIn: ComputedRef<boolean>;
|
|
8
8
|
isAdmin: ComputedRef<boolean>;
|
|
9
9
|
rolePending: Ref<boolean>;
|
|
10
|
+
/** 유효 검열 차단 누적 (lazy decay 반영) */
|
|
11
|
+
moderationBlockCount: ComputedRef<number>;
|
|
12
|
+
/** 글·댓글 작성 제한 중 */
|
|
13
|
+
isWriteRestricted: ComputedRef<boolean>;
|
|
14
|
+
restrictedUntilLabel: ComputedRef<string | null>;
|
|
15
|
+
restrictedMessage: ComputedRef<string | null>;
|
|
10
16
|
signInWithGoogle: () => ReturnType<typeof signInWithPopup>;
|
|
11
17
|
signInWithApple: () => ReturnType<typeof signInWithPopup>;
|
|
12
18
|
signInWithEmail: (email: string, password: string) => ReturnType<typeof signInWithEmailAndPassword>;
|
|
@@ -15,10 +21,16 @@ export interface UseMemiBoardAuthReturn {
|
|
|
15
21
|
canEdit: (post: Pick<PostModel, 'authorUid'>) => boolean;
|
|
16
22
|
canDelete: (post: Pick<PostModel, 'authorUid'>) => boolean;
|
|
17
23
|
canDeleteComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean;
|
|
24
|
+
/** 콘텐츠 검열 차단 1회 기록 후 메시지 조각 반환 */
|
|
25
|
+
recordContentModerationBlock: () => Promise<{
|
|
26
|
+
effectiveCount: number;
|
|
27
|
+
restricted: boolean;
|
|
28
|
+
messageSuffix: string;
|
|
29
|
+
}>;
|
|
30
|
+
refreshBoardUser: () => Promise<void>;
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
20
|
-
* 인증 상태 + 게시판 역할(role)
|
|
21
|
-
* role
|
|
22
|
-
* (관리자 승격은 호스트 프로젝트 소유자가 Firebase 콘솔에서 직접 설정).
|
|
33
|
+
* 인증 상태 + 게시판 역할(role) + 검열 차단 누적.
|
|
34
|
+
* role·경고 카운트는 globalThis 공유 (여러 composable 호출이 같은 상태).
|
|
23
35
|
*/
|
|
24
36
|
export declare function useMemiBoardAuth(): UseMemiBoardAuthReturn;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ModerationResult } from '../types';
|
|
2
2
|
/**
|
|
3
|
-
* 글쓰기 전 블로킹 검열.
|
|
3
|
+
* 글쓰기 전 블로킹 검열.
|
|
4
|
+
* 이용 제한 확인 → 로컬 비속어 → Firebase AI Logic(Gemini).
|
|
5
|
+
* 콘텐츠 차단(local/ai) 시 boardUsers 경고 누적.
|
|
4
6
|
*/
|
|
5
7
|
export declare function useMemiBoardModeration(): {
|
|
6
8
|
checkText: (text: string) => Promise<ModerationResult>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { Attachment } from '../types';
|
|
1
|
+
import { Attachment, EditorImageEntry } from '../types';
|
|
2
|
+
/** 에디터 이미지 최대 크기 (바이트) */
|
|
3
|
+
export declare const EDITOR_IMAGE_MAX_BYTES: number;
|
|
2
4
|
export declare function useMemiBoardStorage(): {
|
|
3
5
|
uploadAttachment: (file: File, postId: string, onProgress?: (ratio: number) => void) => {
|
|
4
6
|
promise: Promise<Attachment>;
|
|
5
7
|
cancel: () => void;
|
|
6
8
|
};
|
|
7
9
|
deleteAttachment: (attachment: Pick<Attachment, "path">) => Promise<void>;
|
|
10
|
+
uploadEditorImage: (file: File, postId: string) => Promise<EditorImageEntry>;
|
|
11
|
+
deleteEditorImage: (entry: Pick<EditorImageEntry, "originalPath" | "thumbnailPath">) => Promise<void>;
|
|
8
12
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export interface MemiBoardModerationOptions {
|
|
|
9
9
|
moderateImages?: boolean;
|
|
10
10
|
/** AI Logic App Check limited-use 토큰 (재생 보호). 기본 true */
|
|
11
11
|
useLimitedUseAppCheckTokens?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* 콘텐츠 검열 차단(로컬/AI flagged) 누적 횟수 이상이 되면 글·댓글 작성 제한.
|
|
14
|
+
* 기본 3. 0 이하면 제한 기능 off.
|
|
15
|
+
*/
|
|
16
|
+
blockBanThreshold?: number;
|
|
17
|
+
/** 경고 1회 차감 간격(ms). 기본 24시간 */
|
|
18
|
+
blockBanDecayMs?: number;
|
|
12
19
|
}
|
|
13
20
|
export interface MemiBoardConfig {
|
|
14
21
|
collectionPrefix: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,4 +12,9 @@ export { useMemiBoardSettings, DEFAULT_CATEGORIES } from './composables/useMemiB
|
|
|
12
12
|
export { versionHistory } from './data/versionHistory';
|
|
13
13
|
export type { VersionHistoryEntry } from './data/versionHistory';
|
|
14
14
|
export { formatDate } from './utils/formatDate';
|
|
15
|
-
export
|
|
15
|
+
export { DEFAULT_BLOCK_BAN_THRESHOLD, DEFAULT_BLOCK_BAN_DECAY_MS, effectiveModerationBlockCount, isModerationWriteRestricted, moderationWriteRestrictedUntilMs, formatRestrictedUntilLabel, } from './utils/moderation-strike';
|
|
16
|
+
export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, CommentModel, BoardUserRole, BoardCategory, BoardSettingsModel, ModerationResult, } from './types';
|
|
17
|
+
export { compressImage } from './utils/compressImage';
|
|
18
|
+
export { extractEditorImageUrls } from './utils/extractEditorImageUrls';
|
|
19
|
+
export { renderMarkdownToHtml } from './utils/renderMarkdown';
|
|
20
|
+
export { EDITOR_IMAGE_MAX_BYTES } from './composables/useMemiBoardStorage';
|