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.
- package/README.md +19 -17
- package/dist/composables/fetchPublicSeo.d.ts +6 -6
- package/dist/composables/useMemiBoardComments.d.ts +3 -3
- package/dist/composables/useMemiBoardLikes.d.ts +1 -8
- package/dist/composables/useMemiBoardModeration.d.ts +1 -1
- package/dist/composables/useMemiBoardPosts.d.ts +8 -28
- package/dist/composables/useMemiBoardSettings.d.ts +27 -9
- package/dist/composables/useMemiBoardStorage.d.ts +2 -2
- package/dist/composables/useMemiBoardUserPosts.d.ts +18 -0
- package/dist/composables/useMemiBoardUsers.d.ts +2 -0
- package/dist/composables/useMemiBoardViews.d.ts +3 -7
- package/dist/config.d.ts +7 -23
- package/dist/heic2any-BXb_U1zK.js +977 -0
- package/dist/heic2any-BXb_U1zK.js.map +1 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +887 -667
- package/dist/index.js.map +1 -1
- package/dist/rolldown-runtime-DtPi1Y-2.js +13 -0
- package/dist/runtime/components/Attachments.vue +2 -1
- package/dist/runtime/components/AuthorMenu.vue +47 -0
- package/dist/runtime/components/CategorySettings.vue +41 -7
- package/dist/runtime/components/CommentForm.vue +9 -5
- package/dist/runtime/components/CommentItem.vue +6 -1
- package/dist/runtime/components/CommentList.vue +7 -4
- package/dist/runtime/components/CommentThread.vue +16 -5
- package/dist/runtime/components/Detail.vue +97 -18
- package/dist/runtime/components/Editor.vue +14 -115
- package/dist/runtime/components/Footer.vue +9 -7
- package/dist/runtime/components/LikeButton.vue +2 -1
- package/dist/runtime/components/List.vue +14 -8
- package/dist/runtime/components/ListDefault.vue +52 -33
- package/dist/runtime/components/ListDense.vue +66 -50
- package/dist/runtime/components/ListImage.vue +10 -2
- package/dist/runtime/components/ListVideo.vue +62 -40
- package/dist/runtime/components/Settings.vue +1 -1
- package/dist/runtime/components/SwipeHint.vue +83 -0
- package/dist/runtime/components/UserPostList.vue +83 -0
- package/dist/types.d.ts +31 -17
- package/dist/utils/boardPaths.d.ts +26 -0
- package/docs/firebase-setup.md +1 -1
- package/docs/firestore.rules.example +119 -227
- package/docs/storage.rules.example +23 -25
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,24 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
import { n as e, t } from "./rolldown-runtime-DtPi1Y-2.js";
|
|
2
|
+
import { computed as n, onScopeDispose as r, reactive as i, ref as a, toValue as o, watch as s } from "vue";
|
|
3
|
+
import { collection as c, collectionGroup as l, deleteDoc as u, deleteField as d, doc as f, documentId as p, endBefore as m, getDoc as h, getDocs as g, increment as _, limit as v, limitToLast as y, onSnapshot as b, orderBy as x, query as S, runTransaction as C, serverTimestamp as w, setDoc as T, startAfter as E, updateDoc as D, where as O, writeBatch as k } from "firebase/firestore";
|
|
4
|
+
import { useCollection as A, useCurrentUser as j, useDocument as M, useFirebaseApp as N, useFirebaseAuth as P, useFirestore as F } from "vuefire";
|
|
5
|
+
import { GoogleAuthProvider as I, OAuthProvider as L, createUserWithEmailAndPassword as R, signInWithEmailAndPassword as z, signInWithPopup as B, signOut as ee, updateProfile as te } from "firebase/auth";
|
|
6
|
+
import { deleteObject as V, getDownloadURL as H, getStorage as U, listAll as ne, ref as W, uploadBytes as re, uploadBytesResumable as ie } from "firebase/storage";
|
|
7
|
+
import { GoogleAIBackend as ae, Schema as oe, getAI as se, getGenerativeModel as ce } from "firebase/ai";
|
|
8
|
+
//#region src/utils/boardPaths.ts
|
|
9
|
+
var le = "memiBoards", ue = "memiBoardUsers";
|
|
10
|
+
function de(e) {
|
|
11
|
+
return {
|
|
12
|
+
boardsCollection: e.boardsCollection?.trim() || "memiBoards",
|
|
13
|
+
usersCollection: e.usersCollection?.trim() || "memiBoardUsers"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function fe(e, t) {
|
|
17
|
+
return c(e, t.boardsCollection);
|
|
18
|
+
}
|
|
19
|
+
function pe(e, t, n) {
|
|
20
|
+
return f(e, t.boardsCollection, n);
|
|
21
|
+
}
|
|
22
|
+
function G(e, t, n) {
|
|
23
|
+
return f(e, t.boardsCollection, n, "settings", "config");
|
|
24
|
+
}
|
|
25
|
+
function K(e, t, n) {
|
|
26
|
+
return c(e, t.boardsCollection, n, "posts");
|
|
27
|
+
}
|
|
28
|
+
function q(e, t, n, r) {
|
|
29
|
+
return f(e, t.boardsCollection, n, "posts", r);
|
|
30
|
+
}
|
|
31
|
+
function me(e, t, n, r) {
|
|
32
|
+
return f(e, t.boardsCollection, n, "posts", r, "body", "main");
|
|
33
|
+
}
|
|
34
|
+
function J(e, t, n, r) {
|
|
35
|
+
return c(e, t.boardsCollection, n, "posts", r, "comments");
|
|
36
|
+
}
|
|
37
|
+
function Y(e, t, n, r, i) {
|
|
38
|
+
return f(e, t.boardsCollection, n, "posts", r, "comments", i);
|
|
39
|
+
}
|
|
40
|
+
function he(e, t, n, r, i) {
|
|
41
|
+
return f(e, t.boardsCollection, n, "posts", r, "likes", i);
|
|
42
|
+
}
|
|
43
|
+
function ge(e, t) {
|
|
44
|
+
return c(e, t.usersCollection);
|
|
45
|
+
}
|
|
46
|
+
function _e(e, t, n) {
|
|
47
|
+
return f(e, t.usersCollection, n);
|
|
48
|
+
}
|
|
49
|
+
function ve(e, t) {
|
|
50
|
+
return `${e.boardsCollection}/${t}`;
|
|
51
|
+
}
|
|
52
|
+
function X(e, t, n) {
|
|
53
|
+
return `${ve(e, t)}/posts/${n}`;
|
|
54
|
+
}
|
|
55
|
+
function ye(e, t) {
|
|
56
|
+
return `${e.boardsCollection}/${e.usersCollection}/${t}`;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/config.ts
|
|
60
|
+
var be = "__MEMI_BOARD_CONFIG__";
|
|
61
|
+
function xe() {
|
|
19
62
|
let e = globalThis;
|
|
20
|
-
return e[
|
|
21
|
-
|
|
63
|
+
return e[be] || (e[be] = i({
|
|
64
|
+
boardsCollection: le,
|
|
65
|
+
usersCollection: ue,
|
|
22
66
|
auth: { providers: ["google", "apple"] },
|
|
23
67
|
moderation: {
|
|
24
68
|
enabled: !0,
|
|
@@ -36,24 +80,27 @@ function ge() {
|
|
|
36
80
|
defaultOgImage: "",
|
|
37
81
|
basePath: "/board"
|
|
38
82
|
}
|
|
39
|
-
})), e[
|
|
83
|
+
})), e[be];
|
|
84
|
+
}
|
|
85
|
+
function Se(e) {
|
|
86
|
+
let t = xe();
|
|
87
|
+
e.boardsCollection != null && e.boardsCollection !== "" && (t.boardsCollection = e.boardsCollection.trim()), e.usersCollection != null && e.usersCollection !== "" && (t.usersCollection = e.usersCollection.trim()), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation), e.seo && Object.assign(t.seo, e.seo);
|
|
40
88
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation), e.seo && Object.assign(t.seo, e.seo);
|
|
89
|
+
function Ce() {
|
|
90
|
+
return xe();
|
|
44
91
|
}
|
|
45
|
-
function
|
|
46
|
-
return
|
|
92
|
+
function Z() {
|
|
93
|
+
return de(Ce());
|
|
47
94
|
}
|
|
48
95
|
//#endregion
|
|
49
96
|
//#region src/utils/boardSeo.ts
|
|
50
|
-
function
|
|
97
|
+
function we(e) {
|
|
51
98
|
if (typeof e != "string") return null;
|
|
52
99
|
let t = e.trim();
|
|
53
100
|
return /^https?:\/\//i.test(t) ? t : null;
|
|
54
101
|
}
|
|
55
|
-
function
|
|
56
|
-
let t =
|
|
102
|
+
function Te(e) {
|
|
103
|
+
let t = we(e);
|
|
57
104
|
if (!t) return null;
|
|
58
105
|
if (!/firebasestorage\.googleapis\.com/i.test(t)) return t;
|
|
59
106
|
try {
|
|
@@ -70,49 +117,43 @@ function ye(e) {
|
|
|
70
117
|
return t;
|
|
71
118
|
}
|
|
72
119
|
}
|
|
73
|
-
function
|
|
120
|
+
function Ee(e, t, n = "") {
|
|
74
121
|
let r = t.replace(/\/$/, ""), i = e.trim();
|
|
75
|
-
return i ? /^https?:\/\//i.test(i) ? i : `${r}${i.startsWith("/") ? i : `/${i}`}` : n ?
|
|
122
|
+
return i ? /^https?:\/\//i.test(i) ? i : `${r}${i.startsWith("/") ? i : `/${i}`}` : n ? Ee(n, t) : r;
|
|
76
123
|
}
|
|
77
|
-
function
|
|
124
|
+
function De(e, t, n, r) {
|
|
78
125
|
let i = r?.trim().slice(0, 40), a = e.trim() || i || "사진", o = t?.trim(), s = n.trim() || "Board";
|
|
79
126
|
return o ? `${a} · ${o} | ${s}` : `${a} | ${s}`;
|
|
80
127
|
}
|
|
81
|
-
function
|
|
128
|
+
function Oe(e) {
|
|
82
129
|
let t = [];
|
|
83
130
|
return e.summary?.trim() ? t.push(e.summary.trim().slice(0, 140)) : t.push("게시판 글"), e.categoryLabel?.trim() && t.push(e.categoryLabel.trim()), e.authorName?.trim() && t.push(e.authorName.trim()), t.join(" · ").slice(0, 200);
|
|
84
131
|
}
|
|
85
|
-
function
|
|
132
|
+
function ke(e, t) {
|
|
86
133
|
let n = t.trim() || "Board", r = e?.trim();
|
|
87
134
|
return r && r !== "전체" ? `${r} 게시판 | ${n}` : `게시판 | ${n}`;
|
|
88
135
|
}
|
|
89
|
-
function
|
|
136
|
+
function Ae(e) {
|
|
90
137
|
if (e.description?.trim()) return e.description.trim().slice(0, 200);
|
|
91
138
|
let t = e.categoryLabel?.trim(), n = t && t !== "전체" ? `${t} 게시판 — 소식을 확인하세요.` : "게시판 — 공지와 이야기를 확인하세요.", r = (e.recentTitles || []).filter(Boolean).slice(0, 3);
|
|
92
139
|
return r.length ? `${n} ${r.map((e) => `「${e}」`).join(" ")}`.slice(0, 200) : n.slice(0, 200);
|
|
93
140
|
}
|
|
94
|
-
function
|
|
141
|
+
function je(e) {
|
|
95
142
|
let t = (e || "/board").trim() || "/board";
|
|
96
143
|
return (t.startsWith("/") ? t : `/${t}`).replace(/\/$/, "") || "/board";
|
|
97
144
|
}
|
|
98
145
|
//#endregion
|
|
99
146
|
//#region src/composables/fetchPublicSeo.ts
|
|
100
|
-
function
|
|
147
|
+
function Me() {
|
|
101
148
|
return {
|
|
102
|
-
db:
|
|
103
|
-
|
|
149
|
+
db: F(),
|
|
150
|
+
paths: Z()
|
|
104
151
|
};
|
|
105
152
|
}
|
|
106
|
-
function
|
|
107
|
-
return o(e.db, `${e.prefix}Posts`);
|
|
108
|
-
}
|
|
109
|
-
function Oe(e, t) {
|
|
110
|
-
return l(e.db, `${e.prefix}Settings`, "config", "categories", t);
|
|
111
|
-
}
|
|
112
|
-
async function ke(e, t) {
|
|
153
|
+
async function Ne(e, t) {
|
|
113
154
|
try {
|
|
114
|
-
let n = await
|
|
115
|
-
if (!n.exists()) return {
|
|
155
|
+
let n = await h(G(e.db, e.paths, t));
|
|
156
|
+
if (!n.exists() && !(await h(G(e.db, e.paths, t))).exists()) return {
|
|
116
157
|
label: t,
|
|
117
158
|
description: "",
|
|
118
159
|
hidden: !1
|
|
@@ -131,7 +172,7 @@ async function ke(e, t) {
|
|
|
131
172
|
};
|
|
132
173
|
}
|
|
133
174
|
}
|
|
134
|
-
function
|
|
175
|
+
function Pe(e) {
|
|
135
176
|
if (e && typeof e == "object" && "toDate" in e) try {
|
|
136
177
|
let t = e.toDate();
|
|
137
178
|
return t instanceof Date ? t.toISOString() : null;
|
|
@@ -140,13 +181,15 @@ function Ae(e) {
|
|
|
140
181
|
}
|
|
141
182
|
return null;
|
|
142
183
|
}
|
|
143
|
-
async function
|
|
184
|
+
async function Fe(e, t, n) {
|
|
144
185
|
let r = e.trim(), i = t.trim();
|
|
145
186
|
if (!r || !i) return null;
|
|
146
|
-
let a = n ??
|
|
187
|
+
let a = n ?? Me();
|
|
188
|
+
a.paths = de(a.paths);
|
|
189
|
+
let o = await Ne(a, r);
|
|
147
190
|
if (o.hidden) return null;
|
|
148
191
|
try {
|
|
149
|
-
let e = (await
|
|
192
|
+
let e = (await g(S(K(a.db, a.paths, r), O("slug", "==", i), O("listed", "==", !0), O("isPublished", "==", !0), v(1)))).docs[0];
|
|
150
193
|
if (!e) return null;
|
|
151
194
|
let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title.trim() : "";
|
|
152
195
|
return n ? {
|
|
@@ -156,18 +199,18 @@ async function je(e, t, n) {
|
|
|
156
199
|
categoryLabel: o.label,
|
|
157
200
|
title: n,
|
|
158
201
|
summary: typeof t.summary == "string" ? t.summary.trim().slice(0, 200) : "",
|
|
159
|
-
previewImage:
|
|
202
|
+
previewImage: Te(t.previewImage),
|
|
160
203
|
authorName: typeof t.authorName == "string" && t.authorName.trim() ? t.authorName.trim() : null,
|
|
161
|
-
createdAt:
|
|
204
|
+
createdAt: Pe(t.createdAt)
|
|
162
205
|
} : null;
|
|
163
206
|
} catch (e) {
|
|
164
207
|
return import.meta.dev && console.warn("[memi-board] fetchPublicPostForSeo failed", e), null;
|
|
165
208
|
}
|
|
166
209
|
}
|
|
167
|
-
async function
|
|
168
|
-
let n = e?.trim() || null, r = t ??
|
|
169
|
-
if (n) {
|
|
170
|
-
let e = await
|
|
210
|
+
async function Ie(e, t) {
|
|
211
|
+
let n = e?.trim() || null, r = t ?? Me();
|
|
212
|
+
if (r.paths = de(r.paths), n) {
|
|
213
|
+
let e = await Ne(r, n);
|
|
171
214
|
if (e.hidden) return {
|
|
172
215
|
kind: "category",
|
|
173
216
|
category: n,
|
|
@@ -176,7 +219,7 @@ async function Me(e, t) {
|
|
|
176
219
|
ogImage: null,
|
|
177
220
|
recentTitles: []
|
|
178
221
|
};
|
|
179
|
-
let { recentTitles: t, ogImage: i } = await
|
|
222
|
+
let { recentTitles: t, ogImage: i } = await Le(r, n);
|
|
180
223
|
return {
|
|
181
224
|
kind: "category",
|
|
182
225
|
category: n,
|
|
@@ -186,32 +229,22 @@ async function Me(e, t) {
|
|
|
186
229
|
recentTitles: t
|
|
187
230
|
};
|
|
188
231
|
}
|
|
189
|
-
let { recentTitles: i, ogImage: a } = await Ne(r, null);
|
|
190
232
|
return {
|
|
191
233
|
kind: "all",
|
|
192
234
|
category: null,
|
|
193
235
|
categoryLabel: "전체",
|
|
194
236
|
description: "게시판 — 공지와 커뮤니티 소식을 확인하세요.",
|
|
195
|
-
ogImage:
|
|
196
|
-
recentTitles:
|
|
237
|
+
ogImage: null,
|
|
238
|
+
recentTitles: []
|
|
197
239
|
};
|
|
198
240
|
}
|
|
199
|
-
async function
|
|
241
|
+
async function Le(e, t) {
|
|
200
242
|
let n = [], r = null;
|
|
201
243
|
try {
|
|
202
|
-
let i = t
|
|
203
|
-
|
|
204
|
-
T("listed", "==", !0),
|
|
205
|
-
v("createdAt", "desc"),
|
|
206
|
-
h(12)
|
|
207
|
-
] : [
|
|
208
|
-
T("listed", "==", !0),
|
|
209
|
-
v("createdAt", "desc"),
|
|
210
|
-
h(12)
|
|
211
|
-
], a = await p(y(De(e), ...i));
|
|
212
|
-
for (let e of a.docs) {
|
|
244
|
+
let i = await g(S(K(e.db, e.paths, t), O("listed", "==", !0), O("isPublished", "==", !0), x("createdAt", "desc"), v(12)));
|
|
245
|
+
for (let e of i.docs) {
|
|
213
246
|
let t = e.data() ?? {}, i = typeof t.title == "string" ? t.title.trim() : "";
|
|
214
|
-
i && n.length < 5 && n.push(i), r ||=
|
|
247
|
+
i && n.length < 5 && n.push(i), r ||= Te(t.previewImage);
|
|
215
248
|
}
|
|
216
249
|
} catch (e) {
|
|
217
250
|
import.meta.dev && console.warn("[memi-board] loadRecentListed failed", e);
|
|
@@ -223,8 +256,8 @@ async function Ne(e, t) {
|
|
|
223
256
|
}
|
|
224
257
|
//#endregion
|
|
225
258
|
//#region src/utils/moderation-strike.ts
|
|
226
|
-
var
|
|
227
|
-
function
|
|
259
|
+
var Re = 3, Q = 864e5;
|
|
260
|
+
function ze(e, t, n = Date.now(), r = Q) {
|
|
228
261
|
let i = Math.max(0, Math.floor(Number(e) || 0));
|
|
229
262
|
if (i <= 0) return 0;
|
|
230
263
|
let a = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -232,7 +265,7 @@ function Fe(e, t, n = Date.now(), r = K) {
|
|
|
232
265
|
let o = Math.floor((n - a) / r);
|
|
233
266
|
return Math.max(0, i - o);
|
|
234
267
|
}
|
|
235
|
-
function
|
|
268
|
+
function Be(e, t, n = Date.now(), r = 3, i = Q) {
|
|
236
269
|
let a = Math.max(0, Math.floor(Number(e) || 0));
|
|
237
270
|
if (a < r) return null;
|
|
238
271
|
let o = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -240,17 +273,17 @@ function q(e, t, n = Date.now(), r = 3, i = K) {
|
|
|
240
273
|
let s = o + (a - (r - 1)) * i;
|
|
241
274
|
return s > n ? s : null;
|
|
242
275
|
}
|
|
243
|
-
function
|
|
244
|
-
return
|
|
276
|
+
function Ve(e, t, n = Date.now(), r = 3, i = Q) {
|
|
277
|
+
return Be(e, t, n, r, i) != null;
|
|
245
278
|
}
|
|
246
|
-
function
|
|
279
|
+
function He(e, t = Date.now()) {
|
|
247
280
|
let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
|
|
248
281
|
if (r <= 0) return "곧 해제";
|
|
249
282
|
if (r < 24) return `약 ${r}시간 후 해제`;
|
|
250
283
|
let i = Math.floor(r / 24), a = r % 24;
|
|
251
284
|
return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
|
|
252
285
|
}
|
|
253
|
-
function
|
|
286
|
+
function Ue(e) {
|
|
254
287
|
if (e == null) return null;
|
|
255
288
|
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
256
289
|
if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
|
|
@@ -262,190 +295,190 @@ function Re(e) {
|
|
|
262
295
|
}
|
|
263
296
|
//#endregion
|
|
264
297
|
//#region src/composables/useMemiBoardAuth.ts
|
|
265
|
-
var
|
|
266
|
-
function
|
|
298
|
+
var We = "__MEMI_BOARD_AUTH_STATE__";
|
|
299
|
+
function Ge() {
|
|
267
300
|
let e = globalThis;
|
|
268
|
-
return e[
|
|
269
|
-
role:
|
|
270
|
-
rolePending:
|
|
271
|
-
rawBlockCount:
|
|
272
|
-
blockAtMs:
|
|
301
|
+
return e[We] || (e[We] = {
|
|
302
|
+
role: a("user"),
|
|
303
|
+
rolePending: a(!0),
|
|
304
|
+
rawBlockCount: a(0),
|
|
305
|
+
blockAtMs: a(null),
|
|
273
306
|
watchStarted: !1
|
|
274
|
-
}), e[
|
|
307
|
+
}), e[We];
|
|
275
308
|
}
|
|
276
|
-
function
|
|
277
|
-
let
|
|
278
|
-
function
|
|
279
|
-
let
|
|
280
|
-
return
|
|
309
|
+
function Ke() {
|
|
310
|
+
let e = Ce(), t = Ge(), r = F(), i = P(), a = j(), { role: o, rolePending: c, rawBlockCount: l, blockAtMs: u } = t;
|
|
311
|
+
function d() {
|
|
312
|
+
let t = e.moderation?.blockBanThreshold;
|
|
313
|
+
return t === 0 ? 0 : typeof t == "number" && t > 0 ? Math.floor(t) : 3;
|
|
281
314
|
}
|
|
282
|
-
function
|
|
283
|
-
let
|
|
284
|
-
return typeof
|
|
315
|
+
function f() {
|
|
316
|
+
let t = e.moderation?.blockBanDecayMs;
|
|
317
|
+
return typeof t == "number" && t > 0 ? t : Q;
|
|
285
318
|
}
|
|
286
|
-
function
|
|
287
|
-
return
|
|
319
|
+
function p(e) {
|
|
320
|
+
return _e(r, Z(), e);
|
|
288
321
|
}
|
|
289
|
-
function
|
|
322
|
+
function m(e) {
|
|
290
323
|
if (!e) {
|
|
291
|
-
|
|
324
|
+
l.value = 0, u.value = null;
|
|
292
325
|
return;
|
|
293
326
|
}
|
|
294
|
-
|
|
327
|
+
l.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), u.value = Ue(e.moderationBlockAt);
|
|
295
328
|
}
|
|
296
|
-
async function
|
|
297
|
-
let t =
|
|
298
|
-
if (!n.exists()) return await
|
|
329
|
+
async function g(e) {
|
|
330
|
+
let t = p(e.uid), n = await h(t);
|
|
331
|
+
if (!n.exists()) return await T(t, {
|
|
299
332
|
role: "user",
|
|
300
333
|
displayName: e.displayName,
|
|
301
334
|
email: e.email,
|
|
302
335
|
photoURL: e.photoURL,
|
|
303
|
-
updatedAt:
|
|
304
|
-
}),
|
|
336
|
+
updatedAt: w()
|
|
337
|
+
}), m({ moderationBlockCount: 0 }), "user";
|
|
305
338
|
let r = n.data();
|
|
306
|
-
return
|
|
339
|
+
return m(r), await D(t, {
|
|
307
340
|
displayName: e.displayName,
|
|
308
341
|
email: e.email,
|
|
309
342
|
photoURL: e.photoURL,
|
|
310
|
-
updatedAt:
|
|
343
|
+
updatedAt: w()
|
|
311
344
|
}), [
|
|
312
345
|
"admin",
|
|
313
346
|
"staff",
|
|
314
347
|
"user"
|
|
315
348
|
].includes(String(r.role)) ? r.role : "user";
|
|
316
349
|
}
|
|
317
|
-
async function
|
|
318
|
-
let e =
|
|
350
|
+
async function v() {
|
|
351
|
+
let e = a.value?.uid;
|
|
319
352
|
if (!e) {
|
|
320
|
-
|
|
353
|
+
m(void 0);
|
|
321
354
|
return;
|
|
322
355
|
}
|
|
323
|
-
let t = await
|
|
324
|
-
|
|
356
|
+
let t = await h(p(e));
|
|
357
|
+
m(t.exists() ? t.data() : void 0);
|
|
325
358
|
}
|
|
326
|
-
|
|
359
|
+
t.watchStarted || (t.watchStarted = !0, s(a, async (e) => {
|
|
327
360
|
if (c.value = !0, !e) {
|
|
328
|
-
|
|
361
|
+
o.value = "user", m(void 0), c.value = !1;
|
|
329
362
|
return;
|
|
330
363
|
}
|
|
331
364
|
try {
|
|
332
|
-
|
|
333
|
-
} catch {
|
|
334
|
-
|
|
365
|
+
o.value = await g(e);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
console.error("[memi-board] ensureUserDoc failed", e), o.value = "user", m(void 0);
|
|
335
368
|
} finally {
|
|
336
369
|
c.value = !1;
|
|
337
370
|
}
|
|
338
371
|
}, { immediate: !0 }));
|
|
339
|
-
let
|
|
340
|
-
let e =
|
|
341
|
-
return e <= 0 ? !1 :
|
|
342
|
-
}),
|
|
343
|
-
let e =
|
|
372
|
+
let y = n(() => !!a.value), b = n(() => o.value === "admin"), x = n(() => o.value === "staff"), S = n(() => b.value || x.value), C = n(() => ze(l.value, u.value, Date.now(), f())), E = n(() => {
|
|
373
|
+
let e = d();
|
|
374
|
+
return e <= 0 ? !1 : Ve(l.value, u.value, Date.now(), e, f());
|
|
375
|
+
}), O = n(() => {
|
|
376
|
+
let e = d();
|
|
344
377
|
if (e <= 0) return null;
|
|
345
|
-
let t =
|
|
346
|
-
return t == null ? null :
|
|
347
|
-
}),
|
|
348
|
-
if (!
|
|
349
|
-
let e =
|
|
378
|
+
let t = Be(l.value, u.value, Date.now(), e, f());
|
|
379
|
+
return t == null ? null : He(t);
|
|
380
|
+
}), k = n(() => {
|
|
381
|
+
if (!E.value) return null;
|
|
382
|
+
let e = O.value;
|
|
350
383
|
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
351
384
|
});
|
|
352
|
-
async function
|
|
353
|
-
let e =
|
|
385
|
+
async function A() {
|
|
386
|
+
let e = a.value?.uid, t = d();
|
|
354
387
|
if (!e || t <= 0) return {
|
|
355
388
|
effectiveCount: 0,
|
|
356
389
|
restricted: !1,
|
|
357
390
|
messageSuffix: ""
|
|
358
391
|
};
|
|
359
|
-
let n =
|
|
360
|
-
r.exists() ? await
|
|
361
|
-
moderationBlockCount:
|
|
362
|
-
moderationBlockAt:
|
|
363
|
-
updatedAt:
|
|
364
|
-
}) : await
|
|
392
|
+
let n = p(e), r = await h(n);
|
|
393
|
+
r.exists() ? await D(n, {
|
|
394
|
+
moderationBlockCount: _(1),
|
|
395
|
+
moderationBlockAt: w(),
|
|
396
|
+
updatedAt: w()
|
|
397
|
+
}) : await T(n, {
|
|
365
398
|
role: "user",
|
|
366
|
-
displayName:
|
|
399
|
+
displayName: a.value?.displayName ?? null,
|
|
367
400
|
moderationBlockCount: 1,
|
|
368
|
-
moderationBlockAt:
|
|
369
|
-
updatedAt:
|
|
401
|
+
moderationBlockAt: w(),
|
|
402
|
+
updatedAt: w()
|
|
370
403
|
});
|
|
371
|
-
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1,
|
|
372
|
-
|
|
373
|
-
let s =
|
|
404
|
+
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, o = Date.now();
|
|
405
|
+
l.value = i, u.value = o;
|
|
406
|
+
let s = ze(i, o, o, f()), c = Ve(i, o, o, t, f()), m = ` (경고 ${s}/${t})`;
|
|
374
407
|
if (c) {
|
|
375
|
-
let e =
|
|
376
|
-
|
|
408
|
+
let e = Be(i, o, o, t, f()), n = e == null ? null : He(e, o);
|
|
409
|
+
m = n ? ` 경고 ${s}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${s}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
377
410
|
}
|
|
378
411
|
return {
|
|
379
412
|
effectiveCount: s,
|
|
380
413
|
restricted: c,
|
|
381
|
-
messageSuffix:
|
|
414
|
+
messageSuffix: m
|
|
382
415
|
};
|
|
383
416
|
}
|
|
384
|
-
function
|
|
417
|
+
function M() {
|
|
385
418
|
if (!i) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
386
419
|
return i;
|
|
387
420
|
}
|
|
388
|
-
async function
|
|
389
|
-
return
|
|
421
|
+
async function N() {
|
|
422
|
+
return B(M(), new I());
|
|
390
423
|
}
|
|
391
|
-
async function
|
|
392
|
-
let e = new
|
|
393
|
-
return e.addScope("email"), e.addScope("name"),
|
|
424
|
+
async function V() {
|
|
425
|
+
let e = new L("apple.com");
|
|
426
|
+
return e.addScope("email"), e.addScope("name"), B(M(), e);
|
|
394
427
|
}
|
|
395
|
-
async function
|
|
396
|
-
return
|
|
428
|
+
async function H(e, t) {
|
|
429
|
+
return z(M(), e, t);
|
|
397
430
|
}
|
|
398
|
-
async function
|
|
399
|
-
let r = await
|
|
400
|
-
return n && await
|
|
431
|
+
async function U(e, t, n) {
|
|
432
|
+
let r = await R(M(), e, t);
|
|
433
|
+
return n && await te(r.user, { displayName: n }), r;
|
|
401
434
|
}
|
|
402
|
-
async function
|
|
403
|
-
await
|
|
435
|
+
async function ne() {
|
|
436
|
+
await ee(M());
|
|
404
437
|
}
|
|
405
438
|
function W(e) {
|
|
406
|
-
return
|
|
439
|
+
return y.value && (a.value?.uid === e.authorUid || S.value);
|
|
407
440
|
}
|
|
408
|
-
function
|
|
409
|
-
return
|
|
441
|
+
function re(e) {
|
|
442
|
+
return y.value && a.value?.uid === e.authorUid;
|
|
410
443
|
}
|
|
411
|
-
function
|
|
412
|
-
return
|
|
444
|
+
function ie(e) {
|
|
445
|
+
return y.value && (a.value?.uid === e.authorUid || S.value);
|
|
413
446
|
}
|
|
414
447
|
return {
|
|
415
|
-
user:
|
|
416
|
-
isSignedIn:
|
|
417
|
-
isAdmin:
|
|
418
|
-
isStaff:
|
|
419
|
-
canManageContent:
|
|
448
|
+
user: a,
|
|
449
|
+
isSignedIn: y,
|
|
450
|
+
isAdmin: b,
|
|
451
|
+
isStaff: x,
|
|
452
|
+
canManageContent: S,
|
|
420
453
|
rolePending: c,
|
|
421
|
-
moderationBlockCount:
|
|
422
|
-
isWriteRestricted:
|
|
423
|
-
restrictedUntilLabel:
|
|
424
|
-
restrictedMessage:
|
|
425
|
-
signInWithGoogle:
|
|
426
|
-
signInWithApple:
|
|
427
|
-
signInWithEmail:
|
|
428
|
-
signUpWithEmail:
|
|
429
|
-
signOut:
|
|
454
|
+
moderationBlockCount: C,
|
|
455
|
+
isWriteRestricted: E,
|
|
456
|
+
restrictedUntilLabel: O,
|
|
457
|
+
restrictedMessage: k,
|
|
458
|
+
signInWithGoogle: N,
|
|
459
|
+
signInWithApple: V,
|
|
460
|
+
signInWithEmail: H,
|
|
461
|
+
signUpWithEmail: U,
|
|
462
|
+
signOut: ne,
|
|
430
463
|
canEdit: W,
|
|
431
464
|
canDelete: W,
|
|
432
|
-
canEditComment:
|
|
433
|
-
canDeleteComment:
|
|
434
|
-
recordContentModerationBlock:
|
|
435
|
-
refreshBoardUser:
|
|
465
|
+
canEditComment: re,
|
|
466
|
+
canDeleteComment: ie,
|
|
467
|
+
recordContentModerationBlock: A,
|
|
468
|
+
refreshBoardUser: v
|
|
436
469
|
};
|
|
437
470
|
}
|
|
438
471
|
//#endregion
|
|
439
472
|
//#region src/utils/slugify.ts
|
|
440
|
-
function
|
|
473
|
+
function qe(e) {
|
|
441
474
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
442
475
|
}
|
|
443
|
-
function
|
|
476
|
+
function Je(e, t = 60) {
|
|
444
477
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
445
478
|
}
|
|
446
479
|
//#endregion
|
|
447
480
|
//#region src/utils/extractEditorImageUrls.ts
|
|
448
|
-
function
|
|
481
|
+
function Ye(e) {
|
|
449
482
|
if (!e) return [];
|
|
450
483
|
let t = /* @__PURE__ */ new Set();
|
|
451
484
|
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
@@ -454,7 +487,7 @@ function Ue(e) {
|
|
|
454
487
|
}
|
|
455
488
|
//#endregion
|
|
456
489
|
//#region src/utils/storagePath.ts
|
|
457
|
-
function
|
|
490
|
+
function Xe(e) {
|
|
458
491
|
try {
|
|
459
492
|
let t = e.match(/\/o\/([^?]+)/);
|
|
460
493
|
return t?.[1] ? decodeURIComponent(t[1]) : null;
|
|
@@ -462,50 +495,50 @@ function We(e) {
|
|
|
462
495
|
return null;
|
|
463
496
|
}
|
|
464
497
|
}
|
|
465
|
-
function
|
|
498
|
+
function Ze(e) {
|
|
466
499
|
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
467
500
|
}
|
|
468
501
|
//#endregion
|
|
469
502
|
//#region src/utils/postBody.ts
|
|
470
|
-
function
|
|
503
|
+
function Qe(e) {
|
|
471
504
|
return String(e || "").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<img\b[^>]*>/gi, " ").replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, " ").replace(/<video\b[\s\S]*?<\/video>/gi, " ").replace(/<audio\b[\s\S]*?<\/audio>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /gi, " ").replace(/ /g, " ").replace(/&[a-z]+;/gi, " ").replace(/&#\d+;/g, " ").replace(/[\u200B-\u200D\uFEFF\u00AD]/g, "").replace(/\s+/g, " ").trim();
|
|
472
505
|
}
|
|
473
|
-
function
|
|
474
|
-
let n =
|
|
506
|
+
function $e(e, t = 40) {
|
|
507
|
+
let n = Qe(e);
|
|
475
508
|
if (!n) return "";
|
|
476
509
|
if (n.length <= t) return n;
|
|
477
510
|
let r = n.slice(0, t), i = r.lastIndexOf(" ");
|
|
478
511
|
return `${(i >= Math.floor(t * .6) ? r.slice(0, i) : r).trimEnd()}…`;
|
|
479
512
|
}
|
|
480
|
-
function
|
|
481
|
-
return
|
|
513
|
+
function et(e) {
|
|
514
|
+
return Qe(e).length > 0;
|
|
482
515
|
}
|
|
483
|
-
function
|
|
516
|
+
function tt(e, t) {
|
|
484
517
|
return /<img\b/i.test(String(e || "")) ? !0 : (t || []).some((e) => !!e?.type?.startsWith("image/"));
|
|
485
518
|
}
|
|
486
|
-
function
|
|
487
|
-
return !
|
|
519
|
+
function nt(e) {
|
|
520
|
+
return !et(e);
|
|
488
521
|
}
|
|
489
522
|
//#endregion
|
|
490
523
|
//#region src/utils/postPreview.ts
|
|
491
|
-
function
|
|
524
|
+
function rt(e) {
|
|
492
525
|
return e.replace(/&/g, "&").replace(/"/g, "\"").replace(/'/g, "'");
|
|
493
526
|
}
|
|
494
|
-
function
|
|
527
|
+
function it(e) {
|
|
495
528
|
if (e) try {
|
|
496
|
-
let t = new URL(
|
|
529
|
+
let t = new URL(rt(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
|
|
497
530
|
if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
|
|
498
531
|
if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
|
|
499
532
|
} catch {
|
|
500
533
|
return;
|
|
501
534
|
}
|
|
502
535
|
}
|
|
503
|
-
function
|
|
504
|
-
let t =
|
|
536
|
+
function at(e) {
|
|
537
|
+
let t = it(e.videoUrl);
|
|
505
538
|
return t ? `https://i.ytimg.com/vi/${t}/hqdefault.jpg` : e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
|
|
506
539
|
}
|
|
507
|
-
function
|
|
508
|
-
let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ?
|
|
540
|
+
function ot(e, t = []) {
|
|
541
|
+
let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? rt(r[1]) : a, s = o ? it(o) : void 0, c = n ? rt(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
|
|
509
542
|
return {
|
|
510
543
|
summary: e.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/\s+/g, " ").trim().slice(0, 160),
|
|
511
544
|
...c ? { previewImage: c } : {},
|
|
@@ -514,7 +547,7 @@ function et(e, t = []) {
|
|
|
514
547
|
}
|
|
515
548
|
//#endregion
|
|
516
549
|
//#region src/composables/useMemiBoardSettings.ts
|
|
517
|
-
var
|
|
550
|
+
var st = [
|
|
518
551
|
{
|
|
519
552
|
id: "free",
|
|
520
553
|
label: "자유",
|
|
@@ -542,170 +575,200 @@ var tt = [
|
|
|
542
575
|
visibility: "public",
|
|
543
576
|
order: 2
|
|
544
577
|
}
|
|
545
|
-
];
|
|
546
|
-
function
|
|
578
|
+
], ct = st;
|
|
579
|
+
function lt(e) {
|
|
547
580
|
return e === "hidden" ? "hidden" : "public";
|
|
548
581
|
}
|
|
549
|
-
function
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
order: typeof
|
|
554
|
-
description:
|
|
555
|
-
visibility:
|
|
556
|
-
listView:
|
|
557
|
-
writeRole:
|
|
558
|
-
commentWriteRole:
|
|
559
|
-
allowedStaffUids:
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
582
|
+
function ut(e, t, n) {
|
|
583
|
+
return {
|
|
584
|
+
id: e,
|
|
585
|
+
label: typeof t.label == "string" && t.label.trim() ? t.label.trim() : e,
|
|
586
|
+
order: typeof t.order == "number" ? t.order : n,
|
|
587
|
+
description: (typeof t.description == "string" ? t.description : "") || "",
|
|
588
|
+
visibility: lt(t.visibility),
|
|
589
|
+
listView: t.listView ?? "default",
|
|
590
|
+
writeRole: t.writeRole ?? "user",
|
|
591
|
+
commentWriteRole: t.commentWriteRole ?? "user",
|
|
592
|
+
allowedStaffUids: Array.isArray(t.allowedStaffUids) ? t.allowedStaffUids : []
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function dt() {
|
|
596
|
+
let e = () => Z(), t = F(), { isSignedIn: r } = Ke(), i = n(() => S(fe(t, e()), x("order", "asc"))), { data: a, pending: o } = A(i, { ssrKey: ye(e(), "boards") }), s = n(() => a.value.map((e, t) => ut(e.id, e, t))), c = s, l = n(() => s.value.filter((e) => e.visibility !== "hidden")), d = l;
|
|
597
|
+
function f(e) {
|
|
598
|
+
if (e) return s.value.find((t) => t.id === e)?.label ?? e;
|
|
599
|
+
}
|
|
600
|
+
let p = f;
|
|
601
|
+
function m(e) {
|
|
602
|
+
if (e) return s.value.find((t) => t.id === e)?.description?.trim() || void 0;
|
|
563
603
|
}
|
|
604
|
+
let h = m;
|
|
564
605
|
function _(e) {
|
|
565
|
-
|
|
606
|
+
return e ? s.value.find((t) => t.id === e)?.visibility ?? "public" : "public";
|
|
566
607
|
}
|
|
608
|
+
let y = _;
|
|
567
609
|
function b(e) {
|
|
568
|
-
return e
|
|
610
|
+
return _(e) === "hidden";
|
|
569
611
|
}
|
|
570
|
-
|
|
571
|
-
|
|
612
|
+
let C = b;
|
|
613
|
+
function D(e) {
|
|
614
|
+
if (e) return s.value.find((t) => t.id === e);
|
|
572
615
|
}
|
|
573
|
-
async function O(
|
|
574
|
-
let i =
|
|
616
|
+
async function O(n, r) {
|
|
617
|
+
let i = K(t, e(), n), a;
|
|
575
618
|
for (;;) {
|
|
576
|
-
let
|
|
577
|
-
if (
|
|
578
|
-
let
|
|
579
|
-
for (let
|
|
580
|
-
if (await
|
|
619
|
+
let e = await g(S(i, x("__name__"), ...a ? [E(a)] : [], v(400)));
|
|
620
|
+
if (e.empty) break;
|
|
621
|
+
let n = k(t);
|
|
622
|
+
for (let t of e.docs) n.update(t.ref, { listed: r });
|
|
623
|
+
if (await n.commit(), a = e.docs[e.docs.length - 1], e.docs.length < 400) break;
|
|
581
624
|
}
|
|
582
625
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (!n || !o) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
587
|
-
let s = nt(e.visibility), c = f.value.find((e) => e.id === n)?.visibility ?? "public";
|
|
588
|
-
await S(i.value, { updatedAt: x() }, { merge: !0 }), await S(l(a.value, n), {
|
|
589
|
-
label: o,
|
|
626
|
+
function j(e, t) {
|
|
627
|
+
return {
|
|
628
|
+
label: e.label.trim(),
|
|
590
629
|
description: (e.description ?? "").trim(),
|
|
591
|
-
visibility:
|
|
630
|
+
visibility: lt(e.visibility),
|
|
592
631
|
listView: e.listView ?? "default",
|
|
593
632
|
writeRole: e.writeRole ?? "user",
|
|
594
633
|
commentWriteRole: e.commentWriteRole ?? "user",
|
|
595
634
|
allowedStaffUids: e.allowedStaffUids ?? [],
|
|
596
635
|
order: t,
|
|
597
|
-
updatedAt:
|
|
598
|
-
}
|
|
636
|
+
updatedAt: w()
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
async function M(n, i = n.order ?? 0) {
|
|
640
|
+
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
641
|
+
let a = n.id.trim(), o = n.label.trim();
|
|
642
|
+
if (!a || !o) throw Error("보드 ID와 이름을 입력해 주세요.");
|
|
643
|
+
let c = lt(n.visibility), l = s.value.find((e) => e.id === a)?.visibility ?? "public", u = j({
|
|
644
|
+
...n,
|
|
645
|
+
label: o,
|
|
646
|
+
visibility: c
|
|
647
|
+
}, i), d = e();
|
|
648
|
+
await T(pe(t, d, a), u, { merge: !0 }), await T(G(t, d, a), u, { merge: !0 }), (l !== c || c === "public") && await O(a, c !== "hidden");
|
|
599
649
|
}
|
|
600
|
-
|
|
650
|
+
let N = M;
|
|
651
|
+
async function P(n) {
|
|
601
652
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
602
|
-
let
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
let
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
visibility: c,
|
|
613
|
-
listView: e.listView ?? "default",
|
|
614
|
-
writeRole: e.writeRole ?? "user",
|
|
615
|
-
commentWriteRole: e.commentWriteRole ?? "user",
|
|
616
|
-
allowedStaffUids: e.allowedStaffUids ?? [],
|
|
617
|
-
order: n,
|
|
618
|
-
updatedAt: x()
|
|
619
|
-
}, { merge: !0 }), (t.get(r) ?? "public") !== c && s.push({
|
|
653
|
+
let i = new Map(s.value.map((e) => [e.id, e.visibility ?? "public"])), a = k(t), o = e(), c = [];
|
|
654
|
+
n.forEach((e, n) => {
|
|
655
|
+
let r = e.id.trim(), s = e.label.trim();
|
|
656
|
+
if (!r || !s) throw Error("보드 ID와 이름을 입력해 주세요.");
|
|
657
|
+
let l = lt(e.visibility), u = j({
|
|
658
|
+
...e,
|
|
659
|
+
label: s,
|
|
660
|
+
visibility: l
|
|
661
|
+
}, n);
|
|
662
|
+
a.set(pe(t, o, r), u, { merge: !0 }), a.set(G(t, o, r), u, { merge: !0 }), (i.get(r) ?? "public") !== l && c.push({
|
|
620
663
|
id: r,
|
|
621
|
-
listed:
|
|
664
|
+
listed: l !== "hidden"
|
|
622
665
|
});
|
|
623
|
-
}), await
|
|
624
|
-
for (let e of
|
|
666
|
+
}), await a.commit();
|
|
667
|
+
for (let e of c) await O(e.id, e.listed);
|
|
625
668
|
}
|
|
626
|
-
|
|
669
|
+
let I = P;
|
|
670
|
+
async function L(n) {
|
|
627
671
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
628
|
-
|
|
672
|
+
let i = e();
|
|
673
|
+
await u(G(t, i, n)).catch(() => {}), await u(pe(t, i, n));
|
|
629
674
|
}
|
|
630
|
-
|
|
675
|
+
let R = L;
|
|
676
|
+
async function z() {
|
|
631
677
|
return Promise.resolve();
|
|
632
678
|
}
|
|
633
|
-
async function
|
|
679
|
+
async function B(e) {
|
|
634
680
|
let t = e.trim();
|
|
635
|
-
if (!t) throw Error("
|
|
681
|
+
if (!t) throw Error("보드 이름을 입력해 주세요.");
|
|
636
682
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
637
|
-
let n =
|
|
683
|
+
let n = s.value.find((e) => e.label === t);
|
|
638
684
|
if (n) return n.id;
|
|
639
|
-
let i =
|
|
640
|
-
for (;
|
|
641
|
-
return await
|
|
685
|
+
let i = qe(t) || `board-${Date.now()}`, a = i, o = 2, c = new Set(s.value.map((e) => e.id));
|
|
686
|
+
for (; c.has(a);) a = `${i}-${o++}`;
|
|
687
|
+
return await M({
|
|
642
688
|
id: a,
|
|
643
689
|
label: t,
|
|
644
690
|
visibility: "public",
|
|
645
691
|
listView: "default",
|
|
646
692
|
writeRole: "user",
|
|
647
693
|
commentWriteRole: "user",
|
|
648
|
-
order:
|
|
694
|
+
order: s.value.length
|
|
649
695
|
}), a;
|
|
650
696
|
}
|
|
651
697
|
return {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
698
|
+
boards: s,
|
|
699
|
+
publicBoards: l,
|
|
700
|
+
boardLabel: f,
|
|
701
|
+
boardDescription: m,
|
|
702
|
+
boardVisibility: _,
|
|
703
|
+
isBoardHidden: b,
|
|
704
|
+
getBoard: D,
|
|
705
|
+
categories: c,
|
|
706
|
+
publicCategories: d,
|
|
707
|
+
categoryLabel: p,
|
|
708
|
+
categoryDescription: h,
|
|
709
|
+
categoryVisibility: y,
|
|
710
|
+
isCategoryHidden: C,
|
|
711
|
+
settingsPending: o,
|
|
712
|
+
ensureSettings: z,
|
|
713
|
+
addBoard: B,
|
|
714
|
+
addCategory: B,
|
|
715
|
+
saveBoard: M,
|
|
716
|
+
saveCategory: N,
|
|
717
|
+
saveBoards: P,
|
|
718
|
+
saveCategories: I,
|
|
719
|
+
deleteBoard: L,
|
|
720
|
+
deleteCategory: R,
|
|
721
|
+
DEFAULT_BOARDS: st,
|
|
722
|
+
DEFAULT_CATEGORIES: ct
|
|
665
723
|
};
|
|
666
724
|
}
|
|
667
725
|
//#endregion
|
|
668
726
|
//#region src/composables/useMemiBoardPosts.ts
|
|
669
|
-
async function
|
|
670
|
-
let t = await
|
|
727
|
+
async function ft(e) {
|
|
728
|
+
let t = await ne(e).catch(() => ({
|
|
671
729
|
items: [],
|
|
672
730
|
prefixes: []
|
|
673
731
|
}));
|
|
674
|
-
await Promise.all([...t.items.map((e) =>
|
|
732
|
+
await Promise.all([...t.items.map((e) => V(e)), ...t.prefixes.map((e) => ft(e))]);
|
|
675
733
|
}
|
|
676
|
-
async function
|
|
734
|
+
async function pt(e, t) {
|
|
677
735
|
let n = [...new Set([...t].filter(Boolean))];
|
|
678
|
-
await Promise.all(n.map((t) =>
|
|
736
|
+
await Promise.all(n.map((t) => V(W(e, t)).catch(() => {})));
|
|
679
737
|
}
|
|
680
|
-
function
|
|
738
|
+
function mt(e) {
|
|
681
739
|
if (!e || typeof e != "object") return !1;
|
|
682
740
|
let t = "code" in e ? String(e.code) : "";
|
|
683
741
|
return t === "permission-denied" || t === "firestore/permission-denied";
|
|
684
742
|
}
|
|
685
|
-
function
|
|
686
|
-
let
|
|
687
|
-
|
|
688
|
-
|
|
743
|
+
function ht(e) {
|
|
744
|
+
let t = o(e)?.trim();
|
|
745
|
+
if (!t) throw Error("[memi-board] boardId 가 필요합니다.");
|
|
746
|
+
return t;
|
|
747
|
+
}
|
|
748
|
+
function gt(e) {
|
|
749
|
+
let t = F(), n = N(), { isBoardHidden: r, getBoard: i } = dt(), a = () => Z(), o = () => ht(e), s = () => K(t, a(), o()), c = (e) => q(t, a(), o(), e), l = (e) => me(t, a(), o(), e), p = (e) => J(t, a(), o(), e);
|
|
750
|
+
function _() {
|
|
751
|
+
return !r(o());
|
|
689
752
|
}
|
|
690
|
-
function
|
|
691
|
-
return
|
|
753
|
+
function b() {
|
|
754
|
+
return i(o())?.listView === "image";
|
|
692
755
|
}
|
|
693
|
-
function
|
|
694
|
-
let t =
|
|
695
|
-
if (!
|
|
756
|
+
function C(e) {
|
|
757
|
+
let t = b();
|
|
758
|
+
if (!et(e.content)) throw Error(t ? "내용을 입력해 주세요." : "본문에 글자를 입력해 주세요. 이미지나 첨부만으로는 게시할 수 없습니다.");
|
|
696
759
|
if (t) {
|
|
697
|
-
if (!
|
|
760
|
+
if (!tt(e.content, e.attachments)) throw Error("사진을 올려 주세요.");
|
|
698
761
|
return;
|
|
699
762
|
}
|
|
700
763
|
if (!e.title?.trim()) throw Error("제목을 입력해 주세요.");
|
|
701
764
|
}
|
|
702
|
-
function
|
|
703
|
-
return
|
|
765
|
+
function A() {
|
|
766
|
+
return f(s()).id;
|
|
704
767
|
}
|
|
705
|
-
async function
|
|
706
|
-
let t = e.pageSize ?? 20, n = [];
|
|
707
|
-
e.
|
|
708
|
-
let r = await
|
|
768
|
+
async function j(e = {}) {
|
|
769
|
+
let t = e.pageSize ?? 20, n = [O("isPublished", "==", !0)];
|
|
770
|
+
e.publicOnly !== !1 && n.push(O("listed", "==", !0)), n.push(x("createdAt", "desc")), e.cursor && n.push(E(e.cursor)), n.push(v(t + 1));
|
|
771
|
+
let r = await g(S(s(), ...n)), i = r.docs.slice(0, t);
|
|
709
772
|
return {
|
|
710
773
|
posts: i.map((e) => ({
|
|
711
774
|
id: e.id,
|
|
@@ -715,48 +778,48 @@ function st() {
|
|
|
715
778
|
hasMore: r.docs.length > t
|
|
716
779
|
};
|
|
717
780
|
}
|
|
718
|
-
async function
|
|
719
|
-
let [t, n] = await Promise.all([
|
|
781
|
+
async function M(e) {
|
|
782
|
+
let [t, n] = await Promise.all([h(c(e)), h(l(e))]);
|
|
720
783
|
return t.exists() ? {
|
|
721
784
|
id: t.id,
|
|
722
785
|
...t.data(),
|
|
723
786
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
724
787
|
} : null;
|
|
725
788
|
}
|
|
726
|
-
async function
|
|
789
|
+
async function P(e) {
|
|
727
790
|
try {
|
|
728
|
-
let
|
|
729
|
-
if (!
|
|
791
|
+
let t = (await g(S(s(), O("slug", "==", e.trim()), v(1)))).docs[0];
|
|
792
|
+
if (!t) return { status: "not-found" };
|
|
730
793
|
try {
|
|
731
|
-
let e = await
|
|
794
|
+
let e = await M(t.id);
|
|
732
795
|
return e ? {
|
|
733
796
|
status: "ok",
|
|
734
797
|
post: e
|
|
735
798
|
} : { status: "not-found" };
|
|
736
799
|
} catch (e) {
|
|
737
|
-
return
|
|
800
|
+
return mt(e) ? { status: "permission-denied" } : {
|
|
738
801
|
status: "error",
|
|
739
802
|
message: e instanceof Error ? e.message : String(e)
|
|
740
803
|
};
|
|
741
804
|
}
|
|
742
805
|
} catch (e) {
|
|
743
|
-
return
|
|
806
|
+
return mt(e) ? { status: "permission-denied" } : {
|
|
744
807
|
status: "error",
|
|
745
808
|
message: e instanceof Error ? e.message : String(e)
|
|
746
809
|
};
|
|
747
810
|
}
|
|
748
811
|
}
|
|
749
812
|
async function I(e) {
|
|
750
|
-
if (!e.
|
|
813
|
+
if (!e.createdAt) return {
|
|
751
814
|
previous: null,
|
|
752
815
|
next: null
|
|
753
816
|
};
|
|
754
817
|
try {
|
|
755
818
|
let t = [
|
|
756
|
-
|
|
757
|
-
...e.listed === !1 ? [] : [
|
|
758
|
-
|
|
759
|
-
], [n, r] = await Promise.all([
|
|
819
|
+
O("isPublished", "==", !0),
|
|
820
|
+
...e.listed === !1 ? [] : [O("listed", "==", !0)],
|
|
821
|
+
x("createdAt", "desc")
|
|
822
|
+
], [n, r] = await Promise.all([g(S(s(), ...t, E(e.createdAt), v(1))), g(S(s(), ...t, m(e.createdAt), y(1)))]), i = (e) => {
|
|
760
823
|
let t = e.docs[0];
|
|
761
824
|
return t ? {
|
|
762
825
|
id: t.id,
|
|
@@ -775,451 +838,557 @@ function st() {
|
|
|
775
838
|
}
|
|
776
839
|
}
|
|
777
840
|
async function L(e) {
|
|
778
|
-
|
|
779
|
-
let t = e.postId ||
|
|
780
|
-
if (n) r = t, i =
|
|
841
|
+
C(e);
|
|
842
|
+
let t = e.postId || A(), n = b(), r, i;
|
|
843
|
+
if (n) r = t, i = $e(e.content);
|
|
781
844
|
else {
|
|
782
845
|
i = e.title.trim();
|
|
783
|
-
let t =
|
|
846
|
+
let t = qe(i) || "post";
|
|
784
847
|
r = t;
|
|
785
848
|
let n = 1;
|
|
786
|
-
for (; !(await
|
|
849
|
+
for (; !(await g(S(s(), O("slug", "==", r), v(1)))).empty;) n++, r = `${t}-${n}`;
|
|
787
850
|
}
|
|
788
|
-
let a =
|
|
789
|
-
return await
|
|
851
|
+
let a = ot(e.content, e.attachments), o = _();
|
|
852
|
+
return await T(c(t), {
|
|
790
853
|
slug: r,
|
|
791
854
|
title: i,
|
|
792
855
|
...a,
|
|
793
856
|
tags: e.tags ?? [],
|
|
794
|
-
...e.category ? { category: e.category } : {},
|
|
795
857
|
attachments: e.attachments ?? [],
|
|
796
858
|
commentCount: 0,
|
|
797
859
|
likeCount: 0,
|
|
798
860
|
viewCount: 0,
|
|
799
861
|
listed: o,
|
|
862
|
+
isPublished: !1,
|
|
800
863
|
authorUid: e.authorUid,
|
|
801
864
|
authorName: e.authorName,
|
|
802
865
|
authorPhoto: e.authorPhoto,
|
|
803
866
|
moderationStatus: "approved",
|
|
804
867
|
moderationModel: e.moderationModel ?? null,
|
|
805
|
-
createdAt:
|
|
806
|
-
updatedAt:
|
|
807
|
-
}), await
|
|
868
|
+
createdAt: w(),
|
|
869
|
+
updatedAt: w()
|
|
870
|
+
}), await T(l(t), { content: e.content }), r;
|
|
808
871
|
}
|
|
809
872
|
async function R(e, t) {
|
|
810
|
-
|
|
811
|
-
let n =
|
|
812
|
-
await Promise.all([
|
|
873
|
+
C(t);
|
|
874
|
+
let n = b() ? $e(t.content) : t.title.trim(), r = ot(t.content, t.attachments), i = _();
|
|
875
|
+
await Promise.all([D(c(e), {
|
|
813
876
|
title: n,
|
|
814
877
|
...r,
|
|
815
|
-
previewImage: r.previewImage ? r.previewImage :
|
|
816
|
-
videoUrl: r.videoUrl ? r.videoUrl :
|
|
878
|
+
previewImage: r.previewImage ? r.previewImage : d(),
|
|
879
|
+
videoUrl: r.videoUrl ? r.videoUrl : d(),
|
|
817
880
|
tags: t.tags ?? [],
|
|
818
|
-
category: t.category ? t.category : c(),
|
|
819
881
|
listed: i,
|
|
820
882
|
attachments: t.attachments ?? [],
|
|
821
|
-
updatedAt:
|
|
822
|
-
}),
|
|
883
|
+
updatedAt: w()
|
|
884
|
+
}), T(l(e), { content: t.content }, { merge: !0 })]);
|
|
823
885
|
}
|
|
824
886
|
async function z(e) {
|
|
825
|
-
|
|
887
|
+
await D(c(e), {
|
|
888
|
+
isPublished: !0,
|
|
889
|
+
publishedAt: w(),
|
|
890
|
+
updatedAt: w()
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
async function B(e) {
|
|
894
|
+
let [r, i] = await Promise.all([h(c(e)), h(l(e))]);
|
|
826
895
|
if (!r.exists()) return;
|
|
827
|
-
let
|
|
828
|
-
for (let e = 0; e <
|
|
829
|
-
let n =
|
|
830
|
-
|
|
896
|
+
let s = r.data(), d = i.exists() ? String(i.data().content ?? "") : "", f = await g(p(e));
|
|
897
|
+
for (let e = 0; e < f.docs.length; e += 450) {
|
|
898
|
+
let n = k(t);
|
|
899
|
+
f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
831
900
|
}
|
|
832
|
-
let
|
|
833
|
-
for (let t of
|
|
834
|
-
|
|
835
|
-
let n =
|
|
836
|
-
n && n !== e &&
|
|
901
|
+
let m = U(n), _ = /* @__PURE__ */ new Set(), v = /* @__PURE__ */ new Set();
|
|
902
|
+
for (let t of s.attachments ?? []) if (t?.path) {
|
|
903
|
+
_.add(t.path);
|
|
904
|
+
let n = Ze(t.path);
|
|
905
|
+
n && n !== e && v.add(n);
|
|
837
906
|
}
|
|
838
|
-
for (let t of
|
|
839
|
-
let n =
|
|
907
|
+
for (let t of Ye(d)) {
|
|
908
|
+
let n = Xe(t);
|
|
840
909
|
if (!n) continue;
|
|
841
|
-
|
|
910
|
+
_.add(n);
|
|
842
911
|
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
843
|
-
r !== n && r.includes("/images/thumbnails/") &&
|
|
912
|
+
r !== n && r.includes("/images/thumbnails/") && _.add(r);
|
|
844
913
|
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
845
|
-
i && !n.includes("/thumbnails/") &&
|
|
846
|
-
let a =
|
|
847
|
-
a && a !== e &&
|
|
914
|
+
i && !n.includes("/thumbnails/") && _.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
915
|
+
let a = Ze(n);
|
|
916
|
+
a && a !== e && v.add(a);
|
|
848
917
|
}
|
|
849
|
-
await
|
|
850
|
-
|
|
851
|
-
await
|
|
918
|
+
await u(l(e));
|
|
919
|
+
let y = o();
|
|
920
|
+
await ft(W(m, X(a(), y, e)));
|
|
921
|
+
for (let e of v) await ft(W(m, X(a(), y, e)));
|
|
922
|
+
await pt(m, _), await u(c(e));
|
|
852
923
|
}
|
|
853
924
|
return {
|
|
854
|
-
createPostId:
|
|
855
|
-
getPosts:
|
|
856
|
-
getPost:
|
|
857
|
-
getPostBySlug:
|
|
925
|
+
createPostId: A,
|
|
926
|
+
getPosts: j,
|
|
927
|
+
getPost: M,
|
|
928
|
+
getPostBySlug: P,
|
|
858
929
|
getAdjacentPosts: I,
|
|
859
930
|
createPost: L,
|
|
860
931
|
updatePost: R,
|
|
861
|
-
|
|
932
|
+
publishPost: z,
|
|
933
|
+
deletePost: B
|
|
862
934
|
};
|
|
863
935
|
}
|
|
864
|
-
var
|
|
865
|
-
function
|
|
866
|
-
let
|
|
867
|
-
function
|
|
936
|
+
var _t = 10;
|
|
937
|
+
function vt(e, t = {}) {
|
|
938
|
+
let i = F(), { isAdmin: o, isStaff: c, user: l } = Ke(), u = () => Z(), d = t.pageSize ?? _t, f = n(() => l.value?.uid ?? ""), m = n(() => ht(e)), h = () => K(i, u(), m.value), _ = n(() => t.publicOnly === void 0 ? !(o.value || c.value) : typeof t.publicOnly == "boolean" ? t.publicOnly : t.publicOnly.value), y = a([]), C = a([]), w = a([]), T = a(!0), D = a(!0), k = a(!1), A = a(""), j = null, M = null, N, P;
|
|
939
|
+
function I(e) {
|
|
868
940
|
return e.createdAt?.toMillis?.() ?? 0;
|
|
869
941
|
}
|
|
870
|
-
let
|
|
942
|
+
let L = n(() => {
|
|
871
943
|
let e = /* @__PURE__ */ new Map();
|
|
872
|
-
for (let t of [
|
|
873
|
-
|
|
944
|
+
for (let t of [
|
|
945
|
+
...C.value,
|
|
946
|
+
...y.value,
|
|
947
|
+
...w.value
|
|
948
|
+
]) t.id && e.set(t.id, t);
|
|
949
|
+
return [...e.values()].sort((e, t) => I(t) - I(e) || String(t.id).localeCompare(String(e.id)));
|
|
874
950
|
});
|
|
875
|
-
function
|
|
876
|
-
let e = [
|
|
877
|
-
return
|
|
951
|
+
function R() {
|
|
952
|
+
let e = [O("isPublished", "==", !0)];
|
|
953
|
+
return _.value && e.push(O("listed", "==", !0)), e;
|
|
954
|
+
}
|
|
955
|
+
function z() {
|
|
956
|
+
N?.(), N = b(S(h(), ...R(), x("createdAt", "desc"), x(p(), "desc"), v(d)), (e) => {
|
|
957
|
+
y.value = e.docs.map((e) => ({
|
|
958
|
+
id: e.id,
|
|
959
|
+
...e.data()
|
|
960
|
+
})), j = e.docs.at(-1) ?? null, M || (D.value = e.docs.length >= d), T.value = !1;
|
|
961
|
+
}, (e) => {
|
|
962
|
+
console.error("[memi-board:postList] onSnapshot failed", e), A.value = e.message || String(e), T.value = !1;
|
|
963
|
+
});
|
|
878
964
|
}
|
|
879
|
-
function
|
|
880
|
-
|
|
881
|
-
|
|
965
|
+
function B() {
|
|
966
|
+
P?.();
|
|
967
|
+
let e = f.value;
|
|
968
|
+
if (!e) {
|
|
969
|
+
w.value = [];
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
P = b(S(h(), O("authorUid", "==", e), O("isPublished", "==", !1), x("createdAt", "desc"), v(20)), (e) => {
|
|
973
|
+
w.value = e.docs.map((e) => ({
|
|
882
974
|
id: e.id,
|
|
883
975
|
...e.data()
|
|
884
|
-
}))
|
|
976
|
+
}));
|
|
885
977
|
}, (e) => {
|
|
886
|
-
console.error("[memi-board:postList] onSnapshot failed", e)
|
|
978
|
+
console.error("[memi-board:postList] own draft onSnapshot failed", e);
|
|
887
979
|
});
|
|
888
980
|
}
|
|
889
|
-
async function
|
|
890
|
-
if (!(
|
|
891
|
-
|
|
981
|
+
async function ee() {
|
|
982
|
+
if (!(k.value || !D.value || T.value)) {
|
|
983
|
+
k.value = !0, A.value = "";
|
|
892
984
|
try {
|
|
893
|
-
let e =
|
|
894
|
-
...
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
...e ? [
|
|
898
|
-
|
|
899
|
-
], n = await
|
|
985
|
+
let e = M ?? j, t = [
|
|
986
|
+
...R(),
|
|
987
|
+
x("createdAt", "desc"),
|
|
988
|
+
x(p(), "desc"),
|
|
989
|
+
...e ? [E(e)] : [],
|
|
990
|
+
v(d + 1)
|
|
991
|
+
], n = await g(S(h(), ...t)), r = n.docs.slice(0, d), i = r.map((e) => ({
|
|
900
992
|
id: e.id,
|
|
901
993
|
...e.data()
|
|
902
|
-
})), a = new Set([...
|
|
903
|
-
|
|
994
|
+
})), a = new Set([...C.value, ...y.value].map((e) => e.id));
|
|
995
|
+
C.value.push(...i.filter((e) => !a.has(e.id))), M = r.at(-1) ?? M, D.value = n.docs.length > d;
|
|
904
996
|
} catch (e) {
|
|
905
|
-
|
|
997
|
+
A.value = e.message || String(e), console.error("[memi-board:postList] loadMore failed", e);
|
|
906
998
|
} finally {
|
|
907
|
-
|
|
999
|
+
k.value = !1;
|
|
908
1000
|
}
|
|
909
1001
|
}
|
|
910
1002
|
}
|
|
911
|
-
function
|
|
912
|
-
N?.(),
|
|
913
|
-
}
|
|
914
|
-
return
|
|
915
|
-
|
|
916
|
-
|
|
1003
|
+
function te() {
|
|
1004
|
+
N?.(), y.value = [], C.value = [], j = null, M = null, D.value = !0, A.value = "", T.value = !0, z(), B();
|
|
1005
|
+
}
|
|
1006
|
+
return s([
|
|
1007
|
+
m,
|
|
1008
|
+
_,
|
|
1009
|
+
f
|
|
1010
|
+
], te, { immediate: !0 }), r(() => {
|
|
1011
|
+
N?.(), P?.();
|
|
1012
|
+
}), {
|
|
1013
|
+
posts: L,
|
|
1014
|
+
postsPending: T,
|
|
917
1015
|
hasMore: D,
|
|
918
|
-
loadingMore:
|
|
919
|
-
loadError:
|
|
920
|
-
loadMore:
|
|
1016
|
+
loadingMore: k,
|
|
1017
|
+
loadError: A,
|
|
1018
|
+
loadMore: ee
|
|
921
1019
|
};
|
|
922
1020
|
}
|
|
923
|
-
function
|
|
924
|
-
let
|
|
1021
|
+
function yt(e, t) {
|
|
1022
|
+
let r = F(), i = () => Z(), a = n(() => ht(e)), o = n(() => typeof t == "string" ? t : t.value), s = n(() => q(r, i(), a.value, o.value)), c = n(() => me(r, i(), a.value, o.value)), { data: l, pending: u } = M(s), { data: d, pending: f } = M(c);
|
|
925
1023
|
return {
|
|
926
|
-
post:
|
|
927
|
-
...
|
|
928
|
-
id:
|
|
1024
|
+
post: n(() => l.value ? {
|
|
1025
|
+
...l.value,
|
|
1026
|
+
id: o.value,
|
|
929
1027
|
content: d.value?.content ?? ""
|
|
930
1028
|
} : null),
|
|
931
|
-
pending:
|
|
1029
|
+
pending: n(() => u.value || f.value)
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
//#endregion
|
|
1033
|
+
//#region src/composables/useMemiBoardUserPosts.ts
|
|
1034
|
+
var bt = 10;
|
|
1035
|
+
function xt(e, t = {}) {
|
|
1036
|
+
let i = F(), c = t.pageSize ?? bt, u = n(() => o(e)?.trim() || ""), d = a([]), f = a([]), m = a(!0), h = a(!0), _ = a(!1), y = a(""), C = null, w = null, T;
|
|
1037
|
+
function D(e) {
|
|
1038
|
+
let t = e.ref.parent.parent?.id ?? "";
|
|
1039
|
+
return {
|
|
1040
|
+
id: e.id,
|
|
1041
|
+
boardId: t,
|
|
1042
|
+
...e.data()
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function k(e) {
|
|
1046
|
+
return e.createdAt?.toMillis?.() ?? 0;
|
|
1047
|
+
}
|
|
1048
|
+
let A = n(() => {
|
|
1049
|
+
let e = /* @__PURE__ */ new Map();
|
|
1050
|
+
for (let t of [...f.value, ...d.value]) t.id && e.set(t.id, t);
|
|
1051
|
+
return [...e.values()].sort((e, t) => k(t) - k(e) || String(t.id).localeCompare(String(e.id)));
|
|
1052
|
+
});
|
|
1053
|
+
function j() {
|
|
1054
|
+
return [
|
|
1055
|
+
O("authorUid", "==", u.value),
|
|
1056
|
+
O("isPublished", "==", !0),
|
|
1057
|
+
O("listed", "==", !0)
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
function M() {
|
|
1061
|
+
if (T?.(), !u.value) {
|
|
1062
|
+
m.value = !1;
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
T = b(S(l(i, "posts"), ...j(), x("createdAt", "desc"), x(p(), "desc"), v(c)), (e) => {
|
|
1066
|
+
d.value = e.docs.map(D), C = e.docs.at(-1) ?? null, w || (h.value = e.docs.length >= c), m.value = !1;
|
|
1067
|
+
}, (e) => {
|
|
1068
|
+
console.error("[memi-board:userPosts] onSnapshot failed", e), y.value = e.message || String(e), m.value = !1;
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
async function N() {
|
|
1072
|
+
if (!(_.value || !h.value || m.value || !u.value)) {
|
|
1073
|
+
_.value = !0, y.value = "";
|
|
1074
|
+
try {
|
|
1075
|
+
let e = w ?? C, t = [
|
|
1076
|
+
...j(),
|
|
1077
|
+
x("createdAt", "desc"),
|
|
1078
|
+
x(p(), "desc"),
|
|
1079
|
+
...e ? [E(e)] : [],
|
|
1080
|
+
v(c + 1)
|
|
1081
|
+
], n = await g(S(l(i, "posts"), ...t)), r = n.docs.slice(0, c), a = r.map(D), o = new Set([...f.value, ...d.value].map((e) => e.id));
|
|
1082
|
+
f.value.push(...a.filter((e) => !o.has(e.id))), w = r.at(-1) ?? w, h.value = n.docs.length > c;
|
|
1083
|
+
} catch (e) {
|
|
1084
|
+
y.value = e.message || String(e), console.error("[memi-board:userPosts] loadMore failed", e);
|
|
1085
|
+
} finally {
|
|
1086
|
+
_.value = !1;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function P() {
|
|
1091
|
+
T?.(), d.value = [], f.value = [], C = null, w = null, h.value = !0, y.value = "", m.value = !0, M();
|
|
1092
|
+
}
|
|
1093
|
+
return s(u, P, { immediate: !0 }), r(() => T?.()), {
|
|
1094
|
+
posts: A,
|
|
1095
|
+
postsPending: m,
|
|
1096
|
+
hasMore: h,
|
|
1097
|
+
loadingMore: _,
|
|
1098
|
+
loadError: y,
|
|
1099
|
+
loadMore: N
|
|
932
1100
|
};
|
|
933
1101
|
}
|
|
934
1102
|
//#endregion
|
|
935
1103
|
//#region src/composables/useMemiBoardComments.ts
|
|
936
|
-
var
|
|
937
|
-
function
|
|
1104
|
+
var St = 10, Ct = 5, wt = 1e3;
|
|
1105
|
+
function Tt(e) {
|
|
938
1106
|
let t = e.trim();
|
|
939
1107
|
if (!t) throw Error("댓글 내용을 입력해 주세요.");
|
|
940
|
-
if (t.length > 1e3) throw Error(`댓글은 ${
|
|
1108
|
+
if (t.length > 1e3) throw Error(`댓글은 ${wt.toLocaleString()}자까지 작성할 수 있습니다.`);
|
|
941
1109
|
return t;
|
|
942
1110
|
}
|
|
943
|
-
function
|
|
944
|
-
let
|
|
945
|
-
function
|
|
1111
|
+
function Et(e, t, i = {}) {
|
|
1112
|
+
let c = F(), l = () => Z(), u = n(() => o(e)), d = n(() => o(t)), m = a([]), h = a([]), y = a(i.subscribe !== !1), C = a([]), T = a(!1), A = a(i.subscribe !== !1), j = a(i.subscribe !== !1), M = null, N, P;
|
|
1113
|
+
function I(e) {
|
|
946
1114
|
return e.createdAt?.toMillis?.() ?? 0;
|
|
947
1115
|
}
|
|
948
|
-
let
|
|
1116
|
+
let L = n(() => {
|
|
949
1117
|
let e = /* @__PURE__ */ new Map();
|
|
950
1118
|
for (let t of [
|
|
951
|
-
...
|
|
952
|
-
...
|
|
953
|
-
...
|
|
1119
|
+
...m.value,
|
|
1120
|
+
...h.value,
|
|
1121
|
+
...C.value
|
|
954
1122
|
]) t.id && e.set(t.id, t);
|
|
955
|
-
return [...e.values()].sort((e, t) =>
|
|
1123
|
+
return [...e.values()].sort((e, t) => I(e) - I(t) || String(e.id).localeCompare(String(t.id)));
|
|
956
1124
|
});
|
|
957
|
-
|
|
958
|
-
if (
|
|
959
|
-
|
|
1125
|
+
s([d, u], async ([e]) => {
|
|
1126
|
+
if (C.value = [], m.value = [], h.value = [], M = null, clearTimeout(N), N = void 0, P?.(), P = void 0, A.value = i.subscribe !== !1, i.subscribe !== !1) {
|
|
1127
|
+
y.value = !0, j.value = !0;
|
|
960
1128
|
try {
|
|
961
|
-
let t =
|
|
1129
|
+
let t = g(S(J(c, l(), u.value, e), x("createdAt", "desc"), x(p(), "desc")));
|
|
962
1130
|
if (await Promise.all([t.then((t) => {
|
|
963
|
-
if (
|
|
1131
|
+
if (d.value !== e) return;
|
|
964
1132
|
let n = t.docs.map((e) => ({
|
|
965
1133
|
id: e.id,
|
|
966
1134
|
...e.data()
|
|
967
1135
|
}));
|
|
968
|
-
|
|
969
|
-
}),
|
|
1136
|
+
h.value = n.filter((e) => e.parentId === void 0);
|
|
1137
|
+
}), z()]), d.value !== e) return;
|
|
970
1138
|
} finally {
|
|
971
|
-
|
|
1139
|
+
d.value === e && (y.value = !1, j.value = !1);
|
|
972
1140
|
}
|
|
973
1141
|
}
|
|
974
1142
|
}, { immediate: !0 });
|
|
975
|
-
function
|
|
976
|
-
P?.(), P =
|
|
977
|
-
let t = new Map(
|
|
1143
|
+
function R() {
|
|
1144
|
+
P?.(), P = b(S(J(c, l(), u.value, d.value), O("parentId", "==", null), x("createdAt", "desc"), x(p(), "desc"), v(1)), (e) => {
|
|
1145
|
+
let t = new Map(m.value.map((e) => [e.id, e]));
|
|
978
1146
|
for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
|
|
979
1147
|
id: n.doc.id,
|
|
980
1148
|
...n.doc.data()
|
|
981
1149
|
});
|
|
982
|
-
|
|
1150
|
+
m.value = [...t.values()];
|
|
983
1151
|
}, (e) => {
|
|
984
1152
|
console.error("[memi-board:comments] onSnapshot failed", e);
|
|
985
1153
|
});
|
|
986
1154
|
}
|
|
987
|
-
async function
|
|
988
|
-
if (!(
|
|
989
|
-
|
|
1155
|
+
async function z() {
|
|
1156
|
+
if (!(T.value || !A.value)) {
|
|
1157
|
+
T.value = !0;
|
|
990
1158
|
try {
|
|
991
1159
|
let e = [
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
...
|
|
996
|
-
|
|
997
|
-
], t = await
|
|
1160
|
+
O("parentId", "==", null),
|
|
1161
|
+
x("createdAt", "asc"),
|
|
1162
|
+
x(p(), "asc"),
|
|
1163
|
+
...M ? [E(M)] : [],
|
|
1164
|
+
v(11)
|
|
1165
|
+
], t = await g(S(J(c, l(), u.value, d.value), ...e)), n = t.docs.slice(0, St), r = n.map((e) => ({
|
|
998
1166
|
id: e.id,
|
|
999
1167
|
...e.data()
|
|
1000
|
-
})), i = new Set(
|
|
1001
|
-
|
|
1002
|
-
N = void 0,
|
|
1168
|
+
})), i = new Set(C.value.map((e) => e.id));
|
|
1169
|
+
C.value.push(...r.filter((e) => !i.has(e.id))), M = n.at(-1) ?? M, t.size <= St && (A.value = !1, N = setTimeout(() => {
|
|
1170
|
+
N = void 0, R();
|
|
1003
1171
|
}, 1e3));
|
|
1004
1172
|
} finally {
|
|
1005
|
-
|
|
1173
|
+
T.value = !1;
|
|
1006
1174
|
}
|
|
1007
1175
|
}
|
|
1008
1176
|
}
|
|
1009
|
-
|
|
1177
|
+
r(() => {
|
|
1010
1178
|
clearTimeout(N), P?.();
|
|
1011
1179
|
});
|
|
1012
|
-
async function
|
|
1013
|
-
let t =
|
|
1014
|
-
n.set(
|
|
1015
|
-
postId:
|
|
1180
|
+
async function B(e) {
|
|
1181
|
+
let t = Tt(e.body), n = k(c), r = f(J(c, l(), u.value, d.value));
|
|
1182
|
+
n.set(r, {
|
|
1183
|
+
postId: d.value,
|
|
1016
1184
|
body: t,
|
|
1017
1185
|
authorUid: e.authorUid,
|
|
1018
1186
|
authorName: e.authorName,
|
|
1019
1187
|
authorPhoto: e.authorPhoto,
|
|
1020
1188
|
moderationStatus: "approved",
|
|
1021
1189
|
parentId: null,
|
|
1022
|
-
rootId:
|
|
1190
|
+
rootId: r.id,
|
|
1023
1191
|
depth: 0,
|
|
1024
1192
|
replyToUid: null,
|
|
1025
1193
|
replyToName: null,
|
|
1026
1194
|
replyCount: 0,
|
|
1027
1195
|
isReply: !1,
|
|
1028
|
-
createdAt:
|
|
1029
|
-
updatedAt:
|
|
1030
|
-
}), n.update(
|
|
1196
|
+
createdAt: w(),
|
|
1197
|
+
updatedAt: w()
|
|
1198
|
+
}), n.update(q(c, l(), u.value, d.value), { commentCount: _(1) }), await n.commit();
|
|
1031
1199
|
}
|
|
1032
|
-
async function
|
|
1033
|
-
let t =
|
|
1200
|
+
async function ee(e) {
|
|
1201
|
+
let t = Tt(e.body);
|
|
1034
1202
|
if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
|
|
1035
|
-
let n =
|
|
1036
|
-
if (!
|
|
1037
|
-
let
|
|
1038
|
-
n.set(
|
|
1039
|
-
postId:
|
|
1203
|
+
let n = k(c), r = J(c, l(), u.value, d.value), i = f(r), a = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
|
|
1204
|
+
if (!a) throw Error("댓글 스레드를 찾을 수 없습니다.");
|
|
1205
|
+
let o = Math.min((e.parent.depth ?? 0) + 1, 2);
|
|
1206
|
+
n.set(i, {
|
|
1207
|
+
postId: d.value,
|
|
1040
1208
|
body: t,
|
|
1041
1209
|
authorUid: e.authorUid,
|
|
1042
1210
|
authorName: e.authorName,
|
|
1043
1211
|
authorPhoto: e.authorPhoto,
|
|
1044
1212
|
moderationStatus: "approved",
|
|
1045
1213
|
parentId: e.parent.id,
|
|
1046
|
-
rootId:
|
|
1047
|
-
depth:
|
|
1214
|
+
rootId: a,
|
|
1215
|
+
depth: o,
|
|
1048
1216
|
replyToUid: e.parent.authorUid,
|
|
1049
1217
|
replyToName: e.parent.authorName,
|
|
1050
1218
|
replyCount: 0,
|
|
1051
1219
|
isReply: !0,
|
|
1052
|
-
createdAt:
|
|
1053
|
-
updatedAt:
|
|
1054
|
-
}), n.update(
|
|
1220
|
+
createdAt: w(),
|
|
1221
|
+
updatedAt: w()
|
|
1222
|
+
}), n.update(f(r, a), { replyCount: _(1) }), n.update(q(c, l(), u.value, d.value), { commentCount: _(1) }), await n.commit();
|
|
1055
1223
|
}
|
|
1056
|
-
async function
|
|
1224
|
+
async function te(e) {
|
|
1057
1225
|
if (!e.id) return;
|
|
1058
1226
|
if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error("답글이 있는 댓글은 현재 삭제할 수 없습니다.");
|
|
1059
|
-
let t =
|
|
1060
|
-
t.delete(
|
|
1227
|
+
let t = k(c);
|
|
1228
|
+
t.delete(Y(c, l(), u.value, d.value, e.id)), e.parentId && e.rootId && t.update(Y(c, l(), u.value, d.value, e.rootId), { replyCount: _(-1) }), t.update(q(c, l(), u.value, d.value), { commentCount: _(-1) }), await t.commit(), C.value = C.value.filter((t) => t.id !== e.id), h.value = h.value.filter((t) => t.id !== e.id), m.value = m.value.filter((t) => t.id !== e.id);
|
|
1061
1229
|
}
|
|
1062
|
-
async function
|
|
1063
|
-
let n =
|
|
1064
|
-
await
|
|
1230
|
+
async function V(e, t) {
|
|
1231
|
+
let n = Tt(t);
|
|
1232
|
+
await D(Y(c, l(), u.value, d.value, e), {
|
|
1065
1233
|
body: n,
|
|
1066
|
-
updatedAt:
|
|
1234
|
+
updatedAt: w()
|
|
1067
1235
|
});
|
|
1068
1236
|
}
|
|
1069
|
-
async function
|
|
1070
|
-
await
|
|
1237
|
+
async function H(e, t, n) {
|
|
1238
|
+
await D(Y(c, l(), u.value, d.value, e), {
|
|
1071
1239
|
isBlinded: t,
|
|
1072
|
-
moderatedAt:
|
|
1240
|
+
moderatedAt: w(),
|
|
1073
1241
|
moderatedBy: n
|
|
1074
1242
|
});
|
|
1075
1243
|
}
|
|
1076
1244
|
return {
|
|
1077
|
-
comments:
|
|
1078
|
-
commentsPending:
|
|
1079
|
-
hasMore:
|
|
1080
|
-
loadingMore:
|
|
1081
|
-
loadMore:
|
|
1082
|
-
addComment:
|
|
1083
|
-
addReply:
|
|
1084
|
-
updateComment:
|
|
1085
|
-
setCommentBlinded:
|
|
1086
|
-
deleteComment:
|
|
1245
|
+
comments: L,
|
|
1246
|
+
commentsPending: n(() => j.value || y.value),
|
|
1247
|
+
hasMore: A,
|
|
1248
|
+
loadingMore: T,
|
|
1249
|
+
loadMore: z,
|
|
1250
|
+
addComment: B,
|
|
1251
|
+
addReply: ee,
|
|
1252
|
+
updateComment: V,
|
|
1253
|
+
setCommentBlinded: H,
|
|
1254
|
+
deleteComment: te
|
|
1087
1255
|
};
|
|
1088
1256
|
}
|
|
1089
|
-
function
|
|
1090
|
-
let i =
|
|
1091
|
-
function
|
|
1257
|
+
function Dt(e, t, n) {
|
|
1258
|
+
let i = F(), o = () => Z(), s = a([]), c = a(!1), l = a(!1), u = a(!0), d = null, f;
|
|
1259
|
+
function m(e) {
|
|
1092
1260
|
return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
|
|
1093
1261
|
}
|
|
1094
|
-
function
|
|
1095
|
-
|
|
1262
|
+
function h() {
|
|
1263
|
+
f ||= b(S(J(i, o(), e, t), O("rootId", "==", n), O("isReply", "==", !0), x("createdAt", "desc"), x(p(), "desc"), v(1)), (e) => {
|
|
1096
1264
|
let t = new Map(s.value.map((e) => [e.id, e]));
|
|
1097
1265
|
for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
|
|
1098
1266
|
id: n.doc.id,
|
|
1099
1267
|
...n.doc.data()
|
|
1100
1268
|
});
|
|
1101
|
-
s.value =
|
|
1269
|
+
s.value = m([...t.values()]);
|
|
1102
1270
|
}, (e) => {
|
|
1103
1271
|
console.error("[memi-board:replies] onSnapshot failed", e);
|
|
1104
1272
|
});
|
|
1105
1273
|
}
|
|
1106
|
-
async function
|
|
1107
|
-
if (!(c.value || !
|
|
1274
|
+
async function _() {
|
|
1275
|
+
if (!(c.value || !u.value)) {
|
|
1108
1276
|
c.value = !0;
|
|
1109
1277
|
try {
|
|
1110
|
-
let
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
...
|
|
1116
|
-
|
|
1117
|
-
],
|
|
1278
|
+
let r = [
|
|
1279
|
+
O("rootId", "==", n),
|
|
1280
|
+
O("isReply", "==", !0),
|
|
1281
|
+
x("createdAt", "asc"),
|
|
1282
|
+
x(p(), "asc"),
|
|
1283
|
+
...d ? [E(d)] : [],
|
|
1284
|
+
v(6)
|
|
1285
|
+
], a = await g(S(J(i, o(), e, t), ...r)), c = new Set(s.value.map((e) => e.id)), f = a.docs.slice(0, Ct), _ = f.map((e) => ({
|
|
1118
1286
|
id: e.id,
|
|
1119
1287
|
...e.data()
|
|
1120
1288
|
})).filter((e) => !c.has(e.id));
|
|
1121
|
-
s.value =
|
|
1289
|
+
s.value = m([...s.value, ..._]), d = f.at(-1) ?? d, l.value = !0, a.size <= Ct && (u.value = !1, h());
|
|
1122
1290
|
} finally {
|
|
1123
1291
|
c.value = !1;
|
|
1124
1292
|
}
|
|
1125
1293
|
}
|
|
1126
1294
|
}
|
|
1127
|
-
function
|
|
1128
|
-
|
|
1295
|
+
function y() {
|
|
1296
|
+
f?.(), f = void 0, s.value = [], d = null, l.value = !1, u.value = !0;
|
|
1129
1297
|
}
|
|
1130
|
-
async function
|
|
1131
|
-
|
|
1298
|
+
async function C() {
|
|
1299
|
+
y(), await _();
|
|
1132
1300
|
}
|
|
1133
|
-
return
|
|
1301
|
+
return r(() => f?.()), {
|
|
1134
1302
|
replies: s,
|
|
1135
1303
|
loading: c,
|
|
1136
1304
|
loaded: l,
|
|
1137
|
-
hasMore:
|
|
1138
|
-
loadMore:
|
|
1139
|
-
refresh:
|
|
1305
|
+
hasMore: u,
|
|
1306
|
+
loadMore: _,
|
|
1307
|
+
refresh: C
|
|
1140
1308
|
};
|
|
1141
1309
|
}
|
|
1142
1310
|
//#endregion
|
|
1143
1311
|
//#region src/composables/useMemiBoardLikes.ts
|
|
1144
|
-
function
|
|
1145
|
-
let n =
|
|
1146
|
-
function
|
|
1147
|
-
return
|
|
1312
|
+
function Ot(e, t) {
|
|
1313
|
+
let n = () => Z(), i = F(), o = j(), c = a(!1), l = a(!1), u;
|
|
1314
|
+
function d(r) {
|
|
1315
|
+
return he(i, n(), e, t, r);
|
|
1148
1316
|
}
|
|
1149
|
-
|
|
1150
|
-
if (
|
|
1317
|
+
s(o, (e) => {
|
|
1318
|
+
if (u?.(), u = void 0, !e) {
|
|
1151
1319
|
c.value = !1;
|
|
1152
1320
|
return;
|
|
1153
1321
|
}
|
|
1154
|
-
|
|
1322
|
+
u = b(d(e.uid), (e) => {
|
|
1155
1323
|
c.value = e.exists();
|
|
1156
1324
|
}, (e) => {
|
|
1157
1325
|
console.error("[memi-board:likes] onSnapshot failed", e);
|
|
1158
1326
|
});
|
|
1159
|
-
}, { immediate: !0 }),
|
|
1160
|
-
async function
|
|
1161
|
-
let
|
|
1162
|
-
if (!
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1327
|
+
}, { immediate: !0 }), r(() => u?.());
|
|
1328
|
+
async function f() {
|
|
1329
|
+
let r = o.value?.uid;
|
|
1330
|
+
if (!r) throw Error("로그인이 필요합니다.");
|
|
1331
|
+
if (l.value) return c.value;
|
|
1332
|
+
l.value = !0;
|
|
1165
1333
|
try {
|
|
1166
|
-
let
|
|
1167
|
-
return await
|
|
1168
|
-
uid:
|
|
1169
|
-
createdAt:
|
|
1170
|
-
}), e.update(
|
|
1334
|
+
let a = d(r), o = q(i, n(), e, t);
|
|
1335
|
+
return await C(i, async (e) => (await e.get(a)).exists() ? (e.delete(a), e.update(o, { likeCount: _(-1) }), !1) : (e.set(a, {
|
|
1336
|
+
uid: r,
|
|
1337
|
+
createdAt: w()
|
|
1338
|
+
}), e.update(o, { likeCount: _(1) }), !0));
|
|
1171
1339
|
} finally {
|
|
1172
|
-
|
|
1340
|
+
l.value = !1;
|
|
1173
1341
|
}
|
|
1174
1342
|
}
|
|
1175
1343
|
return {
|
|
1176
1344
|
isLiked: c,
|
|
1177
|
-
likePending:
|
|
1178
|
-
toggleLike:
|
|
1345
|
+
likePending: l,
|
|
1346
|
+
toggleLike: f
|
|
1179
1347
|
};
|
|
1180
1348
|
}
|
|
1181
1349
|
//#endregion
|
|
1182
1350
|
//#region src/composables/useMemiBoardViews.ts
|
|
1183
|
-
function
|
|
1184
|
-
let e =
|
|
1185
|
-
function
|
|
1186
|
-
|
|
1351
|
+
function kt() {
|
|
1352
|
+
let e = () => Z(), t = F();
|
|
1353
|
+
function n(t, n) {
|
|
1354
|
+
let { boardsCollection: r } = e();
|
|
1355
|
+
return `memi-board:viewed:${r}:${t}:${n}`;
|
|
1187
1356
|
}
|
|
1188
|
-
function
|
|
1357
|
+
function r(e, t) {
|
|
1189
1358
|
if (typeof sessionStorage > "u") return !1;
|
|
1190
1359
|
try {
|
|
1191
|
-
return sessionStorage.getItem(
|
|
1360
|
+
return sessionStorage.getItem(n(e, t)) === "1";
|
|
1192
1361
|
} catch {
|
|
1193
1362
|
return !1;
|
|
1194
1363
|
}
|
|
1195
1364
|
}
|
|
1196
|
-
function
|
|
1365
|
+
function i(e, t) {
|
|
1197
1366
|
if (!(typeof sessionStorage > "u")) try {
|
|
1198
|
-
sessionStorage.setItem(
|
|
1367
|
+
sessionStorage.setItem(n(e, t), "1");
|
|
1199
1368
|
} catch {}
|
|
1200
1369
|
}
|
|
1201
|
-
async function o
|
|
1202
|
-
let
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1370
|
+
async function a(a, o) {
|
|
1371
|
+
let s = a?.trim(), c = o?.trim();
|
|
1372
|
+
if (!(!s || !c) && !r(s, c)) {
|
|
1373
|
+
i(s, c);
|
|
1205
1374
|
try {
|
|
1206
|
-
await
|
|
1375
|
+
await D(q(t, e(), s, c), { viewCount: _(1) });
|
|
1207
1376
|
} catch (e) {
|
|
1208
1377
|
try {
|
|
1209
|
-
sessionStorage.removeItem(
|
|
1378
|
+
sessionStorage.removeItem(n(s, c));
|
|
1210
1379
|
} catch {}
|
|
1211
1380
|
import.meta.dev && console.warn("[memi-board] recordView failed", e);
|
|
1212
1381
|
}
|
|
1213
1382
|
}
|
|
1214
1383
|
}
|
|
1215
1384
|
return {
|
|
1216
|
-
recordView:
|
|
1217
|
-
hasRecordedView:
|
|
1385
|
+
recordView: a,
|
|
1386
|
+
hasRecordedView: r
|
|
1218
1387
|
};
|
|
1219
1388
|
}
|
|
1220
1389
|
//#endregion
|
|
1221
1390
|
//#region src/utils/moderation-prompt.ts
|
|
1222
|
-
var
|
|
1391
|
+
var At = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", jt = {
|
|
1223
1392
|
abuse: "욕설·비속어·모욕",
|
|
1224
1393
|
spam: "스팸·광고",
|
|
1225
1394
|
adult: "선정적·성인 내용",
|
|
@@ -1227,17 +1396,17 @@ var yt = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
|
|
|
1227
1396
|
other: "부적절한 내용",
|
|
1228
1397
|
none: ""
|
|
1229
1398
|
};
|
|
1230
|
-
function
|
|
1399
|
+
function Mt(e) {
|
|
1231
1400
|
if (e.via === "local" && e.localHit) {
|
|
1232
1401
|
let t = e.localHit.trim();
|
|
1233
1402
|
return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
|
|
1234
1403
|
}
|
|
1235
1404
|
let t = (e.reason || "").trim();
|
|
1236
1405
|
if (t) return t;
|
|
1237
|
-
let n = e.category && e.category !== "none" ?
|
|
1406
|
+
let n = e.category && e.category !== "none" ? jt[e.category] || e.category : "";
|
|
1238
1407
|
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
1239
1408
|
}
|
|
1240
|
-
var
|
|
1409
|
+
var Nt = [
|
|
1241
1410
|
"씨발",
|
|
1242
1411
|
"시발",
|
|
1243
1412
|
"시이발",
|
|
@@ -1263,10 +1432,10 @@ var xt = [
|
|
|
1263
1432
|
"bitch",
|
|
1264
1433
|
"asshole"
|
|
1265
1434
|
];
|
|
1266
|
-
function
|
|
1435
|
+
function Pt(e) {
|
|
1267
1436
|
return new RegExp(e.join("|"), "i");
|
|
1268
1437
|
}
|
|
1269
|
-
var
|
|
1438
|
+
var Ft = /* @__PURE__ */ new Set([
|
|
1270
1439
|
"none",
|
|
1271
1440
|
"abuse",
|
|
1272
1441
|
"spam",
|
|
@@ -1274,7 +1443,7 @@ var Ct = /* @__PURE__ */ new Set([
|
|
|
1274
1443
|
"violence",
|
|
1275
1444
|
"other"
|
|
1276
1445
|
]);
|
|
1277
|
-
function
|
|
1446
|
+
function It(e) {
|
|
1278
1447
|
let t = e.trim();
|
|
1279
1448
|
if (!t) return null;
|
|
1280
1449
|
let n;
|
|
@@ -1292,7 +1461,7 @@ function wt(e) {
|
|
|
1292
1461
|
if (typeof n != "object" || !n) return null;
|
|
1293
1462
|
let r = n;
|
|
1294
1463
|
if (typeof r.flagged != "boolean") return null;
|
|
1295
|
-
let i = typeof r.category == "string" &&
|
|
1464
|
+
let i = typeof r.category == "string" && Ft.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
1296
1465
|
return {
|
|
1297
1466
|
flagged: r.flagged,
|
|
1298
1467
|
category: i,
|
|
@@ -1301,20 +1470,20 @@ function wt(e) {
|
|
|
1301
1470
|
}
|
|
1302
1471
|
//#endregion
|
|
1303
1472
|
//#region src/composables/useMemiBoardModeration.ts
|
|
1304
|
-
var
|
|
1473
|
+
var Lt = {
|
|
1305
1474
|
flagged: !1,
|
|
1306
1475
|
category: "none",
|
|
1307
1476
|
reason: "",
|
|
1308
1477
|
via: "empty"
|
|
1309
1478
|
};
|
|
1310
|
-
function
|
|
1311
|
-
let e =
|
|
1479
|
+
function Rt() {
|
|
1480
|
+
let e = Ce().moderation ?? {}, t = N(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = Ke(), a = Pt([...Nt, ...e.localBlocklist ?? []]);
|
|
1312
1481
|
function o(e) {
|
|
1313
1482
|
let t = e.match(a);
|
|
1314
1483
|
return t ? {
|
|
1315
1484
|
flagged: !0,
|
|
1316
1485
|
category: "abuse",
|
|
1317
|
-
reason:
|
|
1486
|
+
reason: Mt({
|
|
1318
1487
|
via: "local",
|
|
1319
1488
|
localHit: t[0] || "",
|
|
1320
1489
|
category: "abuse"
|
|
@@ -1323,21 +1492,21 @@ function Et() {
|
|
|
1323
1492
|
} : null;
|
|
1324
1493
|
}
|
|
1325
1494
|
function s() {
|
|
1326
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
1327
|
-
backend: new
|
|
1495
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
|
|
1496
|
+
backend: new ae(),
|
|
1328
1497
|
useLimitedUseAppCheckTokens: r
|
|
1329
1498
|
});
|
|
1330
|
-
return
|
|
1499
|
+
return ce(i, {
|
|
1331
1500
|
model: n,
|
|
1332
|
-
systemInstruction:
|
|
1501
|
+
systemInstruction: At,
|
|
1333
1502
|
generationConfig: {
|
|
1334
1503
|
temperature: 0,
|
|
1335
1504
|
maxOutputTokens: 256,
|
|
1336
1505
|
responseMimeType: "application/json",
|
|
1337
|
-
responseSchema:
|
|
1338
|
-
flagged:
|
|
1339
|
-
category:
|
|
1340
|
-
reason:
|
|
1506
|
+
responseSchema: oe.object({ properties: {
|
|
1507
|
+
flagged: oe.boolean(),
|
|
1508
|
+
category: oe.string(),
|
|
1509
|
+
reason: oe.string()
|
|
1341
1510
|
} })
|
|
1342
1511
|
}
|
|
1343
1512
|
});
|
|
@@ -1358,7 +1527,7 @@ function Et() {
|
|
|
1358
1527
|
};
|
|
1359
1528
|
}
|
|
1360
1529
|
function l(e, t = "") {
|
|
1361
|
-
let n =
|
|
1530
|
+
let n = Mt({
|
|
1362
1531
|
reason: e.reason,
|
|
1363
1532
|
category: e.category,
|
|
1364
1533
|
via: e.via
|
|
@@ -1386,22 +1555,22 @@ function Et() {
|
|
|
1386
1555
|
};
|
|
1387
1556
|
let i = t.trim();
|
|
1388
1557
|
if (!i) return {
|
|
1389
|
-
...
|
|
1558
|
+
...Lt,
|
|
1390
1559
|
via: "empty"
|
|
1391
1560
|
};
|
|
1392
1561
|
let a = o(i);
|
|
1393
1562
|
if (a) return u(a);
|
|
1394
1563
|
if (e.enabled === !1) return {
|
|
1395
|
-
...
|
|
1564
|
+
...Lt,
|
|
1396
1565
|
via: "disabled"
|
|
1397
1566
|
};
|
|
1398
1567
|
try {
|
|
1399
|
-
let e =
|
|
1568
|
+
let e = It((await s().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
1400
1569
|
if (!e) return c();
|
|
1401
1570
|
let t = {
|
|
1402
1571
|
flagged: e.flagged,
|
|
1403
1572
|
category: e.category,
|
|
1404
|
-
reason:
|
|
1573
|
+
reason: Mt({
|
|
1405
1574
|
reason: e.reason,
|
|
1406
1575
|
category: e.category,
|
|
1407
1576
|
via: "ai"
|
|
@@ -1421,14 +1590,14 @@ function Et() {
|
|
|
1421
1590
|
via: "restricted"
|
|
1422
1591
|
};
|
|
1423
1592
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
1424
|
-
...
|
|
1593
|
+
...Lt,
|
|
1425
1594
|
via: "disabled"
|
|
1426
1595
|
};
|
|
1427
1596
|
try {
|
|
1428
1597
|
let e = await new Promise((e, n) => {
|
|
1429
1598
|
let r = new FileReader();
|
|
1430
1599
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
1431
|
-
}), n =
|
|
1600
|
+
}), n = It((await s().generateContent([{ inlineData: {
|
|
1432
1601
|
mimeType: t.type || "image/jpeg",
|
|
1433
1602
|
data: e
|
|
1434
1603
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -1436,7 +1605,7 @@ function Et() {
|
|
|
1436
1605
|
let r = {
|
|
1437
1606
|
flagged: n.flagged,
|
|
1438
1607
|
category: n.category,
|
|
1439
|
-
reason:
|
|
1608
|
+
reason: Mt({
|
|
1440
1609
|
reason: n.reason,
|
|
1441
1610
|
category: n.category,
|
|
1442
1611
|
via: "ai"
|
|
@@ -1455,7 +1624,7 @@ function Et() {
|
|
|
1455
1624
|
}
|
|
1456
1625
|
//#endregion
|
|
1457
1626
|
//#region src/utils/compressImage.ts
|
|
1458
|
-
async function
|
|
1627
|
+
async function zt(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
1459
1628
|
return new Promise((r, i) => {
|
|
1460
1629
|
let a = new Image(), o = URL.createObjectURL(e);
|
|
1461
1630
|
a.onload = () => {
|
|
@@ -1475,88 +1644,124 @@ async function X(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
|
1475
1644
|
}
|
|
1476
1645
|
//#endregion
|
|
1477
1646
|
//#region src/composables/useMemiBoardStorage.ts
|
|
1478
|
-
var
|
|
1479
|
-
|
|
1480
|
-
|
|
1647
|
+
var Bt = 5242880, Vt = 26214400;
|
|
1648
|
+
function Ht(e) {
|
|
1649
|
+
let t = (e.type || "").toLowerCase(), n = e.name.toLowerCase();
|
|
1650
|
+
return t === "image/heic" || t === "image/heif" || n.endsWith(".heic") || n.endsWith(".heif");
|
|
1651
|
+
}
|
|
1652
|
+
async function Ut(t, n = .85) {
|
|
1653
|
+
let { default: r } = await import("./heic2any-BXb_U1zK.js").then((t) => /* @__PURE__ */ e(t.default, 1)), i = await r({
|
|
1654
|
+
blob: t,
|
|
1655
|
+
toType: "image/jpeg",
|
|
1656
|
+
quality: n
|
|
1657
|
+
}), a = Array.isArray(i) ? i[0] : i;
|
|
1658
|
+
if (!(a instanceof Blob)) throw Error("HEIC 변환 결과가 비어 있습니다.");
|
|
1659
|
+
return a;
|
|
1660
|
+
}
|
|
1661
|
+
async function Wt(e) {
|
|
1662
|
+
if (e.size <= 5242880) return e;
|
|
1663
|
+
let t = await zt(e, {
|
|
1664
|
+
maxWidth: 2560,
|
|
1665
|
+
quality: .85
|
|
1666
|
+
});
|
|
1667
|
+
if (t.size > 5242880 && (t = await zt(e, {
|
|
1668
|
+
maxWidth: 2048,
|
|
1669
|
+
quality: .75
|
|
1670
|
+
})), t.size > 5242880) throw Error("압축 후에도 이미지가 5MB를 초과합니다. 더 작은 이미지를 선택해 주세요.");
|
|
1671
|
+
return t;
|
|
1672
|
+
}
|
|
1673
|
+
async function Gt(e) {
|
|
1674
|
+
if (e.size > 26214400) throw Error(`원본 이미지는 ${Vt / 1024 / 1024}MB 이하여야 합니다.`);
|
|
1675
|
+
if (Ht(e)) try {
|
|
1676
|
+
return {
|
|
1677
|
+
blob: await Wt(await Ut(e, .85)),
|
|
1678
|
+
compressed: !0
|
|
1679
|
+
};
|
|
1680
|
+
} catch (t) {
|
|
1681
|
+
if (t instanceof Error && t.message.includes("압축 후에도")) throw t;
|
|
1682
|
+
try {
|
|
1683
|
+
return {
|
|
1684
|
+
blob: await Wt(await zt(e, {
|
|
1685
|
+
maxWidth: 2560,
|
|
1686
|
+
quality: .85
|
|
1687
|
+
})),
|
|
1688
|
+
compressed: !0
|
|
1689
|
+
};
|
|
1690
|
+
} catch {
|
|
1691
|
+
throw console.error("[memi-board] HEIC convert failed", t), Error("HEIC 사진을 변환하지 못했습니다. 잠시 후 다시 시도하거나 JPG로 저장해 올려 주세요.");
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1481
1694
|
if (e.size <= 5242880) return {
|
|
1482
1695
|
blob: e,
|
|
1483
1696
|
compressed: !1
|
|
1484
1697
|
};
|
|
1485
1698
|
try {
|
|
1486
|
-
let t = await X(e, {
|
|
1487
|
-
maxWidth: 2560,
|
|
1488
|
-
quality: .85
|
|
1489
|
-
});
|
|
1490
|
-
if (t.size > 5242880 && (t = await X(e, {
|
|
1491
|
-
maxWidth: 2048,
|
|
1492
|
-
quality: .75
|
|
1493
|
-
})), t.size > 5242880) throw Error("압축 후에도 이미지가 5MB를 초과합니다. 더 작은 이미지를 선택해 주세요.");
|
|
1494
1699
|
return {
|
|
1495
|
-
blob:
|
|
1700
|
+
blob: await Wt(e),
|
|
1496
1701
|
compressed: !0
|
|
1497
1702
|
};
|
|
1498
1703
|
} catch (e) {
|
|
1499
1704
|
throw e instanceof Error && e.message.includes("압축 후에도") ? e : Error("이 이미지 형식은 브라우저에서 최적화할 수 없습니다. JPG, PNG 또는 WebP로 변환해 주세요.");
|
|
1500
1705
|
}
|
|
1501
1706
|
}
|
|
1502
|
-
function
|
|
1503
|
-
let e =
|
|
1504
|
-
function
|
|
1505
|
-
let
|
|
1506
|
-
contentType:
|
|
1507
|
-
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(
|
|
1707
|
+
function Kt() {
|
|
1708
|
+
let e = () => Z(), t = N();
|
|
1709
|
+
function n(n, r, i, a) {
|
|
1710
|
+
let o = U(t), s = `${X(e(), r, i)}/attachments/${Date.now()}-${Je(n.name)}`, c = W(o, s), l = ie(c, n, {
|
|
1711
|
+
contentType: n.type || "application/octet-stream",
|
|
1712
|
+
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(n.name)}`
|
|
1508
1713
|
});
|
|
1509
1714
|
return {
|
|
1510
|
-
promise: new Promise((
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
},
|
|
1514
|
-
let
|
|
1515
|
-
|
|
1516
|
-
name:
|
|
1517
|
-
url:
|
|
1518
|
-
path:
|
|
1519
|
-
size:
|
|
1520
|
-
type:
|
|
1715
|
+
promise: new Promise((e, t) => {
|
|
1716
|
+
l.on("state_changed", (e) => {
|
|
1717
|
+
a && e.totalBytes > 0 && a(e.bytesTransferred / e.totalBytes);
|
|
1718
|
+
}, t, async () => {
|
|
1719
|
+
let t = await H(c);
|
|
1720
|
+
e({
|
|
1721
|
+
name: n.name,
|
|
1722
|
+
url: t,
|
|
1723
|
+
path: s,
|
|
1724
|
+
size: n.size,
|
|
1725
|
+
type: n.type
|
|
1521
1726
|
});
|
|
1522
1727
|
});
|
|
1523
1728
|
}),
|
|
1524
|
-
cancel: () =>
|
|
1729
|
+
cancel: () => l.cancel()
|
|
1525
1730
|
};
|
|
1526
1731
|
}
|
|
1527
|
-
async function
|
|
1528
|
-
let n =
|
|
1529
|
-
await
|
|
1732
|
+
async function r(e) {
|
|
1733
|
+
let n = U(t);
|
|
1734
|
+
await V(W(n, e.path)).catch(() => {});
|
|
1530
1735
|
}
|
|
1531
|
-
async function
|
|
1532
|
-
if (!
|
|
1533
|
-
let
|
|
1534
|
-
await
|
|
1535
|
-
let
|
|
1736
|
+
async function i(n, r, i) {
|
|
1737
|
+
if (!n.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
1738
|
+
let a = await Gt(n), o = U(t), s = (a.compressed ? "jpg" : n.name.split(".").pop() || n.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", c = Je(n.name.replace(/\.[^.]+$/, "") || "image", 40), l = `${Date.now()}-${c || "image"}`, u = i || `new-${Date.now()}`, d = X(e(), r, u), f = `${d}/images/${l}.${s}`, p = W(o, f);
|
|
1739
|
+
await re(p, a.blob, { contentType: a.blob.type || "image/jpeg" });
|
|
1740
|
+
let m = await H(p), h = await zt(a.blob, {
|
|
1536
1741
|
maxWidth: 400,
|
|
1537
1742
|
quality: .8
|
|
1538
|
-
}),
|
|
1539
|
-
return await
|
|
1540
|
-
originalUrl:
|
|
1541
|
-
originalPath:
|
|
1542
|
-
thumbnailUrl: await
|
|
1543
|
-
thumbnailPath:
|
|
1743
|
+
}), g = `${d}/images/thumbnails/${l}.jpg`, _ = W(o, g);
|
|
1744
|
+
return await re(_, h, { contentType: "image/jpeg" }), {
|
|
1745
|
+
originalUrl: m,
|
|
1746
|
+
originalPath: f,
|
|
1747
|
+
thumbnailUrl: await H(_),
|
|
1748
|
+
thumbnailPath: g
|
|
1544
1749
|
};
|
|
1545
1750
|
}
|
|
1546
|
-
async function
|
|
1547
|
-
let n =
|
|
1548
|
-
await Promise.all([
|
|
1751
|
+
async function a(e) {
|
|
1752
|
+
let n = U(t);
|
|
1753
|
+
await Promise.all([V(W(n, e.originalPath)).catch(() => {}), V(W(n, e.thumbnailPath)).catch(() => {})]);
|
|
1549
1754
|
}
|
|
1550
1755
|
return {
|
|
1551
|
-
uploadAttachment:
|
|
1552
|
-
deleteAttachment:
|
|
1553
|
-
uploadEditorImage:
|
|
1554
|
-
deleteEditorImage:
|
|
1756
|
+
uploadAttachment: n,
|
|
1757
|
+
deleteAttachment: r,
|
|
1758
|
+
uploadEditorImage: i,
|
|
1759
|
+
deleteEditorImage: a
|
|
1555
1760
|
};
|
|
1556
1761
|
}
|
|
1557
1762
|
//#endregion
|
|
1558
1763
|
//#region src/composables/useMemiBoardUsers.ts
|
|
1559
|
-
var
|
|
1764
|
+
var qt = [
|
|
1560
1765
|
{
|
|
1561
1766
|
value: "admin",
|
|
1562
1767
|
label: "관리자",
|
|
@@ -1573,24 +1778,39 @@ var jt = [
|
|
|
1573
1778
|
description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
|
|
1574
1779
|
}
|
|
1575
1780
|
];
|
|
1576
|
-
function
|
|
1577
|
-
let
|
|
1578
|
-
async function
|
|
1579
|
-
if (!
|
|
1580
|
-
await
|
|
1581
|
-
role:
|
|
1582
|
-
updatedAt:
|
|
1781
|
+
function Jt(e = {}) {
|
|
1782
|
+
let t = () => Z(), r = F(), i = n(() => o(e.enabled) ?? !0 ? ge(r, t()) : null), a = A(i, { ssrKey: ye(t(), "users-management") });
|
|
1783
|
+
async function s(e, n) {
|
|
1784
|
+
if (!qt.some((e) => e.value === n)) throw Error("지원하지 않는 역할입니다.");
|
|
1785
|
+
await D(_e(r, t(), e), {
|
|
1786
|
+
role: n,
|
|
1787
|
+
updatedAt: w()
|
|
1583
1788
|
});
|
|
1584
1789
|
}
|
|
1585
1790
|
return {
|
|
1586
|
-
users:
|
|
1587
|
-
usersPending:
|
|
1588
|
-
updateUserRole:
|
|
1791
|
+
users: a,
|
|
1792
|
+
usersPending: a.pending,
|
|
1793
|
+
updateUserRole: s
|
|
1589
1794
|
};
|
|
1590
1795
|
}
|
|
1591
1796
|
//#endregion
|
|
1592
1797
|
//#region src/data/versionHistory.ts
|
|
1593
|
-
var
|
|
1798
|
+
var Yt = [
|
|
1799
|
+
{
|
|
1800
|
+
version: "0.21.1",
|
|
1801
|
+
date: "2026-08-07",
|
|
1802
|
+
highlights: ["관리자가 쓴 댓글에 답글을 달 때 오류가 나던 문제를 고쳤어요"]
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
version: "0.21.0",
|
|
1806
|
+
date: "2026-08-06",
|
|
1807
|
+
highlights: ["카테고리 대신 게시판(보드)만 있어요 — 예전 카테고리 이름이 곧 보드 ID예요", "글은 memiBoards/{보드}/posts, 설정은 settings, 회원 역할은 memiBoardUsers 에 모아요"]
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
version: "0.20.0",
|
|
1811
|
+
date: "2026-08-06",
|
|
1812
|
+
highlights: ["게시판 데이터를 memiBoards/{게시판ID}/… 아래에 모았어요 (여러 게시판을 같은 프로젝트에 둘 수 있어요)", "설정·글·사용자 경로가 게시판마다 분리돼요 — 호스트는 boardId만 맞추면 돼요"]
|
|
1813
|
+
},
|
|
1594
1814
|
{
|
|
1595
1815
|
version: "0.19.3",
|
|
1596
1816
|
date: "2026-08-06",
|
|
@@ -1708,7 +1928,7 @@ var Nt = [
|
|
|
1708
1928
|
highlights: ["글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요", "부적절한 글·댓글은 AI 검열로 막을 수 있어요"],
|
|
1709
1929
|
note: "실제 보안은 호스트 Firebase 규칙 등록이 필요합니다."
|
|
1710
1930
|
}
|
|
1711
|
-
],
|
|
1931
|
+
], Xt = /* @__PURE__ */ t(((e, t) => {
|
|
1712
1932
|
(function(n, r) {
|
|
1713
1933
|
typeof e == "object" && t !== void 0 ? t.exports = r() : typeof define == "function" && define.amd ? define(r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs = r();
|
|
1714
1934
|
})(e, (function() {
|
|
@@ -2002,7 +2222,7 @@ var Nt = [
|
|
|
2002
2222
|
return w(1e3 * e);
|
|
2003
2223
|
}, w.en = b[y], w.Ls = b, w.p = {}, w;
|
|
2004
2224
|
}));
|
|
2005
|
-
})),
|
|
2225
|
+
})), Zt = /* @__PURE__ */ t(((e, t) => {
|
|
2006
2226
|
(function(n, r) {
|
|
2007
2227
|
typeof e == "object" && t !== void 0 ? t.exports = r() : typeof define == "function" && define.amd ? define(r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs_plugin_relativeTime = r();
|
|
2008
2228
|
})(e, (function() {
|
|
@@ -2106,9 +2326,9 @@ var Nt = [
|
|
|
2106
2326
|
};
|
|
2107
2327
|
};
|
|
2108
2328
|
}));
|
|
2109
|
-
})),
|
|
2329
|
+
})), Qt = /* @__PURE__ */ t(((e, t) => {
|
|
2110
2330
|
(function(n, r) {
|
|
2111
|
-
typeof e == "object" && t !== void 0 ? t.exports = r(
|
|
2331
|
+
typeof e == "object" && t !== void 0 ? t.exports = r(Xt()) : typeof define == "function" && define.amd ? define(["dayjs"], r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs_locale_ko = r(n.dayjs);
|
|
2112
2332
|
})(e, (function(e) {
|
|
2113
2333
|
function t(e) {
|
|
2114
2334
|
return e && typeof e == "object" && "default" in e ? e : { default: e };
|
|
@@ -2156,31 +2376,31 @@ var Nt = [
|
|
|
2156
2376
|
};
|
|
2157
2377
|
return n.default.locale(r, null, !0), r;
|
|
2158
2378
|
}));
|
|
2159
|
-
})),
|
|
2160
|
-
|
|
2161
|
-
function
|
|
2379
|
+
})), $t = /* @__PURE__ */ e(Xt(), 1), en = /* @__PURE__ */ e(Zt(), 1);
|
|
2380
|
+
Qt(), $t.default.extend(en.default), $t.default.locale("ko");
|
|
2381
|
+
function tn(e) {
|
|
2162
2382
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
2163
2383
|
}
|
|
2164
|
-
function
|
|
2165
|
-
return e?.toDate ? (0,
|
|
2384
|
+
function nn(e, t = Date.now()) {
|
|
2385
|
+
return e?.toDate ? (0, $t.default)(e.toDate()).from((0, $t.default)(t)) : "방금 전";
|
|
2166
2386
|
}
|
|
2167
|
-
function
|
|
2168
|
-
return e?.toDate ? (0,
|
|
2387
|
+
function rn(e) {
|
|
2388
|
+
return e?.toDate ? (0, $t.default)(e.toDate()).format("YYYY년 M월 D일 HH:mm:ss") : "시간 확인 중";
|
|
2169
2389
|
}
|
|
2170
|
-
function
|
|
2171
|
-
let n =
|
|
2172
|
-
return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${
|
|
2390
|
+
function an(e, t) {
|
|
2391
|
+
let n = rn(e), r = e?.toMillis?.(), i = t?.toMillis?.();
|
|
2392
|
+
return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${rn(t)}`];
|
|
2173
2393
|
}
|
|
2174
2394
|
//#endregion
|
|
2175
2395
|
//#region src/utils/renderMarkdown.ts
|
|
2176
|
-
function
|
|
2396
|
+
function $(e) {
|
|
2177
2397
|
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2178
2398
|
}
|
|
2179
|
-
function
|
|
2399
|
+
function on(e) {
|
|
2180
2400
|
let t = e;
|
|
2181
2401
|
return t = t.replace(/`([^`]+)`/g, "<code>$1</code>"), t = t.replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+)\)/g, "<img src=\"$2\" alt=\"$1\" class=\"max-w-full h-auto rounded-lg my-3\" loading=\"lazy\" />"), t = t.replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-primary underline\">$1</a>"), t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>"), t = t.replace(/\*([^*]+)\*/g, "<em>$1</em>"), t = t.replace(/~~([^~]+)~~/g, "<del>$1</del>"), t;
|
|
2182
2402
|
}
|
|
2183
|
-
function
|
|
2403
|
+
function sn(e) {
|
|
2184
2404
|
if (!e?.trim()) return "";
|
|
2185
2405
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
2186
2406
|
function c() {
|
|
@@ -2189,7 +2409,7 @@ function Ht(e) {
|
|
|
2189
2409
|
return;
|
|
2190
2410
|
}
|
|
2191
2411
|
let e = o;
|
|
2192
|
-
n.push(`<${e}>${s.map((e) => `<li>${
|
|
2412
|
+
n.push(`<${e}>${s.map((e) => `<li>${on(e)}</li>`).join("")}</${e}>`), o = null, s = [];
|
|
2193
2413
|
}
|
|
2194
2414
|
for (; r < t.length;) {
|
|
2195
2415
|
let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
|
|
@@ -2198,7 +2418,7 @@ function Ht(e) {
|
|
|
2198
2418
|
continue;
|
|
2199
2419
|
}
|
|
2200
2420
|
if (i) {
|
|
2201
|
-
a.push(
|
|
2421
|
+
a.push($(e)), r++;
|
|
2202
2422
|
continue;
|
|
2203
2423
|
}
|
|
2204
2424
|
if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(e)) {
|
|
@@ -2209,24 +2429,24 @@ function Ht(e) {
|
|
|
2209
2429
|
if (u) {
|
|
2210
2430
|
c();
|
|
2211
2431
|
let e = u[1].length;
|
|
2212
|
-
n.push(`<h${e} class="font-semibold mt-4 mb-2">${$(
|
|
2432
|
+
n.push(`<h${e} class="font-semibold mt-4 mb-2">${on($(u[2]))}</h${e}>`), r++;
|
|
2213
2433
|
continue;
|
|
2214
2434
|
}
|
|
2215
2435
|
if (e.startsWith("> ")) {
|
|
2216
2436
|
c();
|
|
2217
2437
|
let e = [];
|
|
2218
|
-
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push(
|
|
2219
|
-
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${
|
|
2438
|
+
for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push($((t[r] ?? "").slice(2))), r++;
|
|
2439
|
+
n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${on(e.join("<br />"))}</blockquote>`);
|
|
2220
2440
|
continue;
|
|
2221
2441
|
}
|
|
2222
2442
|
let d = e.match(/^[-*+]\s+(.+)$/);
|
|
2223
2443
|
if (d) {
|
|
2224
|
-
o && o !== "ul" && c(), o = "ul", s.push(
|
|
2444
|
+
o && o !== "ul" && c(), o = "ul", s.push($(d[1])), r++;
|
|
2225
2445
|
continue;
|
|
2226
2446
|
}
|
|
2227
2447
|
let f = e.match(/^\d+\.\s+(.+)$/);
|
|
2228
2448
|
if (f) {
|
|
2229
|
-
o && o !== "ol" && c(), o = "ol", s.push(
|
|
2449
|
+
o && o !== "ol" && c(), o = "ol", s.push($(f[1])), r++;
|
|
2230
2450
|
continue;
|
|
2231
2451
|
}
|
|
2232
2452
|
if (!e.trim()) {
|
|
@@ -2238,13 +2458,13 @@ function Ht(e) {
|
|
|
2238
2458
|
for (; r < t.length;) {
|
|
2239
2459
|
let e = t[r] ?? "";
|
|
2240
2460
|
if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
|
|
2241
|
-
p.push(
|
|
2461
|
+
p.push($(e)), r++;
|
|
2242
2462
|
}
|
|
2243
|
-
n.push(`<p class="my-2 leading-relaxed">${
|
|
2463
|
+
n.push(`<p class="my-2 leading-relaxed">${on(p.join("<br />"))}</p>`);
|
|
2244
2464
|
}
|
|
2245
2465
|
return c(), i && a.length && n.push(`<pre class="overflow-x-auto rounded-lg bg-muted p-3 text-sm"><code>${a.join("\n")}</code></pre>`), n.join("\n");
|
|
2246
2466
|
}
|
|
2247
2467
|
//#endregion
|
|
2248
|
-
export {
|
|
2468
|
+
export { qt as BOARD_USER_ROLES, wt as COMMENT_BODY_MAX_LENGTH, Q as DEFAULT_BLOCK_BAN_DECAY_MS, Re as DEFAULT_BLOCK_BAN_THRESHOLD, st as DEFAULT_BOARDS, le as DEFAULT_BOARDS_COLLECTION, ct as DEFAULT_CATEGORIES, ue as DEFAULT_USERS_COLLECTION, Bt as EDITOR_IMAGE_MAX_BYTES, Vt as EDITOR_IMAGE_SOURCE_MAX_BYTES, we as asHttpUrl, pe as boardDoc, Ae as boardListOgDescription, ke as boardListOgTitle, me as boardPostBodyDoc, Y as boardPostCommentDoc, J as boardPostCommentsCol, q as boardPostDoc, he as boardPostLikeDoc, Oe as boardPostOgDescription, De as boardPostOgTitle, X as boardPostStorageFolder, K as boardPostsCol, G as boardSettingsDoc, ye as boardSsrKey, ve as boardStorageRoot, _e as boardUserDoc, ge as boardUsersCol, fe as boardsCol, ot as buildPostPreview, zt as compressImage, Se as configureMemiBoard, ze as effectiveModerationBlockCount, Ye as extractEditorImageUrls, Ie as fetchPublicListForSeo, Fe as fetchPublicPostForSeo, tn as formatDate, rn as formatFullDate, nn as formatRelativeDate, He as formatRestrictedUntilLabel, an as formatTimestampDetails, tt as hasBodyImage, et as hasBodyText, nt as isContentEmpty, mt as isFirestorePermissionDenied, Ve as isModerationWriteRestricted, Be as moderationWriteRestrictedUntilMs, je as normalizeBasePath, Qe as plainTextFromHtml, Ze as postNamespaceFromStoragePath, sn as renderMarkdownToHtml, de as resolveBoardPathConfig, Me as resolvePublicSeoDb, qe as slugify, Xe as storagePathFromDownloadUrl, $e as titleFromBody, Ee as toAbsoluteUrl, Te as toBoardOgImageUrl, Z as useBoardPathConfig, Ke as useMemiBoardAuth, Et as useMemiBoardComments, Ce as useMemiBoardConfig, Ot as useMemiBoardLikes, Rt as useMemiBoardModeration, yt as useMemiBoardPost, vt as useMemiBoardPostList, gt as useMemiBoardPosts, Dt as useMemiBoardReplies, dt as useMemiBoardSettings, Kt as useMemiBoardStorage, xt as useMemiBoardUserPosts, Jt as useMemiBoardUsers, kt as useMemiBoardViews, Yt as versionHistory, at as videoListCoverUrl, it as youtubeId };
|
|
2249
2469
|
|
|
2250
2470
|
//# sourceMappingURL=index.js.map
|