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/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 { useCollection as o, useCurrentUser as s, useDocument as c, useFirebaseApp as l, useFirebaseAuth as u, useFirestore as d } from "vuefire";
3
- import { collection as f, deleteDoc as p, deleteField as m, doc as h, documentId as g, endBefore as _, getDoc as v, getDocs as y, increment as b, limit as x, limitToLast as S, onSnapshot as C, orderBy as w, query as T, runTransaction as E, serverTimestamp as D, setDoc as O, startAfter as k, updateDoc as A, where as j, writeBatch as M } from "firebase/firestore";
4
- import { GoogleAuthProvider as N, OAuthProvider as P, createUserWithEmailAndPassword as F, signInWithEmailAndPassword as I, signInWithPopup as L, signOut as R, updateProfile as ee } from "firebase/auth";
5
- import { deleteObject as z, getDownloadURL as B, getStorage as V, listAll as te, ref as H, uploadBytes as ne, uploadBytesResumable as re } from "firebase/storage";
6
- import { GoogleAIBackend as ie, Schema as U, getAI as ae, getGenerativeModel as oe } from "firebase/ai";
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 se = Object.create, ce = Object.defineProperty, le = Object.getOwnPropertyDescriptor, ue = Object.getOwnPropertyNames, de = Object.getPrototypeOf, fe = Object.prototype.hasOwnProperty, W = (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 = ue(t), a = 0, o = i.length, s; a < o; a++) s = i[a], !fe.call(e, s) && s !== n && ce(e, s, {
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 = le(t, s)) || r.enumerable
11
+ enumerable: !(r = ce(t, s)) || r.enumerable
12
12
  });
13
13
  return e;
