memi-board 0.18.0 → 0.19.3
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 +163 -17
- package/dist/composables/fetchPublicSeo.d.ts +12 -0
- package/dist/config.d.ts +23 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +701 -456
- package/dist/index.js.map +1 -1
- package/dist/module.mjs +41 -14
- package/dist/module.mjs.map +1 -1
- package/dist/runtime/components/Detail.vue +4 -1
- package/dist/runtime/components/Editor.vue +552 -73
- package/dist/runtime/components/ListDefault.vue +1 -1
- package/dist/runtime/components/ListDense.vue +2 -2
- package/dist/runtime/components/ListImage.vue +10 -4
- package/dist/runtime/composables/useMemiBoardSeo.ts +252 -0
- package/dist/utils/boardSeo.d.ts +44 -0
- package/dist/utils/postBody.d.ts +19 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { computed as e, onScopeDispose as t, reactive as n, ref as r, toValue as i, watch as a } from "vue";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { GoogleAuthProvider as N, OAuthProvider as P, createUserWithEmailAndPassword as F, signInWithEmailAndPassword as I, signInWithPopup as L, signOut as R, updateProfile as
|
|
5
|
-
import { deleteObject as
|
|
6
|
-
import { GoogleAIBackend as
|
|
2
|
+
import { collection as o, deleteDoc as s, deleteField as c, doc as l, documentId as u, endBefore as d, getDoc as f, getDocs as p, increment as m, limit as h, limitToLast as g, onSnapshot as _, orderBy as v, query as y, runTransaction as b, serverTimestamp as x, setDoc as S, startAfter as C, updateDoc as w, where as T, writeBatch as E } from "firebase/firestore";
|
|
3
|
+
import { useCollection as D, useCurrentUser as O, useDocument as k, useFirebaseApp as A, useFirebaseAuth as j, useFirestore as M } from "vuefire";
|
|
4
|
+
import { GoogleAuthProvider as N, OAuthProvider as P, createUserWithEmailAndPassword as F, signInWithEmailAndPassword as I, signInWithPopup as L, signOut as R, updateProfile as z } from "firebase/auth";
|
|
5
|
+
import { deleteObject as B, getDownloadURL as V, getStorage as H, listAll as ee, ref as U, uploadBytes as te, uploadBytesResumable as ne } from "firebase/storage";
|
|
6
|
+
import { GoogleAIBackend as re, Schema as W, getAI as ie, getGenerativeModel as ae } from "firebase/ai";
|
|
7
7
|
//#region \0rolldown/runtime.js
|
|
8
|
-
var
|
|
9
|
-
if (t && typeof t == "object" || typeof t == "function") for (var i =
|
|
8
|
+
var oe = Object.create, se = Object.defineProperty, ce = Object.getOwnPropertyDescriptor, le = Object.getOwnPropertyNames, ue = Object.getPrototypeOf, de = Object.prototype.hasOwnProperty, fe = (e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t.exports), pe = (e, t, n, r) => {
|
|
9
|
+
if (t && typeof t == "object" || typeof t == "function") for (var i = le(t), a = 0, o = i.length, s; a < o; a++) s = i[a], !de.call(e, s) && s !== n && se(e, s, {
|
|
10
10
|
get: ((e) => t[e]).bind(null, s),
|
|
11
|
-
enumerable: !(r =
|
|
11
|
+
enumerable: !(r = ce(t, s)) || r.enumerable
|
|
12
12
|
});
|
|
13
13
|
return e;
|
|
14
|
-
}, me = (e, t, n) => (n = e == null ? {} :
|
|
14
|
+
}, me = (e, t, n) => (n = e == null ? {} : oe(ue(e)), pe(t || !e || !e.__esModule || !de.call(e, "default") ? se(n, "default", {
|
|
15
15
|
value: e,
|
|
16
16
|
enumerable: !0
|
|
17
17
|
}) : n, e)), he = "__MEMI_BOARD_CONFIG__";
|
|
@@ -28,20 +28,203 @@ function ge() {
|
|
|
28
28
|
localBlocklist: [],
|
|
29
29
|
blockBanThreshold: 3,
|
|
30
30
|
blockBanDecayMs: 864e5
|
|
31
|
+
},
|
|
32
|
+
seo: {
|
|
33
|
+
enabled: !0,
|
|
34
|
+
siteName: "Board",
|
|
35
|
+
siteUrl: "",
|
|
36
|
+
defaultOgImage: "",
|
|
37
|
+
basePath: "/board"
|
|
31
38
|
}
|
|
32
39
|
})), e[he];
|
|
33
40
|
}
|
|
34
41
|
function _e(e) {
|
|
35
42
|
let t = ge();
|
|
36
|
-
e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
|
|
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);
|
|
37
44
|
}
|
|
38
45
|
function G() {
|
|
39
46
|
return ge();
|
|
40
47
|
}
|
|
41
48
|
//#endregion
|
|
49
|
+
//#region src/utils/boardSeo.ts
|
|
50
|
+
function ve(e) {
|
|
51
|
+
if (typeof e != "string") return null;
|
|
52
|
+
let t = e.trim();
|
|
53
|
+
return /^https?:\/\//i.test(t) ? t : null;
|
|
54
|
+
}
|
|
55
|
+
function ye(e) {
|
|
56
|
+
let t = ve(e);
|
|
57
|
+
if (!t) return null;
|
|
58
|
+
if (!/firebasestorage\.googleapis\.com/i.test(t)) return t;
|
|
59
|
+
try {
|
|
60
|
+
let e = new URL(t), n = e.pathname.split("/"), r = n[3], i = n.indexOf("o");
|
|
61
|
+
if (!r || i < 0 || !n[i + 1]) return t;
|
|
62
|
+
let a = decodeURIComponent(n.slice(i + 1).join("/")), o = a;
|
|
63
|
+
if (!a.includes("/images/thumbnails/")) {
|
|
64
|
+
let e = a.replace(/\/images\/([^/]+)$/, (e, t) => `/images/thumbnails/${String(t).replace(/\.[^.]+$/, "")}.jpg`);
|
|
65
|
+
if (e === a) return t;
|
|
66
|
+
o = e;
|
|
67
|
+
}
|
|
68
|
+
return `${e.origin}/v0/b/${r}/o/${encodeURIComponent(o)}?alt=media`;
|
|
69
|
+
} catch {
|
|
70
|
+
return t;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function be(e, t, n = "") {
|
|
74
|
+
let r = t.replace(/\/$/, ""), i = e.trim();
|
|
75
|
+
return i ? /^https?:\/\//i.test(i) ? i : `${r}${i.startsWith("/") ? i : `/${i}`}` : n ? be(n, t) : r;
|
|
76
|
+
}
|
|
77
|
+
function xe(e, t, n, r) {
|
|
78
|
+
let i = r?.trim().slice(0, 40), a = e.trim() || i || "사진", o = t?.trim(), s = n.trim() || "Board";
|
|
79
|
+
return o ? `${a} · ${o} | ${s}` : `${a} | ${s}`;
|
|
80
|
+
}
|
|
81
|
+
function Se(e) {
|
|
82
|
+
let t = [];
|
|
83
|
+
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
|
+
}
|
|
85
|
+
function Ce(e, t) {
|
|
86
|
+
let n = t.trim() || "Board", r = e?.trim();
|
|
87
|
+
return r && r !== "전체" ? `${r} 게시판 | ${n}` : `게시판 | ${n}`;
|
|
88
|
+
}
|
|
89
|
+
function we(e) {
|
|
90
|
+
if (e.description?.trim()) return e.description.trim().slice(0, 200);
|
|
91
|
+
let t = e.categoryLabel?.trim(), n = t && t !== "전체" ? `${t} 게시판 — 소식을 확인하세요.` : "게시판 — 공지와 이야기를 확인하세요.", r = (e.recentTitles || []).filter(Boolean).slice(0, 3);
|
|
92
|
+
return r.length ? `${n} ${r.map((e) => `「${e}」`).join(" ")}`.slice(0, 200) : n.slice(0, 200);
|
|
93
|
+
}
|
|
94
|
+
function Te(e) {
|
|
95
|
+
let t = (e || "/board").trim() || "/board";
|
|
96
|
+
return (t.startsWith("/") ? t : `/${t}`).replace(/\/$/, "") || "/board";
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/composables/fetchPublicSeo.ts
|
|
100
|
+
function Ee() {
|
|
101
|
+
return {
|
|
102
|
+
db: M(),
|
|
103
|
+
prefix: G().collectionPrefix
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function De(e) {
|
|
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) {
|
|
113
|
+
try {
|
|
114
|
+
let n = await f(Oe(e, t));
|
|
115
|
+
if (!n.exists()) return {
|
|
116
|
+
label: t,
|
|
117
|
+
description: "",
|
|
118
|
+
hidden: !1
|
|
119
|
+
};
|
|
120
|
+
let r = n.data() ?? {};
|
|
121
|
+
return {
|
|
122
|
+
label: typeof r.label == "string" && r.label.trim() ? r.label.trim() : t,
|
|
123
|
+
description: typeof r.description == "string" ? r.description.trim() : "",
|
|
124
|
+
hidden: r.visibility === "hidden"
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return {
|
|
128
|
+
label: t,
|
|
129
|
+
description: "",
|
|
130
|
+
hidden: !1
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function Ae(e) {
|
|
135
|
+
if (e && typeof e == "object" && "toDate" in e) try {
|
|
136
|
+
let t = e.toDate();
|
|
137
|
+
return t instanceof Date ? t.toISOString() : null;
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
async function je(e, t, n) {
|
|
144
|
+
let r = e.trim(), i = t.trim();
|
|
145
|
+
if (!r || !i) return null;
|
|
146
|
+
let a = n ?? Ee(), o = await ke(a, r);
|
|
147
|
+
if (o.hidden) return null;
|
|
148
|
+
try {
|
|
149
|
+
let e = (await p(y(De(a), T("category", "==", r), T("slug", "==", i), T("listed", "==", !0), h(1)))).docs[0];
|
|
150
|
+
if (!e) return null;
|
|
151
|
+
let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title.trim() : "";
|
|
152
|
+
return n ? {
|
|
153
|
+
id: e.id,
|
|
154
|
+
slug: typeof t.slug == "string" ? t.slug : i,
|
|
155
|
+
category: r,
|
|
156
|
+
categoryLabel: o.label,
|
|
157
|
+
title: n,
|
|
158
|
+
summary: typeof t.summary == "string" ? t.summary.trim().slice(0, 200) : "",
|
|
159
|
+
previewImage: ye(t.previewImage),
|
|
160
|
+
authorName: typeof t.authorName == "string" && t.authorName.trim() ? t.authorName.trim() : null,
|
|
161
|
+
createdAt: Ae(t.createdAt)
|
|
162
|
+
} : null;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return import.meta.dev && console.warn("[memi-board] fetchPublicPostForSeo failed", e), null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function Me(e, t) {
|
|
168
|
+
let n = e?.trim() || null, r = t ?? Ee();
|
|
169
|
+
if (n) {
|
|
170
|
+
let e = await ke(r, n);
|
|
171
|
+
if (e.hidden) return {
|
|
172
|
+
kind: "category",
|
|
173
|
+
category: n,
|
|
174
|
+
categoryLabel: e.label,
|
|
175
|
+
description: "",
|
|
176
|
+
ogImage: null,
|
|
177
|
+
recentTitles: []
|
|
178
|
+
};
|
|
179
|
+
let { recentTitles: t, ogImage: i } = await Ne(r, n);
|
|
180
|
+
return {
|
|
181
|
+
kind: "category",
|
|
182
|
+
category: n,
|
|
183
|
+
categoryLabel: e.label,
|
|
184
|
+
description: e.description || `${e.label} 게시판`,
|
|
185
|
+
ogImage: i,
|
|
186
|
+
recentTitles: t
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
let { recentTitles: i, ogImage: a } = await Ne(r, null);
|
|
190
|
+
return {
|
|
191
|
+
kind: "all",
|
|
192
|
+
category: null,
|
|
193
|
+
categoryLabel: "전체",
|
|
194
|
+
description: "게시판 — 공지와 커뮤니티 소식을 확인하세요.",
|
|
195
|
+
ogImage: a,
|
|
196
|
+
recentTitles: i
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function Ne(e, t) {
|
|
200
|
+
let n = [], r = null;
|
|
201
|
+
try {
|
|
202
|
+
let i = t ? [
|
|
203
|
+
T("category", "==", t),
|
|
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) {
|
|
213
|
+
let t = e.data() ?? {}, i = typeof t.title == "string" ? t.title.trim() : "";
|
|
214
|
+
i && n.length < 5 && n.push(i), r ||= ye(t.previewImage);
|
|
215
|
+
}
|
|
216
|
+
} catch (e) {
|
|
217
|
+
import.meta.dev && console.warn("[memi-board] loadRecentListed failed", e);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
recentTitles: n,
|
|
221
|
+
ogImage: r
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
42
225
|
//#region src/utils/moderation-strike.ts
|
|
43
|
-
var
|
|
44
|
-
function
|
|
226
|
+
var Pe = 3, K = 864e5;
|
|
227
|
+
function Fe(e, t, n = Date.now(), r = K) {
|
|
45
228
|
let i = Math.max(0, Math.floor(Number(e) || 0));
|
|
46
229
|
if (i <= 0) return 0;
|
|
47
230
|
let a = typeof t == "number" && Number.isFinite(t) ? t : null;
|
|
@@ -57,17 +240,17 @@ function q(e, t, n = Date.now(), r = 3, i = K) {
|
|
|
57
240
|
let s = o + (a - (r - 1)) * i;
|
|
58
241
|
return s > n ? s : null;
|
|
59
242
|
}
|
|
60
|
-
function
|
|
243
|
+
function Ie(e, t, n = Date.now(), r = 3, i = K) {
|
|
61
244
|
return q(e, t, n, r, i) != null;
|
|
62
245
|
}
|
|
63
|
-
function
|
|
246
|
+
function Le(e, t = Date.now()) {
|
|
64
247
|
let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
|
|
65
248
|
if (r <= 0) return "곧 해제";
|
|
66
249
|
if (r < 24) return `약 ${r}시간 후 해제`;
|
|
67
250
|
let i = Math.floor(r / 24), a = r % 24;
|
|
68
251
|
return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
|
|
69
252
|
}
|
|
70
|
-
function
|
|
253
|
+
function Re(e) {
|
|
71
254
|
if (e == null) return null;
|
|
72
255
|
if (typeof e == "number" && Number.isFinite(e)) return e;
|
|
73
256
|
if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
|
|
@@ -79,117 +262,117 @@ function Se(e) {
|
|
|
79
262
|
}
|
|
80
263
|
//#endregion
|
|
81
264
|
//#region src/composables/useMemiBoardAuth.ts
|
|
82
|
-
var
|
|
83
|
-
function
|
|
265
|
+
var ze = "__MEMI_BOARD_AUTH_STATE__";
|
|
266
|
+
function Be() {
|
|
84
267
|
let e = globalThis;
|
|
85
|
-
return e[
|
|
268
|
+
return e[ze] || (e[ze] = {
|
|
86
269
|
role: r("user"),
|
|
87
270
|
rolePending: r(!0),
|
|
88
271
|
rawBlockCount: r(0),
|
|
89
272
|
blockAtMs: r(null),
|
|
90
273
|
watchStarted: !1
|
|
91
|
-
}), e[
|
|
274
|
+
}), e[ze];
|
|
92
275
|
}
|
|
93
276
|
function J() {
|
|
94
|
-
let t = G(), n =
|
|
95
|
-
function
|
|
277
|
+
let t = G(), n = Be(), r = M(), i = j(), o = O(), { role: s, rolePending: c, rawBlockCount: u, blockAtMs: d } = n;
|
|
278
|
+
function p() {
|
|
96
279
|
let e = t.moderation?.blockBanThreshold;
|
|
97
280
|
return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
|
|
98
281
|
}
|
|
99
|
-
function
|
|
282
|
+
function h() {
|
|
100
283
|
let e = t.moderation?.blockBanDecayMs;
|
|
101
284
|
return typeof e == "number" && e > 0 ? e : K;
|
|
102
285
|
}
|
|
103
|
-
function
|
|
104
|
-
return
|
|
286
|
+
function g(e) {
|
|
287
|
+
return l(r, `${t.collectionPrefix}Users`, e);
|
|
105
288
|
}
|
|
106
|
-
function
|
|
289
|
+
function _(e) {
|
|
107
290
|
if (!e) {
|
|
108
|
-
|
|
291
|
+
u.value = 0, d.value = null;
|
|
109
292
|
return;
|
|
110
293
|
}
|
|
111
|
-
|
|
294
|
+
u.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), d.value = Re(e.moderationBlockAt);
|
|
112
295
|
}
|
|
113
|
-
async function
|
|
114
|
-
let t =
|
|
115
|
-
if (!n.exists()) return await
|
|
296
|
+
async function v(e) {
|
|
297
|
+
let t = g(e.uid), n = await f(t);
|
|
298
|
+
if (!n.exists()) return await S(t, {
|
|
116
299
|
role: "user",
|
|
117
300
|
displayName: e.displayName,
|
|
118
301
|
email: e.email,
|
|
119
302
|
photoURL: e.photoURL,
|
|
120
|
-
updatedAt:
|
|
121
|
-
}),
|
|
303
|
+
updatedAt: x()
|
|
304
|
+
}), _({ moderationBlockCount: 0 }), "user";
|
|
122
305
|
let r = n.data();
|
|
123
|
-
return
|
|
306
|
+
return _(r), await w(t, {
|
|
124
307
|
displayName: e.displayName,
|
|
125
308
|
email: e.email,
|
|
126
309
|
photoURL: e.photoURL,
|
|
127
|
-
updatedAt:
|
|
310
|
+
updatedAt: x()
|
|
128
311
|
}), [
|
|
129
312
|
"admin",
|
|
130
313
|
"staff",
|
|
131
314
|
"user"
|
|
132
315
|
].includes(String(r.role)) ? r.role : "user";
|
|
133
316
|
}
|
|
134
|
-
async function
|
|
317
|
+
async function y() {
|
|
135
318
|
let e = o.value?.uid;
|
|
136
319
|
if (!e) {
|
|
137
|
-
|
|
320
|
+
_(void 0);
|
|
138
321
|
return;
|
|
139
322
|
}
|
|
140
|
-
let t = await
|
|
141
|
-
|
|
323
|
+
let t = await f(g(e));
|
|
324
|
+
_(t.exists() ? t.data() : void 0);
|
|
142
325
|
}
|
|
143
326
|
n.watchStarted || (n.watchStarted = !0, a(o, async (e) => {
|
|
144
|
-
if (
|
|
145
|
-
|
|
327
|
+
if (c.value = !0, !e) {
|
|
328
|
+
s.value = "user", _(void 0), c.value = !1;
|
|
146
329
|
return;
|
|
147
330
|
}
|
|
148
331
|
try {
|
|
149
|
-
|
|
332
|
+
s.value = await v(e);
|
|
150
333
|
} catch {
|
|
151
|
-
|
|
334
|
+
s.value = "user";
|
|
152
335
|
} finally {
|
|
153
|
-
|
|
336
|
+
c.value = !1;
|
|
154
337
|
}
|
|
155
338
|
}, { immediate: !0 }));
|
|
156
|
-
let
|
|
157
|
-
let e =
|
|
158
|
-
return e <= 0 ? !1 :
|
|
159
|
-
}),
|
|
160
|
-
let e =
|
|
339
|
+
let b = e(() => !!o.value), C = e(() => s.value === "admin"), T = e(() => s.value === "staff"), E = e(() => C.value || T.value), D = e(() => Fe(u.value, d.value, Date.now(), h())), k = e(() => {
|
|
340
|
+
let e = p();
|
|
341
|
+
return e <= 0 ? !1 : Ie(u.value, d.value, Date.now(), e, h());
|
|
342
|
+
}), A = e(() => {
|
|
343
|
+
let e = p();
|
|
161
344
|
if (e <= 0) return null;
|
|
162
|
-
let t = q(
|
|
163
|
-
return t == null ? null :
|
|
164
|
-
}),
|
|
165
|
-
if (!
|
|
166
|
-
let e =
|
|
345
|
+
let t = q(u.value, d.value, Date.now(), e, h());
|
|
346
|
+
return t == null ? null : Le(t);
|
|
347
|
+
}), B = e(() => {
|
|
348
|
+
if (!k.value) return null;
|
|
349
|
+
let e = A.value;
|
|
167
350
|
return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
|
|
168
351
|
});
|
|
169
|
-
async function
|
|
170
|
-
let e = o.value?.uid, t =
|
|
352
|
+
async function V() {
|
|
353
|
+
let e = o.value?.uid, t = p();
|
|
171
354
|
if (!e || t <= 0) return {
|
|
172
355
|
effectiveCount: 0,
|
|
173
356
|
restricted: !1,
|
|
174
357
|
messageSuffix: ""
|
|
175
358
|
};
|
|
176
|
-
let n =
|
|
177
|
-
r.exists() ? await
|
|
178
|
-
moderationBlockCount:
|
|
179
|
-
moderationBlockAt:
|
|
180
|
-
updatedAt:
|
|
181
|
-
}) : await
|
|
359
|
+
let n = g(e), r = await f(n);
|
|
360
|
+
r.exists() ? await w(n, {
|
|
361
|
+
moderationBlockCount: m(1),
|
|
362
|
+
moderationBlockAt: x(),
|
|
363
|
+
updatedAt: x()
|
|
364
|
+
}) : await S(n, {
|
|
182
365
|
role: "user",
|
|
183
366
|
displayName: o.value?.displayName ?? null,
|
|
184
367
|
moderationBlockCount: 1,
|
|
185
|
-
moderationBlockAt:
|
|
186
|
-
updatedAt:
|
|
368
|
+
moderationBlockAt: x(),
|
|
369
|
+
updatedAt: x()
|
|
187
370
|
});
|
|
188
371
|
let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
|
|
189
|
-
|
|
190
|
-
let s =
|
|
372
|
+
u.value = i, d.value = a;
|
|
373
|
+
let s = Fe(i, a, a, h()), c = Ie(i, a, a, t, h()), l = ` (경고 ${s}/${t})`;
|
|
191
374
|
if (c) {
|
|
192
|
-
let e = q(i, a, a, t,
|
|
375
|
+
let e = q(i, a, a, t, h()), n = e == null ? null : Le(e, a);
|
|
193
376
|
l = n ? ` 경고 ${s}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${s}회가 되어 글·댓글 작성이 제한됩니다.`;
|
|
194
377
|
}
|
|
195
378
|
return {
|
|
@@ -198,71 +381,71 @@ function J() {
|
|
|
198
381
|
messageSuffix: l
|
|
199
382
|
};
|
|
200
383
|
}
|
|
201
|
-
function
|
|
384
|
+
function H() {
|
|
202
385
|
if (!i) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
|
|
203
386
|
return i;
|
|
204
387
|
}
|
|
205
|
-
async function
|
|
206
|
-
return L(
|
|
388
|
+
async function ee() {
|
|
389
|
+
return L(H(), new N());
|
|
207
390
|
}
|
|
208
|
-
async function
|
|
391
|
+
async function U() {
|
|
209
392
|
let e = new P("apple.com");
|
|
210
|
-
return e.addScope("email"), e.addScope("name"), L(
|
|
393
|
+
return e.addScope("email"), e.addScope("name"), L(H(), e);
|
|
211
394
|
}
|
|
212
|
-
async function
|
|
213
|
-
return I(
|
|
395
|
+
async function te(e, t) {
|
|
396
|
+
return I(H(), e, t);
|
|
214
397
|
}
|
|
215
|
-
async function
|
|
216
|
-
let r = await F(
|
|
217
|
-
return n && await
|
|
398
|
+
async function ne(e, t, n) {
|
|
399
|
+
let r = await F(H(), e, t);
|
|
400
|
+
return n && await z(r.user, { displayName: n }), r;
|
|
218
401
|
}
|
|
219
|
-
async function
|
|
220
|
-
await R(
|
|
402
|
+
async function re() {
|
|
403
|
+
await R(H());
|
|
221
404
|
}
|
|
222
|
-
function
|
|
223
|
-
return
|
|
405
|
+
function W(e) {
|
|
406
|
+
return b.value && (o.value?.uid === e.authorUid || E.value);
|
|
224
407
|
}
|
|
225
|
-
function
|
|
226
|
-
return
|
|
408
|
+
function ie(e) {
|
|
409
|
+
return b.value && o.value?.uid === e.authorUid;
|
|
227
410
|
}
|
|
228
|
-
function
|
|
229
|
-
return
|
|
411
|
+
function ae(e) {
|
|
412
|
+
return b.value && (o.value?.uid === e.authorUid || E.value);
|
|
230
413
|
}
|
|
231
414
|
return {
|
|
232
415
|
user: o,
|
|
233
|
-
isSignedIn:
|
|
234
|
-
isAdmin:
|
|
416
|
+
isSignedIn: b,
|
|
417
|
+
isAdmin: C,
|
|
235
418
|
isStaff: T,
|
|
236
419
|
canManageContent: E,
|
|
237
|
-
rolePending:
|
|
238
|
-
moderationBlockCount:
|
|
239
|
-
isWriteRestricted:
|
|
240
|
-
restrictedUntilLabel:
|
|
241
|
-
restrictedMessage:
|
|
242
|
-
signInWithGoogle:
|
|
243
|
-
signInWithApple:
|
|
244
|
-
signInWithEmail:
|
|
245
|
-
signUpWithEmail:
|
|
246
|
-
signOut:
|
|
247
|
-
canEdit:
|
|
248
|
-
canDelete:
|
|
249
|
-
canEditComment:
|
|
250
|
-
canDeleteComment:
|
|
251
|
-
recordContentModerationBlock:
|
|
252
|
-
refreshBoardUser:
|
|
420
|
+
rolePending: c,
|
|
421
|
+
moderationBlockCount: D,
|
|
422
|
+
isWriteRestricted: k,
|
|
423
|
+
restrictedUntilLabel: A,
|
|
424
|
+
restrictedMessage: B,
|
|
425
|
+
signInWithGoogle: ee,
|
|
426
|
+
signInWithApple: U,
|
|
427
|
+
signInWithEmail: te,
|
|
428
|
+
signUpWithEmail: ne,
|
|
429
|
+
signOut: re,
|
|
430
|
+
canEdit: W,
|
|
431
|
+
canDelete: W,
|
|
432
|
+
canEditComment: ie,
|
|
433
|
+
canDeleteComment: ae,
|
|
434
|
+
recordContentModerationBlock: V,
|
|
435
|
+
refreshBoardUser: y
|
|
253
436
|
};
|
|
254
437
|
}
|
|
255
438
|
//#endregion
|
|
256
439
|
//#region src/utils/slugify.ts
|
|
257
|
-
function
|
|
440
|
+
function Ve(e) {
|
|
258
441
|
return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
259
442
|
}
|
|
260
|
-
function
|
|
443
|
+
function He(e, t = 60) {
|
|
261
444
|
return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
|
|
262
445
|
}
|
|
263
446
|
//#endregion
|
|
264
447
|
//#region src/utils/extractEditorImageUrls.ts
|
|
265
|
-
function
|
|
448
|
+
function Ue(e) {
|
|
266
449
|
if (!e) return [];
|
|
267
450
|
let t = /* @__PURE__ */ new Set();
|
|
268
451
|
for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&", "&"));
|
|
@@ -271,7 +454,7 @@ function De(e) {
|
|
|
271
454
|
}
|
|
272
455
|
//#endregion
|
|
273
456
|
//#region src/utils/storagePath.ts
|
|
274
|
-
function
|
|
457
|
+
function We(e) {
|
|
275
458
|
try {
|
|
276
459
|
let t = e.match(/\/o\/([^?]+)/);
|
|
277
460
|
return t?.[1] ? decodeURIComponent(t[1]) : null;
|
|
@@ -279,29 +462,50 @@ function Oe(e) {
|
|
|
279
462
|
return null;
|
|
280
463
|
}
|
|
281
464
|
}
|
|
282
|
-
function
|
|
465
|
+
function Ge(e) {
|
|
283
466
|
return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
|
|
284
467
|
}
|
|
285
468
|
//#endregion
|
|
469
|
+
//#region src/utils/postBody.ts
|
|
470
|
+
function Ke(e) {
|
|
471
|
+
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
|
+
}
|
|
473
|
+
function qe(e, t = 40) {
|
|
474
|
+
let n = Ke(e);
|
|
475
|
+
if (!n) return "";
|
|
476
|
+
if (n.length <= t) return n;
|
|
477
|
+
let r = n.slice(0, t), i = r.lastIndexOf(" ");
|
|
478
|
+
return `${(i >= Math.floor(t * .6) ? r.slice(0, i) : r).trimEnd()}…`;
|
|
479
|
+
}
|
|
480
|
+
function Je(e) {
|
|
481
|
+
return Ke(e).length > 0;
|
|
482
|
+
}
|
|
483
|
+
function Ye(e, t) {
|
|
484
|
+
return /<img\b/i.test(String(e || "")) ? !0 : (t || []).some((e) => !!e?.type?.startsWith("image/"));
|
|
485
|
+
}
|
|
486
|
+
function Xe(e) {
|
|
487
|
+
return !Je(e);
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
286
490
|
//#region src/utils/postPreview.ts
|
|
287
|
-
function
|
|
491
|
+
function Ze(e) {
|
|
288
492
|
return e.replace(/&/g, "&").replace(/"/g, "\"").replace(/'/g, "'");
|
|
289
493
|
}
|
|
290
|
-
function
|
|
494
|
+
function Qe(e) {
|
|
291
495
|
if (e) try {
|
|
292
|
-
let t = new URL(
|
|
496
|
+
let t = new URL(Ze(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
|
|
293
497
|
if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
|
|
294
498
|
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;
|
|
295
499
|
} catch {
|
|
296
500
|
return;
|
|
297
501
|
}
|
|
298
502
|
}
|
|
299
|
-
function
|
|
300
|
-
let t =
|
|
503
|
+
function $e(e) {
|
|
504
|
+
let t = Qe(e.videoUrl);
|
|
301
505
|
return t ? `https://i.ytimg.com/vi/${t}/hqdefault.jpg` : e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
|
|
302
506
|
}
|
|
303
|
-
function
|
|
304
|
-
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 ?
|
|
507
|
+
function et(e, t = []) {
|
|
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 ? Ze(r[1]) : a, s = o ? Qe(o) : void 0, c = n ? Ze(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
|
|
305
509
|
return {
|
|
306
510
|
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),
|
|
307
511
|
...c ? { previewImage: c } : {},
|
|
@@ -310,7 +514,7 @@ function Ne(e, t = []) {
|
|
|
310
514
|
}
|
|
311
515
|
//#endregion
|
|
312
516
|
//#region src/composables/useMemiBoardSettings.ts
|
|
313
|
-
var
|
|
517
|
+
var tt = [
|
|
314
518
|
{
|
|
315
519
|
id: "free",
|
|
316
520
|
label: "자유",
|
|
@@ -339,49 +543,49 @@ var Pe = [
|
|
|
339
543
|
order: 2
|
|
340
544
|
}
|
|
341
545
|
];
|
|
342
|
-
function
|
|
546
|
+
function nt(e) {
|
|
343
547
|
return e === "hidden" ? "hidden" : "public";
|
|
344
548
|
}
|
|
345
|
-
function
|
|
346
|
-
let t = G(), n =
|
|
549
|
+
function rt() {
|
|
550
|
+
let t = G(), n = M(), { isSignedIn: r } = J(), i = e(() => l(n, `${t.collectionPrefix}Settings`, "config")), a = e(() => o(n, `${t.collectionPrefix}Settings`, "config", "categories")), c = e(() => y(a.value, v("order", "asc"))), { data: u, pending: d } = D(c, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), f = e(() => u.value.map((e, t) => ({
|
|
347
551
|
...e,
|
|
348
552
|
id: e.id,
|
|
349
553
|
order: typeof e.order == "number" ? e.order : t,
|
|
350
554
|
description: e.description ?? e.desc ?? "",
|
|
351
|
-
visibility:
|
|
555
|
+
visibility: nt(e.visibility),
|
|
352
556
|
listView: e.listView ?? "default",
|
|
353
557
|
writeRole: e.writeRole ?? "user",
|
|
354
558
|
commentWriteRole: e.commentWriteRole ?? "user",
|
|
355
559
|
allowedStaffUids: e.allowedStaffUids ?? []
|
|
356
|
-
}))), m = e(() =>
|
|
560
|
+
}))), m = e(() => f.value.filter((e) => e.visibility !== "hidden"));
|
|
357
561
|
function g(e) {
|
|
358
|
-
if (e) return
|
|
562
|
+
if (e) return f.value.find((t) => t.id === e)?.label ?? e;
|
|
359
563
|
}
|
|
360
564
|
function _(e) {
|
|
361
|
-
if (e) return
|
|
362
|
-
}
|
|
363
|
-
function v(e) {
|
|
364
|
-
return e ? u.value.find((t) => t.id === e)?.visibility ?? "public" : "public";
|
|
565
|
+
if (e) return f.value.find((t) => t.id === e)?.description?.trim() || void 0;
|
|
365
566
|
}
|
|
366
567
|
function b(e) {
|
|
367
|
-
return
|
|
568
|
+
return e ? f.value.find((t) => t.id === e)?.visibility ?? "public" : "public";
|
|
569
|
+
}
|
|
570
|
+
function w(e) {
|
|
571
|
+
return b(e) === "hidden";
|
|
368
572
|
}
|
|
369
|
-
async function
|
|
370
|
-
let i =
|
|
573
|
+
async function O(e, r) {
|
|
574
|
+
let i = o(n, `${t.collectionPrefix}Posts`), a;
|
|
371
575
|
for (;;) {
|
|
372
|
-
let t = await y(
|
|
576
|
+
let t = await p(y(i, T("category", "==", e), v("__name__"), ...a ? [C(a)] : [], h(400)));
|
|
373
577
|
if (t.empty) break;
|
|
374
|
-
let o =
|
|
578
|
+
let o = E(n);
|
|
375
579
|
for (let e of t.docs) o.update(e.ref, { listed: r });
|
|
376
580
|
if (await o.commit(), a = t.docs[t.docs.length - 1], t.docs.length < 400) break;
|
|
377
581
|
}
|
|
378
582
|
}
|
|
379
|
-
async function
|
|
583
|
+
async function k(e, t = e.order ?? 0) {
|
|
380
584
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
381
585
|
let n = e.id.trim(), o = e.label.trim();
|
|
382
586
|
if (!n || !o) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
383
|
-
let s =
|
|
384
|
-
await
|
|
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), {
|
|
385
589
|
label: o,
|
|
386
590
|
description: (e.description ?? "").trim(),
|
|
387
591
|
visibility: s,
|
|
@@ -390,19 +594,19 @@ function Ie() {
|
|
|
390
594
|
commentWriteRole: e.commentWriteRole ?? "user",
|
|
391
595
|
allowedStaffUids: e.allowedStaffUids ?? [],
|
|
392
596
|
order: t,
|
|
393
|
-
updatedAt:
|
|
394
|
-
}, { merge: !0 }), (c !== s || s === "public") && await
|
|
597
|
+
updatedAt: x()
|
|
598
|
+
}, { merge: !0 }), (c !== s || s === "public") && await O(n, s !== "hidden");
|
|
395
599
|
}
|
|
396
|
-
async function
|
|
600
|
+
async function A(e) {
|
|
397
601
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
398
|
-
let t = new Map(
|
|
399
|
-
o.set(i.value, { updatedAt:
|
|
602
|
+
let t = new Map(f.value.map((e) => [e.id, e.visibility ?? "public"])), o = E(n);
|
|
603
|
+
o.set(i.value, { updatedAt: x() }, { merge: !0 });
|
|
400
604
|
let s = [];
|
|
401
605
|
e.forEach((e, n) => {
|
|
402
606
|
let r = e.id.trim(), i = e.label.trim();
|
|
403
607
|
if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
|
|
404
|
-
let c =
|
|
405
|
-
o.set(
|
|
608
|
+
let c = nt(e.visibility);
|
|
609
|
+
o.set(l(a.value, r), {
|
|
406
610
|
label: i,
|
|
407
611
|
description: (e.description ?? "").trim(),
|
|
408
612
|
visibility: c,
|
|
@@ -411,17 +615,17 @@ function Ie() {
|
|
|
411
615
|
commentWriteRole: e.commentWriteRole ?? "user",
|
|
412
616
|
allowedStaffUids: e.allowedStaffUids ?? [],
|
|
413
617
|
order: n,
|
|
414
|
-
updatedAt:
|
|
618
|
+
updatedAt: x()
|
|
415
619
|
}, { merge: !0 }), (t.get(r) ?? "public") !== c && s.push({
|
|
416
620
|
id: r,
|
|
417
621
|
listed: c !== "hidden"
|
|
418
622
|
});
|
|
419
623
|
}), await o.commit();
|
|
420
|
-
for (let e of s) await
|
|
624
|
+
for (let e of s) await O(e.id, e.listed);
|
|
421
625
|
}
|
|
422
|
-
async function
|
|
626
|
+
async function j(e) {
|
|
423
627
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
424
|
-
await
|
|
628
|
+
await s(l(a.value, e));
|
|
425
629
|
}
|
|
426
630
|
async function N() {
|
|
427
631
|
return Promise.resolve();
|
|
@@ -430,66 +634,78 @@ function Ie() {
|
|
|
430
634
|
let t = e.trim();
|
|
431
635
|
if (!t) throw Error("카테고리 이름을 입력해 주세요.");
|
|
432
636
|
if (!r.value) throw Error("로그인이 필요합니다.");
|
|
433
|
-
let n =
|
|
637
|
+
let n = f.value.find((e) => e.label === t);
|
|
434
638
|
if (n) return n.id;
|
|
435
|
-
let i =
|
|
639
|
+
let i = Ve(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(f.value.map((e) => e.id));
|
|
436
640
|
for (; s.has(a);) a = `${i}-${o++}`;
|
|
437
|
-
return await
|
|
641
|
+
return await k({
|
|
438
642
|
id: a,
|
|
439
643
|
label: t,
|
|
440
644
|
visibility: "public",
|
|
441
645
|
listView: "default",
|
|
442
646
|
writeRole: "user",
|
|
443
647
|
commentWriteRole: "user",
|
|
444
|
-
order:
|
|
648
|
+
order: f.value.length
|
|
445
649
|
}), a;
|
|
446
650
|
}
|
|
447
651
|
return {
|
|
448
|
-
categories:
|
|
652
|
+
categories: f,
|
|
449
653
|
publicCategories: m,
|
|
450
654
|
categoryLabel: g,
|
|
451
655
|
categoryDescription: _,
|
|
452
|
-
categoryVisibility:
|
|
453
|
-
isCategoryHidden:
|
|
454
|
-
settingsPending:
|
|
656
|
+
categoryVisibility: b,
|
|
657
|
+
isCategoryHidden: w,
|
|
658
|
+
settingsPending: d,
|
|
455
659
|
ensureSettings: N,
|
|
456
660
|
addCategory: P,
|
|
457
|
-
saveCategory:
|
|
458
|
-
saveCategories:
|
|
459
|
-
deleteCategory:
|
|
460
|
-
DEFAULT_CATEGORIES:
|
|
661
|
+
saveCategory: k,
|
|
662
|
+
saveCategories: A,
|
|
663
|
+
deleteCategory: j,
|
|
664
|
+
DEFAULT_CATEGORIES: tt
|
|
461
665
|
};
|
|
462
666
|
}
|
|
463
667
|
//#endregion
|
|
464
668
|
//#region src/composables/useMemiBoardPosts.ts
|
|
465
|
-
async function
|
|
466
|
-
let t = await
|
|
669
|
+
async function it(e) {
|
|
670
|
+
let t = await ee(e).catch(() => ({
|
|
467
671
|
items: [],
|
|
468
672
|
prefixes: []
|
|
469
673
|
}));
|
|
470
|
-
await Promise.all([...t.items.map((e) =>
|
|
674
|
+
await Promise.all([...t.items.map((e) => B(e)), ...t.prefixes.map((e) => it(e))]);
|
|
471
675
|
}
|
|
472
|
-
async function
|
|
676
|
+
async function at(e, t) {
|
|
473
677
|
let n = [...new Set([...t].filter(Boolean))];
|
|
474
|
-
await Promise.all(n.map((t) =>
|
|
678
|
+
await Promise.all(n.map((t) => B(U(e, t)).catch(() => {})));
|
|
475
679
|
}
|
|
476
|
-
function
|
|
680
|
+
function ot(e) {
|
|
477
681
|
if (!e || typeof e != "object") return !1;
|
|
478
682
|
let t = "code" in e ? String(e.code) : "";
|
|
479
683
|
return t === "permission-denied" || t === "firestore/permission-denied";
|
|
480
684
|
}
|
|
481
|
-
function
|
|
482
|
-
let e = G(), t =
|
|
483
|
-
function
|
|
685
|
+
function st() {
|
|
686
|
+
let e = G(), t = M(), n = A(), { isCategoryHidden: r, categories: i } = rt(), a = () => e.collectionPrefix, u = () => o(t, `${a()}Posts`), m = (e) => l(t, `${a()}Posts`, e), _ = (e) => l(t, `${a()}Posts`, e, "body", "main"), b = (e) => o(t, `${a()}Posts`, e, "comments");
|
|
687
|
+
function D(e) {
|
|
484
688
|
return !r(e);
|
|
485
689
|
}
|
|
486
|
-
function
|
|
487
|
-
return
|
|
690
|
+
function O(e) {
|
|
691
|
+
return e ? i.value.find((t) => t.id === e)?.listView === "image" : !1;
|
|
692
|
+
}
|
|
693
|
+
function k(e) {
|
|
694
|
+
let t = O(e.category);
|
|
695
|
+
if (!Je(e.content)) throw Error(t ? "내용을 입력해 주세요." : "본문에 글자를 입력해 주세요. 이미지나 첨부만으로는 게시할 수 없습니다.");
|
|
696
|
+
if (t) {
|
|
697
|
+
if (!Ye(e.content, e.attachments)) throw Error("사진을 올려 주세요.");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (!e.title?.trim()) throw Error("제목을 입력해 주세요.");
|
|
488
701
|
}
|
|
489
|
-
|
|
702
|
+
function j() {
|
|
703
|
+
return l(u()).id;
|
|
704
|
+
}
|
|
705
|
+
async function N(e = {}) {
|
|
490
706
|
let t = e.pageSize ?? 20, n = [];
|
|
491
|
-
e.category && n.push(
|
|
492
|
-
let r = await y(
|
|
707
|
+
e.category && n.push(T("category", "==", e.category)), e.publicOnly !== !1 && n.push(T("listed", "==", !0)), n.push(v("createdAt", "desc")), e.cursor && n.push(C(e.cursor)), n.push(h(t + 1));
|
|
708
|
+
let r = await p(y(u(), ...n)), i = r.docs.slice(0, t);
|
|
493
709
|
return {
|
|
494
710
|
posts: i.map((e) => ({
|
|
495
711
|
id: e.id,
|
|
@@ -499,48 +715,48 @@ function Be() {
|
|
|
499
715
|
hasMore: r.docs.length > t
|
|
500
716
|
};
|
|
501
717
|
}
|
|
502
|
-
async function
|
|
503
|
-
let [t, n] = await Promise.all([
|
|
718
|
+
async function P(e) {
|
|
719
|
+
let [t, n] = await Promise.all([f(m(e)), f(_(e))]);
|
|
504
720
|
return t.exists() ? {
|
|
505
721
|
id: t.id,
|
|
506
722
|
...t.data(),
|
|
507
723
|
content: n.exists() ? n.data().content ?? "" : ""
|
|
508
724
|
} : null;
|
|
509
725
|
}
|
|
510
|
-
async function
|
|
726
|
+
async function F(e, t) {
|
|
511
727
|
try {
|
|
512
|
-
let n = (await y(
|
|
728
|
+
let n = (await p(y(u(), T("category", "==", e), T("slug", "==", t), h(1)))).docs[0];
|
|
513
729
|
if (!n) return { status: "not-found" };
|
|
514
730
|
try {
|
|
515
|
-
let e = await
|
|
731
|
+
let e = await P(n.id);
|
|
516
732
|
return e ? {
|
|
517
733
|
status: "ok",
|
|
518
734
|
post: e
|
|
519
735
|
} : { status: "not-found" };
|
|
520
736
|
} catch (e) {
|
|
521
|
-
return
|
|
737
|
+
return ot(e) ? { status: "permission-denied" } : {
|
|
522
738
|
status: "error",
|
|
523
739
|
message: e instanceof Error ? e.message : String(e)
|
|
524
740
|
};
|
|
525
741
|
}
|
|
526
742
|
} catch (e) {
|
|
527
|
-
return
|
|
743
|
+
return ot(e) ? { status: "permission-denied" } : {
|
|
528
744
|
status: "error",
|
|
529
745
|
message: e instanceof Error ? e.message : String(e)
|
|
530
746
|
};
|
|
531
747
|
}
|
|
532
748
|
}
|
|
533
|
-
async function
|
|
749
|
+
async function I(e) {
|
|
534
750
|
if (!e.category || !e.createdAt) return {
|
|
535
751
|
previous: null,
|
|
536
752
|
next: null
|
|
537
753
|
};
|
|
538
754
|
try {
|
|
539
755
|
let t = [
|
|
540
|
-
|
|
541
|
-
...e.listed === !1 ? [] : [
|
|
542
|
-
|
|
543
|
-
], [n, r] = await Promise.all([y(
|
|
756
|
+
T("category", "==", e.category),
|
|
757
|
+
...e.listed === !1 ? [] : [T("listed", "==", !0)],
|
|
758
|
+
v("createdAt", "desc")
|
|
759
|
+
], [n, r] = await Promise.all([p(y(u(), ...t, C(e.createdAt), h(1))), p(y(u(), ...t, d(e.createdAt), g(1)))]), i = (e) => {
|
|
544
760
|
let t = e.docs[0];
|
|
545
761
|
return t ? {
|
|
546
762
|
id: t.id,
|
|
@@ -558,236 +774,245 @@ function Be() {
|
|
|
558
774
|
};
|
|
559
775
|
}
|
|
560
776
|
}
|
|
561
|
-
async function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
777
|
+
async function L(e) {
|
|
778
|
+
k(e);
|
|
779
|
+
let t = e.postId || j(), n = O(e.category), r, i;
|
|
780
|
+
if (n) r = t, i = qe(e.content);
|
|
781
|
+
else {
|
|
782
|
+
i = e.title.trim();
|
|
783
|
+
let t = Ve(i) || "post";
|
|
784
|
+
r = t;
|
|
785
|
+
let n = 1;
|
|
786
|
+
for (; !(await p(y(u(), T("category", "==", e.category || ""), T("slug", "==", r), h(1)))).empty;) n++, r = `${t}-${n}`;
|
|
787
|
+
}
|
|
788
|
+
let a = et(e.content, e.attachments), o = D(e.category);
|
|
789
|
+
return await S(m(t), {
|
|
790
|
+
slug: r,
|
|
791
|
+
title: i,
|
|
792
|
+
...a,
|
|
569
793
|
tags: e.tags ?? [],
|
|
570
794
|
...e.category ? { category: e.category } : {},
|
|
571
795
|
attachments: e.attachments ?? [],
|
|
572
796
|
commentCount: 0,
|
|
573
797
|
likeCount: 0,
|
|
574
798
|
viewCount: 0,
|
|
575
|
-
listed:
|
|
799
|
+
listed: o,
|
|
576
800
|
authorUid: e.authorUid,
|
|
577
801
|
authorName: e.authorName,
|
|
578
802
|
authorPhoto: e.authorPhoto,
|
|
579
803
|
moderationStatus: "approved",
|
|
580
804
|
moderationModel: e.moderationModel ?? null,
|
|
581
|
-
createdAt:
|
|
582
|
-
updatedAt:
|
|
583
|
-
}), await
|
|
584
|
-
}
|
|
585
|
-
async function
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
805
|
+
createdAt: x(),
|
|
806
|
+
updatedAt: x()
|
|
807
|
+
}), await S(_(t), { content: e.content }), r;
|
|
808
|
+
}
|
|
809
|
+
async function R(e, t) {
|
|
810
|
+
k(t);
|
|
811
|
+
let n = O(t.category) ? qe(t.content) : t.title.trim(), r = et(t.content, t.attachments), i = D(t.category);
|
|
812
|
+
await Promise.all([w(m(e), {
|
|
813
|
+
title: n,
|
|
814
|
+
...r,
|
|
815
|
+
previewImage: r.previewImage ? r.previewImage : c(),
|
|
816
|
+
videoUrl: r.videoUrl ? r.videoUrl : c(),
|
|
592
817
|
tags: t.tags ?? [],
|
|
593
|
-
category: t.category ? t.category :
|
|
594
|
-
listed:
|
|
818
|
+
category: t.category ? t.category : c(),
|
|
819
|
+
listed: i,
|
|
595
820
|
attachments: t.attachments ?? [],
|
|
596
|
-
updatedAt:
|
|
597
|
-
}),
|
|
821
|
+
updatedAt: x()
|
|
822
|
+
}), S(_(e), { content: t.content }, { merge: !0 })]);
|
|
598
823
|
}
|
|
599
|
-
async function
|
|
600
|
-
let [r,
|
|
824
|
+
async function z(e) {
|
|
825
|
+
let [r, i] = await Promise.all([f(m(e)), f(_(e))]);
|
|
601
826
|
if (!r.exists()) return;
|
|
602
|
-
let
|
|
603
|
-
for (let e = 0; e <
|
|
604
|
-
let n =
|
|
605
|
-
|
|
827
|
+
let o = r.data(), c = i.exists() ? String(i.data().content ?? "") : "", l = await p(b(e));
|
|
828
|
+
for (let e = 0; e < l.docs.length; e += 450) {
|
|
829
|
+
let n = E(t);
|
|
830
|
+
l.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
|
|
606
831
|
}
|
|
607
|
-
let
|
|
608
|
-
for (let t of
|
|
609
|
-
|
|
610
|
-
let n =
|
|
832
|
+
let u = H(n), d = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
|
|
833
|
+
for (let t of o.attachments ?? []) if (t?.path) {
|
|
834
|
+
d.add(t.path);
|
|
835
|
+
let n = Ge(t.path);
|
|
611
836
|
n && n !== e && h.add(n);
|
|
612
837
|
}
|
|
613
|
-
for (let t of
|
|
614
|
-
let n =
|
|
838
|
+
for (let t of Ue(c)) {
|
|
839
|
+
let n = We(t);
|
|
615
840
|
if (!n) continue;
|
|
616
|
-
|
|
841
|
+
d.add(n);
|
|
617
842
|
let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
|
|
618
|
-
r !== n && r.includes("/images/thumbnails/") &&
|
|
843
|
+
r !== n && r.includes("/images/thumbnails/") && d.add(r);
|
|
619
844
|
let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
|
|
620
|
-
i && !n.includes("/thumbnails/") &&
|
|
621
|
-
let a =
|
|
845
|
+
i && !n.includes("/thumbnails/") && d.add(`${i[1]}thumbnails/${i[2]}.jpg`);
|
|
846
|
+
let a = Ge(n);
|
|
622
847
|
a && a !== e && h.add(a);
|
|
623
848
|
}
|
|
624
|
-
await
|
|
625
|
-
for (let e of h) await
|
|
626
|
-
await
|
|
849
|
+
await s(_(e)), await it(U(u, `${a()}/posts/${e}`));
|
|
850
|
+
for (let e of h) await it(U(u, `${a()}/posts/${e}`));
|
|
851
|
+
await at(u, d), await s(m(e));
|
|
627
852
|
}
|
|
628
853
|
return {
|
|
629
|
-
createPostId:
|
|
630
|
-
getPosts:
|
|
631
|
-
getPost:
|
|
632
|
-
getPostBySlug:
|
|
633
|
-
getAdjacentPosts:
|
|
634
|
-
createPost:
|
|
635
|
-
updatePost:
|
|
636
|
-
deletePost:
|
|
854
|
+
createPostId: j,
|
|
855
|
+
getPosts: N,
|
|
856
|
+
getPost: P,
|
|
857
|
+
getPostBySlug: F,
|
|
858
|
+
getAdjacentPosts: I,
|
|
859
|
+
createPost: L,
|
|
860
|
+
updatePost: R,
|
|
861
|
+
deletePost: z
|
|
637
862
|
};
|
|
638
863
|
}
|
|
639
|
-
var
|
|
640
|
-
function
|
|
641
|
-
let
|
|
864
|
+
var ct = 10;
|
|
865
|
+
function lt(n, i = {}) {
|
|
866
|
+
let s = G(), c = M(), { isAdmin: l, isStaff: d } = J(), f = () => s.collectionPrefix, m = () => o(c, `${f()}Posts`), g = i.pageSize ?? ct, b = e(() => typeof n == "string" || n === void 0 ? n : n.value), x = e(() => i.publicOnly === void 0 ? !(l.value || d.value) : typeof i.publicOnly == "boolean" ? i.publicOnly : i.publicOnly.value), S = r([]), w = r([]), E = r(!0), D = r(!0), O = r(!1), k = r(""), A = null, j = null, N;
|
|
642
867
|
function P(e) {
|
|
643
868
|
return e.createdAt?.toMillis?.() ?? 0;
|
|
644
869
|
}
|
|
645
870
|
let F = e(() => {
|
|
646
871
|
let e = /* @__PURE__ */ new Map();
|
|
647
|
-
for (let t of [...
|
|
872
|
+
for (let t of [...w.value, ...S.value]) t.id && e.set(t.id, t);
|
|
648
873
|
return [...e.values()].sort((e, t) => P(t) - P(e) || String(t.id).localeCompare(String(e.id)));
|
|
649
874
|
});
|
|
650
875
|
function I() {
|
|
651
|
-
let e = [], t =
|
|
652
|
-
return t && e.push(
|
|
876
|
+
let e = [], t = b.value;
|
|
877
|
+
return t && e.push(T("category", "==", t)), x.value && e.push(T("listed", "==", !0)), e;
|
|
653
878
|
}
|
|
654
879
|
function L() {
|
|
655
|
-
N?.(), N =
|
|
656
|
-
|
|
880
|
+
N?.(), N = _(y(m(), ...I(), v("createdAt", "desc"), v(u(), "desc"), h(g)), (e) => {
|
|
881
|
+
S.value = e.docs.map((e) => ({
|
|
657
882
|
id: e.id,
|
|
658
883
|
...e.data()
|
|
659
|
-
})), A = e.docs.at(-1) ?? null,
|
|
884
|
+
})), A = e.docs.at(-1) ?? null, j || (D.value = e.docs.length >= g), E.value = !1;
|
|
660
885
|
}, (e) => {
|
|
661
|
-
console.error("[memi-board:postList] onSnapshot failed", e),
|
|
886
|
+
console.error("[memi-board:postList] onSnapshot failed", e), E.value = !1;
|
|
662
887
|
});
|
|
663
888
|
}
|
|
664
889
|
async function R() {
|
|
665
|
-
if (!(
|
|
666
|
-
|
|
890
|
+
if (!(O.value || !D.value || E.value)) {
|
|
891
|
+
O.value = !0, k.value = "";
|
|
667
892
|
try {
|
|
668
|
-
let e =
|
|
893
|
+
let e = j ?? A, t = [
|
|
669
894
|
...I(),
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
...e ? [
|
|
673
|
-
|
|
674
|
-
], n = await y(
|
|
895
|
+
v("createdAt", "desc"),
|
|
896
|
+
v(u(), "desc"),
|
|
897
|
+
...e ? [C(e)] : [],
|
|
898
|
+
h(g + 1)
|
|
899
|
+
], n = await p(y(m(), ...t)), r = n.docs.slice(0, g), i = r.map((e) => ({
|
|
675
900
|
id: e.id,
|
|
676
901
|
...e.data()
|
|
677
|
-
})), a = new Set([...
|
|
678
|
-
|
|
902
|
+
})), a = new Set([...w.value, ...S.value].map((e) => e.id));
|
|
903
|
+
w.value.push(...i.filter((e) => !a.has(e.id))), j = r.at(-1) ?? j, D.value = n.docs.length > g;
|
|
679
904
|
} catch (e) {
|
|
680
|
-
|
|
905
|
+
k.value = e.message || String(e), console.error("[memi-board:postList] loadMore failed", e);
|
|
681
906
|
} finally {
|
|
682
|
-
|
|
907
|
+
O.value = !1;
|
|
683
908
|
}
|
|
684
909
|
}
|
|
685
910
|
}
|
|
686
|
-
function
|
|
687
|
-
N?.(),
|
|
911
|
+
function z() {
|
|
912
|
+
N?.(), S.value = [], w.value = [], A = null, j = null, D.value = !0, k.value = "", E.value = !0, L();
|
|
688
913
|
}
|
|
689
|
-
return a([
|
|
914
|
+
return a([b, x], z, { immediate: !0 }), t(() => N?.()), {
|
|
690
915
|
posts: F,
|
|
691
|
-
postsPending:
|
|
692
|
-
hasMore:
|
|
693
|
-
loadingMore:
|
|
694
|
-
loadError:
|
|
916
|
+
postsPending: E,
|
|
917
|
+
hasMore: D,
|
|
918
|
+
loadingMore: O,
|
|
919
|
+
loadError: k,
|
|
695
920
|
loadMore: R
|
|
696
921
|
};
|
|
697
922
|
}
|
|
698
|
-
function
|
|
699
|
-
let n = G(), r =
|
|
923
|
+
function ut(t) {
|
|
924
|
+
let n = G(), r = M(), i = () => n.collectionPrefix, a = e(() => typeof t == "string" ? t : t.value), o = e(() => l(r, `${i()}Posts`, a.value)), s = e(() => l(r, `${i()}Posts`, a.value, "body", "main")), { data: c, pending: u } = k(o), { data: d, pending: f } = k(s);
|
|
700
925
|
return {
|
|
701
|
-
post: e(() =>
|
|
702
|
-
...
|
|
926
|
+
post: e(() => c.value ? {
|
|
927
|
+
...c.value,
|
|
703
928
|
id: a.value,
|
|
704
|
-
content:
|
|
929
|
+
content: d.value?.content ?? ""
|
|
705
930
|
} : null),
|
|
706
|
-
pending: e(() => u.value ||
|
|
931
|
+
pending: e(() => u.value || f.value)
|
|
707
932
|
};
|
|
708
933
|
}
|
|
709
934
|
//#endregion
|
|
710
935
|
//#region src/composables/useMemiBoardComments.ts
|
|
711
|
-
var
|
|
712
|
-
function
|
|
936
|
+
var dt = 10, ft = 5, pt = 1e3;
|
|
937
|
+
function mt(e) {
|
|
713
938
|
let t = e.trim();
|
|
714
939
|
if (!t) throw Error("댓글 내용을 입력해 주세요.");
|
|
715
|
-
if (t.length > 1e3) throw Error(`댓글은 ${
|
|
940
|
+
if (t.length > 1e3) throw Error(`댓글은 ${pt.toLocaleString()}자까지 작성할 수 있습니다.`);
|
|
716
941
|
return t;
|
|
717
942
|
}
|
|
718
|
-
function
|
|
719
|
-
let
|
|
943
|
+
function ht(n, i = {}) {
|
|
944
|
+
let s = G(), c = M(), d = () => s.collectionPrefix, f = e(() => typeof n == "string" ? n : n.value), g = r([]), b = r([]), S = r(i.subscribe !== !1), D = r([]), O = r(!1), k = r(i.subscribe !== !1), A = r(i.subscribe !== !1), j = null, N, P;
|
|
720
945
|
function F(e) {
|
|
721
946
|
return e.createdAt?.toMillis?.() ?? 0;
|
|
722
947
|
}
|
|
723
948
|
let I = e(() => {
|
|
724
949
|
let e = /* @__PURE__ */ new Map();
|
|
725
950
|
for (let t of [
|
|
726
|
-
...
|
|
727
|
-
...
|
|
728
|
-
...
|
|
951
|
+
...g.value,
|
|
952
|
+
...b.value,
|
|
953
|
+
...D.value
|
|
729
954
|
]) t.id && e.set(t.id, t);
|
|
730
955
|
return [...e.values()].sort((e, t) => F(e) - F(t) || String(e.id).localeCompare(String(t.id)));
|
|
731
956
|
});
|
|
732
|
-
a(
|
|
733
|
-
if (
|
|
734
|
-
|
|
957
|
+
a(f, async (e) => {
|
|
958
|
+
if (D.value = [], g.value = [], b.value = [], j = null, clearTimeout(N), N = void 0, P?.(), P = void 0, k.value = i.subscribe !== !1, i.subscribe !== !1) {
|
|
959
|
+
S.value = !0, A.value = !0;
|
|
735
960
|
try {
|
|
736
|
-
let t = y(
|
|
961
|
+
let t = p(y(o(c, `${d()}Posts`, e, "comments"), v("createdAt", "desc"), v(u(), "desc")));
|
|
737
962
|
if (await Promise.all([t.then((t) => {
|
|
738
|
-
if (
|
|
963
|
+
if (f.value !== e) return;
|
|
739
964
|
let n = t.docs.map((e) => ({
|
|
740
965
|
id: e.id,
|
|
741
966
|
...e.data()
|
|
742
967
|
}));
|
|
743
|
-
|
|
744
|
-
}), R()]),
|
|
968
|
+
b.value = n.filter((e) => e.parentId === void 0);
|
|
969
|
+
}), R()]), f.value !== e) return;
|
|
745
970
|
} finally {
|
|
746
|
-
|
|
971
|
+
f.value === e && (S.value = !1, A.value = !1);
|
|
747
972
|
}
|
|
748
973
|
}
|
|
749
974
|
}, { immediate: !0 });
|
|
750
975
|
function L() {
|
|
751
|
-
P?.(), P =
|
|
752
|
-
let t = new Map(
|
|
976
|
+
P?.(), P = _(y(o(c, `${d()}Posts`, f.value, "comments"), T("parentId", "==", null), v("createdAt", "desc"), v(u(), "desc"), h(1)), (e) => {
|
|
977
|
+
let t = new Map(g.value.map((e) => [e.id, e]));
|
|
753
978
|
for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
|
|
754
979
|
id: n.doc.id,
|
|
755
980
|
...n.doc.data()
|
|
756
981
|
});
|
|
757
|
-
|
|
982
|
+
g.value = [...t.values()];
|
|
758
983
|
}, (e) => {
|
|
759
984
|
console.error("[memi-board:comments] onSnapshot failed", e);
|
|
760
985
|
});
|
|
761
986
|
}
|
|
762
987
|
async function R() {
|
|
763
|
-
if (!(
|
|
764
|
-
|
|
988
|
+
if (!(O.value || !k.value)) {
|
|
989
|
+
O.value = !0;
|
|
765
990
|
try {
|
|
766
991
|
let e = [
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
...
|
|
771
|
-
|
|
772
|
-
], t = await y(
|
|
992
|
+
T("parentId", "==", null),
|
|
993
|
+
v("createdAt", "asc"),
|
|
994
|
+
v(u(), "asc"),
|
|
995
|
+
...j ? [C(j)] : [],
|
|
996
|
+
h(11)
|
|
997
|
+
], t = await p(y(o(c, `${d()}Posts`, f.value, "comments"), ...e)), n = t.docs.slice(0, dt), r = n.map((e) => ({
|
|
773
998
|
id: e.id,
|
|
774
999
|
...e.data()
|
|
775
|
-
})), i = new Set(
|
|
776
|
-
|
|
1000
|
+
})), i = new Set(D.value.map((e) => e.id));
|
|
1001
|
+
D.value.push(...r.filter((e) => !i.has(e.id))), j = n.at(-1) ?? j, t.size <= dt && (k.value = !1, N = setTimeout(() => {
|
|
777
1002
|
N = void 0, L();
|
|
778
1003
|
}, 1e3));
|
|
779
1004
|
} finally {
|
|
780
|
-
|
|
1005
|
+
O.value = !1;
|
|
781
1006
|
}
|
|
782
1007
|
}
|
|
783
1008
|
}
|
|
784
1009
|
t(() => {
|
|
785
1010
|
clearTimeout(N), P?.();
|
|
786
1011
|
});
|
|
787
|
-
async function
|
|
788
|
-
let t =
|
|
1012
|
+
async function z(e) {
|
|
1013
|
+
let t = mt(e.body), n = E(c), r = d(), i = l(o(c, `${r}Posts`, f.value, "comments"));
|
|
789
1014
|
n.set(i, {
|
|
790
|
-
postId:
|
|
1015
|
+
postId: f.value,
|
|
791
1016
|
body: t,
|
|
792
1017
|
authorUid: e.authorUid,
|
|
793
1018
|
authorName: e.authorName,
|
|
@@ -800,163 +1025,163 @@ function Je(n, i = {}) {
|
|
|
800
1025
|
replyToName: null,
|
|
801
1026
|
replyCount: 0,
|
|
802
1027
|
isReply: !1,
|
|
803
|
-
createdAt:
|
|
804
|
-
updatedAt:
|
|
805
|
-
}), n.update(
|
|
1028
|
+
createdAt: x(),
|
|
1029
|
+
updatedAt: x()
|
|
1030
|
+
}), n.update(l(c, `${r}Posts`, f.value), { commentCount: m(1) }), await n.commit();
|
|
806
1031
|
}
|
|
807
|
-
async function
|
|
808
|
-
let t =
|
|
1032
|
+
async function B(e) {
|
|
1033
|
+
let t = mt(e.body);
|
|
809
1034
|
if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
|
|
810
|
-
let n =
|
|
811
|
-
if (!
|
|
1035
|
+
let n = E(c), r = d(), i = o(c, `${r}Posts`, f.value, "comments"), a = l(i), s = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
|
|
1036
|
+
if (!s) throw Error("댓글 스레드를 찾을 수 없습니다.");
|
|
812
1037
|
let u = Math.min((e.parent.depth ?? 0) + 1, 2);
|
|
813
1038
|
n.set(a, {
|
|
814
|
-
postId:
|
|
1039
|
+
postId: f.value,
|
|
815
1040
|
body: t,
|
|
816
1041
|
authorUid: e.authorUid,
|
|
817
1042
|
authorName: e.authorName,
|
|
818
1043
|
authorPhoto: e.authorPhoto,
|
|
819
1044
|
moderationStatus: "approved",
|
|
820
1045
|
parentId: e.parent.id,
|
|
821
|
-
rootId:
|
|
1046
|
+
rootId: s,
|
|
822
1047
|
depth: u,
|
|
823
1048
|
replyToUid: e.parent.authorUid,
|
|
824
1049
|
replyToName: e.parent.authorName,
|
|
825
1050
|
replyCount: 0,
|
|
826
1051
|
isReply: !0,
|
|
827
|
-
createdAt:
|
|
828
|
-
updatedAt:
|
|
829
|
-
}), n.update(
|
|
1052
|
+
createdAt: x(),
|
|
1053
|
+
updatedAt: x()
|
|
1054
|
+
}), n.update(l(i, s), { replyCount: m(1) }), n.update(l(c, `${r}Posts`, f.value), { commentCount: m(1) }), await n.commit();
|
|
830
1055
|
}
|
|
831
|
-
async function
|
|
1056
|
+
async function V(e) {
|
|
832
1057
|
if (!e.id) return;
|
|
833
1058
|
if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error("답글이 있는 댓글은 현재 삭제할 수 없습니다.");
|
|
834
|
-
let t =
|
|
835
|
-
t.delete(
|
|
1059
|
+
let t = E(c), n = d();
|
|
1060
|
+
t.delete(l(c, `${n}Posts`, f.value, "comments", e.id)), e.parentId && e.rootId && t.update(l(c, `${n}Posts`, f.value, "comments", e.rootId), { replyCount: m(-1) }), t.update(l(c, `${n}Posts`, f.value), { commentCount: m(-1) }), await t.commit(), D.value = D.value.filter((t) => t.id !== e.id), b.value = b.value.filter((t) => t.id !== e.id), g.value = g.value.filter((t) => t.id !== e.id);
|
|
836
1061
|
}
|
|
837
|
-
async function
|
|
838
|
-
let n =
|
|
839
|
-
await
|
|
1062
|
+
async function H(e, t) {
|
|
1063
|
+
let n = mt(t);
|
|
1064
|
+
await w(l(c, `${d()}Posts`, f.value, "comments", e), {
|
|
840
1065
|
body: n,
|
|
841
|
-
updatedAt:
|
|
1066
|
+
updatedAt: x()
|
|
842
1067
|
});
|
|
843
1068
|
}
|
|
844
|
-
async function
|
|
845
|
-
await
|
|
1069
|
+
async function ee(e, t, n) {
|
|
1070
|
+
await w(l(c, `${d()}Posts`, f.value, "comments", e), {
|
|
846
1071
|
isBlinded: t,
|
|
847
|
-
moderatedAt:
|
|
1072
|
+
moderatedAt: x(),
|
|
848
1073
|
moderatedBy: n
|
|
849
1074
|
});
|
|
850
1075
|
}
|
|
851
1076
|
return {
|
|
852
1077
|
comments: I,
|
|
853
|
-
commentsPending: e(() =>
|
|
854
|
-
hasMore:
|
|
855
|
-
loadingMore:
|
|
1078
|
+
commentsPending: e(() => A.value || S.value),
|
|
1079
|
+
hasMore: k,
|
|
1080
|
+
loadingMore: O,
|
|
856
1081
|
loadMore: R,
|
|
857
|
-
addComment:
|
|
858
|
-
addReply:
|
|
859
|
-
updateComment:
|
|
860
|
-
setCommentBlinded:
|
|
861
|
-
deleteComment:
|
|
1082
|
+
addComment: z,
|
|
1083
|
+
addReply: B,
|
|
1084
|
+
updateComment: H,
|
|
1085
|
+
setCommentBlinded: ee,
|
|
1086
|
+
deleteComment: V
|
|
862
1087
|
};
|
|
863
1088
|
}
|
|
864
|
-
function
|
|
865
|
-
let i = G(), a =
|
|
866
|
-
function
|
|
1089
|
+
function gt(e, n) {
|
|
1090
|
+
let i = G(), a = M(), s = r([]), c = r(!1), l = r(!1), d = r(!0), f = null, m;
|
|
1091
|
+
function g(e) {
|
|
867
1092
|
return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
|
|
868
1093
|
}
|
|
869
|
-
function
|
|
870
|
-
|
|
871
|
-
let t = new Map(
|
|
1094
|
+
function b() {
|
|
1095
|
+
m ||= _(y(o(a, `${i.collectionPrefix}Posts`, e, "comments"), T("rootId", "==", n), T("isReply", "==", !0), v("createdAt", "desc"), v(u(), "desc"), h(1)), (e) => {
|
|
1096
|
+
let t = new Map(s.value.map((e) => [e.id, e]));
|
|
872
1097
|
for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
|
|
873
1098
|
id: n.doc.id,
|
|
874
1099
|
...n.doc.data()
|
|
875
1100
|
});
|
|
876
|
-
|
|
1101
|
+
s.value = g([...t.values()]);
|
|
877
1102
|
}, (e) => {
|
|
878
1103
|
console.error("[memi-board:replies] onSnapshot failed", e);
|
|
879
1104
|
});
|
|
880
1105
|
}
|
|
881
|
-
async function
|
|
882
|
-
if (!(
|
|
883
|
-
|
|
1106
|
+
async function x() {
|
|
1107
|
+
if (!(c.value || !d.value)) {
|
|
1108
|
+
c.value = !0;
|
|
884
1109
|
try {
|
|
885
1110
|
let t = [
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
...
|
|
891
|
-
|
|
892
|
-
], r = await y(
|
|
1111
|
+
T("rootId", "==", n),
|
|
1112
|
+
T("isReply", "==", !0),
|
|
1113
|
+
v("createdAt", "asc"),
|
|
1114
|
+
v(u(), "asc"),
|
|
1115
|
+
...f ? [C(f)] : [],
|
|
1116
|
+
h(6)
|
|
1117
|
+
], r = await p(y(o(a, `${i.collectionPrefix}Posts`, e, "comments"), ...t)), c = new Set(s.value.map((e) => e.id)), m = r.docs.slice(0, ft), _ = m.map((e) => ({
|
|
893
1118
|
id: e.id,
|
|
894
1119
|
...e.data()
|
|
895
|
-
})).filter((e) => !
|
|
896
|
-
|
|
1120
|
+
})).filter((e) => !c.has(e.id));
|
|
1121
|
+
s.value = g([...s.value, ..._]), f = m.at(-1) ?? f, l.value = !0, r.size <= ft && (d.value = !1, b());
|
|
897
1122
|
} finally {
|
|
898
|
-
|
|
1123
|
+
c.value = !1;
|
|
899
1124
|
}
|
|
900
1125
|
}
|
|
901
1126
|
}
|
|
902
|
-
function
|
|
903
|
-
|
|
1127
|
+
function S() {
|
|
1128
|
+
m?.(), m = void 0, s.value = [], f = null, l.value = !1, d.value = !0;
|
|
904
1129
|
}
|
|
905
|
-
async function
|
|
906
|
-
|
|
1130
|
+
async function w() {
|
|
1131
|
+
S(), await x();
|
|
907
1132
|
}
|
|
908
|
-
return t(() =>
|
|
909
|
-
replies:
|
|
910
|
-
loading:
|
|
911
|
-
loaded:
|
|
912
|
-
hasMore:
|
|
913
|
-
loadMore:
|
|
914
|
-
refresh:
|
|
1133
|
+
return t(() => m?.()), {
|
|
1134
|
+
replies: s,
|
|
1135
|
+
loading: c,
|
|
1136
|
+
loaded: l,
|
|
1137
|
+
hasMore: d,
|
|
1138
|
+
loadMore: x,
|
|
1139
|
+
refresh: w
|
|
915
1140
|
};
|
|
916
1141
|
}
|
|
917
1142
|
//#endregion
|
|
918
1143
|
//#region src/composables/useMemiBoardLikes.ts
|
|
919
|
-
function
|
|
920
|
-
let n = G(), i =
|
|
921
|
-
function
|
|
922
|
-
return
|
|
1144
|
+
function _t(e) {
|
|
1145
|
+
let n = G(), i = M(), o = O(), s = () => n.collectionPrefix, c = r(!1), u = r(!1), d;
|
|
1146
|
+
function f(t) {
|
|
1147
|
+
return l(i, `${s()}Posts`, e, "likes", t);
|
|
923
1148
|
}
|
|
924
1149
|
a(o, (e) => {
|
|
925
|
-
if (
|
|
926
|
-
|
|
1150
|
+
if (d?.(), d = void 0, !e) {
|
|
1151
|
+
c.value = !1;
|
|
927
1152
|
return;
|
|
928
1153
|
}
|
|
929
|
-
|
|
930
|
-
|
|
1154
|
+
d = _(f(e.uid), (e) => {
|
|
1155
|
+
c.value = e.exists();
|
|
931
1156
|
}, (e) => {
|
|
932
1157
|
console.error("[memi-board:likes] onSnapshot failed", e);
|
|
933
1158
|
});
|
|
934
|
-
}, { immediate: !0 }), t(() =>
|
|
935
|
-
async function
|
|
1159
|
+
}, { immediate: !0 }), t(() => d?.());
|
|
1160
|
+
async function p() {
|
|
936
1161
|
let t = o.value?.uid;
|
|
937
1162
|
if (!t) throw Error("로그인이 필요합니다.");
|
|
938
|
-
if (u.value) return
|
|
1163
|
+
if (u.value) return c.value;
|
|
939
1164
|
u.value = !0;
|
|
940
1165
|
try {
|
|
941
|
-
let n =
|
|
942
|
-
return await
|
|
1166
|
+
let n = f(t), r = l(i, `${s()}Posts`, e);
|
|
1167
|
+
return await b(i, async (e) => (await e.get(n)).exists() ? (e.delete(n), e.update(r, { likeCount: m(-1) }), !1) : (e.set(n, {
|
|
943
1168
|
uid: t,
|
|
944
|
-
createdAt:
|
|
945
|
-
}), e.update(r, { likeCount:
|
|
1169
|
+
createdAt: x()
|
|
1170
|
+
}), e.update(r, { likeCount: m(1) }), !0));
|
|
946
1171
|
} finally {
|
|
947
1172
|
u.value = !1;
|
|
948
1173
|
}
|
|
949
1174
|
}
|
|
950
1175
|
return {
|
|
951
|
-
isLiked:
|
|
1176
|
+
isLiked: c,
|
|
952
1177
|
likePending: u,
|
|
953
|
-
toggleLike:
|
|
1178
|
+
toggleLike: p
|
|
954
1179
|
};
|
|
955
1180
|
}
|
|
956
1181
|
//#endregion
|
|
957
1182
|
//#region src/composables/useMemiBoardViews.ts
|
|
958
|
-
function
|
|
959
|
-
let e = G(), t =
|
|
1183
|
+
function vt() {
|
|
1184
|
+
let e = G(), t = M(), n = e.collectionPrefix;
|
|
960
1185
|
function r(e) {
|
|
961
1186
|
return `memi-board:viewed:${n}:${e}`;
|
|
962
1187
|
}
|
|
@@ -978,7 +1203,7 @@ function Ze() {
|
|
|
978
1203
|
if (o && !i(o)) {
|
|
979
1204
|
a(o);
|
|
980
1205
|
try {
|
|
981
|
-
await
|
|
1206
|
+
await w(l(t, `${n}Posts`, o), { viewCount: m(1) });
|
|
982
1207
|
} catch (e) {
|
|
983
1208
|
try {
|
|
984
1209
|
sessionStorage.removeItem(r(o));
|
|
@@ -994,7 +1219,7 @@ function Ze() {
|
|
|
994
1219
|
}
|
|
995
1220
|
//#endregion
|
|
996
1221
|
//#region src/utils/moderation-prompt.ts
|
|
997
|
-
var
|
|
1222
|
+
var yt = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", bt = {
|
|
998
1223
|
abuse: "욕설·비속어·모욕",
|
|
999
1224
|
spam: "스팸·광고",
|
|
1000
1225
|
adult: "선정적·성인 내용",
|
|
@@ -1009,10 +1234,10 @@ function Y(e) {
|
|
|
1009
1234
|
}
|
|
1010
1235
|
let t = (e.reason || "").trim();
|
|
1011
1236
|
if (t) return t;
|
|
1012
|
-
let n = e.category && e.category !== "none" ?
|
|
1237
|
+
let n = e.category && e.category !== "none" ? bt[e.category] || e.category : "";
|
|
1013
1238
|
return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
|
|
1014
1239
|
}
|
|
1015
|
-
var
|
|
1240
|
+
var xt = [
|
|
1016
1241
|
"씨발",
|
|
1017
1242
|
"시발",
|
|
1018
1243
|
"시이발",
|
|
@@ -1038,10 +1263,10 @@ var et = [
|
|
|
1038
1263
|
"bitch",
|
|
1039
1264
|
"asshole"
|
|
1040
1265
|
];
|
|
1041
|
-
function
|
|
1266
|
+
function St(e) {
|
|
1042
1267
|
return new RegExp(e.join("|"), "i");
|
|
1043
1268
|
}
|
|
1044
|
-
var
|
|
1269
|
+
var Ct = /* @__PURE__ */ new Set([
|
|
1045
1270
|
"none",
|
|
1046
1271
|
"abuse",
|
|
1047
1272
|
"spam",
|
|
@@ -1049,7 +1274,7 @@ var nt = /* @__PURE__ */ new Set([
|
|
|
1049
1274
|
"violence",
|
|
1050
1275
|
"other"
|
|
1051
1276
|
]);
|
|
1052
|
-
function
|
|
1277
|
+
function wt(e) {
|
|
1053
1278
|
let t = e.trim();
|
|
1054
1279
|
if (!t) return null;
|
|
1055
1280
|
let n;
|
|
@@ -1067,7 +1292,7 @@ function rt(e) {
|
|
|
1067
1292
|
if (typeof n != "object" || !n) return null;
|
|
1068
1293
|
let r = n;
|
|
1069
1294
|
if (typeof r.flagged != "boolean") return null;
|
|
1070
|
-
let i = typeof r.category == "string" &&
|
|
1295
|
+
let i = typeof r.category == "string" && Ct.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
|
|
1071
1296
|
return {
|
|
1072
1297
|
flagged: r.flagged,
|
|
1073
1298
|
category: i,
|
|
@@ -1076,14 +1301,14 @@ function rt(e) {
|
|
|
1076
1301
|
}
|
|
1077
1302
|
//#endregion
|
|
1078
1303
|
//#region src/composables/useMemiBoardModeration.ts
|
|
1079
|
-
var
|
|
1304
|
+
var Tt = {
|
|
1080
1305
|
flagged: !1,
|
|
1081
1306
|
category: "none",
|
|
1082
1307
|
reason: "",
|
|
1083
1308
|
via: "empty"
|
|
1084
1309
|
};
|
|
1085
|
-
function
|
|
1086
|
-
let e = G().moderation ?? {}, t =
|
|
1310
|
+
function Et() {
|
|
1311
|
+
let e = G().moderation ?? {}, t = A(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = J(), a = St([...xt, ...e.localBlocklist ?? []]);
|
|
1087
1312
|
function o(e) {
|
|
1088
1313
|
let t = e.match(a);
|
|
1089
1314
|
return t ? {
|
|
@@ -1098,21 +1323,21 @@ function at() {
|
|
|
1098
1323
|
} : null;
|
|
1099
1324
|
}
|
|
1100
1325
|
function s() {
|
|
1101
|
-
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i =
|
|
1102
|
-
backend: new
|
|
1326
|
+
let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ie(t, {
|
|
1327
|
+
backend: new re(),
|
|
1103
1328
|
useLimitedUseAppCheckTokens: r
|
|
1104
1329
|
});
|
|
1105
|
-
return
|
|
1330
|
+
return ae(i, {
|
|
1106
1331
|
model: n,
|
|
1107
|
-
systemInstruction:
|
|
1332
|
+
systemInstruction: yt,
|
|
1108
1333
|
generationConfig: {
|
|
1109
1334
|
temperature: 0,
|
|
1110
1335
|
maxOutputTokens: 256,
|
|
1111
1336
|
responseMimeType: "application/json",
|
|
1112
|
-
responseSchema:
|
|
1113
|
-
flagged:
|
|
1114
|
-
category:
|
|
1115
|
-
reason:
|
|
1337
|
+
responseSchema: W.object({ properties: {
|
|
1338
|
+
flagged: W.boolean(),
|
|
1339
|
+
category: W.string(),
|
|
1340
|
+
reason: W.string()
|
|
1116
1341
|
} })
|
|
1117
1342
|
}
|
|
1118
1343
|
});
|
|
@@ -1132,7 +1357,7 @@ function at() {
|
|
|
1132
1357
|
via: "ai-error-allow"
|
|
1133
1358
|
};
|
|
1134
1359
|
}
|
|
1135
|
-
function
|
|
1360
|
+
function l(e, t = "") {
|
|
1136
1361
|
let n = Y({
|
|
1137
1362
|
reason: e.reason,
|
|
1138
1363
|
category: e.category,
|
|
@@ -1143,16 +1368,16 @@ function at() {
|
|
|
1143
1368
|
reason: r
|
|
1144
1369
|
};
|
|
1145
1370
|
}
|
|
1146
|
-
async function
|
|
1371
|
+
async function u(e) {
|
|
1147
1372
|
if (!e.flagged || e.error || e.via !== "local" && e.via !== "ai") return e;
|
|
1148
1373
|
try {
|
|
1149
1374
|
let { messageSuffix: t } = await i();
|
|
1150
|
-
return
|
|
1375
|
+
return l(e, t);
|
|
1151
1376
|
} catch (t) {
|
|
1152
|
-
return console.warn("[memi-board] record moderation strike failed", t instanceof Error ? t.message : t),
|
|
1377
|
+
return console.warn("[memi-board] record moderation strike failed", t instanceof Error ? t.message : t), l(e);
|
|
1153
1378
|
}
|
|
1154
1379
|
}
|
|
1155
|
-
async function
|
|
1380
|
+
async function d(t) {
|
|
1156
1381
|
if (n.value) return {
|
|
1157
1382
|
flagged: !0,
|
|
1158
1383
|
category: "other",
|
|
@@ -1161,17 +1386,17 @@ function at() {
|
|
|
1161
1386
|
};
|
|
1162
1387
|
let i = t.trim();
|
|
1163
1388
|
if (!i) return {
|
|
1164
|
-
...
|
|
1389
|
+
...Tt,
|
|
1165
1390
|
via: "empty"
|
|
1166
1391
|
};
|
|
1167
1392
|
let a = o(i);
|
|
1168
|
-
if (a) return
|
|
1393
|
+
if (a) return u(a);
|
|
1169
1394
|
if (e.enabled === !1) return {
|
|
1170
|
-
...
|
|
1395
|
+
...Tt,
|
|
1171
1396
|
via: "disabled"
|
|
1172
1397
|
};
|
|
1173
1398
|
try {
|
|
1174
|
-
let e =
|
|
1399
|
+
let e = wt((await s().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
|
|
1175
1400
|
if (!e) return c();
|
|
1176
1401
|
let t = {
|
|
1177
1402
|
flagged: e.flagged,
|
|
@@ -1183,12 +1408,12 @@ function at() {
|
|
|
1183
1408
|
}),
|
|
1184
1409
|
via: "ai"
|
|
1185
1410
|
};
|
|
1186
|
-
return t.flagged ?
|
|
1411
|
+
return t.flagged ? u(t) : t;
|
|
1187
1412
|
} catch (e) {
|
|
1188
1413
|
return console.warn("[memi-board] moderation AI failed", e instanceof Error ? e.message : e), c();
|
|
1189
1414
|
}
|
|
1190
1415
|
}
|
|
1191
|
-
async function
|
|
1416
|
+
async function f(t) {
|
|
1192
1417
|
if (n.value) return {
|
|
1193
1418
|
flagged: !0,
|
|
1194
1419
|
category: "other",
|
|
@@ -1196,14 +1421,14 @@ function at() {
|
|
|
1196
1421
|
via: "restricted"
|
|
1197
1422
|
};
|
|
1198
1423
|
if (e.enabled === !1 || !e.moderateImages) return {
|
|
1199
|
-
...
|
|
1424
|
+
...Tt,
|
|
1200
1425
|
via: "disabled"
|
|
1201
1426
|
};
|
|
1202
1427
|
try {
|
|
1203
1428
|
let e = await new Promise((e, n) => {
|
|
1204
1429
|
let r = new FileReader();
|
|
1205
1430
|
r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
|
|
1206
|
-
}), n =
|
|
1431
|
+
}), n = wt((await s().generateContent([{ inlineData: {
|
|
1207
1432
|
mimeType: t.type || "image/jpeg",
|
|
1208
1433
|
data: e
|
|
1209
1434
|
} }, "이 이미지를 심사해 주세요."])).response.text());
|
|
@@ -1218,14 +1443,14 @@ function at() {
|
|
|
1218
1443
|
}),
|
|
1219
1444
|
via: "ai"
|
|
1220
1445
|
};
|
|
1221
|
-
return r.flagged ?
|
|
1446
|
+
return r.flagged ? u(r) : r;
|
|
1222
1447
|
} catch (e) {
|
|
1223
1448
|
return console.warn("[memi-board] moderation AI image failed", e instanceof Error ? e.message : e), c();
|
|
1224
1449
|
}
|
|
1225
1450
|
}
|
|
1226
1451
|
return {
|
|
1227
|
-
checkText:
|
|
1228
|
-
checkImage:
|
|
1452
|
+
checkText: d,
|
|
1453
|
+
checkImage: f
|
|
1229
1454
|
};
|
|
1230
1455
|
}
|
|
1231
1456
|
//#endregion
|
|
@@ -1250,9 +1475,9 @@ async function X(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
|
|
|
1250
1475
|
}
|
|
1251
1476
|
//#endregion
|
|
1252
1477
|
//#region src/composables/useMemiBoardStorage.ts
|
|
1253
|
-
var
|
|
1254
|
-
async function
|
|
1255
|
-
if (e.size > 26214400) throw Error(`원본 이미지는 ${
|
|
1478
|
+
var Dt = 5242880, Ot = 26214400;
|
|
1479
|
+
async function kt(e) {
|
|
1480
|
+
if (e.size > 26214400) throw Error(`원본 이미지는 ${Ot / 1024 / 1024}MB 이하여야 합니다.`);
|
|
1256
1481
|
if (e.size <= 5242880) return {
|
|
1257
1482
|
blob: e,
|
|
1258
1483
|
compressed: !1
|
|
@@ -1274,10 +1499,10 @@ async function ct(e) {
|
|
|
1274
1499
|
throw e instanceof Error && e.message.includes("압축 후에도") ? e : Error("이 이미지 형식은 브라우저에서 최적화할 수 없습니다. JPG, PNG 또는 WebP로 변환해 주세요.");
|
|
1275
1500
|
}
|
|
1276
1501
|
}
|
|
1277
|
-
function
|
|
1278
|
-
let e = G(), t =
|
|
1502
|
+
function At() {
|
|
1503
|
+
let e = G(), t = A(), n = () => e.collectionPrefix;
|
|
1279
1504
|
function r(e, r, i) {
|
|
1280
|
-
let a =
|
|
1505
|
+
let a = H(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${He(e.name)}`, s = U(a, o), c = ne(s, e, {
|
|
1281
1506
|
contentType: e.type || "application/octet-stream",
|
|
1282
1507
|
contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
|
|
1283
1508
|
});
|
|
@@ -1286,7 +1511,7 @@ function lt() {
|
|
|
1286
1511
|
c.on("state_changed", (e) => {
|
|
1287
1512
|
i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
|
|
1288
1513
|
}, n, async () => {
|
|
1289
|
-
let n = await
|
|
1514
|
+
let n = await V(s);
|
|
1290
1515
|
t({
|
|
1291
1516
|
name: e.name,
|
|
1292
1517
|
url: n,
|
|
@@ -1300,27 +1525,27 @@ function lt() {
|
|
|
1300
1525
|
};
|
|
1301
1526
|
}
|
|
1302
1527
|
async function i(e) {
|
|
1303
|
-
let n =
|
|
1304
|
-
await
|
|
1528
|
+
let n = H(t);
|
|
1529
|
+
await B(U(n, e.path)).catch(() => {});
|
|
1305
1530
|
}
|
|
1306
1531
|
async function a(e, r) {
|
|
1307
1532
|
if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
|
|
1308
|
-
let i = await
|
|
1309
|
-
await
|
|
1310
|
-
let f = await
|
|
1533
|
+
let i = await kt(e), a = H(t), o = (i.compressed ? "jpg" : e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", s = He(e.name.replace(/\.[^.]+$/, "") || "image", 40), c = `${Date.now()}-${s || "image"}`, l = r || `new-${Date.now()}`, u = `${n()}/posts/${l}/images/${c}.${o}`, d = U(a, u);
|
|
1534
|
+
await te(d, i.blob, { contentType: i.blob.type || "image/jpeg" });
|
|
1535
|
+
let f = await V(d), p = await X(i.blob, {
|
|
1311
1536
|
maxWidth: 400,
|
|
1312
1537
|
quality: .8
|
|
1313
|
-
}), m = `${n()}/posts/${l}/images/thumbnails/${c}.jpg`, h =
|
|
1314
|
-
return await
|
|
1538
|
+
}), m = `${n()}/posts/${l}/images/thumbnails/${c}.jpg`, h = U(a, m);
|
|
1539
|
+
return await te(h, p, { contentType: "image/jpeg" }), {
|
|
1315
1540
|
originalUrl: f,
|
|
1316
1541
|
originalPath: u,
|
|
1317
|
-
thumbnailUrl: await
|
|
1542
|
+
thumbnailUrl: await V(h),
|
|
1318
1543
|
thumbnailPath: m
|
|
1319
1544
|
};
|
|
1320
1545
|
}
|
|
1321
1546
|
async function o(e) {
|
|
1322
|
-
let n =
|
|
1323
|
-
await Promise.all([
|
|
1547
|
+
let n = H(t);
|
|
1548
|
+
await Promise.all([B(U(n, e.originalPath)).catch(() => {}), B(U(n, e.thumbnailPath)).catch(() => {})]);
|
|
1324
1549
|
}
|
|
1325
1550
|
return {
|
|
1326
1551
|
uploadAttachment: r,
|
|
@@ -1331,7 +1556,7 @@ function lt() {
|
|
|
1331
1556
|
}
|
|
1332
1557
|
//#endregion
|
|
1333
1558
|
//#region src/composables/useMemiBoardUsers.ts
|
|
1334
|
-
var
|
|
1559
|
+
var jt = [
|
|
1335
1560
|
{
|
|
1336
1561
|
value: "admin",
|
|
1337
1562
|
label: "관리자",
|
|
@@ -1348,24 +1573,44 @@ var ut = [
|
|
|
1348
1573
|
description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
|
|
1349
1574
|
}
|
|
1350
1575
|
];
|
|
1351
|
-
function
|
|
1352
|
-
let n = G(), r =
|
|
1353
|
-
async function
|
|
1354
|
-
if (!
|
|
1355
|
-
await
|
|
1576
|
+
function Mt(t = {}) {
|
|
1577
|
+
let n = G(), r = M(), a = `${n.collectionPrefix}Users`, s = e(() => i(t.enabled) ?? !0 ? o(r, a) : null), c = D(s, { ssrKey: `${a}-management` });
|
|
1578
|
+
async function u(e, t) {
|
|
1579
|
+
if (!jt.some((e) => e.value === t)) throw Error("지원하지 않는 역할입니다.");
|
|
1580
|
+
await w(l(r, a, e), {
|
|
1356
1581
|
role: t,
|
|
1357
|
-
updatedAt:
|
|
1582
|
+
updatedAt: x()
|
|
1358
1583
|
});
|
|
1359
1584
|
}
|
|
1360
1585
|
return {
|
|
1361
1586
|
users: c,
|
|
1362
1587
|
usersPending: c.pending,
|
|
1363
|
-
updateUserRole:
|
|
1588
|
+
updateUserRole: u
|
|
1364
1589
|
};
|
|
1365
1590
|
}
|
|
1366
1591
|
//#endregion
|
|
1367
1592
|
//#region src/data/versionHistory.ts
|
|
1368
|
-
var
|
|
1593
|
+
var Nt = [
|
|
1594
|
+
{
|
|
1595
|
+
version: "0.19.3",
|
|
1596
|
+
date: "2026-08-06",
|
|
1597
|
+
highlights: ["이미지 목록 게시판은 인스타처럼 제목 없이 사진+내용만 올리면 돼요", "사진을 여러 장 올리고 끌어 순서를 바꿀 수 있어요 (첫 장이 대표)"]
|
|
1598
|
+
},
|
|
1599
|
+
{
|
|
1600
|
+
version: "0.19.2",
|
|
1601
|
+
date: "2026-08-06",
|
|
1602
|
+
highlights: ["이미지만 넣고 글 없이 올리는 건 막았어요 — 본문에 글자를 넣어야 해요"]
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
version: "0.19.1",
|
|
1606
|
+
date: "2026-08-06",
|
|
1607
|
+
highlights: ["공유 미리보기 이미지를 가벼운 썸네일로 쓰도록 바꿨어요 (카톡 등에 더 잘 맞아요)"]
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
version: "0.19.0",
|
|
1611
|
+
date: "2026-08-06",
|
|
1612
|
+
highlights: ["게시판 목록·글 공유 미리보기(제목·설명·대표 이미지)를 모듈만으로 붙일 수 있어요", "호스트가 따로 SEO API를 만들 필요 없이, 공개 글 기준으로 미리보기가 채워져요"]
|
|
1613
|
+
},
|
|
1369
1614
|
{
|
|
1370
1615
|
version: "0.18.0",
|
|
1371
1616
|
date: "2026-08-06",
|
|
@@ -1463,7 +1708,7 @@ var ft = [
|
|
|
1463
1708
|
highlights: ["글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요", "부적절한 글·댓글은 AI 검열로 막을 수 있어요"],
|
|
1464
1709
|
note: "실제 보안은 호스트 Firebase 규칙 등록이 필요합니다."
|
|
1465
1710
|
}
|
|
1466
|
-
],
|
|
1711
|
+
], Pt = /* @__PURE__ */ fe(((e, t) => {
|
|
1467
1712
|
(function(n, r) {
|
|
1468
1713
|
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();
|
|
1469
1714
|
})(e, (function() {
|
|
@@ -1757,7 +2002,7 @@ var ft = [
|
|
|
1757
2002
|
return w(1e3 * e);
|
|
1758
2003
|
}, w.en = b[y], w.Ls = b, w.p = {}, w;
|
|
1759
2004
|
}));
|
|
1760
|
-
})),
|
|
2005
|
+
})), Ft = /* @__PURE__ */ fe(((e, t) => {
|
|
1761
2006
|
(function(n, r) {
|
|
1762
2007
|
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();
|
|
1763
2008
|
})(e, (function() {
|
|
@@ -1861,9 +2106,9 @@ var ft = [
|
|
|
1861
2106
|
};
|
|
1862
2107
|
};
|
|
1863
2108
|
}));
|
|
1864
|
-
})),
|
|
2109
|
+
})), It = /* @__PURE__ */ fe(((e, t) => {
|
|
1865
2110
|
(function(n, r) {
|
|
1866
|
-
typeof e == "object" && t !== void 0 ? t.exports = r(
|
|
2111
|
+
typeof e == "object" && t !== void 0 ? t.exports = r(Pt()) : typeof define == "function" && define.amd ? define(["dayjs"], r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs_locale_ko = r(n.dayjs);
|
|
1867
2112
|
})(e, (function(e) {
|
|
1868
2113
|
function t(e) {
|
|
1869
2114
|
return e && typeof e == "object" && "default" in e ? e : { default: e };
|
|
@@ -1911,20 +2156,20 @@ var ft = [
|
|
|
1911
2156
|
};
|
|
1912
2157
|
return n.default.locale(r, null, !0), r;
|
|
1913
2158
|
}));
|
|
1914
|
-
})), Z = /* @__PURE__ */ me(
|
|
1915
|
-
|
|
1916
|
-
function
|
|
2159
|
+
})), Z = /* @__PURE__ */ me(Pt(), 1), Lt = /* @__PURE__ */ me(Ft(), 1);
|
|
2160
|
+
It(), Z.default.extend(Lt.default), Z.default.locale("ko");
|
|
2161
|
+
function Rt(e) {
|
|
1917
2162
|
return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
|
|
1918
2163
|
}
|
|
1919
|
-
function
|
|
2164
|
+
function zt(e, t = Date.now()) {
|
|
1920
2165
|
return e?.toDate ? (0, Z.default)(e.toDate()).from((0, Z.default)(t)) : "방금 전";
|
|
1921
2166
|
}
|
|
1922
|
-
function
|
|
2167
|
+
function Bt(e) {
|
|
1923
2168
|
return e?.toDate ? (0, Z.default)(e.toDate()).format("YYYY년 M월 D일 HH:mm:ss") : "시간 확인 중";
|
|
1924
2169
|
}
|
|
1925
|
-
function
|
|
1926
|
-
let n =
|
|
1927
|
-
return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${
|
|
2170
|
+
function Vt(e, t) {
|
|
2171
|
+
let n = Bt(e), r = e?.toMillis?.(), i = t?.toMillis?.();
|
|
2172
|
+
return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${Bt(t)}`];
|
|
1928
2173
|
}
|
|
1929
2174
|
//#endregion
|
|
1930
2175
|
//#region src/utils/renderMarkdown.ts
|
|
@@ -1935,7 +2180,7 @@ function $(e) {
|
|
|
1935
2180
|
let t = e;
|
|
1936
2181
|
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;
|
|
1937
2182
|
}
|
|
1938
|
-
function
|
|
2183
|
+
function Ht(e) {
|
|
1939
2184
|
if (!e?.trim()) return "";
|
|
1940
2185
|
let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
|
|
1941
2186
|
function c() {
|
|
@@ -2000,6 +2245,6 @@ function xt(e) {
|
|
|
2000
2245
|
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");
|
|
2001
2246
|
}
|
|
2002
2247
|
//#endregion
|
|
2003
|
-
export {
|
|
2248
|
+
export { jt as BOARD_USER_ROLES, pt as COMMENT_BODY_MAX_LENGTH, K as DEFAULT_BLOCK_BAN_DECAY_MS, Pe as DEFAULT_BLOCK_BAN_THRESHOLD, tt as DEFAULT_CATEGORIES, Dt as EDITOR_IMAGE_MAX_BYTES, Ot as EDITOR_IMAGE_SOURCE_MAX_BYTES, ve as asHttpUrl, we as boardListOgDescription, Ce as boardListOgTitle, Se as boardPostOgDescription, xe as boardPostOgTitle, et as buildPostPreview, X as compressImage, _e as configureMemiBoard, Fe as effectiveModerationBlockCount, Ue as extractEditorImageUrls, Me as fetchPublicListForSeo, je as fetchPublicPostForSeo, Rt as formatDate, Bt as formatFullDate, zt as formatRelativeDate, Le as formatRestrictedUntilLabel, Vt as formatTimestampDetails, Ye as hasBodyImage, Je as hasBodyText, Xe as isContentEmpty, ot as isFirestorePermissionDenied, Ie as isModerationWriteRestricted, q as moderationWriteRestrictedUntilMs, Te as normalizeBasePath, Ke as plainTextFromHtml, Ge as postNamespaceFromStoragePath, Ht as renderMarkdownToHtml, Ee as resolvePublicSeoDb, Ve as slugify, We as storagePathFromDownloadUrl, qe as titleFromBody, be as toAbsoluteUrl, ye as toBoardOgImageUrl, J as useMemiBoardAuth, ht as useMemiBoardComments, G as useMemiBoardConfig, _t as useMemiBoardLikes, Et as useMemiBoardModeration, ut as useMemiBoardPost, lt as useMemiBoardPostList, st as useMemiBoardPosts, gt as useMemiBoardReplies, rt as useMemiBoardSettings, At as useMemiBoardStorage, Mt as useMemiBoardUsers, vt as useMemiBoardViews, Nt as versionHistory, $e as videoListCoverUrl, Qe as youtubeId };
|
|
2004
2249
|
|
|
2005
2250
|
//# sourceMappingURL=index.js.map
|