memi-board 0.19.3 → 0.22.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.
Files changed (43) hide show
  1. package/README.md +19 -17
  2. package/dist/composables/fetchPublicSeo.d.ts +6 -6
  3. package/dist/composables/useMemiBoardComments.d.ts +3 -3
  4. package/dist/composables/useMemiBoardLikes.d.ts +1 -8
  5. package/dist/composables/useMemiBoardModeration.d.ts +1 -1
  6. package/dist/composables/useMemiBoardPosts.d.ts +8 -28
  7. package/dist/composables/useMemiBoardSettings.d.ts +27 -9
  8. package/dist/composables/useMemiBoardStorage.d.ts +2 -2
  9. package/dist/composables/useMemiBoardUserPosts.d.ts +18 -0
  10. package/dist/composables/useMemiBoardUsers.d.ts +2 -0
  11. package/dist/composables/useMemiBoardViews.d.ts +3 -7
  12. package/dist/config.d.ts +7 -23
  13. package/dist/heic2any-BXb_U1zK.js +977 -0
  14. package/dist/heic2any-BXb_U1zK.js.map +1 -0
  15. package/dist/index.d.ts +6 -3
  16. package/dist/index.js +887 -667
  17. package/dist/index.js.map +1 -1
  18. package/dist/rolldown-runtime-DtPi1Y-2.js +13 -0
  19. package/dist/runtime/components/Attachments.vue +2 -1
  20. package/dist/runtime/components/AuthorMenu.vue +47 -0
  21. package/dist/runtime/components/CategorySettings.vue +41 -7
  22. package/dist/runtime/components/CommentForm.vue +9 -5
  23. package/dist/runtime/components/CommentItem.vue +6 -1
  24. package/dist/runtime/components/CommentList.vue +7 -4
  25. package/dist/runtime/components/CommentThread.vue +16 -5
  26. package/dist/runtime/components/Detail.vue +97 -18
  27. package/dist/runtime/components/Editor.vue +14 -115
  28. package/dist/runtime/components/Footer.vue +9 -7
  29. package/dist/runtime/components/LikeButton.vue +2 -1
  30. package/dist/runtime/components/List.vue +14 -8
  31. package/dist/runtime/components/ListDefault.vue +52 -33
  32. package/dist/runtime/components/ListDense.vue +66 -50
  33. package/dist/runtime/components/ListImage.vue +10 -2
  34. package/dist/runtime/components/ListVideo.vue +62 -40
  35. package/dist/runtime/components/Settings.vue +1 -1
  36. package/dist/runtime/components/SwipeHint.vue +83 -0
  37. package/dist/runtime/components/UserPostList.vue +83 -0
  38. package/dist/types.d.ts +31 -17
  39. package/dist/utils/boardPaths.d.ts +26 -0
  40. package/docs/firebase-setup.md +1 -1
  41. package/docs/firestore.rules.example +119 -227
  42. package/docs/storage.rules.example +23 -25
  43. 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 프로젝트는 호스트마다 달라도 되고, 같은 프로젝트를 공유해도 `collectionPrefix`로 컬렉션만 나누면 된다.
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: {prefix}Posts, {prefix}Users, {prefix}Settings
75
- collectionPrefix: 'board',
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
- | `collectionPrefix` | `'memiBoard'` | Firestore/Storage 접두 (`board` → `boardPosts` …) |
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.collectionPrefix` · `auth` · `moderation` · **`seo`**
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
- (`memiBoard*` 이름을 쓰는 `collectionPrefix`에 맞게 치환)
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": "boardPosts",
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에서 `{prefix}Users/{uid}` 문서를 찾는다.
275
+ 2. Firebase Console → Firestore에서 `memiBoards/{boardId}/users/{uid}` 문서를 찾는다.
274
276
  3. 해당 문서의 `role`을 문자열 `admin`으로 변경한다.
275
- - `collectionPrefix: 'board'`라면 `boardUsers/{uid}`
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는 게시판의 `{prefix}Users/{uid}.role == 'admin'`을 검사한다. 컴포넌트도 같은 role을 확인하므로 기본 설치에서는 `authorized`를 전달하지 않는다.
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
- `{prefix}/posts/{postId}/images/*` + `.../images/thumbnails/*` (최적화 이미지+썸네일).
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
- - 문서: `{prefix}Settings/config/categories/{categoryId}` → `{ label, listView, writeRole, order, updatedAt }`
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
- | 글 수정·삭제 | 작성자 또는 `{prefix}Users/{uid}.role in ['admin', 'staff']` |
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
- 콘텐츠 차단이 누적되면 `{prefix}Users/{uid}` 에 `moderationBlockCount` / `moderationBlockAt` 이 기록된다.
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` (`collectionPrefix` · `auth` · `moderation` · **`seo`**)
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에서 `{prefix}Users/{uid}.role = 'admin'`
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 프로젝트에 호스트 앱 유저 컬렉션이 있어도 무방 — 게시판 역할은 **`{prefix}Users` 전용**이다.
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
- prefix: string;
6
+ paths: BoardPathConfig;
6
7
  };
7
- /** setup 동기 구간에서만 호출 — useFirestore + config 캡처 */
8
8
  export declare function resolvePublicSeoDb(): PublicSeoDb;
