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.
- package/README.md +17 -2
- package/dist/composables/boardLookup.d.ts +8 -0
- package/dist/composables/useMemiBoardAuth.d.ts +4 -3
- package/dist/composables/useMemiBoardCommentFeed.d.ts +18 -0
- package/dist/composables/useMemiBoardComments.d.ts +2 -0
- package/dist/composables/useMemiBoardLikes.d.ts +9 -0
- package/dist/composables/useMemiBoardMiniPosts.d.ts +12 -0
- package/dist/composables/useMemiBoardReports.d.ts +114 -0
- package/dist/composables/useMemiBoardSections.d.ts +13 -0
- package/dist/composables/useMemiBoardSettings.d.ts +2 -0
- package/dist/config.d.ts +2 -0
- package/dist/{heic2any-BWRKffWN.js → heic2any-D2QKy06h.js} +2 -2
- package/dist/{heic2any-BWRKffWN.js.map → heic2any-D2QKy06h.js.map} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +1161 -647
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CategorySettings.vue +3 -5
- package/dist/runtime/components/CommentFeed.vue +141 -0
- package/dist/runtime/components/CommentForm.vue +2 -2
- package/dist/runtime/components/CommentItem.vue +55 -5
- package/dist/runtime/components/CommentList.vue +10 -2
- package/dist/runtime/components/Detail.vue +8 -0
- package/dist/runtime/components/ListDefault.vue +2 -2
- package/dist/runtime/components/ListDense.vue +2 -2
- package/dist/runtime/components/ListVideo.vue +2 -2
- package/dist/runtime/components/Mini.vue +37 -0
- package/dist/runtime/components/MiniCard.vue +51 -0
- package/dist/runtime/components/MiniComments.vue +75 -0
- package/dist/runtime/components/MiniList.vue +36 -0
- package/dist/runtime/components/MiniPicker.vue +131 -0
- package/dist/runtime/components/MiniPost.vue +52 -0
- package/dist/runtime/components/MiniPosts.vue +115 -0
- package/dist/runtime/components/ReportButton.vue +117 -0
- package/dist/runtime/components/ReportList.vue +124 -0
- package/dist/runtime/components/Section.vue +62 -0
- package/dist/runtime/components/Settings.vue +3 -4
- package/dist/runtime/components/UserCommentList.vue +3 -0
- package/dist/{slugify-B5XHr4SZ.js → slugify-Cyq-Gg8X.js} +47 -36
- package/dist/slugify-Cyq-Gg8X.js.map +1 -0
- package/dist/storage.js +11 -11
- package/dist/types.d.ts +54 -3
- package/dist/utils/boardAccess.d.ts +7 -0
- package/dist/utils/boardPaths.d.ts +10 -3
- package/dist/utils/deletePostCascade.d.ts +7 -1
- package/dist/utils/section.d.ts +31 -0
- package/docs/firestore.indexes.json.example +38 -0
- package/docs/firestore.rules.example +78 -22
- package/package.json +1 -1
- 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.
|
|
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
|
-
###
|
|
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 {
|
|
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-
|
|
977
|
+
//# sourceMappingURL=heic2any-D2QKy06h.js.map
|