memi-board 0.4.12 → 0.4.14
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/dist/composables/useMemiBoardSettings.d.ts +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +198 -136
- package/dist/index.js.map +1 -1
- package/dist/runtime/components/Settings.vue +154 -0
- package/dist/types.d.ts +3 -0
- package/dist/utils/storagePath.d.ts +7 -0
- package/docs/storage.rules.example +4 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { computed as e, reactive as t, ref as n, watch as r } from "vue";
|
|
2
2
|
import { useCollection as i, useCurrentUser as a, useDocument as o, useFirebaseApp as s, useFirebaseAuth as c, useFirestore as l } from "vuefire";
|
|
3
3
|
import { collection as u, deleteDoc as d, deleteField as f, doc as p, getDoc as m, getDocs as h, increment as g, limit as _, orderBy as v, query as y, serverTimestamp as b, setDoc as x, startAfter as S, updateDoc as C, where as ee, writeBatch as w } from "firebase/firestore";
|
|
4
|
-
import { GoogleAuthProvider as T, OAuthProvider as te, createUserWithEmailAndPassword as ne, signInWithEmailAndPassword as re, signInWithPopup as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as N, Schema as P, getAI as
|
|
4
|
+
import { GoogleAuthProvider as T, OAuthProvider as te, createUserWithEmailAndPassword as ne, signInWithEmailAndPassword as re, signInWithPopup as ie, signOut as ae, updateProfile as oe } from "firebase/auth";
|
|
5
|
+
import { deleteObject as E, getDownloadURL as D, getStorage as O, listAll as k, ref as A, uploadBytes as j, uploadBytesResumable as M } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as N, Schema as P, getAI as se, getGenerativeModel as ce } from "firebase/ai";
|
|
7
7
|
//#region src/config.ts
|
|
8
|
-
var
|
|
9
|
-
function
|
|
8
|
+
var F = "__MEMI_BOARD_CONFIG__";
|
|
9
|
+
function I() {
|
|
10
10
|
let e = globalThis;
|
|
11
|
-
return e[
|
|
11
|
+
return e[F] || (e[F] = t({
|
|
12
12
|
collectionPrefix: "memiBoard",
|
|
13
13
|
auth: { providers: ["google", "apple"] },
|
|
14
14
|
moderation: {
|
|
@@ -20,19 +20,19 @@ function R() {
|
|
|
20
20
|
blockBanThreshold: 3,
|
|
21
21
|
blockBanDecayMs: 864e5
|
|
22
22
|
}
|
|
23
|
-
})), e[
|
|
23
|
+
})), e[F];
|
|
24
24
|
}
|
|
25
|
-
function
|
|
26
|
-
let t =
|
|
25
|
+
function L(e) {
|
|
26
|
+
let t = I();
|
|
27
27
|
e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
|
|
28
28
|
}
|
|
29
|
-
function
|
|
30
|
-
return
|
|
29
|
+
function R() {
|
|
30
|
+
return I();
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/utils/moderation-strike.ts
|
|
34
|
-
var
|
|
35
|
-
function
|
|
34
|
+
var le = 3, z = 864e5;
|
|
35
|
+
function B(e, t, n = Date.now(), r = z) {
|
|
36
36
|
let i = Math.max(0, Math.floor(Number(e) || 0));
|
|
37
37
|
if (i <= 0) return 0;
|
|
38
38
|
let a = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -40,7 +40,7 @@ function H(e, t, n = Date.now(), r = V) {
|
|
|
40
40
|
let o = Math.floor((n - a) / r);
|
|
41
41
|
return Math.max(0, i - o);
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function V(e, t, n = Date.now(), r = 3, i = z) {
|
|
44
44
|
let a = Math.max(0, Math.floor(Number(e) || 0));
|
|
45
45
|
if (a < r) return null;
|
|
46
46
|
let o = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -48,17 +48,17 @@ function U(e, t, n = Date.now(), r = 3, i = V) {
|
|
|
48
48
|
let s = o + (a - (r - 1)) * i;
|
|
49
49
|
return s > n ? s : null;
|
|
50
50
|
}
|
|
51
|
-
function
|
|
52
|
-
return
|
|
51
|
+
function H(e, t, n = Date.now(), r = 3, i = z) {
|
|
52
|
+
return V(e, t, n, r, i) != null;
|
|
53
53
|
}
|
|
54
|
-
function
|
|
54
|
+
function U(e, t = Date.now()) {
|
|
55
55
|
let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
|
|
56
56
|
if (r <= 0) return "곧 해제";
|
|
57
57
|
if (r < 24) return `약 ${r}시간 후 해제`;
|
|
58
58
|
let i = Math.floor(r / 24), a = r % 24;
|
|
59
59
|
return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
|
|
60
60
|
}
|
|
61
|
-
function
|
|
61
|
+
function ue(e) {
|
|
62
62
|
if (e == null) return null;
|
|
63
63
|
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
64
64
|
if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
|
|
@@ -70,26 +70,26 @@ function ce(e) {
|
|
|
70
70
|
}
|
|
71
71
|
//#endregion
|
|
72
72
|
//#region src/composables/useMemiBoardAuth.ts
|
|
73
|
-
var
|
|
74
|
-
function
|
|
73
|
+
var W = "__MEMI_BOARD_AUTH_STATE__";
|
|
74
|
+
function de() {
|
|
75
75
|
let e = globalThis;
|
|
76
|
-
return e[
|
|
76
|
+
return e[W] || (e[W] = {
|
|
77
77
|
role: n("user"),
|
|
78
78
|
rolePending: n(!0),
|
|
79
79
|
rawBlockCount: n(0),
|
|
80
80
|
blockAtMs: n(null),
|
|
81
81
|
watchStarted: !1
|
|
82
|
-
}), e[
|
|
82
|
+
}), e[W];
|
|
83
83
|
}
|
|
84
|
-
function
|
|
85
|
-
let t =
|
|
84
|
+
function G() {
|
|
85
|
+
let t = R(), n = de(), i = l(), o = c(), s = a(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: h } = n;
|
|
86
86
|
function _() {
|
|
87
87
|
let e = t.moderation?.blockBanThreshold;
|
|
88
88
|
return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
|
|
89
89
|
}
|
|
90
90
|
function v() {
|
|
91
91
|
let e = t.moderation?.blockBanDecayMs;
|
|
92
|
-
return typeof e == "number" && e > 0 ? e :
|
|
92
|
+
return typeof e == "number" && e > 0 ? e : z;
|
|
93
93
|
}
|
|
94
94
|
function y(e) {
|
|
95
95
|
return p(i, `${t.collectionPrefix}Users`, e);
|
|
@@ -99,7 +99,7 @@ function q() {
|
|
|
99
99
|
f.value = 0, h.value = null;
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value =
|
|
102
|
+
f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
|
|
103
103
|
}
|
|
104
104
|
async function ee(e) {
|
|
105
105
|
let t = y(e.uid), n = await m(t);
|
|
@@ -136,20 +136,20 @@ function q() {
|
|
|
136
136
|
d.value = !1;
|
|
137
137
|
}
|
|
138
138
|
}, { immediate: !0 }));
|
|
139
|
-
let
|
|
139
|
+
let E = e(() => !!s.value), D = e(() => u.value === "admin"), O = e(() => B(f.value, h.value, Date.now(), v())), k = e(() => {
|
|
140
140
|
let e = _();
|
|
141
|
-
return e <= 0 ? !1 :
|
|
142
|
-
}),
|
|
141
|
+
return e <= 0 ? !1 : H(f.value, h.value, Date.now(), e, v());
|
|
142
|
+
}), A = e(() => {
|
|
143
143
|
let e = _();
|
|
144
144
|
if (e <= 0) return null;
|
|
145
|
-
let t =
|
|
146
|
-
return t == null ? null :
|
|
147
|
-
}),
|
|
148
|
-
if (!
|
|
149
|
-
let e =
|
|
145
|
+
let t = V(f.value, h.value, Date.now(), e, v());
|
|
146
|
+
return t == null ? null : U(t);
|
|
147
|
+
}), j = e(() => {
|
|
148
|
+
if (!k.value) return null;
|
|
149
|
+
let e = A.value;
|
|
150
150
|
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
151
151
|
});
|
|
152
|
-
async function
|
|
152
|
+
async function M() {
|
|
153
153
|
let e = s.value?.uid, t = _();
|
|
154
154
|
if (!e || t <= 0) return {
|
|
155
155
|
effectiveCount: 0,
|
|
@@ -170,9 +170,9 @@ function q() {
|
|
|
170
170
|
});
|
|
171
171
|
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
|
|
172
172
|
f.value = i, h.value = a;
|
|
173
|
-
let o =
|
|
173
|
+
let o = B(i, a, a, v()), c = H(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
|
|
174
174
|
if (c) {
|
|
175
|
-
let e =
|
|
175
|
+
let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
|
|
176
176
|
l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
177
177
|
}
|
|
178
178
|
return {
|
|
@@ -186,68 +186,94 @@ function q() {
|
|
|
186
186
|
return o;
|
|
187
187
|
}
|
|
188
188
|
async function P() {
|
|
189
|
-
return
|
|
189
|
+
return ie(N(), new T());
|
|
190
190
|
}
|
|
191
|
-
async function
|
|
191
|
+
async function se() {
|
|
192
192
|
let e = new te("apple.com");
|
|
193
|
-
return e.addScope("email"), e.addScope("name"),
|
|
193
|
+
return e.addScope("email"), e.addScope("name"), ie(N(), e);
|
|
194
194
|
}
|
|
195
|
-
async function
|
|
195
|
+
async function ce(e, t) {
|
|
196
196
|
return re(N(), e, t);
|
|
197
197
|
}
|
|
198
|
-
async function
|
|
198
|
+
async function F(e, t, n) {
|
|
199
199
|
let r = await ne(N(), e, t);
|
|
200
|
-
return n && await
|
|
200
|
+
return n && await oe(r.user, { displayName: n }), r;
|
|
201
201
|
}
|
|
202
|
-
async function
|
|
203
|
-
await
|
|
202
|
+
async function I() {
|
|
203
|
+
await ae(N());
|
|
204
204
|
}
|
|
205
|
-
function
|
|
206
|
-
return
|
|
205
|
+
function L(e) {
|
|
206
|
+
return E.value && (s.value?.uid === e.authorUid || D.value);
|
|
207
207
|
}
|
|
208
|
-
function
|
|
209
|
-
return
|
|
208
|
+
function le(e) {
|
|
209
|
+
return E.value && (s.value?.uid === e.authorUid || D.value);
|
|
210
210
|
}
|
|
211
211
|
return {
|
|
212
212
|
user: s,
|
|
213
|
-
isSignedIn:
|
|
214
|
-
isAdmin:
|
|
213
|
+
isSignedIn: E,
|
|
214
|
+
isAdmin: D,
|
|
215
215
|
rolePending: d,
|
|
216
|
-
moderationBlockCount:
|
|
217
|
-
isWriteRestricted:
|
|
218
|
-
restrictedUntilLabel:
|
|
219
|
-
restrictedMessage:
|
|
216
|
+
moderationBlockCount: O,
|
|
217
|
+
isWriteRestricted: k,
|
|
218
|
+
restrictedUntilLabel: A,
|
|
219
|
+
restrictedMessage: j,
|
|
220
220
|
signInWithGoogle: P,
|
|
221
|
-
signInWithApple:
|
|
222
|
-
signInWithEmail:
|
|
223
|
-
signUpWithEmail:
|
|
224
|
-
signOut:
|
|
225
|
-
canEdit:
|
|
226
|
-
canDelete:
|
|
227
|
-
canDeleteComment:
|
|
228
|
-
recordContentModerationBlock:
|
|
221
|
+
signInWithApple: se,
|
|
222
|
+
signInWithEmail: ce,
|
|
223
|
+
signUpWithEmail: F,
|
|
224
|
+
signOut: I,
|
|
225
|
+
canEdit: L,
|
|
226
|
+
canDelete: L,
|
|
227
|
+
canDeleteComment: le,
|
|
228
|
+
recordContentModerationBlock: M,
|
|
229
229
|
refreshBoardUser: w
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
232
|
//#endregion
|
|
233
233
|
//#region src/utils/slugify.ts
|
|
234
|
-
function
|
|
234
|
+
function K(e) {
|
|
235
235
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
236
236
|
}
|
|
237
|
-
function
|
|
237
|
+
function fe(e, t = 60) {
|
|
238
238
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
239
239
|
}
|
|
240
240
|
//#endregion
|
|
241
|
+
//#region src/utils/extractEditorImageUrls.ts
|
|
242
|
+
function pe(e) {
|
|
243
|
+
if (!e) return [];
|
|
244
|
+
let t = /* @__PURE__ */ new Set();
|
|
245
|
+
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
246
|
+
for (let n of e.matchAll(/<img[^>]*\ssrc=["'](https?:\/\/[^"']+)["']/gi)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
247
|
+
return [...t];
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/utils/storagePath.ts
|
|
251
|
+
function me(e) {
|
|
252
|
+
try {
|
|
253
|
+
let t = e.match(/\/o\/([^?]+)/);
|
|
254
|
+
return t?.[1] ? decodeURIComponent(t[1]) : null;
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function q(e) {
|
|
260
|
+
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
261
|
+
}
|
|
262
|
+
//#endregion
|
|
241
263
|
//#region src/composables/useMemiBoardPosts.ts
|
|
242
|
-
async function
|
|
243
|
-
let t = await
|
|
264
|
+
async function J(e) {
|
|
265
|
+
let t = await k(e).catch(() => ({
|
|
244
266
|
items: [],
|
|
245
267
|
prefixes: []
|
|
246
268
|
}));
|
|
247
|
-
await Promise.all([...t.items.map((e) =>
|
|
269
|
+
await Promise.all([...t.items.map((e) => E(e)), ...t.prefixes.map((e) => J(e))]);
|
|
270
|
+
}
|
|
271
|
+
async function he(e, t) {
|
|
272
|
+
let n = [...new Set([...t].filter(Boolean))];
|
|
273
|
+
await Promise.all(n.map((t) => E(A(e, t)).catch(() => {})));
|
|
248
274
|
}
|
|
249
|
-
function
|
|
250
|
-
let e =
|
|
275
|
+
function ge() {
|
|
276
|
+
let e = R(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
|
|
251
277
|
async function g(e = {}) {
|
|
252
278
|
let t = e.pageSize ?? 20, n = [];
|
|
253
279
|
e.category && n.push(ee("category", "==", e.category)), n.push(v("createdAt", "desc")), e.cursor && n.push(S(e.cursor)), n.push(_(t + 1));
|
|
@@ -270,7 +296,7 @@ function fe() {
|
|
|
270
296
|
} : null;
|
|
271
297
|
}
|
|
272
298
|
async function te(e) {
|
|
273
|
-
let t =
|
|
299
|
+
let t = K(e.title) || "post", n = t, r = 1;
|
|
274
300
|
for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
|
|
275
301
|
return await Promise.all([x(a(n), {
|
|
276
302
|
title: e.title,
|
|
@@ -297,13 +323,33 @@ function fe() {
|
|
|
297
323
|
}), x(o(e), { content: t.content }, { merge: !0 })]);
|
|
298
324
|
}
|
|
299
325
|
async function re(e) {
|
|
300
|
-
let i = await
|
|
301
|
-
|
|
326
|
+
let [i, s] = await Promise.all([m(a(e)), m(o(e))]);
|
|
327
|
+
if (!i.exists()) return;
|
|
328
|
+
let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await h(c(e));
|
|
329
|
+
for (let e = 0; e < f.docs.length; e += 450) {
|
|
302
330
|
let n = w(t);
|
|
303
|
-
|
|
331
|
+
f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
304
332
|
}
|
|
305
|
-
let
|
|
306
|
-
|
|
333
|
+
let p = O(n), g = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
|
|
334
|
+
for (let t of l.attachments ?? []) if (t?.path) {
|
|
335
|
+
g.add(t.path);
|
|
336
|
+
let n = q(t.path);
|
|
337
|
+
n && n !== e && _.add(n);
|
|
338
|
+
}
|
|
339
|
+
for (let t of pe(u)) {
|
|
340
|
+
let n = me(t);
|
|
341
|
+
if (!n) continue;
|
|
342
|
+
g.add(n);
|
|
343
|
+
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
344
|
+
r !== n && r.includes("/images/thumbnails/") && g.add(r);
|
|
345
|
+
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
346
|
+
i && !n.includes("/thumbnails/") && g.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
347
|
+
let a = q(n);
|
|
348
|
+
a && a !== e && _.add(a);
|
|
349
|
+
}
|
|
350
|
+
await d(o(e)), await J(A(p, `${r()}/posts/${e}`));
|
|
351
|
+
for (let e of _) await J(A(p, `${r()}/posts/${e}`));
|
|
352
|
+
await he(p, g), await d(a(e));
|
|
307
353
|
}
|
|
308
354
|
return {
|
|
309
355
|
getPosts: g,
|
|
@@ -315,8 +361,8 @@ function fe() {
|
|
|
315
361
|
}
|
|
316
362
|
//#endregion
|
|
317
363
|
//#region src/composables/useMemiBoardComments.ts
|
|
318
|
-
function
|
|
319
|
-
let n =
|
|
364
|
+
function _e(t) {
|
|
365
|
+
let n = R(), r = l(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => y(u(r, `${a()}Posts`, o.value, "comments"), v("createdAt", "asc"))), c = i(s);
|
|
320
366
|
async function d(e) {
|
|
321
367
|
let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
|
|
322
368
|
t.set(i, {
|
|
@@ -341,7 +387,7 @@ function pe(t) {
|
|
|
341
387
|
}
|
|
342
388
|
//#endregion
|
|
343
389
|
//#region src/utils/moderation-prompt.ts
|
|
344
|
-
var
|
|
390
|
+
var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", ye = {
|
|
345
391
|
abuse: "욕설·비속어·모욕",
|
|
346
392
|
spam: "스팸·광고",
|
|
347
393
|
adult: "선정적·성인 내용",
|
|
@@ -356,10 +402,10 @@ function Y(e) {
|
|
|
356
402
|
}
|
|
357
403
|
let t = (e.reason || "").trim();
|
|
358
404
|
if (t) return t;
|
|
359
|
-
let n = e.category && e.category !== "none" ?
|
|
405
|
+
let n = e.category && e.category !== "none" ? ye[e.category] || e.category : "";
|
|
360
406
|
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
361
407
|
}
|
|
362
|
-
var
|
|
408
|
+
var be = [
|
|
363
409
|
"씨발",
|
|
364
410
|
"시발",
|
|
365
411
|
"시이발",
|
|
@@ -385,10 +431,10 @@ var ge = [
|
|
|
385
431
|
"bitch",
|
|
386
432
|
"asshole"
|
|
387
433
|
];
|
|
388
|
-
function
|
|
434
|
+
function xe(e) {
|
|
389
435
|
return new RegExp(e.join("|"), "i");
|
|
390
436
|
}
|
|
391
|
-
var
|
|
437
|
+
var Se = /* @__PURE__ */ new Set([
|
|
392
438
|
"none",
|
|
393
439
|
"abuse",
|
|
394
440
|
"spam",
|
|
@@ -396,7 +442,7 @@ var ve = /* @__PURE__ */ new Set([
|
|
|
396
442
|
"violence",
|
|
397
443
|
"other"
|
|
398
444
|
]);
|
|
399
|
-
function
|
|
445
|
+
function Ce(e) {
|
|
400
446
|
let t = e.trim();
|
|
401
447
|
if (!t) return null;
|
|
402
448
|
let n;
|
|
@@ -414,7 +460,7 @@ function ye(e) {
|
|
|
414
460
|
if (typeof n != "object" || !n) return null;
|
|
415
461
|
let r = n;
|
|
416
462
|
if (typeof r.flagged != "boolean") return null;
|
|
417
|
-
let i = typeof r.category == "string" &&
|
|
463
|
+
let i = typeof r.category == "string" && Se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
418
464
|
return {
|
|
419
465
|
flagged: r.flagged,
|
|
420
466
|
category: i,
|
|
@@ -429,8 +475,8 @@ var X = {
|
|
|
429
475
|
reason: "",
|
|
430
476
|
via: "empty"
|
|
431
477
|
};
|
|
432
|
-
function
|
|
433
|
-
let e =
|
|
478
|
+
function we() {
|
|
479
|
+
let e = R().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = xe([...be, ...e.localBlocklist ?? []]);
|
|
434
480
|
function o(e) {
|
|
435
481
|
let t = e.match(a);
|
|
436
482
|
return t ? {
|
|
@@ -445,13 +491,13 @@ function be() {
|
|
|
445
491
|
} : null;
|
|
446
492
|
}
|
|
447
493
|
function c() {
|
|
448
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
494
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
|
|
449
495
|
backend: new N(),
|
|
450
496
|
useLimitedUseAppCheckTokens: r
|
|
451
497
|
});
|
|
452
|
-
return
|
|
498
|
+
return ce(i, {
|
|
453
499
|
model: n,
|
|
454
|
-
systemInstruction:
|
|
500
|
+
systemInstruction: ve,
|
|
455
501
|
generationConfig: {
|
|
456
502
|
temperature: 0,
|
|
457
503
|
maxOutputTokens: 256,
|
|
@@ -518,7 +564,7 @@ function be() {
|
|
|
518
564
|
via: "disabled"
|
|
519
565
|
};
|
|
520
566
|
try {
|
|
521
|
-
let e =
|
|
567
|
+
let e = Ce((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
522
568
|
if (!e) return l();
|
|
523
569
|
let t = {
|
|
524
570
|
flagged: e.flagged,
|
|
@@ -550,7 +596,7 @@ function be() {
|
|
|
550
596
|
let e = await new Promise((e, n) => {
|
|
551
597
|
let r = new FileReader();
|
|
552
598
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
553
|
-
}), n =
|
|
599
|
+
}), n = Ce((await c().generateContent([{ inlineData: {
|
|
554
600
|
mimeType: t.type || "image/jpeg",
|
|
555
601
|
data: e
|
|
556
602
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -577,7 +623,7 @@ function be() {
|
|
|
577
623
|
}
|
|
578
624
|
//#endregion
|
|
579
625
|
//#region src/utils/compressImage.ts
|
|
580
|
-
async function
|
|
626
|
+
async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
581
627
|
return new Promise((r, i) => {
|
|
582
628
|
let a = new Image(), o = URL.createObjectURL(e);
|
|
583
629
|
a.onload = () => {
|
|
@@ -597,11 +643,11 @@ async function xe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
|
597
643
|
}
|
|
598
644
|
//#endregion
|
|
599
645
|
//#region src/composables/useMemiBoardStorage.ts
|
|
600
|
-
var
|
|
601
|
-
function
|
|
602
|
-
let e =
|
|
646
|
+
var Ee = 5242880;
|
|
647
|
+
function De() {
|
|
648
|
+
let e = R(), t = s(), n = () => e.collectionPrefix;
|
|
603
649
|
function r(e, r, i) {
|
|
604
|
-
let a =
|
|
650
|
+
let a = O(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = A(a, o), c = M(s, e, {
|
|
605
651
|
contentType: e.type || "application/octet-stream",
|
|
606
652
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
607
653
|
});
|
|
@@ -610,7 +656,7 @@ function Ce() {
|
|
|
610
656
|
c.on("state_changed", (e) => {
|
|
611
657
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
612
658
|
}, n, async () => {
|
|
613
|
-
let n = await
|
|
659
|
+
let n = await D(s);
|
|
614
660
|
t({
|
|
615
661
|
name: e.name,
|
|
616
662
|
url: n,
|
|
@@ -624,28 +670,28 @@ function Ce() {
|
|
|
624
670
|
};
|
|
625
671
|
}
|
|
626
672
|
async function i(e) {
|
|
627
|
-
let n =
|
|
628
|
-
await
|
|
673
|
+
let n = O(t);
|
|
674
|
+
await E(A(n, e.path)).catch(() => {});
|
|
629
675
|
}
|
|
630
676
|
async function a(e, r) {
|
|
631
677
|
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
632
|
-
if (e.size > 5242880) throw Error(`이미지는 ${
|
|
633
|
-
let i =
|
|
634
|
-
await
|
|
635
|
-
let d = await
|
|
678
|
+
if (e.size > 5242880) throw Error(`이미지는 ${Ee / 1024 / 1024}MB 이하여야 합니다.`);
|
|
679
|
+
let i = O(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = A(i, l);
|
|
680
|
+
await j(u, e, { contentType: e.type || "image/png" });
|
|
681
|
+
let d = await D(u), f = await Te(e, {
|
|
636
682
|
maxWidth: 400,
|
|
637
683
|
quality: .8
|
|
638
|
-
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m =
|
|
639
|
-
return await
|
|
684
|
+
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = A(i, p);
|
|
685
|
+
return await j(m, f, { contentType: "image/jpeg" }), {
|
|
640
686
|
originalUrl: d,
|
|
641
687
|
originalPath: l,
|
|
642
|
-
thumbnailUrl: await
|
|
688
|
+
thumbnailUrl: await D(m),
|
|
643
689
|
thumbnailPath: p
|
|
644
690
|
};
|
|
645
691
|
}
|
|
646
692
|
async function o(e) {
|
|
647
|
-
let n =
|
|
648
|
-
await Promise.all([
|
|
693
|
+
let n = O(t);
|
|
694
|
+
await Promise.all([E(A(n, e.originalPath)).catch(() => {}), E(A(n, e.thumbnailPath)).catch(() => {})]);
|
|
649
695
|
}
|
|
650
696
|
return {
|
|
651
697
|
uploadAttachment: r,
|
|
@@ -659,26 +705,44 @@ function Ce() {
|
|
|
659
705
|
var Z = [
|
|
660
706
|
{
|
|
661
707
|
id: "free",
|
|
662
|
-
label: "자유"
|
|
708
|
+
label: "자유",
|
|
709
|
+
listView: "default"
|
|
663
710
|
},
|
|
664
711
|
{
|
|
665
712
|
id: "notice",
|
|
666
|
-
label: "공지"
|
|
713
|
+
label: "공지",
|
|
714
|
+
listView: "default"
|
|
667
715
|
},
|
|
668
716
|
{
|
|
669
717
|
id: "question",
|
|
670
|
-
label: "질문"
|
|
718
|
+
label: "질문",
|
|
719
|
+
listView: "default"
|
|
671
720
|
}
|
|
672
721
|
];
|
|
673
|
-
function
|
|
674
|
-
let t =
|
|
722
|
+
function Oe() {
|
|
723
|
+
let t = R(), n = l(), { isSignedIn: i, isAdmin: a } = G(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
|
|
675
724
|
let e = c.value?.categories;
|
|
676
725
|
return e && e.length > 0 ? e : Z;
|
|
677
726
|
});
|
|
678
727
|
function f(e) {
|
|
679
728
|
if (e) return d.value.find((t) => t.id === e)?.label ?? e;
|
|
680
729
|
}
|
|
681
|
-
async function h() {
|
|
730
|
+
async function h(e) {
|
|
731
|
+
if (!i.value) throw Error("로그인이 필요합니다.");
|
|
732
|
+
let t = e.map((e) => ({
|
|
733
|
+
id: e.id.trim(),
|
|
734
|
+
label: e.label.trim(),
|
|
735
|
+
listView: e.listView ?? "default"
|
|
736
|
+
}));
|
|
737
|
+
if (!t.length) throw Error("카테고리를 하나 이상 만들어 주세요.");
|
|
738
|
+
if (t.some((e) => !e.id || !e.label)) throw Error("카테고리 이름을 모두 입력해 주세요.");
|
|
739
|
+
if (new Set(t.map((e) => e.id)).size !== t.length) throw Error("카테고리 ID가 중복되었습니다.");
|
|
740
|
+
await x(s.value, {
|
|
741
|
+
categories: t,
|
|
742
|
+
updatedAt: b()
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
async function g() {
|
|
682
746
|
if (!i.value) return;
|
|
683
747
|
let e = s.value, t = await m(e).catch(() => null);
|
|
684
748
|
!t || t.exists() || await x(e, {
|
|
@@ -686,14 +750,14 @@ function we() {
|
|
|
686
750
|
updatedAt: b()
|
|
687
751
|
});
|
|
688
752
|
}
|
|
689
|
-
async function
|
|
753
|
+
async function _(e) {
|
|
690
754
|
let t = e.trim();
|
|
691
755
|
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
692
756
|
if (!i.value) throw Error("로그인이 필요합니다.");
|
|
693
|
-
await
|
|
694
|
-
let n = d.value.find((e) => e.label === t || e.id ===
|
|
757
|
+
await g();
|
|
758
|
+
let n = d.value.find((e) => e.label === t || e.id === K(t));
|
|
695
759
|
if (n) return n.id;
|
|
696
|
-
let r =
|
|
760
|
+
let r = K(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
|
|
697
761
|
if (a.has(r)) {
|
|
698
762
|
let e = 2;
|
|
699
763
|
for (; a.has(`${r}-${e}`);) e++;
|
|
@@ -701,7 +765,8 @@ function we() {
|
|
|
701
765
|
}
|
|
702
766
|
let o = [...d.value, {
|
|
703
767
|
id: r,
|
|
704
|
-
label: t
|
|
768
|
+
label: t,
|
|
769
|
+
listView: "default"
|
|
705
770
|
}], c = s.value;
|
|
706
771
|
return (await m(c)).exists() ? await C(c, {
|
|
707
772
|
categories: o,
|
|
@@ -716,19 +781,25 @@ function we() {
|
|
|
716
781
|
i,
|
|
717
782
|
u
|
|
718
783
|
], async ([e, t, n]) => {
|
|
719
|
-
n || c.value || !e && !t || await
|
|
784
|
+
n || c.value || !e && !t || await g().catch(() => {});
|
|
720
785
|
}, { immediate: !0 }), {
|
|
721
786
|
categories: d,
|
|
722
787
|
categoryLabel: f,
|
|
723
788
|
settingsPending: u,
|
|
724
|
-
ensureSettings:
|
|
725
|
-
addCategory:
|
|
789
|
+
ensureSettings: g,
|
|
790
|
+
addCategory: _,
|
|
791
|
+
saveCategories: h,
|
|
726
792
|
DEFAULT_CATEGORIES: Z
|
|
727
793
|
};
|
|
728
794
|
}
|
|
729
795
|
//#endregion
|
|
730
796
|
//#region src/data/versionHistory.ts
|
|
731
|
-
var
|
|
797
|
+
var ke = [
|
|
798
|
+
{
|
|
799
|
+
version: "0.4.14",
|
|
800
|
+
date: "2026-08-03",
|
|
801
|
+
highlights: ["관리자가 카테고리 이름과 순서를 한 화면에서 관리할 수 있어요", "카테고리마다 일반·이미지·영상 목록 방식을 선택할 수 있어요"]
|
|
802
|
+
},
|
|
732
803
|
{
|
|
733
804
|
version: "0.2.2",
|
|
734
805
|
date: "2026-07-31",
|
|
@@ -755,19 +826,10 @@ var Te = [
|
|
|
755
826
|
];
|
|
756
827
|
//#endregion
|
|
757
828
|
//#region src/utils/formatDate.ts
|
|
758
|
-
function
|
|
829
|
+
function Ae(e) {
|
|
759
830
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
760
831
|
}
|
|
761
832
|
//#endregion
|
|
762
|
-
//#region src/utils/extractEditorImageUrls.ts
|
|
763
|
-
function De(e) {
|
|
764
|
-
if (!e) return [];
|
|
765
|
-
let t = /* @__PURE__ */ new Set();
|
|
766
|
-
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
767
|
-
for (let n of e.matchAll(/<img[^>]*\ssrc=["'](https?:\/\/[^"']+)["']/gi)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
768
|
-
return [...t];
|
|
769
|
-
}
|
|
770
|
-
//#endregion
|
|
771
833
|
//#region src/utils/renderMarkdown.ts
|
|
772
834
|
function Q(e) {
|
|
773
835
|
return e.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -776,7 +838,7 @@ function $(e) {
|
|
|
776
838
|
let t = e;
|
|
777
839
|
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;
|
|
778
840
|
}
|
|
779
|
-
function
|
|
841
|
+
function je(e) {
|
|
780
842
|
if (!e?.trim()) return "";
|
|
781
843
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
782
844
|
function c() {
|
|
@@ -841,6 +903,6 @@ function Oe(e) {
|
|
|
841
903
|
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");
|
|
842
904
|
}
|
|
843
905
|
//#endregion
|
|
844
|
-
export {
|
|
906
|
+
export { z as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, Z as DEFAULT_CATEGORIES, Ee as EDITOR_IMAGE_MAX_BYTES, Te as compressImage, L as configureMemiBoard, B as effectiveModerationBlockCount, pe as extractEditorImageUrls, Ae as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, je as renderMarkdownToHtml, me as storagePathFromDownloadUrl, G as useMemiBoardAuth, _e as useMemiBoardComments, R as useMemiBoardConfig, we as useMemiBoardModeration, ge as useMemiBoardPosts, Oe as useMemiBoardSettings, De as useMemiBoardStorage, ke as versionHistory };
|
|
845
907
|
|
|
846
908
|
//# sourceMappingURL=index.js.map
|