memi-board 0.10.0 → 0.11.1
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 +3 -1
- package/dist/composables/useMemiBoardAuth.d.ts +1 -0
- package/dist/composables/useMemiBoardComments.d.ts +63 -0
- package/dist/composables/useMemiBoardLikes.d.ts +12 -0
- package/dist/composables/useMemiBoardPosts.d.ts +29 -0
- package/dist/composables/useMemiBoardStorage.d.ts +2 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +961 -311
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/CategorySettings.vue +90 -0
- package/dist/runtime/components/CommentForm.vue +3 -1
- package/dist/runtime/components/CommentItem.vue +186 -21
- package/dist/runtime/components/CommentThread.vue +2 -0
- package/dist/runtime/components/Detail.vue +47 -31
- package/dist/runtime/components/Editor.vue +4 -4
- package/dist/runtime/components/Footer.vue +45 -0
- package/dist/runtime/components/LikeButton.vue +35 -0
- package/dist/runtime/components/List.vue +97 -63
- package/dist/runtime/components/ListDefault.vue +14 -2
- package/dist/runtime/components/ListImage.vue +13 -2
- package/dist/runtime/components/ListVideo.vue +13 -2
- package/dist/runtime/components/Settings.vue +19 -7
- package/dist/types.d.ts +8 -0
- package/dist/utils/formatDate.d.ts +4 -0
- package/docs/firestore.rules.example +60 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -264,7 +264,9 @@ const router = useRouter()
|
|
|
264
264
|
본문은 Nuxt UI **`UEditor`**(TipTap, `content-type="html"`). 호스트 `@nuxt/ui` 4.x 에 Editor 포함 필요.
|
|
265
265
|
|
|
266
266
|
**에디터 이미지:** 툴바 / 붙여넣기 / 드롭 → Firebase Storage
|
|
267
|
-
`{prefix}/posts/{postId}/images/*` + `.../images/thumbnails/*` (
|
|
267
|
+
`{prefix}/posts/{postId}/images/*` + `.../images/thumbnails/*` (최적화 이미지+썸네일).
|
|
268
|
+
|
|
269
|
+
본문 이미지는 휴대폰 원본 기준 최대 25MB까지 선택할 수 있다. 5MB를 넘으면 브라우저에서 최대 2560px·JPEG 85%로 최적화하고, 결과가 여전히 5MB를 넘으면 2048px·75%로 한 번 더 줄인 뒤 업로드한다. 5MB 이하 이미지는 원본을 유지한다.
|
|
268
270
|
본문 HTML에는 원본 URL. 수정 중 버려진 파일은 호스트 스케줄러로 정리 (`extractEditorImageUrls` 로 본문 참조 비교).
|
|
269
271
|
|
|
270
272
|
**composables:**
|
|
@@ -22,6 +22,7 @@ export interface UseMemiBoardAuthReturn {
|
|
|
22
22
|
signOut: () => Promise<void>;
|
|
23
23
|
canEdit: (post: Pick<PostModel, 'authorUid'>) => boolean;
|
|
24
24
|
canDelete: (post: Pick<PostModel, 'authorUid'>) => boolean;
|
|
25
|
+
canEditComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean;
|
|
25
26
|
canDeleteComment: (comment: Pick<CommentModel, 'authorUid'>) => boolean;
|
|
26
27
|
/** 콘텐츠 검열 차단 1회 기록 후 메시지 조각 반환 */
|
|
27
28
|
recordContentModerationBlock: () => Promise<{
|
|
@@ -9,6 +9,7 @@ export interface AddCommentInput {
|
|
|
9
9
|
export interface AddReplyInput extends AddCommentInput {
|
|
10
10
|
parent: CommentModel;
|
|
11
11
|
}
|
|
12
|
+
export declare const COMMENT_BODY_MAX_LENGTH = 1000;
|
|
12
13
|
/** 10개씩 순차 조회한 뒤 마지막 페이지에서만 최신 1개를 실시간 구독한다. */
|
|
13
14
|
export declare function useMemiBoardComments(postId: string | Ref<string>, options?: {
|
|
14
15
|
subscribe?: boolean;
|
|
@@ -20,6 +21,8 @@ export declare function useMemiBoardComments(postId: string | Ref<string>, optio
|
|
|
20
21
|
loadMore: () => Promise<void>;
|
|
21
22
|
addComment: (input: AddCommentInput) => Promise<void>;
|
|
22
23
|
addReply: (input: AddReplyInput) => Promise<void>;
|
|
24
|
+
updateComment: (commentId: string, body: string) => Promise<void>;
|
|
25
|
+
setCommentBlinded: (commentId: string, isBlinded: boolean, moderatorUid: string) => Promise<void>;
|
|
23
26
|
deleteComment: (comment: CommentModel) => Promise<void>;
|
|
24
27
|
};
|
|
25
28
|
/** 펼친 스레드의 답글을 5개씩 일회성 조회한다. */
|
|
@@ -46,6 +49,36 @@ export declare function useMemiBoardReplies(postId: string, rootId: string): {
|
|
|
46
49
|
};
|
|
47
50
|
valueOf: () => string;
|
|
48
51
|
};
|
|
52
|
+
updatedAt?: {
|
|
53
|
+
readonly seconds: number;
|
|
54
|
+
readonly nanoseconds: number;
|
|
55
|
+
toDate: () => Date;
|
|
56
|
+
toMillis: () => number;
|
|
57
|
+
isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
|
|
58
|
+
toString: () => string;
|
|
59
|
+
toJSON: () => {
|
|
60
|
+
seconds: number;
|
|
61
|
+
nanoseconds: number;
|
|
62
|
+
type: string;
|
|
63
|
+
};
|
|
64
|
+
valueOf: () => string;
|
|
65
|
+
} | undefined;
|
|
66
|
+
isBlinded?: boolean | undefined;
|
|
67
|
+
moderatedAt?: {
|
|
68
|
+
readonly seconds: number;
|
|
69
|
+
readonly nanoseconds: number;
|
|
70
|
+
toDate: () => Date;
|
|
71
|
+
toMillis: () => number;
|
|
72
|
+
isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
|
|
73
|
+
toString: () => string;
|
|
74
|
+
toJSON: () => {
|
|
75
|
+
seconds: number;
|
|
76
|
+
nanoseconds: number;
|
|
77
|
+
type: string;
|
|
78
|
+
};
|
|
79
|
+
valueOf: () => string;
|
|
80
|
+
} | undefined;
|
|
81
|
+
moderatedBy?: string | undefined;
|
|
49
82
|
parentId?: string | null | undefined;
|
|
50
83
|
rootId?: string | undefined;
|
|
51
84
|
depth?: number | undefined;
|
|
@@ -75,6 +108,36 @@ export declare function useMemiBoardReplies(postId: string, rootId: string): {
|
|
|
75
108
|
};
|
|
76
109
|
valueOf: () => string;
|
|
77
110
|
};
|
|
111
|
+
updatedAt?: {
|
|
112
|
+
readonly seconds: number;
|
|
113
|
+
readonly nanoseconds: number;
|
|
114
|
+
toDate: () => Date;
|
|
115
|
+
toMillis: () => number;
|
|
116
|
+
isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
|
|
117
|
+
toString: () => string;
|
|
118
|
+
toJSON: () => {
|
|
119
|
+
seconds: number;
|
|
120
|
+
nanoseconds: number;
|
|
121
|
+
type: string;
|
|
122
|
+
};
|
|
123
|
+
valueOf: () => string;
|
|
124
|
+
} | undefined;
|
|
125
|
+
isBlinded?: boolean | undefined;
|
|
126
|
+
moderatedAt?: {
|
|
127
|
+
readonly seconds: number;
|
|
128
|
+
readonly nanoseconds: number;
|
|
129
|
+
toDate: () => Date;
|
|
130
|
+
toMillis: () => number;
|
|
131
|
+
isEqual: (other: import('firebase/firestore').Timestamp) => boolean;
|
|
132
|
+
toString: () => string;
|
|
133
|
+
toJSON: () => {
|
|
134
|
+
seconds: number;
|
|
135
|
+
nanoseconds: number;
|
|
136
|
+
type: string;
|
|
137
|
+
};
|
|
138
|
+
valueOf: () => string;
|
|
139
|
+
} | undefined;
|
|
140
|
+
moderatedBy?: string | undefined;
|
|
78
141
|
parentId?: string | null | undefined;
|
|
79
142
|
rootId?: string | undefined;
|
|
80
143
|
depth?: number | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
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): {
|
|
9
|
+
isLiked: import('vue').Ref<boolean, boolean>;
|
|
10
|
+
likePending: import('vue').Ref<boolean, boolean>;
|
|
11
|
+
toggleLike: () => Promise<boolean>;
|
|
12
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
1
2
|
import { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore';
|
|
2
3
|
import { Attachment, PostDetail, PostModel } from '../types';
|
|
3
4
|
export interface CreatePostInput {
|
|
@@ -43,3 +44,31 @@ export declare function useMemiBoardPosts(): {
|
|
|
43
44
|
updatePost: (id: string, input: UpdatePostInput) => Promise<void>;
|
|
44
45
|
deletePost: (id: string) => Promise<void>;
|
|
45
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* 최신 {pageSize}건은 onSnapshot으로 실시간 구독해 목록에 보이는 동안 좋아요·댓글 수가
|
|
49
|
+
* 바로 갱신되게 하고, 그 이전 글은 "더보기" 클릭 시 1회성 getDocs로 이어서 불러온다.
|
|
50
|
+
* 두 구간이 살짝 겹쳐도 id 기준으로 합쳐 중복 없이 렌더한다.
|
|
51
|
+
*
|
|
52
|
+
* 알려진 한계: "더보기"로 이전 글을 불러온 뒤 새 글이 pageSize개 이상 연달아 올라오면
|
|
53
|
+
* 실시간 구간과 페이지 구간 사이에 짧은 공백이 생길 수 있다 — 새로고침하면 바로 채워지고,
|
|
54
|
+
* 이 정도 트래픽 폭이면 실사용에서 발생 가능성이 낮아 별도 보정 로직은 두지 않았다.
|
|
55
|
+
*/
|
|
56
|
+
export declare function useMemiBoardPostList(category?: string | Ref<string | undefined>, options?: {
|
|
57
|
+
pageSize?: number;
|
|
58
|
+
}): {
|
|
59
|
+
posts: import('vue').ComputedRef<PostModel[]>;
|
|
60
|
+
postsPending: Ref<boolean, boolean>;
|
|
61
|
+
hasMore: Ref<boolean, boolean>;
|
|
62
|
+
loadingMore: Ref<boolean, boolean>;
|
|
63
|
+
loadError: Ref<string, string>;
|
|
64
|
+
loadMore: () => Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* 게시글 상세를 VueFire useDocument로 실시간 구독한다 (메타 + 본문).
|
|
68
|
+
* 좋아요·댓글 수 등 다른 사람의 변경이 화면에 바로 반영된다 — 좋아요 토글도
|
|
69
|
+
* 별도 로컬 낙관적 갱신 없이 이 구독이 그대로 반영해 준다.
|
|
70
|
+
*/
|
|
71
|
+
export declare function useMemiBoardPost(postId: string | Ref<string>): {
|
|
72
|
+
post: import('vue').ComputedRef<PostDetail | null>;
|
|
73
|
+
pending: import('vue').ComputedRef<boolean>;
|
|
74
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Attachment, EditorImageEntry } from '../types';
|
|
2
2
|
/** 에디터 이미지 최대 크기 (바이트) */
|
|
3
3
|
export declare const EDITOR_IMAGE_MAX_BYTES: number;
|
|
4
|
+
/** 휴대폰 원본 이미지 선택 허용 크기. 큰 이미지는 업로드 전에 최적화한다. */
|
|
5
|
+
export declare const EDITOR_IMAGE_SOURCE_MAX_BYTES: number;
|
|
4
6
|
export declare function useMemiBoardStorage(): {
|
|
5
7
|
uploadAttachment: (file: File, postId: string, onProgress?: (ratio: number) => void) => {
|
|
6
8
|
promise: Promise<Attachment>;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
export { configureMemiBoard, useMemiBoardConfig } from './config';
|
|
3
3
|
export type { MemiBoardConfig, MemiBoardAuthOptions, MemiBoardModerationOptions } from './config';
|
|
4
4
|
export { useMemiBoardAuth } from './composables/useMemiBoardAuth';
|
|
5
|
-
export { useMemiBoardPosts } from './composables/useMemiBoardPosts';
|
|
5
|
+
export { useMemiBoardPosts, useMemiBoardPostList, useMemiBoardPost } from './composables/useMemiBoardPosts';
|
|
6
6
|
export type { CreatePostInput, UpdatePostInput } from './composables/useMemiBoardPosts';
|
|
7
|
-
export { useMemiBoardComments, useMemiBoardReplies } from './composables/useMemiBoardComments';
|
|
7
|
+
export { COMMENT_BODY_MAX_LENGTH, useMemiBoardComments, useMemiBoardReplies } from './composables/useMemiBoardComments';
|
|
8
8
|
export type { AddCommentInput, AddReplyInput } from './composables/useMemiBoardComments';
|
|
9
|
+
export { useMemiBoardLikes } from './composables/useMemiBoardLikes';
|
|
9
10
|
export { useMemiBoardModeration } from './composables/useMemiBoardModeration';
|
|
10
11
|
export { useMemiBoardStorage } from './composables/useMemiBoardStorage';
|
|
11
12
|
export { useMemiBoardSettings, DEFAULT_CATEGORIES } from './composables/useMemiBoardSettings';
|
|
12
13
|
export { useMemiBoardUsers, BOARD_USER_ROLES } from './composables/useMemiBoardUsers';
|
|
13
14
|
export { versionHistory } from './data/versionHistory';
|
|
14
15
|
export type { VersionHistoryEntry } from './data/versionHistory';
|
|
15
|
-
export { formatDate } from './utils/formatDate';
|
|
16
|
+
export { formatDate, formatFullDate, formatRelativeDate, formatTimestampDetails } from './utils/formatDate';
|
|
16
17
|
export { slugify } from './utils/slugify';
|
|
17
18
|
export { DEFAULT_BLOCK_BAN_THRESHOLD, DEFAULT_BLOCK_BAN_DECAY_MS, effectiveModerationBlockCount, isModerationWriteRestricted, moderationWriteRestrictedUntilMs, formatRestrictedUntilLabel, } from './utils/moderation-strike';
|
|
18
19
|
export type { Attachment, EditorImageEntry, ModerationStatus, PostModel, PostDetail, CommentModel, BoardUserRole, BoardUserModel, BoardCategory, BoardListView, BoardWriteRole, BoardSettingsModel, ModerationResult, } from './types';
|
|
@@ -20,4 +21,4 @@ export { compressImage } from './utils/compressImage';
|
|
|
20
21
|
export { extractEditorImageUrls } from './utils/extractEditorImageUrls';
|
|
21
22
|
export { renderMarkdownToHtml } from './utils/renderMarkdown';
|
|
22
23
|
export { storagePathFromDownloadUrl, postNamespaceFromStoragePath } from './utils/storagePath';
|
|
23
|
-
export { EDITOR_IMAGE_MAX_BYTES } from './composables/useMemiBoardStorage';
|
|
24
|
+
export { EDITOR_IMAGE_MAX_BYTES, EDITOR_IMAGE_SOURCE_MAX_BYTES } from './composables/useMemiBoardStorage';
|