memi-board 0.32.0 → 0.37.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 (49) hide show
  1. package/README.md +17 -2
  2. package/dist/composables/boardLookup.d.ts +8 -0
  3. package/dist/composables/useMemiBoardAuth.d.ts +4 -3
  4. package/dist/composables/useMemiBoardCommentFeed.d.ts +18 -0
  5. package/dist/composables/useMemiBoardComments.d.ts +2 -0
  6. package/dist/composables/useMemiBoardLikes.d.ts +9 -0
  7. package/dist/composables/useMemiBoardMiniPosts.d.ts +12 -0
  8. package/dist/composables/useMemiBoardReports.d.ts +114 -0
  9. package/dist/composables/useMemiBoardSections.d.ts +13 -0
  10. package/dist/composables/useMemiBoardSettings.d.ts +2 -0
  11. package/dist/config.d.ts +2 -0
  12. package/dist/{heic2any-BWRKffWN.js → heic2any-D2QKy06h.js} +2 -2
  13. package/dist/{heic2any-BWRKffWN.js.map → heic2any-D2QKy06h.js.map} +1 -1
  14. package/dist/index.d.ts +12 -3
  15. package/dist/index.js +1161 -647
  16. package/dist/index.js.map +1 -1
  17. package/dist/runtime/components/CategorySettings.vue +3 -5
  18. package/dist/runtime/components/CommentFeed.vue +141 -0
  19. package/dist/runtime/components/CommentForm.vue +2 -2
  20. package/dist/runtime/components/CommentItem.vue +55 -5
  21. package/dist/runtime/components/CommentList.vue +10 -2
  22. package/dist/runtime/components/Detail.vue +8 -0
  23. package/dist/runtime/components/ListDefault.vue +2 -2
  24. package/dist/runtime/components/ListDense.vue +2 -2
  25. package/dist/runtime/components/ListVideo.vue +2 -2
  26. package/dist/runtime/components/Mini.vue +37 -0
  27. package/dist/runtime/components/MiniCard.vue +51 -0
  28. package/dist/runtime/components/MiniComments.vue +75 -0
  29. package/dist/runtime/components/MiniList.vue +36 -0
  30. package/dist/runtime/components/MiniPicker.vue +131 -0
  31. package/dist/runtime/components/MiniPost.vue +52 -0
  32. package/dist/runtime/components/MiniPosts.vue +115 -0
  33. package/dist/runtime/components/ReportButton.vue +117 -0
  34. package/dist/runtime/components/ReportList.vue +124 -0
  35. package/dist/runtime/components/Section.vue +62 -0
  36. package/dist/runtime/components/Settings.vue +3 -4
  37. package/dist/runtime/components/UserCommentList.vue +3 -0
  38. package/dist/{slugify-B5XHr4SZ.js → slugify-Cyq-Gg8X.js} +47 -36
  39. package/dist/slugify-Cyq-Gg8X.js.map +1 -0
  40. package/dist/storage.js +11 -11
  41. package/dist/types.d.ts +54 -3
  42. package/dist/utils/boardAccess.d.ts +7 -0
  43. package/dist/utils/boardPaths.d.ts +10 -3
  44. package/dist/utils/deletePostCascade.d.ts +7 -1
  45. package/dist/utils/section.d.ts +31 -0
  46. package/docs/firestore.indexes.json.example +38 -0
  47. package/docs/firestore.rules.example +78 -22
  48. package/package.json +1 -1
  49. package/dist/slugify-B5XHr4SZ.js.map +0 -1
