memi-board 0.5.0 → 0.9.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 CHANGED
@@ -222,7 +222,7 @@ const router = useRouter()
222
222
  ### 제공하는 API
223
223
 
224
224
  **컴포넌트 (auto-import):**
225
- `MemiBoardList`, `MemiBoardDetail`, `MemiBoardEditor`, `MemiBoardCommentList`, `MemiBoardCommentForm`, `MemiBoardAttachments`, `MemiBoardSignIn`, `MemiBoardVersionHistory`
225
+ `MemiBoardList`, `MemiBoardDetail`, `MemiBoardEditor`, `MemiBoardCommentList`, `MemiBoardCommentForm`, `MemiBoardAttachments`, `MemiBoardSignIn`, `MemiBoardSettings`, `MemiBoardUsers`, `MemiBoardVersionHistory`
226
226
 
227
227
  **List props:** `pageSize`, `category`, `postLinkBase`, `getPostLink(post)`, `linkBase`(deprecated)
228
228
 
@@ -234,13 +234,15 @@ const router = useRouter()
234
234
  본문 HTML에는 원본 URL. 수정 중 버려진 파일은 호스트 스케줄러로 정리 (`extractEditorImageUrls` 로 본문 참조 비교).
235
235
 
236
236
  **composables:**
237
- `useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`
237
+ `useMemiBoardAuth`, `useMemiBoardPosts`, `useMemiBoardComments`, `useMemiBoardStorage`, `useMemiBoardModeration`, `useMemiBoardSettings`, `useMemiBoardUsers`
238
+
239
+ 댓글은 시간순으로 10개씩 cursor 조회하며, 실제 `댓글 더보기` 버튼이 화면에 들어오면 1초 후 자동으로 다음 페이지를 읽는다. 기존 댓글을 모두 읽은 뒤에만 최신 댓글 1개를 실시간 구독한다. 대댓글은 5개씩 조회하고 모두 읽은 뒤 최신 답글 1개를 실시간 구독하며, 화면 깊이는 최대 2단계로 표시한다.
238
240
 
239
241
  ### 카테고리
240
242
 
241
- - 문서: `{prefix}Settings/config/categories/{categoryId}` → `{ label, listView, order, updatedAt }`
243
+ - 문서: `{prefix}Settings/config/categories/{categoryId}` → `{ label, listView, writeRole, order, updatedAt }`
242
244
  - 설정이 없으면 빈 목록이며 기본 카테고리를 자동 생성하지 않음
243
- - `MemiBoardSettings`에서 관리자가 카테고리별 저장·삭제·순서·리스트뷰 관리
245
+ - `MemiBoardSettings`에서 관리자가 카테고리별 저장·삭제·순서·리스트뷰·글쓰기 최소 역할 관리
244
246
  - `categoryId`는 문서 ID이자 게시글의 `category` 참조값이므로 생성 후 변경하지 않는 것을 권장
245
247
 
246
248
  ### 로그인 공급자
@@ -263,10 +265,11 @@ const router = useRouter()
263
265
  | 동작 | 조건 |
264
266
  |------|------|
265
267
  | 글/댓글 읽기 | 공개 |
266
- | 글/댓글 작성 | 로그인 + `moderationStatus == 'approved'` (클라가 항상 approved 로 씀 — 검열 통과 후에만 write) |
267
- | 글 수정·삭제 | 작성자 또는 `{prefix}Users/{uid}.role == 'admin'` |
268
- | 댓글 삭제 | 댓글 작성자 · 글 작성자 · admin |
269
- | 관리자 승격 | **패키지가 하지 않음** — 콘솔에서 `role: 'admin'` 수동 설정 |
268
+ | 글 작성 | 로그인 + 카테고리 `writeRole` 충족 + `moderationStatus == 'approved'` |
269
+ | 댓글 작성 | 로그인 + `moderationStatus == 'approved'` (클라가 항상 approved 로 씀 — 검열 통과 후에만 write) |
270
+ | 글 수정·삭제 | 작성자 또는 `{prefix}Users/{uid}.role in ['admin', 'staff']` |
271
+ | 댓글 삭제 | 댓글 작성자 · 글 작성자 · admin · staff |
272
+ | 역할 관리 | `MemiBoardUsers`에서 admin이 `admin`·`staff`·`user` 변경 |
270
273
 
