memi-board 0.5.0 → 0.7.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 +9 -8
- package/dist/composables/useMemiBoardAuth.d.ts +2 -0
- package/dist/composables/useMemiBoardUsers.d.ts +11 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +206 -145
- package/dist/index.js.map +1 -1
- package/dist/module.mjs +4 -0
- package/dist/module.mjs.map +1 -1
- package/dist/runtime/components/Settings.vue +24 -4
- package/dist/runtime/components/Users.vue +64 -0
- package/dist/types.d.ts +12 -2
- package/docs/firestore.rules.example +24 -11
- package/docs/storage.rules.example +6 -6
- 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, useFirebaseApp as o, useFirebaseAuth as s, useFirestore as c } from "vuefire";
|
|
3
3
|
import { collection as l, deleteDoc as u, deleteField as d, doc as f, endBefore as p, getDoc as m, getDocs as h, increment as g, limit as _, limitToLast as v, orderBy as y, query as b, serverTimestamp as x, setDoc as S, startAfter as C, updateDoc as w, where as T, writeBatch as E } from "firebase/firestore";
|
|
4
|
-
import { GoogleAuthProvider as D, OAuthProvider as ee, createUserWithEmailAndPassword as O, signInWithEmailAndPassword as te, signInWithPopup as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as
|
|
4
|
+
import { GoogleAuthProvider as D, OAuthProvider as ee, createUserWithEmailAndPassword as O, signInWithEmailAndPassword as te, signInWithPopup as ne, signOut as re, updateProfile as ie } from "firebase/auth";
|
|
5
|
+
import { deleteObject as k, getDownloadURL as A, getStorage as j, listAll as ae, ref as M, uploadBytes as N, uploadBytesResumable as oe } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as se, Schema as P, getAI as ce, getGenerativeModel as le } 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 ue(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 L() {
|
|
30
|
+
return I();
|
|
31
31
|
}
|
|
32
32
|
//#endregion
|
|
33
33
|
//#region src/utils/moderation-strike.ts
|
|
34
|
-
var
|
|
35
|
-
function
|
|
34
|
+
var R = 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 V(e, t, n = Date.now(), r = B) {
|
|
|
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 H(e, t, n = Date.now(), r = 3, i = B) {
|
|
|
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 de(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 ue(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 fe() {
|
|
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 = L(), n = fe(), i = c(), o = s(), l = a(), { role: u, rolePending: d, rawBlockCount: p, 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 f(i, `${t.collectionPrefix}Users`, e);
|
|
@@ -99,20 +99,28 @@ function K() {
|
|
|
99
99
|
p.value = 0, h.value = null;
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
102
|
-
p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value =
|
|
102
|
+
p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = de(e.moderationBlockAt);
|
|
103
103
|
}
|
|
104
104
|
async function C(e) {
|
|
105
105
|
let t = y(e.uid), n = await m(t);
|
|
106
106
|
if (!n.exists()) return await S(t, {
|
|
107
107
|
role: "user",
|
|
108
108
|
displayName: e.displayName,
|
|
109
|
+
email: e.email,
|
|
110
|
+
photoURL: e.photoURL,
|
|
109
111
|
updatedAt: x()
|
|
110
112
|
}), b({ moderationBlockCount: 0 }), "user";
|
|
111
113
|
let r = n.data();
|
|
112
114
|
return b(r), await w(t, {
|
|
113
115
|
displayName: e.displayName,
|
|
116
|
+
email: e.email,
|
|
117
|
+
photoURL: e.photoURL,
|
|
114
118
|
updatedAt: x()
|
|
115
|
-
}),
|
|
119
|
+
}), [
|
|
120
|
+
"admin",
|
|
121
|
+
"staff",
|
|
122
|
+
"user"
|
|
123
|
+
].includes(String(r.role)) ? r.role : "user";
|
|
116
124
|
}
|
|
117
125
|
async function T() {
|
|
118
126
|
let e = l.value?.uid;
|
|
@@ -136,20 +144,20 @@ function K() {
|
|
|
136
144
|
d.value = !1;
|
|
137
145
|
}
|
|
138
146
|
}, { immediate: !0 }));
|
|
139
|
-
let E = e(() => !!l.value),
|
|
147
|
+
let E = e(() => !!l.value), k = e(() => u.value === "admin"), A = e(() => u.value === "staff"), j = e(() => k.value || A.value), ae = e(() => B(p.value, h.value, Date.now(), v())), M = e(() => {
|
|
140
148
|
let e = _();
|
|
141
|
-
return e <= 0 ? !1 :
|
|
149
|
+
return e <= 0 ? !1 : H(p.value, h.value, Date.now(), e, v());
|
|
142
150
|
}), N = e(() => {
|
|
143
151
|
let e = _();
|
|
144
152
|
if (e <= 0) return null;
|
|
145
|
-
let t =
|
|
146
|
-
return t == null ? null :
|
|
147
|
-
}),
|
|
153
|
+
let t = V(p.value, h.value, Date.now(), e, v());
|
|
154
|
+
return t == null ? null : U(t);
|
|
155
|
+
}), oe = e(() => {
|
|
148
156
|
if (!M.value) return null;
|
|
149
157
|
let e = N.value;
|
|
150
158
|
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
151
159
|
});
|
|
152
|
-
async function
|
|
160
|
+
async function se() {
|
|
153
161
|
let e = l.value?.uid, t = _();
|
|
154
162
|
if (!e || t <= 0) return {
|
|
155
163
|
effectiveCount: 0,
|
|
@@ -170,9 +178,9 @@ function K() {
|
|
|
170
178
|
});
|
|
171
179
|
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
|
|
172
180
|
p.value = i, h.value = a;
|
|
173
|
-
let o =
|
|
181
|
+
let o = B(i, a, a, v()), s = H(i, a, a, t, v()), c = ` (경고 ${o}/${t})`;
|
|
174
182
|
if (s) {
|
|
175
|
-
let e =
|
|
183
|
+
let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
|
|
176
184
|
c = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
177
185
|
}
|
|
178
186
|
return {
|
|
@@ -181,65 +189,67 @@ function K() {
|
|
|
181
189
|
messageSuffix: c
|
|
182
190
|
};
|
|
183
191
|
}
|
|
184
|
-
function
|
|
192
|
+
function P() {
|
|
185
193
|
if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
186
194
|
return o;
|
|
187
195
|
}
|
|
188
|
-
async function
|
|
189
|
-
return
|
|
196
|
+
async function ce() {
|
|
197
|
+
return ne(P(), new D());
|
|
190
198
|
}
|
|
191
|
-
async function
|
|
199
|
+
async function le() {
|
|
192
200
|
let e = new ee("apple.com");
|
|
193
|
-
return e.addScope("email"), e.addScope("name"),
|
|
201
|
+
return e.addScope("email"), e.addScope("name"), ne(P(), e);
|
|
194
202
|
}
|
|
195
|
-
async function
|
|
196
|
-
return te(
|
|
203
|
+
async function F(e, t) {
|
|
204
|
+
return te(P(), e, t);
|
|
197
205
|
}
|
|
198
|
-
async function
|
|
199
|
-
let r = await O(
|
|
200
|
-
return n && await
|
|
206
|
+
async function I(e, t, n) {
|
|
207
|
+
let r = await O(P(), e, t);
|
|
208
|
+
return n && await ie(r.user, { displayName: n }), r;
|
|
201
209
|
}
|
|
202
|
-
async function
|
|
203
|
-
await
|
|
210
|
+
async function ue() {
|
|
211
|
+
await re(P());
|
|
204
212
|
}
|
|
205
213
|
function R(e) {
|
|
206
|
-
return E.value && (l.value?.uid === e.authorUid ||
|
|
214
|
+
return E.value && (l.value?.uid === e.authorUid || j.value);
|
|
207
215
|
}
|
|
208
|
-
function
|
|
209
|
-
return E.value && (l.value?.uid === e.authorUid ||
|
|
216
|
+
function W(e) {
|
|
217
|
+
return E.value && (l.value?.uid === e.authorUid || j.value);
|
|
210
218
|
}
|
|
211
219
|
return {
|
|
212
220
|
user: l,
|
|
213
221
|
isSignedIn: E,
|
|
214
|
-
isAdmin:
|
|
222
|
+
isAdmin: k,
|
|
223
|
+
isStaff: A,
|
|
224
|
+
canManageContent: j,
|
|
215
225
|
rolePending: d,
|
|
216
|
-
moderationBlockCount:
|
|
226
|
+
moderationBlockCount: ae,
|
|
217
227
|
isWriteRestricted: M,
|
|
218
228
|
restrictedUntilLabel: N,
|
|
219
|
-
restrictedMessage:
|
|
220
|
-
signInWithGoogle:
|
|
221
|
-
signInWithApple:
|
|
222
|
-
signInWithEmail:
|
|
223
|
-
signUpWithEmail:
|
|
224
|
-
signOut:
|
|
229
|
+
restrictedMessage: oe,
|
|
230
|
+
signInWithGoogle: ce,
|
|
231
|
+
signInWithApple: le,
|
|
232
|
+
signInWithEmail: F,
|
|
233
|
+
signUpWithEmail: I,
|
|
234
|
+
signOut: ue,
|
|
225
235
|
canEdit: R,
|
|
226
236
|
canDelete: R,
|
|
227
|
-
canDeleteComment:
|
|
228
|
-
recordContentModerationBlock:
|
|
237
|
+
canDeleteComment: W,
|
|
238
|
+
recordContentModerationBlock: se,
|
|
229
239
|
refreshBoardUser: T
|
|
230
240
|
};
|
|
231
241
|
}
|
|
232
242
|
//#endregion
|
|
233
243
|
//#region src/utils/slugify.ts
|
|
234
|
-
function
|
|
244
|
+
function K(e) {
|
|
235
245
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
236
246
|
}
|
|
237
|
-
function
|
|
247
|
+
function pe(e, t = 60) {
|
|
238
248
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
239
249
|
}
|
|
240
250
|
//#endregion
|
|
241
251
|
//#region src/utils/extractEditorImageUrls.ts
|
|
242
|
-
function
|
|
252
|
+
function me(e) {
|
|
243
253
|
if (!e) return [];
|
|
244
254
|
let t = /* @__PURE__ */ new Set();
|
|
245
255
|
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
@@ -248,7 +258,7 @@ function pe(e) {
|
|
|
248
258
|
}
|
|
249
259
|
//#endregion
|
|
250
260
|
//#region src/utils/storagePath.ts
|
|
251
|
-
function
|
|
261
|
+
function he(e) {
|
|
252
262
|
try {
|
|
253
263
|
let t = e.match(/\/o\/([^?]+)/);
|
|
254
264
|
return t?.[1] ? decodeURIComponent(t[1]) : null;
|
|
@@ -256,25 +266,25 @@ function me(e) {
|
|
|
256
266
|
return null;
|
|
257
267
|
}
|
|
258
268
|
}
|
|
259
|
-
function
|
|
269
|
+
function q(e) {
|
|
260
270
|
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
261
271
|
}
|
|
262
272
|
//#endregion
|
|
263
273
|
//#region src/utils/postPreview.ts
|
|
264
|
-
function
|
|
274
|
+
function J(e) {
|
|
265
275
|
return e.replace(/&/g, "&").replace(/"/g, "\"").replace(/'/g, "'");
|
|
266
276
|
}
|
|
267
|
-
function
|
|
277
|
+
function ge(e) {
|
|
268
278
|
try {
|
|
269
|
-
let t = new URL(
|
|
279
|
+
let t = new URL(J(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
|
|
270
280
|
if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
|
|
271
281
|
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;
|
|
272
282
|
} catch {
|
|
273
283
|
return;
|
|
274
284
|
}
|
|
275
285
|
}
|
|
276
|
-
function
|
|
277
|
-
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 ?
|
|
286
|
+
function _e(e, t = []) {
|
|
287
|
+
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 ? J(r[1]) : a, s = o ? ge(o) : void 0, c = n ? J(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
|
|
278
288
|
return {
|
|
279
289
|
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),
|
|
280
290
|
...c ? { previewImage: c } : {},
|
|
@@ -283,19 +293,19 @@ function ge(e, t = []) {
|
|
|
283
293
|
}
|
|
284
294
|
//#endregion
|
|
285
295
|
//#region src/composables/useMemiBoardPosts.ts
|
|
286
|
-
async function
|
|
287
|
-
let t = await
|
|
296
|
+
async function Y(e) {
|
|
297
|
+
let t = await ae(e).catch(() => ({
|
|
288
298
|
items: [],
|
|
289
299
|
prefixes: []
|
|
290
300
|
}));
|
|
291
|
-
await Promise.all([...t.items.map((e) =>
|
|
301
|
+
await Promise.all([...t.items.map((e) => k(e)), ...t.prefixes.map((e) => Y(e))]);
|
|
292
302
|
}
|
|
293
|
-
async function
|
|
303
|
+
async function ve(e, t) {
|
|
294
304
|
let n = [...new Set([...t].filter(Boolean))];
|
|
295
|
-
await Promise.all(n.map((t) =>
|
|
305
|
+
await Promise.all(n.map((t) => k(M(e, t)).catch(() => {})));
|
|
296
306
|
}
|
|
297
|
-
function
|
|
298
|
-
let e =
|
|
307
|
+
function ye() {
|
|
308
|
+
let e = L(), t = c(), n = o(), r = () => e.collectionPrefix, i = () => l(t, `${r()}Posts`), a = (e) => f(t, `${r()}Posts`, e), s = (e) => f(t, `${r()}Posts`, e, "body", "main"), g = (e) => l(t, `${r()}Posts`, e, "comments");
|
|
299
309
|
function D() {
|
|
300
310
|
return f(i()).id;
|
|
301
311
|
}
|
|
@@ -324,7 +334,7 @@ function ve() {
|
|
|
324
334
|
let n = (await h(b(i(), T("category", "==", e), T("slug", "==", t), _(1)))).docs[0];
|
|
325
335
|
return n ? O(n.id) : null;
|
|
326
336
|
}
|
|
327
|
-
async function
|
|
337
|
+
async function ne(e) {
|
|
328
338
|
if (!e.category || !e.createdAt) return {
|
|
329
339
|
previous: null,
|
|
330
340
|
next: null
|
|
@@ -341,10 +351,10 @@ function ve() {
|
|
|
341
351
|
next: a(r)
|
|
342
352
|
};
|
|
343
353
|
}
|
|
344
|
-
async function
|
|
345
|
-
let t =
|
|
354
|
+
async function re(e) {
|
|
355
|
+
let t = K(e.title) || "post", n = t, r = 1;
|
|
346
356
|
for (; !(await h(b(i(), T("category", "==", e.category || ""), T("slug", "==", n), _(1)))).empty;) r++, n = `${t}-${r}`;
|
|
347
|
-
let o = e.postId || D(), c =
|
|
357
|
+
let o = e.postId || D(), c = _e(e.content, e.attachments);
|
|
348
358
|
return await S(a(o), {
|
|
349
359
|
slug: n,
|
|
350
360
|
title: e.title,
|
|
@@ -362,8 +372,8 @@ function ve() {
|
|
|
362
372
|
updatedAt: x()
|
|
363
373
|
}), await S(s(o), { content: e.content }), n;
|
|
364
374
|
}
|
|
365
|
-
async function
|
|
366
|
-
let n =
|
|
375
|
+
async function ie(e, t) {
|
|
376
|
+
let n = _e(t.content, t.attachments);
|
|
367
377
|
await Promise.all([w(a(e), {
|
|
368
378
|
title: t.title,
|
|
369
379
|
...n,
|
|
@@ -375,7 +385,7 @@ function ve() {
|
|
|
375
385
|
updatedAt: x()
|
|
376
386
|
}), S(s(e), { content: t.content }, { merge: !0 })]);
|
|
377
387
|
}
|
|
378
|
-
async function
|
|
388
|
+
async function k(e) {
|
|
379
389
|
let [i, o] = await Promise.all([m(a(e)), m(s(e))]);
|
|
380
390
|
if (!i.exists()) return;
|
|
381
391
|
let c = i.data(), l = o.exists() ? String(o.data().content ?? "") : "", d = await h(g(e));
|
|
@@ -383,42 +393,42 @@ function ve() {
|
|
|
383
393
|
let n = E(t);
|
|
384
394
|
d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
385
395
|
}
|
|
386
|
-
let f =
|
|
396
|
+
let f = j(n), p = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
|
|
387
397
|
for (let t of c.attachments ?? []) if (t?.path) {
|
|
388
398
|
p.add(t.path);
|
|
389
|
-
let n =
|
|
399
|
+
let n = q(t.path);
|
|
390
400
|
n && n !== e && _.add(n);
|
|
391
401
|
}
|
|
392
|
-
for (let t of
|
|
393
|
-
let n =
|
|
402
|
+
for (let t of me(l)) {
|
|
403
|
+
let n = he(t);
|
|
394
404
|
if (!n) continue;
|
|
395
405
|
p.add(n);
|
|
396
406
|
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
397
407
|
r !== n && r.includes("/images/thumbnails/") && p.add(r);
|
|
398
408
|
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
399
409
|
i && !n.includes("/thumbnails/") && p.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
400
|
-
let a =
|
|
410
|
+
let a = q(n);
|
|
401
411
|
a && a !== e && _.add(a);
|
|
402
412
|
}
|
|
403
|
-
await u(s(e)), await
|
|
404
|
-
for (let e of _) await
|
|
405
|
-
await
|
|
413
|
+
await u(s(e)), await Y(M(f, `${r()}/posts/${e}`));
|
|
414
|
+
for (let e of _) await Y(M(f, `${r()}/posts/${e}`));
|
|
415
|
+
await ve(f, p), await u(a(e));
|
|
406
416
|
}
|
|
407
417
|
return {
|
|
408
418
|
createPostId: D,
|
|
409
419
|
getPosts: ee,
|
|
410
420
|
getPost: O,
|
|
411
421
|
getPostBySlug: te,
|
|
412
|
-
getAdjacentPosts:
|
|
413
|
-
createPost:
|
|
414
|
-
updatePost:
|
|
415
|
-
deletePost:
|
|
422
|
+
getAdjacentPosts: ne,
|
|
423
|
+
createPost: re,
|
|
424
|
+
updatePost: ie,
|
|
425
|
+
deletePost: k
|
|
416
426
|
};
|
|
417
427
|
}
|
|
418
428
|
//#endregion
|
|
419
429
|
//#region src/composables/useMemiBoardComments.ts
|
|
420
|
-
function
|
|
421
|
-
let n =
|
|
430
|
+
function be(t) {
|
|
431
|
+
let n = L(), r = c(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => b(l(r, `${a()}Posts`, o.value, "comments"), y("createdAt", "asc"))), u = i(s);
|
|
422
432
|
async function d(e) {
|
|
423
433
|
let t = E(r), n = a(), i = f(l(r, `${n}Posts`, o.value, "comments"));
|
|
424
434
|
t.set(i, {
|
|
@@ -443,7 +453,7 @@ function ye(t) {
|
|
|
443
453
|
}
|
|
444
454
|
//#endregion
|
|
445
455
|
//#region src/utils/moderation-prompt.ts
|
|
446
|
-
var
|
|
456
|
+
var xe = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", Se = {
|
|
447
457
|
abuse: "욕설·비속어·모욕",
|
|
448
458
|
spam: "스팸·광고",
|
|
449
459
|
adult: "선정적·성인 내용",
|
|
@@ -451,17 +461,17 @@ var be = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
|
|
|
451
461
|
other: "부적절한 내용",
|
|
452
462
|
none: ""
|
|
453
463
|
};
|
|
454
|
-
function
|
|
464
|
+
function X(e) {
|
|
455
465
|
if (e.via === "local" && e.localHit) {
|
|
456
466
|
let t = e.localHit.trim();
|
|
457
467
|
return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
|
|
458
468
|
}
|
|
459
469
|
let t = (e.reason || "").trim();
|
|
460
470
|
if (t) return t;
|
|
461
|
-
let n = e.category && e.category !== "none" ?
|
|
471
|
+
let n = e.category && e.category !== "none" ? Se[e.category] || e.category : "";
|
|
462
472
|
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
463
473
|
}
|
|
464
|
-
var
|
|
474
|
+
var Ce = [
|
|
465
475
|
"씨발",
|
|
466
476
|
"시발",
|
|
467
477
|
"시이발",
|
|
@@ -487,10 +497,10 @@ var Se = [
|
|
|
487
497
|
"bitch",
|
|
488
498
|
"asshole"
|
|
489
499
|
];
|
|
490
|
-
function
|
|
500
|
+
function we(e) {
|
|
491
501
|
return new RegExp(e.join("|"), "i");
|
|
492
502
|
}
|
|
493
|
-
var
|
|
503
|
+
var Te = /* @__PURE__ */ new Set([
|
|
494
504
|
"none",
|
|
495
505
|
"abuse",
|
|
496
506
|
"spam",
|
|
@@ -498,7 +508,7 @@ var we = /* @__PURE__ */ new Set([
|
|
|
498
508
|
"violence",
|
|
499
509
|
"other"
|
|
500
510
|
]);
|
|
501
|
-
function
|
|
511
|
+
function Ee(e) {
|
|
502
512
|
let t = e.trim();
|
|
503
513
|
if (!t) return null;
|
|
504
514
|
let n;
|
|
@@ -516,7 +526,7 @@ function Te(e) {
|
|
|
516
526
|
if (typeof n != "object" || !n) return null;
|
|
517
527
|
let r = n;
|
|
518
528
|
if (typeof r.flagged != "boolean") return null;
|
|
519
|
-
let i = typeof r.category == "string" &&
|
|
529
|
+
let i = typeof r.category == "string" && Te.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
520
530
|
return {
|
|
521
531
|
flagged: r.flagged,
|
|
522
532
|
category: i,
|
|
@@ -525,20 +535,20 @@ function Te(e) {
|
|
|
525
535
|
}
|
|
526
536
|
//#endregion
|
|
527
537
|
//#region src/composables/useMemiBoardModeration.ts
|
|
528
|
-
var
|
|
538
|
+
var Z = {
|
|
529
539
|
flagged: !1,
|
|
530
540
|
category: "none",
|
|
531
541
|
reason: "",
|
|
532
542
|
via: "empty"
|
|
533
543
|
};
|
|
534
544
|
function De() {
|
|
535
|
-
let e =
|
|
545
|
+
let e = L().moderation ?? {}, t = o(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = we([...Ce, ...e.localBlocklist ?? []]);
|
|
536
546
|
function s(e) {
|
|
537
547
|
let t = e.match(a);
|
|
538
548
|
return t ? {
|
|
539
549
|
flagged: !0,
|
|
540
550
|
category: "abuse",
|
|
541
|
-
reason:
|
|
551
|
+
reason: X({
|
|
542
552
|
via: "local",
|
|
543
553
|
localHit: t[0] || "",
|
|
544
554
|
category: "abuse"
|
|
@@ -547,21 +557,21 @@ function De() {
|
|
|
547
557
|
} : null;
|
|
548
558
|
}
|
|
549
559
|
function c() {
|
|
550
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
551
|
-
backend: new
|
|
560
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ce(t, {
|
|
561
|
+
backend: new se(),
|
|
552
562
|
useLimitedUseAppCheckTokens: r
|
|
553
563
|
});
|
|
554
|
-
return
|
|
564
|
+
return le(i, {
|
|
555
565
|
model: n,
|
|
556
|
-
systemInstruction:
|
|
566
|
+
systemInstruction: xe,
|
|
557
567
|
generationConfig: {
|
|
558
568
|
temperature: 0,
|
|
559
569
|
maxOutputTokens: 256,
|
|
560
570
|
responseMimeType: "application/json",
|
|
561
|
-
responseSchema:
|
|
562
|
-
flagged:
|
|
563
|
-
category:
|
|
564
|
-
reason:
|
|
571
|
+
responseSchema: P.object({ properties: {
|
|
572
|
+
flagged: P.boolean(),
|
|
573
|
+
category: P.string(),
|
|
574
|
+
reason: P.string()
|
|
565
575
|
} })
|
|
566
576
|
}
|
|
567
577
|
});
|
|
@@ -582,7 +592,7 @@ function De() {
|
|
|
582
592
|
};
|
|
583
593
|
}
|
|
584
594
|
function u(e, t = "") {
|
|
585
|
-
let n =
|
|
595
|
+
let n = X({
|
|
586
596
|
reason: e.reason,
|
|
587
597
|
category: e.category,
|
|
588
598
|
via: e.via
|
|
@@ -610,22 +620,22 @@ function De() {
|
|
|
610
620
|
};
|
|
611
621
|
let i = t.trim();
|
|
612
622
|
if (!i) return {
|
|
613
|
-
...
|
|
623
|
+
...Z,
|
|
614
624
|
via: "empty"
|
|
615
625
|
};
|
|
616
626
|
let a = s(i);
|
|
617
627
|
if (a) return d(a);
|
|
618
628
|
if (e.enabled === !1) return {
|
|
619
|
-
...
|
|
629
|
+
...Z,
|
|
620
630
|
via: "disabled"
|
|
621
631
|
};
|
|
622
632
|
try {
|
|
623
|
-
let e =
|
|
633
|
+
let e = Ee((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
624
634
|
if (!e) return l();
|
|
625
635
|
let t = {
|
|
626
636
|
flagged: e.flagged,
|
|
627
637
|
category: e.category,
|
|
628
|
-
reason:
|
|
638
|
+
reason: X({
|
|
629
639
|
reason: e.reason,
|
|
630
640
|
category: e.category,
|
|
631
641
|
via: "ai"
|
|
@@ -645,14 +655,14 @@ function De() {
|
|
|
645
655
|
via: "restricted"
|
|
646
656
|
};
|
|
647
657
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
648
|
-
...
|
|
658
|
+
...Z,
|
|
649
659
|
via: "disabled"
|
|
650
660
|
};
|
|
651
661
|
try {
|
|
652
662
|
let e = await new Promise((e, n) => {
|
|
653
663
|
let r = new FileReader();
|
|
654
664
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
655
|
-
}), n =
|
|
665
|
+
}), n = Ee((await c().generateContent([{ inlineData: {
|
|
656
666
|
mimeType: t.type || "image/jpeg",
|
|
657
667
|
data: e
|
|
658
668
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -660,7 +670,7 @@ function De() {
|
|
|
660
670
|
let r = {
|
|
661
671
|
flagged: n.flagged,
|
|
662
672
|
category: n.category,
|
|
663
|
-
reason:
|
|
673
|
+
reason: X({
|
|
664
674
|
reason: n.reason,
|
|
665
675
|
category: n.category,
|
|
666
676
|
via: "ai"
|
|
@@ -701,9 +711,9 @@ async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
|
701
711
|
//#region src/composables/useMemiBoardStorage.ts
|
|
702
712
|
var ke = 5242880;
|
|
703
713
|
function Ae() {
|
|
704
|
-
let e =
|
|
714
|
+
let e = L(), t = o(), n = () => e.collectionPrefix;
|
|
705
715
|
function r(e, r, i) {
|
|
706
|
-
let a =
|
|
716
|
+
let a = j(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${pe(e.name)}`, s = M(a, o), c = oe(s, e, {
|
|
707
717
|
contentType: e.type || "application/octet-stream",
|
|
708
718
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
709
719
|
});
|
|
@@ -712,7 +722,7 @@ function Ae() {
|
|
|
712
722
|
c.on("state_changed", (e) => {
|
|
713
723
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
714
724
|
}, n, async () => {
|
|
715
|
-
let n = await
|
|
725
|
+
let n = await A(s);
|
|
716
726
|
t({
|
|
717
727
|
name: e.name,
|
|
718
728
|
url: n,
|
|
@@ -726,28 +736,28 @@ function Ae() {
|
|
|
726
736
|
};
|
|
727
737
|
}
|
|
728
738
|
async function i(e) {
|
|
729
|
-
let n =
|
|
730
|
-
await
|
|
739
|
+
let n = j(t);
|
|
740
|
+
await k(M(n, e.path)).catch(() => {});
|
|
731
741
|
}
|
|
732
742
|
async function a(e, r) {
|
|
733
743
|
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
734
744
|
if (e.size > 5242880) throw Error(`이미지는 ${ke / 1024 / 1024}MB 이하여야 합니다.`);
|
|
735
|
-
let i =
|
|
736
|
-
await
|
|
737
|
-
let d = await
|
|
745
|
+
let i = j(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = pe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = M(i, l);
|
|
746
|
+
await N(u, e, { contentType: e.type || "image/png" });
|
|
747
|
+
let d = await A(u), f = await Oe(e, {
|
|
738
748
|
maxWidth: 400,
|
|
739
749
|
quality: .8
|
|
740
|
-
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m =
|
|
741
|
-
return await
|
|
750
|
+
}), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = M(i, p);
|
|
751
|
+
return await N(m, f, { contentType: "image/jpeg" }), {
|
|
742
752
|
originalUrl: d,
|
|
743
753
|
originalPath: l,
|
|
744
|
-
thumbnailUrl: await
|
|
754
|
+
thumbnailUrl: await A(m),
|
|
745
755
|
thumbnailPath: p
|
|
746
756
|
};
|
|
747
757
|
}
|
|
748
758
|
async function s(e) {
|
|
749
|
-
let n =
|
|
750
|
-
await Promise.all([
|
|
759
|
+
let n = j(t);
|
|
760
|
+
await Promise.all([k(M(n, e.originalPath)).catch(() => {}), k(M(n, e.thumbnailPath)).catch(() => {})]);
|
|
751
761
|
}
|
|
752
762
|
return {
|
|
753
763
|
uploadAttachment: r,
|
|
@@ -763,27 +773,31 @@ var je = [
|
|
|
763
773
|
id: "free",
|
|
764
774
|
label: "자유",
|
|
765
775
|
listView: "default",
|
|
776
|
+
writeRole: "user",
|
|
766
777
|
order: 0
|
|
767
778
|
},
|
|
768
779
|
{
|
|
769
780
|
id: "notice",
|
|
770
781
|
label: "공지",
|
|
771
782
|
listView: "default",
|
|
783
|
+
writeRole: "admin",
|
|
772
784
|
order: 1
|
|
773
785
|
},
|
|
774
786
|
{
|
|
775
787
|
id: "question",
|
|
776
788
|
label: "질문",
|
|
777
789
|
listView: "default",
|
|
790
|
+
writeRole: "user",
|
|
778
791
|
order: 2
|
|
779
792
|
}
|
|
780
793
|
];
|
|
781
794
|
function Me() {
|
|
782
|
-
let t =
|
|
795
|
+
let t = L(), n = c(), { isSignedIn: r } = G(), a = e(() => f(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => l(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => b(o.value, y("order", "asc"))), { data: d, pending: p } = i(s, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), m = e(() => d.value.map((e, t) => ({
|
|
783
796
|
...e,
|
|
784
797
|
id: e.id,
|
|
785
798
|
order: typeof e.order == "number" ? e.order : t,
|
|
786
|
-
listView: e.listView ?? "default"
|
|
799
|
+
listView: e.listView ?? "default",
|
|
800
|
+
writeRole: e.writeRole ?? "user"
|
|
787
801
|
})));
|
|
788
802
|
function h(e) {
|
|
789
803
|
if (e) return m.value.find((t) => t.id === e)?.label ?? e;
|
|
@@ -795,6 +809,7 @@ function Me() {
|
|
|
795
809
|
await S(a.value, { updatedAt: x() }, { merge: !0 }), await S(f(o.value, n), {
|
|
796
810
|
label: i,
|
|
797
811
|
listView: e.listView ?? "default",
|
|
812
|
+
writeRole: e.writeRole ?? "user",
|
|
798
813
|
order: t,
|
|
799
814
|
updatedAt: x()
|
|
800
815
|
}, { merge: !0 });
|
|
@@ -808,6 +823,7 @@ function Me() {
|
|
|
808
823
|
t.set(f(o.value, r), {
|
|
809
824
|
label: i,
|
|
810
825
|
listView: e.listView ?? "default",
|
|
826
|
+
writeRole: e.writeRole ?? "user",
|
|
811
827
|
order: n,
|
|
812
828
|
updatedAt: x()
|
|
813
829
|
}, { merge: !0 });
|
|
@@ -826,12 +842,13 @@ function Me() {
|
|
|
826
842
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
827
843
|
let n = m.value.find((e) => e.label === t);
|
|
828
844
|
if (n) return n.id;
|
|
829
|
-
let i =
|
|
845
|
+
let i = K(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
|
|
830
846
|
for (; s.has(a);) a = `${i}-${o++}`;
|
|
831
847
|
return await g({
|
|
832
848
|
id: a,
|
|
833
849
|
label: t,
|
|
834
850
|
listView: "default",
|
|
851
|
+
writeRole: "user",
|
|
835
852
|
order: m.value.length
|
|
836
853
|
}), a;
|
|
837
854
|
}
|
|
@@ -848,8 +865,52 @@ function Me() {
|
|
|
848
865
|
};
|
|
849
866
|
}
|
|
850
867
|
//#endregion
|
|
851
|
-
//#region src/
|
|
868
|
+
//#region src/composables/useMemiBoardUsers.ts
|
|
852
869
|
var Ne = [
|
|
870
|
+
{
|
|
871
|
+
value: "admin",
|
|
872
|
+
label: "관리자",
|
|
873
|
+
description: "사용자 역할, 게시판 설정과 모든 콘텐츠를 관리합니다."
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
value: "staff",
|
|
877
|
+
label: "스태프",
|
|
878
|
+
description: "모든 게시글과 댓글을 관리하지만 사용자 역할과 게시판 설정은 바꿀 수 없습니다."
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
value: "user",
|
|
882
|
+
label: "사용자",
|
|
883
|
+
description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
|
|
884
|
+
}
|
|
885
|
+
];
|
|
886
|
+
function Pe() {
|
|
887
|
+
let e = L(), t = c(), n = `${e.collectionPrefix}Users`, r = i(l(t, n), { ssrKey: `${n}-management` });
|
|
888
|
+
async function a(e, r) {
|
|
889
|
+
if (!Ne.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
|
|
890
|
+
await w(f(t, n, e), {
|
|
891
|
+
role: r,
|
|
892
|
+
updatedAt: x()
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
users: r,
|
|
897
|
+
usersPending: r.pending,
|
|
898
|
+
updateUserRole: a
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
//#endregion
|
|
902
|
+
//#region src/data/versionHistory.ts
|
|
903
|
+
var Fe = [
|
|
904
|
+
{
|
|
905
|
+
version: "0.7.0",
|
|
906
|
+
date: "2026-08-04",
|
|
907
|
+
highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"]
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
version: "0.6.0",
|
|
911
|
+
date: "2026-08-04",
|
|
912
|
+
highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"]
|
|
913
|
+
},
|
|
853
914
|
{
|
|
854
915
|
version: "0.5.0",
|
|
855
916
|
date: "2026-08-04",
|
|
@@ -896,7 +957,7 @@ var Ne = [
|
|
|
896
957
|
];
|
|
897
958
|
//#endregion
|
|
898
959
|
//#region src/utils/formatDate.ts
|
|
899
|
-
function
|
|
960
|
+
function Ie(e) {
|
|
900
961
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
901
962
|
}
|
|
902
963
|
//#endregion
|
|
@@ -908,7 +969,7 @@ function $(e) {
|
|
|
908
969
|
let t = e;
|
|
909
970
|
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;
|
|
910
971
|
}
|
|
911
|
-
function
|
|
972
|
+
function Le(e) {
|
|
912
973
|
if (!e?.trim()) return "";
|
|
913
974
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
914
975
|
function c() {
|
|
@@ -973,6 +1034,6 @@ function Fe(e) {
|
|
|
973
1034
|
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");
|
|
974
1035
|
}
|
|
975
1036
|
//#endregion
|
|
976
|
-
export {
|
|
1037
|
+
export { Ne as BOARD_USER_ROLES, z as DEFAULT_BLOCK_BAN_DECAY_MS, R as DEFAULT_BLOCK_BAN_THRESHOLD, je as DEFAULT_CATEGORIES, ke as EDITOR_IMAGE_MAX_BYTES, Oe as compressImage, ue as configureMemiBoard, B as effectiveModerationBlockCount, me as extractEditorImageUrls, Ie as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, Le as renderMarkdownToHtml, K as slugify, he as storagePathFromDownloadUrl, G as useMemiBoardAuth, be as useMemiBoardComments, L as useMemiBoardConfig, De as useMemiBoardModeration, ye as useMemiBoardPosts, Me as useMemiBoardSettings, Ae as useMemiBoardStorage, Pe as useMemiBoardUsers, Fe as versionHistory };
|
|
977
1038
|
|
|
978
1039
|
//# sourceMappingURL=index.js.map
|