14
- }, me = (e, t, n) => (n = e == null ? {} : se(de(e)), pe(t || !e || !e.__esModule || !fe.call(e, "default") ? ce(n, "default", {
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 ve = 3, K = 864e5;
44
- function ye(e, t, n = Date.now(), r = K) {
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 be(e, t, n = Date.now(), r = 3, i = K) {
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 xe(e, t = Date.now()) {
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 Se(e) {
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 Ce = "__MEMI_BOARD_AUTH_STATE__";
83
- function we() {
265
+ var ze = "__MEMI_BOARD_AUTH_STATE__";
266
+ function Be() {
84
267
  let e = globalThis;
85
- return e[Ce] || (e[Ce] = {
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[Ce];
274
+ }), e[ze];
92
275
  }
93
276
  function J() {
94
- let t = G(), n = we(), r = d(), i = u(), o = s(), { role: c, rolePending: l, rawBlockCount: f, blockAtMs: p } = n;
95
- function m() {
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 g() {
282
+ function h() {
100
283
  let e = t.moderation?.blockBanDecayMs;
101
284
  return typeof e == "number" && e > 0 ? e : K;
102
285
  }
103
- function _(e) {
104
- return h(r, `${t.collectionPrefix}Users`, e);
286
+ function g(e) {
287
+ return l(r, `${t.collectionPrefix}Users`, e);
105
288
  }
106
- function y(e) {
289
+ function _(e) {
107
290
  if (!e) {
108
- f.value = 0, p.value = null;
291
+ u.value = 0, d.value = null;
109
292
  return;
110
293
  }
111
- f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), p.value = Se(e.moderationBlockAt);
294
+ u.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), d.value = Re(e.moderationBlockAt);
112
295
  }
113
- async function x(e) {
114
- let t = _(e.uid), n = await v(t);
115
- if (!n.exists()) return await O(t, {
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: D()
121
- }), y({ moderationBlockCount: 0 }), "user";
303
+ updatedAt: x()
304
+ }), _({ moderationBlockCount: 0 }), "user";
122
305
  let r = n.data();
123
- return y(r), await A(t, {
306
+ return _(r), await w(t, {
124
307
  displayName: e.displayName,
125
308
  email: e.email,
126
309
  photoURL: e.photoURL,
127
- updatedAt: D()
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 S() {
317
+ async function y() {
135
318
  let e = o.value?.uid;
136
319
  if (!e) {
137
- y(void 0);
320
+ _(void 0);
138
321
  return;
139
322
  }
140
- let t = await v(_(e));
141
- y(t.exists() ? t.data() : void 0);
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 (l.value = !0, !e) {
145
- c.value = "user", y(void 0), l.value = !1;
327
+ if (c.value = !0, !e) {
328
+ s.value = "user", _(void 0), c.value = !1;
146
329
  return;
147
330
  }
148
331
  try {
149
- c.value = await x(e);
332
+ s.value = await v(e);
150
333
  } catch {
151
- c.value = "user";
334
+ s.value = "user";
152
335
  } finally {
153
- l.value = !1;
336
+ c.value = !1;
154
337
  }
155
338
  }, { immediate: !0 }));
156
- let C = e(() => !!o.value), w = e(() => c.value === "admin"), T = e(() => c.value === "staff"), E = e(() => w.value || T.value), k = e(() => ye(f.value, p.value, Date.now(), g())), j = e(() => {
157
- let e = m();
158
- return e <= 0 ? !1 : be(f.value, p.value, Date.now(), e, g());
159
- }), M = e(() => {
160
- let e = m();
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(f.value, p.value, Date.now(), e, g());
163
- return t == null ? null : xe(t);
164
- }), z = e(() => {
165
- if (!j.value) return null;
166
- let e = M.value;
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 B() {
170
- let e = o.value?.uid, t = m();
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 = _(e), r = await v(n);
177
- r.exists() ? await A(n, {
178
- moderationBlockCount: b(1),
179
- moderationBlockAt: D(),
180
- updatedAt: D()
181
- }) : await O(n, {
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: D(),
186
- updatedAt: D()
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
- f.value = i, p.value = a;
190
- let s = ye(i, a, a, g()), c = be(i, a, a, t, g()), l = ` (경고 ${s}/${t})`;
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, g()), n = e == null ? null : xe(e, a);
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 V() {
384
+ function H() {
202
385
  if (!i) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
203
386
  return i;
204
387
  }
205
- async function te() {
206
- return L(V(), new N());
388
+ async function ee() {
389
+ return L(H(), new N());
207
390
  }
208
- async function H() {
391
+ async function U() {
209
392
  let e = new P("apple.com");
210
- return e.addScope("email"), e.addScope("name"), L(V(), e);
393
+ return e.addScope("email"), e.addScope("name"), L(H(), e);
211
394
  }
212
- async function ne(e, t) {
213
- return I(V(), e, t);
395
+ async function te(e, t) {
396
+ return I(H(), e, t);
214
397
  }
215
- async function re(e, t, n) {
216
- let r = await F(V(), e, t);
217
- return n && await ee(r.user, { displayName: n }), r;
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 ie() {
220
- await R(V());
402
+ async function re() {
403
+ await R(H());
221
404
  }
222
- function U(e) {
223
- return C.value && (o.value?.uid === e.authorUid || E.value);
405
+ function W(e) {
406
+ return b.value && (o.value?.uid === e.authorUid || E.value);
224
407
  }
225
- function ae(e) {
226
- return C.value && o.value?.uid === e.authorUid;
408
+ function ie(e) {
409
+ return b.value && o.value?.uid === e.authorUid;
227
410
  }
228
- function oe(e) {
229
- return C.value && (o.value?.uid === e.authorUid || E.value);
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: C,
234
- isAdmin: w,
416
+ isSignedIn: b,
417
+ isAdmin: C,
235
418
  isStaff: T,
236
419
  canManageContent: E,
237
- rolePending: l,
238
- moderationBlockCount: k,
239
- isWriteRestricted: j,
240
- restrictedUntilLabel: M,
241
- restrictedMessage: z,
242
- signInWithGoogle: te,
243
- signInWithApple: H,
244
- signInWithEmail: ne,
245
- signUpWithEmail: re,
246
- signOut: ie,
247
- canEdit: U,
248
- canDelete: U,
249
- canEditComment: ae,
250
- canDeleteComment: oe,
251
- recordContentModerationBlock: B,
252
- refreshBoardUser: S
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 Te(e) {
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 Ee(e, t = 60) {
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 De(e) {
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("&amp;", "&"));
@@ -271,7 +454,7 @@ function De(e) {
271
454
  }
272
455
  //#endregion
273
456
  //#region src/utils/storagePath.ts
274
- function Oe(e) {
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 ke(e) {
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(/&nbsp;/gi, " ").replace(/&#160;/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 Ae(e) {
491
+ function Ze(e) {
288
492
  return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
289
493
  }
290
- function je(e) {
494
+ function Qe(e) {
291
495
  if (e) try {
292
- let t = new URL(Ae(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
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 Me(e) {
300
- let t = je(e.videoUrl);
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 Ne(e, t = []) {
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 ? Ae(r[1]) : a, s = o ? je(o) : void 0, c = n ? Ae(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
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(/&nbsp;/g, " ").replace(/&amp;/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 Pe = [
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 Fe(e) {
546
+ function nt(e) {
343
547
  return e === "hidden" ? "hidden" : "public";
344
548
  }
345
- function Ie() {
346
- let t = G(), n = d(), { isSignedIn: r } = J(), i = e(() => h(n, `${t.collectionPrefix}Settings`, "config")), a = e(() => f(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => T(a.value, w("order", "asc"))), { data: c, pending: l } = o(s, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), u = e(() => c.value.map((e, t) => ({
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: Fe(e.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(() => u.value.filter((e) => e.visibility !== "hidden"));
560
+ }))), m = e(() => f.value.filter((e) => e.visibility !== "hidden"));
357
561
  function g(e) {
358
- if (e) return u.value.find((t) => t.id === e)?.label ?? e;
562
+ if (e) return f.value.find((t) => t.id === e)?.label ?? e;
359
563
  }
360
564
  function _(e) {
361
- if (e) return u.value.find((t) => t.id === e)?.description?.trim() || void 0;
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 v(e) === "hidden";
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 S(e, r) {
370
- let i = f(n, `${t.collectionPrefix}Posts`), a;
573
+ async function O(e, r) {
574
+ let i = o(n, `${t.collectionPrefix}Posts`), a;
371
575
  for (;;) {
372
- let t = await y(T(i, j("category", "==", e), w("__name__"), ...a ? [k(a)] : [], x(400)));
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 = M(n);
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 C(e, t = e.order ?? 0) {
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 = Fe(e.visibility), c = u.value.find((e) => e.id === n)?.visibility ?? "public";
384
- await O(i.value, { updatedAt: D() }, { merge: !0 }), await O(h(a.value, n), {
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: D()
394
- }, { merge: !0 }), (c !== s || s === "public") && await S(n, s !== "hidden");
597
+ updatedAt: x()
598
+ }, { merge: !0 }), (c !== s || s === "public") && await O(n, s !== "hidden");
395
599
  }
396
- async function E(e) {
600
+ async function A(e) {
397
601
  if (!r.value) throw Error("로그인이 필요합니다.");
398
- let t = new Map(u.value.map((e) => [e.id, e.visibility ?? "public"])), o = M(n);
399
- o.set(i.value, { updatedAt: D() }, { merge: !0 });
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 = Fe(e.visibility);
405
- o.set(h(a.value, r), {
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: D()
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 S(e.id, e.listed);
624
+ for (let e of s) await O(e.id, e.listed);
421
625
  }
422
- async function A(e) {
626
+ async function j(e) {
423
627
  if (!r.value) throw Error("로그인이 필요합니다.");
424
- await p(h(a.value, e));
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 = u.value.find((e) => e.label === t);
637
+ let n = f.value.find((e) => e.label === t);
434
638
  if (n) return n.id;
435
- let i = Te(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(u.value.map((e) => e.id));
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 C({
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: u.value.length
648
+ order: f.value.length
445
649
  }), a;
446
650
  }
447
651
  return {
448
- categories: u,
652
+ categories: f,
449
653
  publicCategories: m,
450
654
  categoryLabel: g,
451
655
  categoryDescription: _,
452
- categoryVisibility: v,
453
- isCategoryHidden: b,
454
- settingsPending: l,
656
+ categoryVisibility: b,
657
+ isCategoryHidden: w,
658
+ settingsPending: d,
455
659
  ensureSettings: N,
456
660
  addCategory: P,
457
- saveCategory: C,
458
- saveCategories: E,
459
- deleteCategory: A,
460
- DEFAULT_CATEGORIES: Pe
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 Le(e) {
466
- let t = await te(e).catch(() => ({
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) => z(e)), ...t.prefixes.map((e) => Le(e))]);
674
+ await Promise.all([...t.items.map((e) => B(e)), ...t.prefixes.map((e) => it(e))]);
471
675
  }
472
- async function Re(e, t) {
676
+ async function at(e, t) {
473
677
  let n = [...new Set([...t].filter(Boolean))];
474
- await Promise.all(n.map((t) => z(H(e, t)).catch(() => {})));
678
+ await Promise.all(n.map((t) => B(U(e, t)).catch(() => {})));
475
679
  }
476
- function ze(e) {
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 Be() {
482
- let e = G(), t = d(), n = l(), { isCategoryHidden: r } = Ie(), i = () => e.collectionPrefix, a = () => f(t, `${i()}Posts`), o = (e) => h(t, `${i()}Posts`, e), s = (e) => h(t, `${i()}Posts`, e, "body", "main"), c = (e) => f(t, `${i()}Posts`, e, "comments");
483
- function u(e) {
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 g() {
487
- return h(a()).id;
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
- async function b(e = {}) {
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(j("category", "==", e.category)), e.publicOnly !== !1 && n.push(j("listed", "==", !0)), n.push(w("createdAt", "desc")), e.cursor && n.push(k(e.cursor)), n.push(x(t + 1));
492
- let r = await y(T(a(), ...n)), i = r.docs.slice(0, t);
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 C(e) {
503
- let [t, n] = await Promise.all([v(o(e)), v(s(e))]);
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 E(e, t) {
726
+ async function F(e, t) {
511
727
  try {
512
- let n = (await y(T(a(), j("category", "==", e), j("slug", "==", t), x(1)))).docs[0];
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 C(n.id);
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 ze(e) ? { status: "permission-denied" } : {
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 ze(e) ? { status: "permission-denied" } : {
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 N(e) {
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
- j("category", "==", e.category),
541
- ...e.listed === !1 ? [] : [j("listed", "==", !0)],
542
- w("createdAt", "desc")
543
- ], [n, r] = await Promise.all([y(T(a(), ...t, k(e.createdAt), x(1))), y(T(a(), ...t, _(e.createdAt), S(1)))]), i = (e) => {
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 P(e) {
562
- let t = Te(e.title) || "post", n = t, r = 1;
563
- for (; !(await y(T(a(), j("category", "==", e.category || ""), j("slug", "==", n), x(1)))).empty;) r++, n = `${t}-${r}`;
564
- let i = e.postId || g(), c = Ne(e.content, e.attachments), l = u(e.category);
565
- return await O(o(i), {
566
- slug: n,
567
- title: e.title,
568
- ...c,
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: l,
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: D(),
582
- updatedAt: D()
583
- }), await O(s(i), { content: e.content }), n;
584
- }
585
- async function F(e, t) {
586
- let n = Ne(t.content, t.attachments), r = u(t.category);
587
- await Promise.all([A(o(e), {
588
- title: t.title,
589
- ...n,
590
- previewImage: n.previewImage ? n.previewImage : m(),
591
- videoUrl: n.videoUrl ? n.videoUrl : m(),
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 : m(),
594
- listed: r,
818
+ category: t.category ? t.category : c(),
819
+ listed: i,
595
820
  attachments: t.attachments ?? [],
596
- updatedAt: D()
597
- }), O(s(e), { content: t.content }, { merge: !0 })]);
821
+ updatedAt: x()
822
+ }), S(_(e), { content: t.content }, { merge: !0 })]);
598
823
  }
599
- async function I(e) {
600
- let [r, a] = await Promise.all([v(o(e)), v(s(e))]);
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 l = r.data(), u = a.exists() ? String(a.data().content ?? "") : "", d = await y(c(e));
603
- for (let e = 0; e < d.docs.length; e += 450) {
604
- let n = M(t);
605
- d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
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 f = V(n), m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
608
- for (let t of l.attachments ?? []) if (t?.path) {
609
- m.add(t.path);
610
- let n = ke(t.path);
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 De(u)) {
614
- let n = Oe(t);
838
+ for (let t of Ue(c)) {
839
+ let n = We(t);
615
840
  if (!n) continue;
616
- m.add(n);
841
+ d.add(n);
617
842
  let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
618
- r !== n && r.includes("/images/thumbnails/") && m.add(r);
843
+ r !== n && r.includes("/images/thumbnails/") && d.add(r);
619
844
  let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
620
- i && !n.includes("/thumbnails/") && m.add(`${i[1]}thumbnails/${i[2]}.jpg`);
621
- let a = ke(n);
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 p(s(e)), await Le(H(f, `${i()}/posts/${e}`));
625
- for (let e of h) await Le(H(f, `${i()}/posts/${e}`));
626
- await Re(f, m), await p(o(e));
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: g,
630
- getPosts: b,
631
- getPost: C,
632
- getPostBySlug: E,
633
- getAdjacentPosts: N,
634
- createPost: P,
635
- updatePost: F,
636
- deletePost: I
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 Ve = 10;
640
- function He(n, i = {}) {
641
- let o = G(), s = d(), { isAdmin: c, isStaff: l } = J(), u = () => o.collectionPrefix, p = () => f(s, `${u()}Posts`), m = i.pageSize ?? Ve, h = e(() => typeof n == "string" || n === void 0 ? n : n.value), _ = e(() => i.publicOnly === void 0 ? !(c.value || l.value) : typeof i.publicOnly == "boolean" ? i.publicOnly : i.publicOnly.value), v = r([]), b = r([]), S = r(!0), E = r(!0), D = r(!1), O = r(""), A = null, M = null, N;
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 [...b.value, ...v.value]) t.id && e.set(t.id, t);
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 = h.value;
652
- return t && e.push(j("category", "==", t)), _.value && e.push(j("listed", "==", !0)), e;
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 = C(T(p(), ...I(), w("createdAt", "desc"), w(g(), "desc"), x(m)), (e) => {
656
- v.value = e.docs.map((e) => ({
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, M || (E.value = e.docs.length >= m), S.value = !1;
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), S.value = !1;
886
+ console.error("[memi-board:postList] onSnapshot failed", e), E.value = !1;
662
887
  });
663
888
  }
664
889
  async function R() {
665
- if (!(D.value || !E.value || S.value)) {
666
- D.value = !0, O.value = "";
890
+ if (!(O.value || !D.value || E.value)) {
891
+ O.value = !0, k.value = "";
667
892
  try {
668
- let e = M ?? A, t = [
893
+ let e = j ?? A, t = [
669
894
  ...I(),
670
- w("createdAt", "desc"),
671
- w(g(), "desc"),
672
- ...e ? [k(e)] : [],
673
- x(m + 1)
674
- ], n = await y(T(p(), ...t)), r = n.docs.slice(0, m), i = r.map((e) => ({
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([...b.value, ...v.value].map((e) => e.id));
678
- b.value.push(...i.filter((e) => !a.has(e.id))), M = r.at(-1) ?? M, E.value = n.docs.length > m;
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
- O.value = e.message || String(e), console.error("[memi-board:postList] loadMore failed", e);
905
+ k.value = e.message || String(e), console.error("[memi-board:postList] loadMore failed", e);
681
906
  } finally {
682
- D.value = !1;
907
+ O.value = !1;
683
908
  }
684
909
  }
685
910
  }
686
- function ee() {
687
- N?.(), v.value = [], b.value = [], A = null, M = null, E.value = !0, O.value = "", S.value = !0, L();
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([h, _], ee, { immediate: !0 }), t(() => N?.()), {
914
+ return a([b, x], z, { immediate: !0 }), t(() => N?.()), {
690
915
  posts: F,
691
- postsPending: S,
692
- hasMore: E,
693
- loadingMore: D,
694
- loadError: O,
916
+ postsPending: E,
917
+ hasMore: D,
918
+ loadingMore: O,
919
+ loadError: k,
695
920
  loadMore: R
696
921
  };
697
922
  }
698
- function Ue(t) {
699
- let n = G(), r = d(), i = () => n.collectionPrefix, a = e(() => typeof t == "string" ? t : t.value), o = e(() => h(r, `${i()}Posts`, a.value)), s = e(() => h(r, `${i()}Posts`, a.value, "body", "main")), { data: l, pending: u } = c(o), { data: f, pending: p } = c(s);
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(() => l.value ? {
702
- ...l.value,
926
+ post: e(() => c.value ? {
927
+ ...c.value,
703
928
  id: a.value,
704
- content: f.value?.content ?? ""
929
+ content: d.value?.content ?? ""
705
930
  } : null),
706
- pending: e(() => u.value || p.value)
931
+ pending: e(() => u.value || f.value)
707
932
  };
708
933
  }
709
934
  //#endregion
710
935
  //#region src/composables/useMemiBoardComments.ts
711
- var We = 10, Ge = 5, Ke = 1e3;
712
- function qe(e) {
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(`댓글은 ${Ke.toLocaleString()}자까지 작성할 수 있습니다.`);
940
+ if (t.length > 1e3) throw Error(`댓글은 ${pt.toLocaleString()}자까지 작성할 수 있습니다.`);
716
941
  return t;
717
942
  }
718
- function Je(n, i = {}) {
719
- let o = G(), s = d(), c = () => o.collectionPrefix, l = e(() => typeof n == "string" ? n : n.value), u = r([]), p = r([]), m = r(i.subscribe !== !1), _ = r([]), v = r(!1), S = r(i.subscribe !== !1), E = r(i.subscribe !== !1), O = null, N, P;
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
- ...u.value,
727
- ...p.value,
728
- ..._.value
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(l, async (e) => {
733
- if (_.value = [], u.value = [], p.value = [], O = null, clearTimeout(N), N = void 0, P?.(), P = void 0, S.value = i.subscribe !== !1, i.subscribe !== !1) {
734
- m.value = !0, E.value = !0;
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(T(f(s, `${c()}Posts`, e, "comments"), w("createdAt", "desc"), w(g(), "desc")));
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 (l.value !== e) return;
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
- p.value = n.filter((e) => e.parentId === void 0);
744
- }), R()]), l.value !== e) return;
968
+ b.value = n.filter((e) => e.parentId === void 0);
969
+ }), R()]), f.value !== e) return;
745
970
  } finally {
746
- l.value === e && (m.value = !1, E.value = !1);
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 = C(T(f(s, `${c()}Posts`, l.value, "comments"), j("parentId", "==", null), w("createdAt", "desc"), w(g(), "desc"), x(1)), (e) => {
752
- let t = new Map(u.value.map((e) => [e.id, e]));
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
- u.value = [...t.values()];
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 (!(v.value || !S.value)) {
764
- v.value = !0;
988
+ if (!(O.value || !k.value)) {
989
+ O.value = !0;
765
990
  try {
766
991
  let e = [
767
- j("parentId", "==", null),
768
- w("createdAt", "asc"),
769
- w(g(), "asc"),
770
- ...O ? [k(O)] : [],
771
- x(11)
772
- ], t = await y(T(f(s, `${c()}Posts`, l.value, "comments"), ...e)), n = t.docs.slice(0, We), r = n.map((e) => ({
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(_.value.map((e) => e.id));
776
- _.value.push(...r.filter((e) => !i.has(e.id))), O = n.at(-1) ?? O, t.size <= We && (S.value = !1, N = setTimeout(() => {
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
- v.value = !1;
1005
+ O.value = !1;
781
1006
  }
782
1007
  }
783
1008
  }
784
1009
  t(() => {
785
1010
  clearTimeout(N), P?.();
786
1011
  });
787
- async function ee(e) {
788
- let t = qe(e.body), n = M(s), r = c(), i = h(f(s, `${r}Posts`, l.value, "comments"));
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: l.value,
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: D(),
804
- updatedAt: D()
805
- }), n.update(h(s, `${r}Posts`, l.value), { commentCount: b(1) }), await n.commit();
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 z(e) {
808
- let t = qe(e.body);
1032
+ async function B(e) {
1033
+ let t = mt(e.body);
809
1034
  if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
810
- let n = M(s), r = c(), i = f(s, `${r}Posts`, l.value, "comments"), a = h(i), o = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
811
- if (!o) throw Error("댓글 스레드를 찾을 수 없습니다.");
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: l.value,
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: o,
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: D(),
828
- updatedAt: D()
829
- }), n.update(h(i, o), { replyCount: b(1) }), n.update(h(s, `${r}Posts`, l.value), { commentCount: b(1) }), await n.commit();
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 B(e) {
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 = M(s), n = c();
835
- t.delete(h(s, `${n}Posts`, l.value, "comments", e.id)), e.parentId && e.rootId && t.update(h(s, `${n}Posts`, l.value, "comments", e.rootId), { replyCount: b(-1) }), t.update(h(s, `${n}Posts`, l.value), { commentCount: b(-1) }), await t.commit(), _.value = _.value.filter((t) => t.id !== e.id), p.value = p.value.filter((t) => t.id !== e.id), u.value = u.value.filter((t) => t.id !== e.id);
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 V(e, t) {
838
- let n = qe(t);
839
- await A(h(s, `${c()}Posts`, l.value, "comments", e), {
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: D()
1066
+ updatedAt: x()
842
1067
  });
843
1068
  }
844
- async function te(e, t, n) {
845
- await A(h(s, `${c()}Posts`, l.value, "comments", e), {
1069
+ async function ee(e, t, n) {
1070
+ await w(l(c, `${d()}Posts`, f.value, "comments", e), {
846
1071
  isBlinded: t,
847
- moderatedAt: D(),
1072
+ moderatedAt: x(),
848
1073
  moderatedBy: n
849
1074
  });
850
1075
  }
851
1076
  return {
852
1077
  comments: I,
853
- commentsPending: e(() => E.value || m.value),
854
- hasMore: S,
855
- loadingMore: v,
1078
+ commentsPending: e(() => A.value || S.value),
1079
+ hasMore: k,
1080
+ loadingMore: O,
856
1081
  loadMore: R,
857
- addComment: ee,
858
- addReply: z,
859
- updateComment: V,
860
- setCommentBlinded: te,
861
- deleteComment: B
1082
+ addComment: z,
1083
+ addReply: B,
1084
+ updateComment: H,
1085
+ setCommentBlinded: ee,
1086
+ deleteComment: V
862
1087
  };
863
1088
  }
864
- function Ye(e, n) {
865
- let i = G(), a = d(), o = r([]), s = r(!1), c = r(!1), l = r(!0), u = null, p;
866
- function m(e) {
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 h() {
870
- p ||= C(T(f(a, `${i.collectionPrefix}Posts`, e, "comments"), j("rootId", "==", n), j("isReply", "==", !0), w("createdAt", "desc"), w(g(), "desc"), x(1)), (e) => {
871
- let t = new Map(o.value.map((e) => [e.id, e]));
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
- o.value = m([...t.values()]);
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 (!(s.value || !l.value)) {
883
- s.value = !0;
1106
+ async function x() {
1107
+ if (!(c.value || !d.value)) {
1108
+ c.value = !0;
884
1109
  try {
885
1110
  let t = [
886
- j("rootId", "==", n),
887
- j("isReply", "==", !0),
888
- w("createdAt", "asc"),
889
- w(g(), "asc"),
890
- ...u ? [k(u)] : [],
891
- x(6)
892
- ], r = await y(T(f(a, `${i.collectionPrefix}Posts`, e, "comments"), ...t)), s = new Set(o.value.map((e) => e.id)), d = r.docs.slice(0, Ge), p = d.map((e) => ({
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) => !s.has(e.id));
896
- o.value = m([...o.value, ...p]), u = d.at(-1) ?? u, c.value = !0, r.size <= Ge && (l.value = !1, h());
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
- s.value = !1;
1123
+ c.value = !1;
899
1124
  }
900
1125
  }
901
1126
  }
902
- function v() {
903
- p?.(), p = void 0, o.value = [], u = null, c.value = !1, l.value = !0;
1127
+ function S() {
1128
+ m?.(), m = void 0, s.value = [], f = null, l.value = !1, d.value = !0;
904
1129
  }
905
- async function b() {
906
- v(), await _();
1130
+ async function w() {
1131
+ S(), await x();
907
1132
  }
908
- return t(() => p?.()), {
909
- replies: o,
910
- loading: s,
911
- loaded: c,
912
- hasMore: l,
913
- loadMore: _,
914
- refresh: b
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 Xe(e) {
920
- let n = G(), i = d(), o = s(), c = () => n.collectionPrefix, l = r(!1), u = r(!1), f;
921
- function p(t) {
922
- return h(i, `${c()}Posts`, e, "likes", t);
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 (f?.(), f = void 0, !e) {
926
- l.value = !1;
1150
+ if (d?.(), d = void 0, !e) {
1151
+ c.value = !1;
927
1152
  return;
928
1153
  }
929
- f = C(p(e.uid), (e) => {
930
- l.value = e.exists();
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(() => f?.());
935
- async function m() {
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 l.value;
1163
+ if (u.value) return c.value;
939
1164
  u.value = !0;
940
1165
  try {
941
- let n = p(t), r = h(i, `${c()}Posts`, e);
942
- return await E(i, async (e) => (await e.get(n)).exists() ? (e.delete(n), e.update(r, { likeCount: b(-1) }), !1) : (e.set(n, {
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: D()
945
- }), e.update(r, { likeCount: b(1) }), !0));
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: l,
1176
+ isLiked: c,
952
1177
  likePending: u,
953
- toggleLike: m
1178
+ toggleLike: p
954
1179
  };
955
1180
  }
956
1181
  //#endregion
957
1182
  //#region src/composables/useMemiBoardViews.ts
958
- function Ze() {
959
- let e = G(), t = d(), n = e.collectionPrefix;
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 A(h(t, `${n}Posts`, o), { viewCount: b(1) });
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 Qe = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", $e = {
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" ? $e[e.category] || e.category : "";
1237
+ let n = e.category && e.category !== "none" ? bt[e.category] || e.category : "";
1013
1238
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
1014
1239
  }
1015
- var et = [
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 tt(e) {
1266
+ function St(e) {
1042
1267
  return new RegExp(e.join("|"), "i");
1043
1268
  }
1044
- var nt = /* @__PURE__ */ new Set([
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 rt(e) {
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" && nt.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
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 it = {
1304
+ var Tt = {
1080
1305
  flagged: !1,
1081
1306
  category: "none",
1082
1307
  reason: "",
1083
1308
  via: "empty"
1084
1309
  };
1085
- function at() {
1086
- let e = G().moderation ?? {}, t = l(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = J(), a = tt([...et, ...e.localBlocklist ?? []]);
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 = ae(t, {
1102
- backend: new ie(),
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 oe(i, {
1330
+ return ae(i, {
1106
1331
  model: n,
1107
- systemInstruction: Qe,
1332
+ systemInstruction: yt,
1108
1333
  generationConfig: {
1109
1334
  temperature: 0,
1110
1335
  maxOutputTokens: 256,
1111
1336
  responseMimeType: "application/json",
1112
- responseSchema: U.object({ properties: {
1113
- flagged: U.boolean(),
1114
- category: U.string(),
1115
- reason: U.string()
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 u(e, t = "") {
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 d(e) {
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 u(e, t);
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), u(e);
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 f(t) {
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
- ...it,
1389
+ ...Tt,
1165
1390
  via: "empty"
1166
1391
  };
1167
1392
  let a = o(i);
1168
- if (a) return d(a);
1393
+ if (a) return u(a);
1169
1394
  if (e.enabled === !1) return {
1170
- ...it,
1395
+ ...Tt,
1171
1396
  via: "disabled"
1172
1397
  };
1173
1398
  try {
1174
- let e = rt((await s().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
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 ? d(t) : t;
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 p(t) {
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
- ...it,
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 = rt((await s().generateContent([{ inlineData: {
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 ? d(r) : r;
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: f,
1228
- checkImage: p
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 ot = 5242880, st = 26214400;
1254
- async function ct(e) {
1255
- if (e.size > 26214400) throw Error(`원본 이미지는 ${st / 1024 / 1024}MB 이하여야 합니다.`);
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 lt() {
1278
- let e = G(), t = l(), n = () => e.collectionPrefix;
1502
+ function At() {
1503
+ let e = G(), t = A(), n = () => e.collectionPrefix;
1279
1504
  function r(e, r, i) {
1280
- let a = V(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${Ee(e.name)}`, s = H(a, o), c = re(s, e, {
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 B(s);
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 = V(t);
1304
- await z(H(n, e.path)).catch(() => {});
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 ct(e), a = V(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 = Ee(e.name.replace(/\.[^.]+$/, "") || "image", 40), c = `${Date.now()}-${s || "image"}`, l = r || `new-${Date.now()}`, u = `${n()}/posts/${l}/images/${c}.${o}`, d = H(a, u);
1309
- await ne(d, i.blob, { contentType: i.blob.type || "image/jpeg" });
1310
- let f = await B(d), p = await X(i.blob, {
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 = H(a, m);
1314
- return await ne(h, p, { contentType: "image/jpeg" }), {
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 B(h),
1542
+ thumbnailUrl: await V(h),
1318
1543
  thumbnailPath: m
1319
1544
  };
1320
1545
  }
1321
1546
  async function o(e) {
1322
- let n = V(t);
1323
- await Promise.all([z(H(n, e.originalPath)).catch(() => {}), z(H(n, e.thumbnailPath)).catch(() => {})]);
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 ut = [
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 dt(t = {}) {
1352
- let n = G(), r = d(), a = `${n.collectionPrefix}Users`, s = e(() => i(t.enabled) ?? !0 ? f(r, a) : null), c = o(s, { ssrKey: `${a}-management` });
1353
- async function l(e, t) {
1354
- if (!ut.some((e) => e.value === t)) throw Error("지원하지 않는 역할입니다.");
1355
- await A(h(r, a, e), {
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: D()
1582
+ updatedAt: x()
1358
1583
  });
1359
1584
  }
1360
1585
  return {
1361
1586
  users: c,
1362
1587
  usersPending: c.pending,
1363
- updateUserRole: l
1588
+ updateUserRole: u
1364
1589
  };
1365
1590
  }
1366
1591
  //#endregion
1367
1592
  //#region src/data/versionHistory.ts
1368
- var ft = [
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
- ], pt = /* @__PURE__ */ W(((e, t) => {
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
- })), mt = /* @__PURE__ */ W(((e, t) => {
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
- })), ht = /* @__PURE__ */ W(((e, t) => {
2109
+ })), It = /* @__PURE__ */ fe(((e, t) => {
1865
2110
  (function(n, r) {
1866
- 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);
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(pt(), 1), gt = /* @__PURE__ */ me(mt(), 1);
1915
- ht(), Z.default.extend(gt.default), Z.default.locale("ko");
1916
- function _t(e) {
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 vt(e, t = Date.now()) {
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 yt(e) {
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 bt(e, t) {
1926
- let n = yt(e), r = e?.toMillis?.(), i = t?.toMillis?.();
1927
- return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${yt(t)}`];
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 xt(e) {
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 { ut as BOARD_USER_ROLES, Ke as COMMENT_BODY_MAX_LENGTH, K as DEFAULT_BLOCK_BAN_DECAY_MS, ve as DEFAULT_BLOCK_BAN_THRESHOLD, Pe as DEFAULT_CATEGORIES, ot as EDITOR_IMAGE_MAX_BYTES, st as EDITOR_IMAGE_SOURCE_MAX_BYTES, Ne as buildPostPreview, X as compressImage, _e as configureMemiBoard, ye as effectiveModerationBlockCount, De as extractEditorImageUrls, _t as formatDate, yt as formatFullDate, vt as formatRelativeDate, xe as formatRestrictedUntilLabel, bt as formatTimestampDetails, ze as isFirestorePermissionDenied, be as isModerationWriteRestricted, q as moderationWriteRestrictedUntilMs, ke as postNamespaceFromStoragePath, xt as renderMarkdownToHtml, Te as slugify, Oe as storagePathFromDownloadUrl, J as useMemiBoardAuth, Je as useMemiBoardComments, G as useMemiBoardConfig, Xe as useMemiBoardLikes, at as useMemiBoardModeration, Ue as useMemiBoardPost, He as useMemiBoardPostList, Be as useMemiBoardPosts, Ye as useMemiBoardReplies, Ie as useMemiBoardSettings, lt as useMemiBoardStorage, dt as useMemiBoardUsers, Ze as useMemiBoardViews, ft as versionHistory, Me as videoListCoverUrl, je as youtubeId };
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