271
274
  **알려진 한계:** AI 검열은 클라이언트 전용. SDK로 직접 write 하면 우회 가능. 중요 서비스면 Cloud Functions 검증을 호스트가 추가(패키지 범위 밖).
272
275
 
@@ -6,6 +6,8 @@ export interface UseMemiBoardAuthReturn {
6
6
  user: ReturnType<typeof useCurrentUser>;
7
7
  isSignedIn: ComputedRef<boolean>;
8
8
  isAdmin: ComputedRef<boolean>;
9
+ isStaff: ComputedRef<boolean>;
10
+ canManageContent: ComputedRef<boolean>;
9
11
  rolePending: Ref<boolean>;
10
12
  /** 유효 검열 차단 누적 (lazy decay 반영) */
11
13
  moderationBlockCount: ComputedRef<number>;
@@ -6,9 +6,86 @@ export interface AddCommentInput {
6
6
  authorName: string | null;
7
7
  authorPhoto: string | null;
8
8
  }
9
- /** postId 서브컬렉션 댓글을 실시간(onSnapshot)으로 구독하고, 작성/삭제 시 부모 글의 commentCount를 같은 batch로 증감한다. */
10
- export declare function useMemiBoardComments(postId: string | Ref<string>): {
11
- comments: import('vuefire')._RefFirestore<import('vuefire').VueFirestoreQueryData<CommentModel>>;
9
+ export interface AddReplyInput extends AddCommentInput {
10
+ parent: CommentModel;
11
+ }
12
+ /** 10개씩 순차 조회한 뒤 마지막 페이지에서만 최신 1개를 실시간 구독한다. */
13
+ export declare function useMemiBoardComments(postId: string | Ref<string>, options?: {
14
+ subscribe?: boolean;
15
+ }): {
16
+ comments: import('vue').ComputedRef<CommentModel[]>;
17
+ commentsPending: import('vue').ComputedRef<boolean>;
18
+ hasMore: Ref<boolean, boolean>;
19
+ loadingMore: Ref<boolean, boolean>;
20
+ loadMore: () => Promise<void>;
12
21
  addComment: (input: AddCommentInput) => Promise<void>;
13
- deleteComment: (commentId: string) => Promise<void>;
22
+ addReply: (input: AddReplyInput) => Promise<void>;
23
+ deleteComment: (comment: CommentModel) => Promise<void>;
24
+ };
25
+ /** 펼친 스레드의 답글을 5개씩 일회성 조회한다. */
26
+ export declare function useMemiBoardReplies(postId: string, rootId: string): {
27
+ replies: Ref<{
28
+ id?: string | undefined;
29
+ postId: string;
30
+ body: string;
31
+ authorUid: string;
32
+ authorName: string | null;
33
+ authorPhoto: string | null;
34
+ moderationStatus: import('..').ModerationStatus;
35
+ createdAt: {
36
+ readonly seconds: number;
37
+ readonly nanoseconds: number;
38
+ toDate: () => Date;
39
+ toMillis: () => number;
40
+ isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
41
+ toString: () => string;
42
+ toJSON: () => {
43
+ seconds: number;
44
+ nanoseconds: number;
45
+ type: string;
46
+ };
47
+ valueOf: () => string;
48
+ };
49
+ parentId?: string | null | undefined;
50
+ rootId?: string | undefined;
51
+ depth?: number | undefined;
52
+ replyToUid?: string | null | undefined;
53
+ replyToName?: string | null | undefined;
54
+ replyCount?: number | undefined;
55
+ isReply?: boolean | undefined;
56
+ }[], CommentModel[] | {
57
+ id?: string | undefined;
58
+ postId: string;
59
+ body: string;
60
+ authorUid: string;
61
+ authorName: string | null;
62
+ authorPhoto: string | null;
63
+ moderationStatus: import('..').ModerationStatus;
64
+ createdAt: {
65
+ readonly seconds: number;
66
+ readonly nanoseconds: number;
67
+ toDate: () => Date;
68
+ toMillis: () => number;
69
+ isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
70
+ toString: () => string;
71
+ toJSON: () => {
72
+ seconds: number;
73
+ nanoseconds: number;
74
+ type: string;
75
+ };
76
+ valueOf: () => string;
77
+ };
78
+ parentId?: string | null | undefined;
79
+ rootId?: string | undefined;
80
+ depth?: number | undefined;
81
+ replyToUid?: string | null | undefined;
82
+ replyToName?: string | null | undefined;
83
+ replyCount?: number | undefined;
84
+ isReply?: boolean | undefined;
85
+ }[]>;
86
+ loading: Ref<boolean, boolean>;
87
+ loaded: Ref<boolean, boolean>;
88
+ hasMore: Ref<boolean, boolean>;
89
+ loadMore: () => Promise<void>;
90
+ refresh: () => Promise<void>;
14
91
  };
@@ -0,0 +1,11 @@
1
+ import { BoardUserModel, BoardUserRole } from '../types';
2
+ export declare const BOARD_USER_ROLES: Array<{
3
+ value: BoardUserRole;
4
+ label: string;
5
+ description: string;
6
+ }>;
7
+ export declare function useMemiBoardUsers(): {
8
+ users: import('vuefire')._RefFirestore<import('vuefire').VueFirestoreQueryData<BoardUserModel>>;
9
+ usersPending: import('vue').Ref<boolean, boolean>;
10
+ updateUserRole: (uid: string, role: BoardUserRole) => Promise<void>;
11
+ };
package/dist/index.d.ts CHANGED
@@ -4,17 +4,18 @@ export type { MemiBoardConfig, MemiBoardAuthOptions, MemiBoardModerationOptions
4
4
  export { useMemiBoardAuth } from './composables/useMemiBoardAuth';
5
5
  export { useMemiBoardPosts } from './composables/useMemiBoardPosts';
6
6
  export type { CreatePostInput, UpdatePostInput } from './composables/useMemiBoardPosts';
7
- export { useMemiBoardComments } from './composables/useMemiBoardComments';
8
- export type { AddCommentInput } from './composables/useMemiBoardComments';
7
+ export { useMemiBoardComments, useMemiBoardReplies } from './composables/useMemiBoardComments';
8
+ export type { AddCommentInput, AddReplyInput } from './composables/useMemiBoardComments';
9
9
  export { useMemiBoardModeration } from './composables/useMemiBoardModeration';
10
10
  export { useMemiBoardStorage } from './composables/useMemiBoardStorage';
11
11
  export { useMemiBoardSettings, DEFAULT_CATEGORIES } from './composables/useMemiBoardSettings';
12
+ export { useMemiBoardUsers, BOARD_USER_ROLES } from './composables/useMemiBoardUsers';
12
13
  export { versionHistory } from './data/versionHistory';
13
14
  export type { VersionHistoryEntry } from './data/versionHistory';
14
15
  export { formatDate } from './utils/formatDate';
15
16
  export { slugify } from './utils/slugify';
16
17
  export { DEFAULT_BLOCK_BAN_THRESHOLD, DEFAULT_BLOCK_BAN_DECAY_MS, effectiveModerationBlockCount, isModerationWriteRestricted, moderationWriteRestrictedUntilMs, formatRestrictedUntilLabel, } from './utils/moderation-strike';
17
- export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, CommentModel, BoardUserRole, BoardCategory, BoardListView, BoardSettingsModel, ModerationResult, } from './types';
18
+ export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, CommentModel, BoardUserRole, BoardUserModel, BoardCategory, BoardListView, BoardWriteRole, BoardSettingsModel, ModerationResult, } from './types';
18
19
  export { compressImage } from './utils/compressImage';
19
20
  export { extractEditorImageUrls } from './utils/extractEditorImageUrls';
20
21
  export { renderMarkdownToHtml } from './utils/renderMarkdown';