package/README.md CHANGED
@@ -221,6 +221,7 @@ const { post: postSeo } = await useMemiBoardPostSeo({
221
221
  - [ ] 목록·상세 페이지에 `await useMemiBoard*Seo(…)` 한 줄
222
222
  - [ ] 호스트 SEO API **만들지 않음**
223
223
  - [ ] CSS `@source` (아래)
224
+ - [ ] (선택) 읽은 글 표시 CSS (아래)
224
225
  - [ ] Rules·indexes **deploy**
225
226
  - [ ] 최초 admin + 첫 카테고리
226
227
 
@@ -235,7 +236,20 @@ const { post: postSeo } = await useMemiBoardPostSeo({
235
236
  @source "../../../node_modules/memi-board/dist/runtime/components";
236
237
  ```
237
238
 
238
- ### 5. Security Rules + 인덱스 배포
239
+ ### 5. 읽은 글 표시 (선택)
240
+
241
+ 목록·미니 컴포넌트의 글 링크는 `.memi-board-post-link` 클래스가 붙어 있고, 제목 텍스트는 `text-inherit`로 링크 색을 따른다. 패키지는 색을 강제하지 않으므로, 방문한 글을 흐리게 보이고 싶으면 호스트 CSS에 `:visited` 스타일만 추가하면 된다.
242
+
243
+ ```css
244
+ /* app/assets/css/main.css */
245
+ .memi-board-post-link:visited {
246
+ color: var(--ui-text-muted);
247
+ }
248
+ ```
249
+
250
+ 브라우저 네이티브 `:visited` 라서 서버·DB 저장이 필요 없다. 단, SPA 클라이언트 라우팅(`NuxtLink`)으로만 이동한 경우 브라우저에 "실제 방문"으로 기록되지 않을 수 있어 완전히 신뢰할 수는 없다 — 정확한 읽음 추적이 필요하면 별도로 방문 글 ID를 저장하는 방식을 구현해야 한다.
251
+
252
+ ### 6. Security Rules + 인덱스 배포
239
253
 
240
254
  1. [docs/firestore.rules.example](docs/firestore.rules.example) → 호스트 `firestore.rules`에 병합
241
255
  (`memiBoardPosts`/`memiBoardComments`/`memiBoardLikes`/`memiBoardSettings`/`memiBoardUsers` — 전부 최상위 flat 컬렉션)
@@ -257,7 +271,7 @@ const { post: postSeo } = await useMemiBoardPostSeo({
257
271
 
258
272
  > ⚠️ npm 패키지 tarball에도 `docs/` 가 포함된다(0.4.7+). 구버전이면 GitHub의 `docs/`를 보면 된다.
259
273
 
260
- ### 6. 최초 관리자와 카테고리 만들기
274
+ ### 7. 최초 관리자와 카테고리 만들기
261
275
 
262
276
  Rules는 일반 사용자가 스스로 관리자가 되는 것을 막는다. 따라서 최초 한 번은 다음 순서가 필요하다.
263
277
 
@@ -493,6 +507,7 @@ export default defineNuxtPlugin(() => {
493
507
  - [ ] 목록·상세 페이지에 `await useMemiBoardListSeo` / `useMemiBoardPostSeo`
494
508
  - [ ] 호스트 SEO API 없음
495
509
  - [ ] CSS `@source` …/memi-board/dist/runtime/components
510
+ - [ ] (선택) `.memi-board-post-link:visited` 읽은 글 표시 CSS
496
511
  - [ ] Rules 예시 병합 + **deploy**
497
512
  - [ ] 카테고리·listed 쿼리 사용 시 indexes deploy
498
513
  - [ ] 페이지·미들웨어 직접 작성 (또는 playground 복사)
@@ -0,0 +1,8 @@
1
+ type BoardLookup = (boardId: string) => {
2
+ allowedStaffUids?: string[];
3
+ } | undefined;
4
+ export declare function registerBoardLookup(fn: BoardLookup): void;
5
+ export declare function resolveBoardLookup(boardId: string): {
6
+ allowedStaffUids?: string[];
7
+ } | undefined;
8
+ export {};
@@ -8,6 +8,7 @@ export interface UseMemiBoardAuthReturn {
8
8
  isAdmin: ComputedRef<boolean>;
9
9
  isStaff: ComputedRef<boolean>;
10
10
  canManageContent: ComputedRef<boolean>;
11
+ canManageBoard: (boardId: string | undefined | null) => boolean;
11
12
  rolePending: Ref<boolean>;
12
13
  /** 유효 검열 차단 누적 (lazy decay 반영) */
13
14
  moderationBlockCount: ComputedRef<number>;
@@ -20,10 +21,10 @@ export interface UseMemiBoardAuthReturn {
20
21
  signInWithEmail: (email: string, password: string) => ReturnType<typeof signInWithEmailAndPassword>;
21
22
  signUpWithEmail: (email: string, password: string, displayName?: string) => ReturnType<typeof createUserWithEmailAndPassword>;
22
23
  signOut: () => Promise<void>;
23
- canEdit: (post: Pick<PostModel, 'authorUid'>) => boolean;
24
- canDelete: (post: Pick<PostModel, 'authorUid'>) => boolean;
24
+ canEdit: (post: Pick<PostModel, 'authorUid' | 'boardId'>) => boolean;
25
+ canDelete: (post: Pick<PostModel, 'authorUid' | 'boardId'>) => boolean;
25
26
  canEditComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean;
26
- canDeleteComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean;
27
+ canDeleteComment: (comment: Pick<CommentModel, 'authorUid' | 'boardId'>) => boolean;
27
28
  /** 콘텐츠 검열 차단 1회 기록 후 메시지 조각 반환 */
28
29
  recordContentModerationBlock: () => Promise<{
29
30
  effectiveCount: number;
@@ -0,0 +1,18 @@
1
+ import { MaybeRefOrGetter } from 'vue';
2
+ import { CommentModel } from '../types';
3
+ export type CommentFeedSort = 'latest' | 'likes';
4
+ /**
5
+ * 보드를 가로질러 댓글을 모은다. 최신순은 createdAt, 좋아요순은 likeCount.
6
+ * likeCount 가 없는 예전 댓글은 좋아요순 쿼리에 나오지 않는다(표시 시 0).
7
+ */
8
+ export declare function useMemiBoardCommentFeed(sort?: MaybeRefOrGetter<CommentFeedSort>, options?: {
9
+ pageSize?: MaybeRefOrGetter<number>;
10
+ }): {
11
+ comments: import('vue').ComputedRef<CommentModel[]>;
12
+ commentsPending: 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
+ sort: import('vue').ComputedRef<CommentFeedSort>;
18
+ };
@@ -90,6 +90,7 @@ export declare function useMemiBoardReplies(boardId: string, postId: string, roo
90
90
  replyToName?: string | null | undefined;
91
91
  replyCount?: number | undefined;
92
92
  isReply?: boolean | undefined;
93
+ likeCount?: number | undefined;
93
94
  }[], CommentModel[] | {
94
95
  id?: string | undefined;
95
96
  postId: string;
@@ -150,6 +151,7 @@ export declare function useMemiBoardReplies(boardId: string, postId: string, roo
150
151
  replyToName?: string | null | undefined;
151
152
  replyCount?: number | undefined;
152
153
  isReply?: boolean | undefined;
154
+ likeCount?: number | undefined;
153
155
  }[]>;
154
156
  loading: Ref<boolean, boolean>;
155
157
  loaded: Ref<boolean, boolean>;
@@ -1,5 +1,14 @@
1
+ import { InjectionKey, MaybeRefOrGetter } from 'vue';
1
2
  export declare function useMemiBoardLikes(boardId: string, postId: string): {
2
3
  isLiked: import('vue').Ref<boolean, boolean>;
3
4
  likePending: import('vue').Ref<boolean, boolean>;
4
5
  toggleLike: () => Promise<boolean>;
5
6
  };
7
+ export interface MemiBoardCommentLikesApi {
8
+ isLiked: (commentId: string | undefined) => boolean;
9
+ isPending: (commentId: string | undefined) => boolean;
10
+ toggleLike: (commentId: string) => Promise<boolean>;
11
+ }
12
+ export declare const memiBoardCommentLikesKey: InjectionKey<MemiBoardCommentLikesApi>;
13
+ /** 글 하나에서 내가 누른 댓글 좋아요를 쿼리 1번으로 구독한다. 댓글마다 리스너를 달지 않는다. */
14
+ export declare function useMemiBoardCommentLikes(boardId: MaybeRefOrGetter<string>, postId: MaybeRefOrGetter<string>): MemiBoardCommentLikesApi;
@@ -0,0 +1,12 @@
1
+ import { MaybeRefOrGetter } from 'vue';
2
+ import { PostModel } from '../types';
3
+ export declare function useMemiBoardMiniLatest(boardId: MaybeRefOrGetter<string | null>, count: MaybeRefOrGetter<number>): {
4
+ posts: import('vue').ComputedRef<import('vuefire').VueFirestoreQueryData<PostModel>>;
5
+ pending: import('vue').Ref<boolean, boolean>;
6
+ error: import('vue').Ref<import('firebase/firestore').FirestoreError | undefined, import('firebase/firestore').FirestoreError | undefined>;
7
+ };
8
+ export declare function useMemiBoardMiniPicked(postIds: MaybeRefOrGetter<string[]>): {
9
+ posts: import('vue').ComputedRef<PostModel[]>;
10
+ pending: import('vue').Ref<boolean, boolean>;
11
+ error: import('vue').Ref<import('firebase/firestore').FirestoreError | undefined, import('firebase/firestore').FirestoreError | undefined>;
12
+ };
@@ -0,0 +1,114 @@
1
+ import { MaybeRefOrGetter } from 'vue';
2
+ import { BoardReportModel, BoardReportReason, BoardReportStatus } from '../types';
3
+ export declare const REPORT_DETAIL_MAX_LENGTH = 200;
4
+ export declare const REPORT_REASONS: {
5
+ value: BoardReportReason;
6
+ label: string;
7
+ }[];
8
+ export declare function reportReasonLabel(reason: BoardReportReason | string | undefined): string;
9
+ export declare function useMemiBoardReports(boardId: string, postId: string): {
10
+ hasReported: import('vue').Ref<boolean, boolean>;
11
+ pending: import('vue').Ref<boolean, boolean>;
12
+ submitReport: (input: {
13
+ reason: BoardReportReason;
14
+ detail?: string;
15
+ postTitle?: string;
16
+ authorUid?: string;
17
+ authorName?: string | null;
18
+ }) => Promise<void>;
19
+ };
20
+ export declare function useMemiBoardReportQueue(options?: {
21
+ pageSize?: number;
22
+ boardId?: MaybeRefOrGetter<string>;
23
+ }): {
24
+ reports: import('vue').Ref<{
25
+ id?: string | undefined;
26
+ uid: string;
27
+ target: import('..').BoardReportTarget;
28
+ postId: string;
29
+ boardId: string;
30
+ reason: BoardReportReason;
31
+ detail: string;
32
+ status: BoardReportStatus;
33
+ createdAt: {
34
+ readonly seconds: number;
35
+ readonly nanoseconds: number;
36
+ toDate: () => Date;
37
+ toMillis: () => number;
38
+ isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
39
+ toString: () => string;
40
+ toJSON: () => {
41
+ seconds: number;
42
+ nanoseconds: number;
43
+ type: string;
44
+ };
45
+ valueOf: () => string;
46
+ };
47
+ postTitle?: string | undefined;
48
+ authorUid?: string | undefined;
49
+ authorName?: string | null | undefined;
50
+ reviewedAt?: {
51
+ readonly seconds: number;
52
+ readonly nanoseconds: number;
53
+ toDate: () => Date;
54
+ toMillis: () => number;
55
+ isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
56
+ toString: () => string;
57
+ toJSON: () => {
58
+ seconds: number;
59
+ nanoseconds: number;
60
+ type: string;
61
+ };
62
+ valueOf: () => string;
63
+ } | undefined;
64
+ reviewedBy?: string | undefined;
65
+ }[], BoardReportModel[] | {
66
+ id?: string | undefined;
67
+ uid: string;
68
+ target: import('..').BoardReportTarget;
69
+ postId: string;
70
+ boardId: string;
71
+ reason: BoardReportReason;
72
+ detail: string;
73
+ status: BoardReportStatus;
74
+ createdAt: {
75
+ readonly seconds: number;
76
+ readonly nanoseconds: number;
77
+ toDate: () => Date;
78
+ toMillis: () => number;
79
+ isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
80
+ toString: () => string;
81
+ toJSON: () => {
82
+ seconds: number;
83
+ nanoseconds: number;
84
+ type: string;
85
+ };
86
+ valueOf: () => string;
87
+ };
88
+ postTitle?: string | undefined;
89
+ authorUid?: string | undefined;
90
+ authorName?: string | null | undefined;
91
+ reviewedAt?: {
92
+ readonly seconds: number;
93
+ readonly nanoseconds: number;
94
+ toDate: () => Date;
95
+ toMillis: () => number;
96
+ isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
97
+ toString: () => string;
98
+ toJSON: () => {
99
+ seconds: number;
100
+ nanoseconds: number;
101
+ type: string;
102
+ };
103
+ valueOf: () => string;
104
+ } | undefined;
105
+ reviewedBy?: string | undefined;
106
+ }[]>;
107
+ pending: import('vue').Ref<boolean, boolean>;
108
+ loadingMore: import('vue').Ref<boolean, boolean>;
109
+ hasMore: import('vue').Ref<boolean, boolean>;
110
+ loadError: import('vue').Ref<string, string>;
111
+ loadMore: () => Promise<void>;
112
+ dismissReport: (report: BoardReportModel) => Promise<void>;
113
+ actionReport: (report: BoardReportModel) => Promise<void>;
114
+ };
@@ -0,0 +1,13 @@
1
+ import { BoardSection, BoardSectionLayout } from '../types';
2
+ export declare const DEFAULT_BOARD_SECTIONS: BoardSection[];
3
+ export declare function normalizeBoardSection(raw: Record<string, unknown>, order: number): BoardSection;
4
+ /** 호스트 `settings/homeLayout` 문서를 읽는다. 기존 메인 레이아웃 경로를 유지한다. */
5
+ export declare function useMemiBoardSections(): {
6
+ settings: import('vue').Ref<import('vuefire').VueFirestoreDocumentData<BoardSectionLayout> | undefined, import('vuefire').VueFirestoreDocumentData<BoardSectionLayout> | undefined>;
7
+ pending: import('vue').Ref<boolean, boolean>;
8
+ sections: import('vue').ComputedRef<BoardSection[]>;
9
+ blocks: import('vue').ComputedRef<BoardSection[]>;
10
+ hasSavedLayout: import('vue').ComputedRef<boolean>;
11
+ saveSections: (next: BoardSection[]) => Promise<void>;
12
+ saveBlocks: (next: BoardSection[]) => Promise<void>;
13
+ };
@@ -16,6 +16,8 @@ export declare function useMemiBoardSettings(): {
16
16
  boardVisibility: (id: string | undefined) => BoardVisibility;
17
17
  isBoardHidden: (id: string | undefined) => boolean;
18
18
  getBoard: (id: string | undefined) => BoardModel | undefined;
19
+ canManageBoard: (boardId: string | undefined | null) => boolean;
20
+ managedBoards: import('vue').ComputedRef<BoardModel[]>;
19
21
  categories: import('vue').ComputedRef<BoardModel[]>;
20
22
  publicCategories: import('vue').ComputedRef<BoardModel[]>;
21
23
  categoryLabel: (id: string | undefined) => string | undefined;
package/dist/config.d.ts CHANGED
@@ -30,6 +30,8 @@ export interface MemiBoardConfig {
30
30
  commentsCollection: string;
31
31
  /** 좋아요 컬렉션. 기본 `memiBoardLikes` */
32
32
  likesCollection: string;
33
+ /** 신고 컬렉션. 기본 `memiBoardReports` */
34
+ reportsCollection: string;
33
35
  /** 보드 설정 컬렉션. 기본 `memiBoardSettings` */
34
36
  settingsCollection: string;
35
37
  /** 역할 문서 컬렉션. 기본 `memiBoardUsers` */
@@ -1,4 +1,4 @@
1
- import { O as e } from "./slugify-B5XHr4SZ.js";
1
+ import { M as e } from "./slugify-Cyq-Gg8X.js";
2
2
  //#region node_modules/.pnpm/heic2any@0.0.4/node_modules/heic2any/dist/heic2any.js
3
3
  var t = /* @__PURE__ */ e(((e, t) => {
4
4
  (function(n, r, i, a) {
@@ -974,4 +974,4 @@ var t = /* @__PURE__ */ e(((e, t) => {
974
974
  //#endregion
975
975
  export default t();
976
976
 
977
- //# sourceMappingURL=heic2any-BWRKffWN.js.map
977
+ //# sourceMappingURL=heic2any-D2QKy06h.js.map