memi-board 0.19.3 → 0.21.0
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 +19 -17
- package/dist/composables/fetchPublicSeo.d.ts +6 -6
- package/dist/composables/useMemiBoardComments.d.ts +3 -3
- package/dist/composables/useMemiBoardLikes.d.ts +1 -8
- package/dist/composables/useMemiBoardModeration.d.ts +1 -1
- package/dist/composables/useMemiBoardPosts.d.ts +7 -28
- package/dist/composables/useMemiBoardSettings.d.ts +27 -9
- package/dist/composables/useMemiBoardStorage.d.ts +2 -2
- package/dist/composables/useMemiBoardUsers.d.ts +2 -0
- package/dist/composables/useMemiBoardViews.d.ts +3 -7
- package/dist/config.d.ts +7 -23
- package/dist/heic2any-BXb_U1zK.js +977 -0
- package/dist/heic2any-BXb_U1zK.js.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +779 -673
- package/dist/index.js.map +1 -1
- package/dist/rolldown-runtime-DtPi1Y-2.js +13 -0
- package/dist/runtime/components/Attachments.vue +2 -1
- package/dist/runtime/components/CategorySettings.vue +1 -1
- package/dist/runtime/components/CommentForm.vue +9 -5
- package/dist/runtime/components/CommentItem.vue +6 -1
- package/dist/runtime/components/CommentList.vue +7 -4
- package/dist/runtime/components/CommentThread.vue +16 -5
- package/dist/runtime/components/Detail.vue +12 -10
- package/dist/runtime/components/Editor.vue +13 -114
- package/dist/runtime/components/LikeButton.vue +2 -1
- package/dist/runtime/components/List.vue +11 -8
- package/dist/runtime/components/Settings.vue +1 -1
- package/dist/types.d.ts +20 -17
- package/dist/utils/boardPaths.d.ts +26 -0
- package/docs/firebase-setup.md +1 -1
- package/docs/firestore.rules.example +99 -226
- package/docs/storage.rules.example +23 -25
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Nuxt 4 + Nuxt UI + Firebase 프로젝트에 게시판을 붙이는 npm 패키지
|
|
|
10
10
|
**서버 / Firebase Admin 불필요** (권한은 Rules, 검열은 브라우저 — 우회 가능, 아래 한계 참고).
|
|
11
11
|
공개 목록·글 상세만 SSR로 OG를 채우고, 쓰기·설정은 CSR로 두면 된다.
|
|
12
12
|
|
|
13
|
-
같은 스택(Nuxt 4 · `@nuxt/ui` · `nuxt-vuefire` · Firebase)이면 **다른 호스트 앱에도 그대로 재사용** 가능하다. Firebase 프로젝트는 호스트마다 달라도 되고, 같은
|
|
13
|
+
같은 스택(Nuxt 4 · `@nuxt/ui` · `nuxt-vuefire` · Firebase)이면 **다른 호스트 앱에도 그대로 재사용** 가능하다. Firebase 프로젝트는 호스트마다 달라도 되고, 같은 프로젝트에서는 `boardId`로 게시판 인스턴스를 나눈다 (`memiBoards/{boardId}/…`).
|
|
14
14
|
|
|
15
15
|
## 플랫폼 요구사항
|
|
16
16
|
|
|
@@ -71,8 +71,9 @@ export default defineNuxtConfig({
|
|
|
71
71
|
],
|
|
72
72
|
|
|
73
73
|
memiBoard: {
|
|
74
|
-
// Firestore: {
|
|
75
|
-
|
|
74
|
+
// Firestore: memiBoards/{boardId}/posts|settings|users
|
|
75
|
+
boardsCollection: 'memiBoards', // 기본값
|
|
76
|
+
boardId: 'main', // 게시판 인스턴스 ID (필수에 가깝다)
|
|
76
77
|
auth: { providers: ['google', 'apple'] }, // + 'emailPassword'
|
|
77
78
|
moderation: {
|
|
78
79
|
enabled: true,
|
|
@@ -134,7 +135,8 @@ export default defineNuxtConfig({
|
|
|
134
135
|
|
|
135
136
|
| 키 | 기본 | 설명 |
|
|
136
137
|
|----|------|------|
|
|
137
|
-
| `
|
|
138
|
+
| `boardsCollection` | `'memiBoards'` | 루트 컬렉션 이름 |
|
|
139
|
+
| `boardId` | `'default'` | `memiBoards/{boardId}/…` 게시판 문서 ID |
|
|
138
140
|
| `auth.providers` | `['google','apple']` | 로그인 공급자 |
|
|
139
141
|
| `moderation.*` | (아래 AI 절) | 로컬·Gemini 검열 |
|
|
140
142
|
| `seo.enabled` | `true` | `false`면 SEO composable no-op |
|
|
@@ -211,7 +213,7 @@ const { post: postSeo } = await useMemiBoardPostSeo({
|
|
|
211
213
|
### 3. 부모 설정 체크리스트
|
|
212
214
|
|
|
213
215
|
- [ ] `modules`에 `@nuxt/ui`, `nuxt-vuefire`, `memi-board`
|
|
214
|
-
- [ ] `memiBoard.
|
|
216
|
+
- [ ] `memiBoard.boardId` · `auth` · `moderation` · **`seo`**
|
|
215
217
|
- [ ] `seo.siteUrl` / `seo.siteName` / `seo.defaultOgImage` / `seo.basePath`
|
|
216
218
|
- [ ] 공개 board 경로 **SSR on**, 쓰기·설정만 `ssr: false`
|
|
217
219
|
- [ ] 목록·상세 페이지에 `await useMemiBoard*Seo(…)` 한 줄
|
|
@@ -234,7 +236,7 @@ const { post: postSeo } = await useMemiBoardPostSeo({
|
|
|
234
236
|
### 5. Security Rules + 인덱스 배포
|
|
235
237
|
|
|
236
238
|
1. [docs/firestore.rules.example](docs/firestore.rules.example) → 호스트 `firestore.rules`에 병합
|
|
237
|
-
(`
|
|
239
|
+
(`memiBoards/{boardId}/…` 경로 — boardId 는 호스트 설정과 동일)
|
|
238
240
|
2. [docs/storage.rules.example](docs/storage.rules.example) → `storage.rules` 병합
|
|
239
241
|
3. 카테고리 필터 목록을 쓰면 복합 인덱스 필요 (아래)
|
|
240
242
|
4. 배포:
|
|
@@ -252,7 +254,7 @@ const { post: postSeo } = await useMemiBoardPostSeo({
|
|
|
252
254
|
{
|
|
253
255
|
"indexes": [
|
|
254
256
|
{
|
|
255
|
-
"collectionGroup": "
|
|
257
|
+
"collectionGroup": "posts",
|
|
256
258
|
"queryScope": "COLLECTION",
|
|
257
259
|
"fields": [
|
|
258
260
|
{ "fieldPath": "category", "order": "ASCENDING" },
|
|
@@ -270,9 +272,9 @@ const { post: postSeo } = await useMemiBoardPostSeo({
|
|
|
270
272
|
Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따라서 최초 한 번은 다음 순서가 필요하다.
|
|
271
273
|
|
|
272
274
|
1. 아래 게시판 페이지를 만든 뒤 Google 또는 Apple로 로그인한다.
|
|
273
|
-
2. Firebase Console → Firestore에서 `{
|
|
275
|
+
2. Firebase Console → Firestore에서 `memiBoards/{boardId}/users/{uid}` 문서를 찾는다.
|
|
274
276
|
3. 해당 문서의 `role`을 문자열 `admin`으로 변경한다.
|
|
275
|
-
- `
|
|
277
|
+
- 예: `boardId: 'main'` → `memiBoards/main/users/{uid}`
|
|
276
278
|
- UID는 Firebase Console → Authentication → Users에서도 확인할 수 있다.
|
|
277
279
|
4. `/board-settings`에서 첫 카테고리를 만든다. 카테고리가 없으면 글을 작성할 수 없다.
|
|
278
280
|
5. 이후 역할 변경은 `/board-users`의 `MemiBoardUsers`에서 처리한다.
|
|
@@ -358,7 +360,7 @@ const router = useRouter()
|
|
|
358
360
|
|
|
359
361
|
### 관리 페이지
|
|
360
362
|
|
|
361
|
-
기본 Rules는 게시판의 `{
|
|
363
|
+
기본 Rules는 게시판의 `memiBoards/{boardId}/users/{uid}.role == 'admin'`을 검사한다. 컴포넌트도 같은 role을 확인하므로 기본 설치에서는 `authorized`를 전달하지 않는다.
|
|
362
364
|
|
|
363
365
|
```vue
|
|
364
366
|
<!-- app/pages/board-settings.vue -->
|
|
@@ -387,7 +389,7 @@ const router = useRouter()
|
|
|
387
389
|
본문은 Nuxt UI **`UEditor`**(TipTap, `content-type="html"`). 호스트 `@nuxt/ui` 4.x 에 Editor 포함 필요.
|
|
388
390
|
|
|
389
391
|
**에디터 이미지:** 툴바 / 붙여넣기 / 드롭 → Firebase Storage
|
|
390
|
-
`{
|
|
392
|
+
`memiBoards/{boardId}/posts/{postId}/images/*` + `.../images/thumbnails/*` (최적화 이미지+썸네일).
|
|
391
393
|
|
|
392
394
|
본문 이미지는 휴대폰 원본 기준 최대 25MB까지 선택할 수 있다. 5MB를 넘으면 브라우저에서 최대 2560px·JPEG 85%로 최적화하고, 결과가 여전히 5MB를 넘으면 2048px·75%로 한 번 더 줄인 뒤 업로드한다. 5MB 이하 이미지는 원본을 유지한다.
|
|
393
395
|
본문 HTML에는 원본 URL. 수정 중 버려진 파일은 호스트 스케줄러로 정리 (`extractEditorImageUrls` 로 본문 참조 비교).
|
|
@@ -403,7 +405,7 @@ const router = useRouter()
|
|
|
403
405
|
|
|
404
406
|
### 카테고리
|
|
405
407
|
|
|
406
|
-
- 문서: `{
|
|
408
|
+
- 문서: `memiBoards/{boardId}/settings/config/categories/{categoryId}` → `{ label, listView, writeRole, order, updatedAt }`
|
|
407
409
|
- 설정이 없으면 빈 목록이며 기본 카테고리를 자동 생성하지 않음
|
|
408
410
|
- `MemiBoardSettings`에서 관리자가 카테고리별 저장·삭제·순서·리스트뷰·글쓰기 최소 역할 관리
|
|
409
411
|
- `categoryId`는 문서 ID이자 게시글의 `category` 참조값이므로 생성 후 변경하지 않는 것을 권장
|
|
@@ -430,7 +432,7 @@ const router = useRouter()
|
|
|
430
432
|
| 글/댓글 읽기 | 공개 |
|
|
431
433
|
| 글 작성 | 로그인 + 카테고리 `writeRole` 충족 + `moderationStatus == 'approved'` |
|
|
432
434
|
| 댓글 작성 | 로그인 + `moderationStatus == 'approved'` (클라가 항상 approved 로 씀 — 검열 통과 후에만 write) |
|
|
433
|
-
| 글 수정·삭제 | 작성자 또는 `{
|
|
435
|
+
| 글 수정·삭제 | 작성자 또는 `memiBoards/{boardId}/users/{uid}.role in ['admin', 'staff']` |
|
|
434
436
|
| 댓글 삭제 | 댓글 작성자 · 글 작성자 · admin · staff |
|
|
435
437
|
| 역할 관리 | `MemiBoardUsers`에서 admin이 `admin`·`staff`·`user` 변경 |
|
|
436
438
|
|
|
@@ -450,7 +452,7 @@ const router = useRouter()
|
|
|
450
452
|
| `blockBanThreshold` | `3` | 콘텐츠 차단(local/ai) 누적 이상이면 글·댓글 제한. `0` = off |
|
|
451
453
|
| `blockBanDecayMs` | `86400000` | 경고 1회 차감 간격(기본 24h) |
|
|
452
454
|
|
|
453
|
-
콘텐츠 차단이 누적되면 `{
|
|
455
|
+
콘텐츠 차단이 누적되면 `memiBoards/{boardId}/users/{uid}` 에 `moderationBlockCount` / `moderationBlockAt` 이 기록된다.
|
|
454
456
|
3회면 약 하루 동안 작성 불가(24h마다 1 차감). Rules 예시에서 본인은 **+1만** 가능하고 감소·리셋은 admin.
|
|
455
457
|
|
|
456
458
|
콘솔에서 **AI Logic(Gemini Developer API)** 활성화. 신규 프로젝트는 구형 `gemini-2.5-flash` 가 404 날 수 있음 → 3.x Flash / Flash-Lite 사용.
|
|
@@ -488,7 +490,7 @@ export default defineNuxtPlugin(() => {
|
|
|
488
490
|
- [ ] 동일 스택: Nuxt 4 + `@nuxt/ui` 4 + `nuxt-vuefire` + Firebase
|
|
489
491
|
- [ ] `pnpm add memi-board @nuxt/ui nuxt-vuefire vuefire firebase dayjs`
|
|
490
492
|
- [ ] `modules`에 `@nuxt/ui`, `nuxt-vuefire`, `memi-board`
|
|
491
|
-
- [ ] `memiBoard` (`
|
|
493
|
+
- [ ] `memiBoard` (`boardId` · `auth` · `moderation` · **`seo`**)
|
|
492
494
|
- [ ] 공개 목록·상세 **SSR on**, 쓰기·설정만 `ssr: false` (전 경로 CSR 금지)
|
|
493
495
|
- [ ] 목록·상세 페이지에 `await useMemiBoardListSeo` / `useMemiBoardPostSeo`
|
|
494
496
|
- [ ] 호스트 SEO API 없음
|
|
@@ -496,12 +498,12 @@ export default defineNuxtPlugin(() => {
|
|
|
496
498
|
- [ ] Rules 예시 병합 + **deploy**
|
|
497
499
|
- [ ] 카테고리·listed 쿼리 사용 시 indexes deploy
|
|
498
500
|
- [ ] 페이지·미들웨어 직접 작성 (또는 playground 복사)
|
|
499
|
-
- [ ] 최초 로그인 후 Firebase Console에서 `{
|
|
501
|
+
- [ ] 최초 로그인 후 Firebase Console에서 `memiBoards/{boardId}/users/{uid}.role = 'admin'`
|
|
500
502
|
- [ ] 설정 페이지에서 첫 카테고리 생성
|
|
501
503
|
- [ ] (검열) AI Logic 활성화 + 권장 모델 + App Check
|
|
502
504
|
- [ ] `pnpm why vuefire` 로 이중 인스턴스 없는지 확인
|
|
503
505
|
|
|
504
|
-
같은 Firebase 프로젝트에 호스트 앱 유저 컬렉션이 있어도 무방 — 게시판 역할은 **`{
|
|
506
|
+
같은 Firebase 프로젝트에 호스트 앱 유저 컬렉션이 있어도 무방 — 게시판 역할은 **`memiBoards/{boardId}/users` 전용**이다.
|
|
505
507
|
|
|
506
508
|
## 문제 해결
|
|
507
509
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Firestore } from 'firebase/firestore';
|
|
2
|
+
import { BoardPathConfig } from '../utils/boardPaths';
|
|
2
3
|
import { BoardListSeoPayload, BoardPostSeoPayload } from '../utils/boardSeo';
|
|
3
4
|
export type PublicSeoDb = {
|
|
4
5
|
db: Firestore;
|
|
5
|
-
|
|
6
|
+
paths: BoardPathConfig;
|
|
6
7
|
};
|
|
7
|
-
/** setup 동기 구간에서만 호출 — useFirestore + config 캡처 */
|
|
8
8
|
export declare function resolvePublicSeoDb(): PublicSeoDb;
|
|
9
|
-
/** 공개 글 1건 (
|
|
10
|
-
export declare function fetchPublicPostForSeo(
|
|
11
|
-
/** 전체 또는
|
|
12
|
-
export declare function fetchPublicListForSeo(
|
|
9
|
+
/** 공개 글 1건 (boardId + slug) */
|
|
10
|
+
export declare function fetchPublicPostForSeo(boardId: string, slug: string, ctx?: PublicSeoDb): Promise<BoardPostSeoPayload | null>;
|
|
11
|
+
/** 전체 보드 목록 또는 단일 보드 SEO */
|
|
12
|
+
export declare function fetchPublicListForSeo(boardId?: string | null, ctx?: PublicSeoDb): Promise<BoardListSeoPayload>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ref } from 'vue';
|
|
1
|
+
import { MaybeRefOrGetter, Ref } from 'vue';
|
|
2
2
|
import { CommentModel } from '../types';
|
|
3
3
|
export interface AddCommentInput {
|
|
4
4
|
body: string;
|
|
@@ -11,7 +11,7 @@ export interface AddReplyInput extends AddCommentInput {
|
|
|
11
11
|
}
|
|
12
12
|
export declare const COMMENT_BODY_MAX_LENGTH = 1000;
|
|
13
13
|
/** 10개씩 순차 조회한 뒤 마지막 페이지에서만 최신 1개를 실시간 구독한다. */
|
|
14
|
-
export declare function useMemiBoardComments(
|
|
14
|
+
export declare function useMemiBoardComments(boardId: MaybeRefOrGetter<string>, postId: MaybeRefOrGetter<string>, options?: {
|
|
15
15
|
subscribe?: boolean;
|
|
16
16
|
}): {
|
|
17
17
|
comments: import('vue').ComputedRef<CommentModel[]>;
|
|
@@ -26,7 +26,7 @@ export declare function useMemiBoardComments(postId: string | Ref<string>, optio
|
|
|
26
26
|
deleteComment: (comment: CommentModel) => Promise<void>;
|
|
27
27
|
};
|
|
28
28
|
/** 펼친 스레드의 답글을 5개씩 일회성 조회한다. */
|
|
29
|
-
export declare function useMemiBoardReplies(postId: string, rootId: string): {
|
|
29
|
+
export declare function useMemiBoardReplies(boardId: string, postId: string, rootId: string): {
|
|
30
30
|
replies: Ref<{
|
|
31
31
|
id?: string | undefined;
|
|
32
32
|
postId: string;
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* 게시글 좋아요 상태 구독 + 토글.
|
|
3
|
-
* `{prefix}Posts/{postId}/likes/{uid}` 문서 존재 여부가 "좋아요함"의 기준이고,
|
|
4
|
-
* `likeCount`는 그 문서 수를 반영하는 클라이언트 편의 카운터일 뿐이다(commentCount와 동일한 성격).
|
|
5
|
-
* 토글은 배치가 아니라 트랜잭션으로 처리한다 — "있으면 취소, 없으면 추가"라는 read-then-branch라
|
|
6
|
-
* 다중 탭 경합 시에도 likeCount가 실제 likes 문서 수와 어긋나지 않게 하기 위함이다.
|
|
7
|
-
*/
|
|
8
|
-
export declare function useMemiBoardLikes(postId: string): {
|
|
1
|
+
export declare function useMemiBoardLikes(boardId: string, postId: string): {
|
|
9
2
|
isLiked: import('vue').Ref<boolean, boolean>;
|
|
10
3
|
likePending: import('vue').Ref<boolean, boolean>;
|
|
11
4
|
toggleLike: () => Promise<boolean>;
|
|
@@ -2,7 +2,7 @@ import { ModerationResult } from '../types';
|
|
|
2
2
|
/**
|
|
3
3
|
* 글쓰기 전 블로킹 검열.
|
|
4
4
|
* 이용 제한 확인 → 로컬 비속어 → Firebase AI Logic(Gemini).
|
|
5
|
-
* 콘텐츠 차단(local/ai) 시
|
|
5
|
+
* 콘텐츠 차단(local/ai) 시 board users 경고 누적.
|
|
6
6
|
*/
|
|
7
7
|
export declare function useMemiBoardModeration(): {
|
|
8
8
|
checkText: (text: string) => Promise<ModerationResult>;
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { Ref } from 'vue';
|
|
1
|
+
import { MaybeRefOrGetter, Ref } from 'vue';
|
|
2
2
|
import { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore';
|
|
3
3
|
import { Attachment, PostDetail, PostModel } from '../types';
|
|
4
4
|
export interface CreatePostInput {
|
|
5
|
-
/** 작성 화면 진입 시 미리 만든 Firestore 자동 ID. Storage namespace와 동일하게 사용한다. */
|
|
6
5
|
postId?: string;
|
|
7
6
|
title: string;
|
|
8
7
|
content: string;
|
|
9
8
|
tags?: string[];
|
|
10
|
-
category?: string;
|
|
11
9
|
attachments?: Attachment[];
|
|
12
10
|
authorUid: string;
|
|
13
11
|
authorName: string | null;
|
|
@@ -18,10 +16,8 @@ export interface UpdatePostInput {
|
|
|
18
16
|
title: string;
|
|
19
17
|
content: string;
|
|
20
18
|
tags?: string[];
|
|
21
|
-
category?: string;
|
|
22
19
|
attachments?: Attachment[];
|
|
23
20
|
}
|
|
24
|
-
/** getPostBySlug 결과 — not-found 와 permission-denied 를 UI 에서 구분한다. */
|
|
25
21
|
export type GetPostBySlugResult = {
|
|
26
22
|
status: 'ok';
|
|
27
23
|
post: PostDetail;
|
|
@@ -34,15 +30,12 @@ export type GetPostBySlugResult = {
|
|
|
34
30
|
message: string;
|
|
35
31
|
};
|
|
36
32
|
export declare function isFirestorePermissionDenied(error: unknown): boolean;
|
|
37
|
-
/**
|
|
38
|
-
export declare function useMemiBoardPosts(): {
|
|
33
|
+
/** 목록은 posts 메타만, 본문(body/main)은 상세에서만. */
|
|
34
|
+
export declare function useMemiBoardPosts(boardId: MaybeRefOrGetter<string>): {
|
|
39
35
|
createPostId: () => string;
|
|
40
36
|
getPosts: (opts?: {
|
|
41
37
|
pageSize?: number;
|
|
42
38
|
cursor?: QueryDocumentSnapshot<DocumentData>;
|
|
43
|
-
/** 지정 시 해당 카테고리 글만 (boardSettings 의 category id) */
|
|
44
|
-
category?: string;
|
|
45
|
-
/** true 면 listed==true 만 (비권한 목록). staff 전체 보기는 false */
|
|
46
39
|
publicOnly?: boolean;
|
|
47
40
|
}) => Promise<{
|
|
48
41
|
posts: PostModel[];
|
|
@@ -50,7 +43,7 @@ export declare function useMemiBoardPosts(): {
|
|
|
50
43
|
hasMore: boolean;
|
|
51
44
|
}>;
|
|
52
45
|
getPost: (id: string) => Promise<PostDetail | null>;
|
|
53
|
-
getPostBySlug: (
|
|
46
|
+
getPostBySlug: (slug: string) => Promise<GetPostBySlugResult>;
|
|
54
47
|
getAdjacentPosts: (current: PostModel) => Promise<{
|
|
55
48
|
previous: PostModel | null;
|
|
56
49
|
next: PostModel | null;
|
|
@@ -59,17 +52,8 @@ export declare function useMemiBoardPosts(): {
|
|
|
59
52
|
updatePost: (id: string, input: UpdatePostInput) => Promise<void>;
|
|
60
53
|
deletePost: (id: string) => Promise<void>;
|
|
61
54
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
* 바로 갱신되게 하고, 그 이전 글은 "더보기" 클릭 시 1회성 getDocs로 이어서 불러온다.
|
|
65
|
-
* 두 구간이 살짝 겹쳐도 id 기준으로 합쳐 중복 없이 렌더한다.
|
|
66
|
-
*
|
|
67
|
-
* 알려진 한계: "더보기"로 이전 글을 불러온 뒤 새 글이 pageSize개 이상 연달아 올라오면
|
|
68
|
-
* 실시간 구간과 페이지 구간 사이에 짧은 공백이 생길 수 있다 — 새로고침하면 바로 채워지고,
|
|
69
|
-
* 이 정도 트래픽 폭이면 실사용에서 발생 가능성이 낮아 별도 보정 로직은 두지 않았다.
|
|
70
|
-
*/
|
|
71
|
-
export declare function useMemiBoardPostList(category?: string | Ref<string | undefined>, options?: {
|
|
72
|
-
pageSize?: number; /** staff 등 숨김 글 포함 시 false. 기본 true */
|
|
55
|
+
export declare function useMemiBoardPostList(boardId: MaybeRefOrGetter<string>, options?: {
|
|
56
|
+
pageSize?: number;
|
|
73
57
|
publicOnly?: boolean | Ref<boolean>;
|
|
74
58
|
}): {
|
|
75
59
|
posts: import('vue').ComputedRef<PostModel[]>;
|
|
@@ -79,12 +63,7 @@ export declare function useMemiBoardPostList(category?: string | Ref<string | un
|
|
|
79
63
|
loadError: Ref<string, string>;
|
|
80
64
|
loadMore: () => Promise<void>;
|
|
81
65
|
};
|
|
82
|
-
|
|
83
|
-
* 게시글 상세를 VueFire useDocument로 실시간 구독한다 (메타 + 본문).
|
|
84
|
-
* 좋아요·댓글 수 등 다른 사람의 변경이 화면에 바로 반영된다 — 좋아요 토글도
|
|
85
|
-
* 별도 로컬 낙관적 갱신 없이 이 구독이 그대로 반영해 준다.
|
|
86
|
-
*/
|
|
87
|
-
export declare function useMemiBoardPost(postId: string | Ref<string>): {
|
|
66
|
+
export declare function useMemiBoardPost(boardId: MaybeRefOrGetter<string>, postId: string | Ref<string>): {
|
|
88
67
|
post: import('vue').ComputedRef<PostDetail | null>;
|
|
89
68
|
pending: import('vue').ComputedRef<boolean>;
|
|
90
69
|
};
|
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
/** 필요할 때 호스트가 명시적으로
|
|
3
|
-
export declare const
|
|
4
|
-
/**
|
|
1
|
+
import { BoardModel, BoardVisibility } from '../types';
|
|
2
|
+
/** 필요할 때 호스트가 명시적으로 시드할 수 있는 예시 보드. 자동 적용하지 않는다. */
|
|
3
|
+
export declare const DEFAULT_BOARDS: BoardModel[];
|
|
4
|
+
/** @deprecated DEFAULT_BOARDS */
|
|
5
|
+
export declare const DEFAULT_CATEGORIES: BoardModel[];
|
|
6
|
+
/**
|
|
7
|
+
* 보드 목록·설정.
|
|
8
|
+
* - 목록: memiBoards 컬렉션 (부모 문서에 메타 복제)
|
|
9
|
+
* - 설정 원본: memiBoards/{boardId}/settings/config
|
|
10
|
+
*/
|
|
5
11
|
export declare function useMemiBoardSettings(): {
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
boards: import('vue').ComputedRef<BoardModel[]>;
|
|
13
|
+
publicBoards: import('vue').ComputedRef<BoardModel[]>;
|
|
14
|
+
boardLabel: (id: string | undefined) => string | undefined;
|
|
15
|
+
boardDescription: (id: string | undefined) => string | undefined;
|
|
16
|
+
boardVisibility: (id: string | undefined) => BoardVisibility;
|
|
17
|
+
isBoardHidden: (id: string | undefined) => boolean;
|
|
18
|
+
getBoard: (id: string | undefined) => BoardModel | undefined;
|
|
19
|
+
categories: import('vue').ComputedRef<BoardModel[]>;
|
|
20
|
+
publicCategories: import('vue').ComputedRef<BoardModel[]>;
|
|
8
21
|
categoryLabel: (id: string | undefined) => string | undefined;
|
|
9
22
|
categoryDescription: (id: string | undefined) => string | undefined;
|
|
10
23
|
categoryVisibility: (id: string | undefined) => BoardVisibility;
|
|
11
24
|
isCategoryHidden: (id: string | undefined) => boolean;
|
|
12
25
|
settingsPending: import('vue').Ref<boolean, boolean>;
|
|
13
26
|
ensureSettings: () => Promise<void>;
|
|
27
|
+
addBoard: (label: string) => Promise<string>;
|
|
14
28
|
addCategory: (label: string) => Promise<string>;
|
|
15
|
-
|
|
16
|
-
|
|
29
|
+
saveBoard: (board: BoardModel, order?: number) => Promise<void>;
|
|
30
|
+
saveCategory: (board: BoardModel, order?: number) => Promise<void>;
|
|
31
|
+
saveBoards: (next: BoardModel[]) => Promise<void>;
|
|
32
|
+
saveCategories: (next: BoardModel[]) => Promise<void>;
|
|
33
|
+
deleteBoard: (id: string) => Promise<void>;
|
|
17
34
|
deleteCategory: (id: string) => Promise<void>;
|
|
18
|
-
|
|
35
|
+
DEFAULT_BOARDS: BoardModel[];
|
|
36
|
+
DEFAULT_CATEGORIES: BoardModel[];
|
|
19
37
|
};
|
|
@@ -4,11 +4,11 @@ export declare const EDITOR_IMAGE_MAX_BYTES: number;
|
|
|
4
4
|
/** 휴대폰 원본 이미지 선택 허용 크기. 큰 이미지는 업로드 전에 최적화한다. */
|
|
5
5
|
export declare const EDITOR_IMAGE_SOURCE_MAX_BYTES: number;
|
|
6
6
|
export declare function useMemiBoardStorage(): {
|
|
7
|
-
uploadAttachment: (file: File, postId: string, onProgress?: (ratio: number) => void) => {
|
|
7
|
+
uploadAttachment: (file: File, boardId: string, postId: string, onProgress?: (ratio: number) => void) => {
|
|
8
8
|
promise: Promise<Attachment>;
|
|
9
9
|
cancel: () => void;
|
|
10
10
|
};
|
|
11
11
|
deleteAttachment: (attachment: Pick<Attachment, "path">) => Promise<void>;
|
|
12
|
-
uploadEditorImage: (file: File, postId: string) => Promise<EditorImageEntry>;
|
|
12
|
+
uploadEditorImage: (file: File, boardId: string, postId: string) => Promise<EditorImageEntry>;
|
|
13
13
|
deleteEditorImage: (entry: Pick<EditorImageEntry, "originalPath" | "thumbnailPath">) => Promise<void>;
|
|
14
14
|
};
|
|
@@ -9,6 +9,8 @@ export declare const BOARD_USER_ROLES: Array<{
|
|
|
9
9
|
* 전체 사용자 목록은 관리자만 읽을 수 있다(rules). enabled가 false인 동안은
|
|
10
10
|
* 쿼리 자체를 시작하지 않는다 — 그냥 호출만 하면 권한 확인 전에 구독이 걸려서
|
|
11
11
|
* 관리자가 아니거나 아직 role을 확인 중일 때 permission-denied로 화면이 죽는다.
|
|
12
|
+
*
|
|
13
|
+
* 경로: memiBoards/{boardId}/users/{uid}
|
|
12
14
|
*/
|
|
13
15
|
export declare function useMemiBoardUsers(options?: {
|
|
14
16
|
enabled?: MaybeRefOrGetter<boolean>;
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 게시글
|
|
3
|
-
*
|
|
4
|
-
* - 로그인 불필요: rules 가 viewCount +1 만 허용하면 익명도 증가 가능.
|
|
5
|
-
* - 신뢰 수준은 likeCount/commentCount 와 같은 클라이언트 편의 카운터.
|
|
6
|
-
* - 같은 브라우저·세션에서 같은 글을 반복 열어 폭증하지 않도록 sessionStorage 로 1회만 기록.
|
|
2
|
+
* 게시글 조회수 (보드 단위 posts).
|
|
7
3
|
*/
|
|
8
4
|
export declare function useMemiBoardViews(): {
|
|
9
|
-
recordView: (postId: string) => Promise<void>;
|
|
10
|
-
hasRecordedView: (postId: string) => boolean;
|
|
5
|
+
recordView: (boardId: string, postId: string) => Promise<void>;
|
|
6
|
+
hasRecordedView: (boardId: string, postId: string) => boolean;
|
|
11
7
|
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BoardPathConfig } from './utils/boardPaths';
|
|
1
2
|
export interface MemiBoardAuthOptions {
|
|
2
3
|
providers?: Array<'google' | 'apple' | 'emailPassword'>;
|
|
3
4
|
}
|
|
@@ -7,47 +8,30 @@ export interface MemiBoardModerationOptions {
|
|
|
7
8
|
localBlocklist?: string[];
|
|
8
9
|
onError?: 'allow' | 'block';
|
|
9
10
|
moderateImages?: boolean;
|
|
10
|
-
/** AI Logic App Check limited-use 토큰 (재생 보호). 기본 true */
|
|
11
11
|
useLimitedUseAppCheckTokens?: boolean;
|
|
12
|
-
/**
|
|
13
|
-
* 콘텐츠 검열 차단(로컬/AI flagged) 누적 횟수 이상이 되면 글·댓글 작성 제한.
|
|
14
|
-
* 기본 3. 0 이하면 제한 기능 off.
|
|
15
|
-
*/
|
|
16
12
|
blockBanThreshold?: number;
|
|
17
|
-
/** 경고 1회 차감 간격(ms). 기본 24시간 */
|
|
18
13
|
blockBanDecayMs?: number;
|
|
19
14
|
}
|
|
20
|
-
/**
|
|
21
|
-
* 게시판 SEO (호스트 API 없이 Firestore 직조회 + useSeoMeta).
|
|
22
|
-
* 호스트는 siteUrl/siteName 과 공개 board 경로 SSR on 만 맞추면 된다.
|
|
23
|
-
*/
|
|
24
15
|
export interface MemiBoardSeoOptions {
|
|
25
|
-
/** false 면 useMemiBoard*Seo 가 no-op. 기본 true */
|
|
26
16
|
enabled?: boolean;
|
|
27
|
-
/** og:site_name · title 접미사. 예: Loop Waiting */
|
|
28
17
|
siteName?: string;
|
|
29
|
-
/**
|
|
30
|
-
* 절대 URL용 origin. 예: https://loopwait.com
|
|
31
|
-
* 비우면 useRequestURL / NUXT_PUBLIC_SITE_URL 순으로 추론.
|
|
32
|
-
*/
|
|
33
18
|
siteUrl?: string;
|
|
34
|
-
/** 글에 previewImage 없을 때. 상대경로 또는 https */
|
|
35
19
|
defaultOgImage?: string;
|
|
36
20
|
/**
|
|
37
21
|
* 호스트 게시판 base path (trailing slash 없음). 기본 '/board'
|
|
38
|
-
* canonical: {base}, {base}/{
|
|
22
|
+
* canonical: {base}, {base}/{boardId}, {base}/{boardId}/{slug}
|
|
39
23
|
*/
|
|
40
24
|
basePath?: string;
|
|
41
25
|
}
|
|
42
26
|
export interface MemiBoardConfig {
|
|
43
|
-
|
|
27
|
+
/** 루트 컬렉션. 기본 `memiBoards` */
|
|
28
|
+
boardsCollection: string;
|
|
29
|
+
/** 역할 문서 컬렉션. 기본 `memiBoardUsers` */
|
|
30
|
+
usersCollection: string;
|
|
44
31
|
auth: MemiBoardAuthOptions;
|
|
45
32
|
moderation: MemiBoardModerationOptions;
|
|
46
33
|
seo: MemiBoardSeoOptions;
|
|
47
34
|
}
|
|
48
|
-
/**
|
|
49
|
-
* 앱 부팅 시(플러그인 등에서) 한 번 호출한다. 모든 요청/세션이 같은 값을 보므로
|
|
50
|
-
* 서버 렌더링에서도 안전하다 — 사용자별/요청별로 달라지는 값이 아니다.
|
|
51
|
-
*/
|
|
52
35
|
export declare function configureMemiBoard(options: Partial<MemiBoardConfig>): void;
|
|
53
36
|
export declare function useMemiBoardConfig(): MemiBoardConfig;
|
|
37
|
+
export declare function useBoardPathConfig(): BoardPathConfig;
|