9
- /** 공개 글 1건 (category + slug). 없거나 숨김이면 null. */
10
- export declare function fetchPublicPostForSeo(category: string, slug: string, ctx?: PublicSeoDb): Promise<BoardPostSeoPayload | null>;
11
- /** 전체 또는 카테고리 공개 목록 SEO 페이로드. */
12
- export declare function fetchPublicListForSeo(category?: string | null, ctx?: PublicSeoDb): Promise<BoardListSeoPayload>;
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(postId: string | Ref<string>, options?: {
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) 시 boardUsers 경고 누적.
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
- /** 목록 조회는 posts/{id} 메타만 읽고, 본문(body/main)은 상세 조회 시에만 읽는다. */
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,26 +43,18 @@ export declare function useMemiBoardPosts(): {
50
43
  hasMore: boolean;
51
44
  }>;
52
45
  getPost: (id: string) => Promise<PostDetail | null>;
53
- getPostBySlug: (category: string, slug: string) => Promise<GetPostBySlugResult>;
46
+ getPostBySlug: (slug: string) => Promise<GetPostBySlugResult>;
54
47
  getAdjacentPosts: (current: PostModel) => Promise<{
55
48
  previous: PostModel | null;
56
49
  next: PostModel | null;
57
50
  }>;
58
51
  createPost: (input: CreatePostInput) => Promise<string>;
59
52
  updatePost: (id: string, input: UpdatePostInput) => Promise<void>;
53
+ publishPost: (id: string) => Promise<void>;
60
54
  deletePost: (id: string) => Promise<void>;
61
55
  };
62
- /**
63
- * 최신 {pageSize}건은 onSnapshot으로 실시간 구독해 목록에 보이는 동안 좋아요·댓글 수가
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 */
56
+ export declare function useMemiBoardPostList(boardId: MaybeRefOrGetter<string>, options?: {
57
+ pageSize?: number;
73
58
  publicOnly?: boolean | Ref<boolean>;
74
59
  }): {
75
60
  posts: import('vue').ComputedRef<PostModel[]>;
@@ -79,12 +64,7 @@ export declare function useMemiBoardPostList(category?: string | Ref<string | un
79
64
  loadError: Ref<string, string>;
80
65
  loadMore: () => Promise<void>;
81
66
  };
82
- /**
83
- * 게시글 상세를 VueFire useDocument로 실시간 구독한다 (메타 + 본문).
84
- * 좋아요·댓글 수 등 다른 사람의 변경이 화면에 바로 반영된다 — 좋아요 토글도
85
- * 별도 로컬 낙관적 갱신 없이 이 구독이 그대로 반영해 준다.
86
- */
87
- export declare function useMemiBoardPost(postId: string | Ref<string>): {
67
+ export declare function useMemiBoardPost(boardId: MaybeRefOrGetter<string>, postId: string | Ref<string>): {
88
68
  post: import('vue').ComputedRef<PostDetail | null>;
89
69
  pending: import('vue').ComputedRef<boolean>;
90
70
  };
@@ -1,19 +1,37 @@
1
- import { BoardCategory, BoardVisibility } from '../types';
2
- /** 필요할 때 호스트가 명시적으로 사용할 수 있는 예시 카테고리. 자동 적용하지 않는다. */
3
- export declare const DEFAULT_CATEGORIES: BoardCategory[];
4
- /** 게시판 카테고리 — `{prefix}Settings/config/categories/{categoryId}` 개별 문서. */
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
- categories: import('vue').ComputedRef<BoardCategory[]>;
7
- publicCategories: import('vue').ComputedRef<BoardCategory[]>;
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
- saveCategory: (category: BoardCategory, order?: number) => Promise<void>;
16
- saveCategories: (next: BoardCategory[]) => Promise<void>;
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
- DEFAULT_CATEGORIES: BoardCategory[];
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
  };
@@ -0,0 +1,18 @@
1
+ import { MaybeRefOrGetter } from 'vue';
2
+ import { UserPostModel } from '../types';
3
+ /**
4
+ * 작성자 uid로 모든 보드를 가로질러 글을 모은다 (collectionGroup 'posts').
5
+ * 공개 글만 — canReadPost rules와 같은 기준(listed && isPublished)이라
6
+ * 본인이 아니면 숨김 보드·미게시 글은 애초에 안 온다.
7
+ */
8
+ export declare function useMemiBoardUserPosts(uid: MaybeRefOrGetter<string>, options?: {
9
+ pageSize?: number;
10
+ }): {
11
+ posts: import('vue').ComputedRef<UserPostModel[]>;
12
+ postsPending: import('vue').Ref<boolean, boolean>;
13
+ hasMore: import('vue').Ref<boolean, boolean>;
14
+ loadingMore: import('vue').Ref<boolean, boolean>;
15
+ loadError: import('vue').Ref<string, string>;
16
+ loadMore: () => Promise<void>;
17
+ };
18
+ export type { UserPostModel };
@@ -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}/{category}, {base}/{category}/{slug}
22
+ * canonical: {base}, {base}/{boardId}, {base}/{boardId}/{slug}
39
23
  */
40
24
  basePath?: string;
41
25
  }
42
26
  export interface MemiBoardConfig {
43
- collectionPrefix: string;
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;