memi-board 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { computed as e, reactive as t, ref as n, watch as r } from "vue";
2
- import { useCollection as i, useCurrentUser as a, useFirebaseApp as o, useFirebaseAuth as s, useFirestore as c } from "vuefire";
3
- import { collection as l, deleteDoc as u, deleteField as d, doc as f, endBefore as p, getDoc as m, getDocs as h, increment as g, limit as _, limitToLast as v, orderBy as y, query as b, serverTimestamp as x, setDoc as S, startAfter as C, updateDoc as w, where as T, writeBatch as E } from "firebase/firestore";
4
- import { GoogleAuthProvider as D, OAuthProvider as ee, createUserWithEmailAndPassword as O, signInWithEmailAndPassword as te, signInWithPopup as ne, signOut as re, updateProfile as ie } from "firebase/auth";
5
- import { deleteObject as k, getDownloadURL as A, getStorage as j, listAll as ae, ref as M, uploadBytes as N, uploadBytesResumable as oe } from "firebase/storage";
6
- import { GoogleAIBackend as se, Schema as P, getAI as ce, getGenerativeModel as le } from "firebase/ai";
1
+ import { computed as e, onScopeDispose as t, reactive as n, ref as r, watch as i } from "vue";
2
+ import { useCollection as a, useCurrentUser as o, useFirebaseApp as s, useFirebaseAuth as c, useFirestore as l } from "vuefire";
3
+ import { collection as u, deleteDoc as d, deleteField as f, doc as p, documentId as m, endBefore as h, getDoc as g, getDocs as _, increment as v, limit as y, limitToLast as b, onSnapshot as x, orderBy as S, query as C, serverTimestamp as w, setDoc as T, startAfter as E, updateDoc as D, where as O, writeBatch as k } from "firebase/firestore";
4
+ import { GoogleAuthProvider as A, OAuthProvider as j, createUserWithEmailAndPassword as M, signInWithEmailAndPassword as N, signInWithPopup as P, signOut as ee, updateProfile as te } from "firebase/auth";
5
+ import { deleteObject as F, getDownloadURL as I, getStorage as L, listAll as ne, ref as R, uploadBytes as z, uploadBytesResumable as re } from "firebase/storage";
6
+ import { GoogleAIBackend as ie, Schema as B, getAI as ae, getGenerativeModel as oe } from "firebase/ai";
7
7
  //#region src/config.ts
8
- var F = "__MEMI_BOARD_CONFIG__";
9
- function I() {
8
+ var V = "__MEMI_BOARD_CONFIG__";
9
+ function H() {
10
10
  let e = globalThis;
11
- return e[F] || (e[F] = t({
11
+ return e[V] || (e[V] = n({
12
12
  collectionPrefix: "memiBoard",
13
13
  auth: { providers: ["google", "apple"] },
14
14
  moderation: {
@@ -20,19 +20,19 @@ function I() {
20
20
  blockBanThreshold: 3,
21
21
  blockBanDecayMs: 864e5
22
22
  }
23
- })), e[F];
23
+ })), e[V];
24
24
  }
25
- function ue(e) {
26
- let t = I();
25
+ function se(e) {
26
+ let t = H();
27
27
  e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
28
28
  }
29
- function L() {
30
- return I();
29
+ function U() {
30
+ return H();
31
31
  }
32
32
  //#endregion
33
33
  //#region src/utils/moderation-strike.ts
34
- var R = 3, z = 864e5;
35
- function B(e, t, n = Date.now(), r = z) {
34
+ var ce = 3, W = 864e5;
35
+ function G(e, t, n = Date.now(), r = W) {
36
36
  let i = Math.max(0, Math.floor(Number(e) || 0));
37
37
  if (i <= 0) return 0;
38
38
  let a = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -40,7 +40,7 @@ function B(e, t, n = Date.now(), r = z) {
40
40
  let o = Math.floor((n - a) / r);
41
41
  return Math.max(0, i - o);
42
42
  }
43
- function V(e, t, n = Date.now(), r = 3, i = z) {
43
+ function K(e, t, n = Date.now(), r = 3, i = W) {
44
44
  let a = Math.max(0, Math.floor(Number(e) || 0));
45
45
  if (a < r) return null;
46
46
  let o = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -48,17 +48,17 @@ function V(e, t, n = Date.now(), r = 3, i = z) {
48
48
  let s = o + (a - (r - 1)) * i;
49
49
  return s > n ? s : null;
50
50
  }
51
- function H(e, t, n = Date.now(), r = 3, i = z) {
52
- return V(e, t, n, r, i) != null;
51
+ function q(e, t, n = Date.now(), r = 3, i = W) {
52
+ return K(e, t, n, r, i) != null;
53
53
  }
54
- function U(e, t = Date.now()) {
54
+ function J(e, t = Date.now()) {
55
55
  let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
56
56
  if (r <= 0) return "곧 해제";
57
57
  if (r < 24) return `약 ${r}시간 후 해제`;
58
58
  let i = Math.floor(r / 24), a = r % 24;
59
59
  return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
60
60
  }
61
- function de(e) {
61
+ function le(e) {
62
62
  if (e == null) return null;
63
63
  if (typeof e == "number" && Number.isFinite(e)) return e;
64
64
  if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
@@ -70,186 +70,186 @@ function de(e) {
70
70
  }
71
71
  //#endregion
72
72
  //#region src/composables/useMemiBoardAuth.ts
73
- var W = "__MEMI_BOARD_AUTH_STATE__";
74
- function fe() {
73
+ var Y = "__MEMI_BOARD_AUTH_STATE__";
74
+ function ue() {
75
75
  let e = globalThis;
76
- return e[W] || (e[W] = {
77
- role: n("user"),
78
- rolePending: n(!0),
79
- rawBlockCount: n(0),
80
- blockAtMs: n(null),
76
+ return e[Y] || (e[Y] = {
77
+ role: r("user"),
78
+ rolePending: r(!0),
79
+ rawBlockCount: r(0),
80
+ blockAtMs: r(null),
81
81
  watchStarted: !1
82
- }), e[W];
82
+ }), e[Y];
83
83
  }
84
- function G() {
85
- let t = L(), n = fe(), i = c(), o = s(), l = a(), { role: u, rolePending: d, rawBlockCount: p, blockAtMs: h } = n;
86
- function _() {
84
+ function X() {
85
+ let t = U(), n = ue(), r = l(), a = c(), s = o(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: m } = n;
86
+ function h() {
87
87
  let e = t.moderation?.blockBanThreshold;
88
88
  return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
89
89
  }
90
- function v() {
90
+ function _() {
91
91
  let e = t.moderation?.blockBanDecayMs;
92
- return typeof e == "number" && e > 0 ? e : z;
92
+ return typeof e == "number" && e > 0 ? e : W;
93
93
  }
94
94
  function y(e) {
95
- return f(i, `${t.collectionPrefix}Users`, e);
95
+ return p(r, `${t.collectionPrefix}Users`, e);
96
96
  }
97
97
  function b(e) {
98
98
  if (!e) {
99
- p.value = 0, h.value = null;
99
+ f.value = 0, m.value = null;
100
100
  return;
101
101
  }
102
- p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = de(e.moderationBlockAt);
102
+ f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), m.value = le(e.moderationBlockAt);
103
103
  }
104
- async function C(e) {
105
- let t = y(e.uid), n = await m(t);
106
- if (!n.exists()) return await S(t, {
104
+ async function x(e) {
105
+ let t = y(e.uid), n = await g(t);
106
+ if (!n.exists()) return await T(t, {
107
107
  role: "user",
108
108
  displayName: e.displayName,
109
109
  email: e.email,
110
110
  photoURL: e.photoURL,
111
- updatedAt: x()
111
+ updatedAt: w()
112
112
  }), b({ moderationBlockCount: 0 }), "user";
113
113
  let r = n.data();
114
- return b(r), await w(t, {
114
+ return b(r), await D(t, {
115
115
  displayName: e.displayName,
116
116
  email: e.email,
117
117
  photoURL: e.photoURL,
118
- updatedAt: x()
118
+ updatedAt: w()
119
119
  }), [
120
120
  "admin",
121
121
  "staff",
122
122
  "user"
123
123
  ].includes(String(r.role)) ? r.role : "user";
124
124
  }
125
- async function T() {
126
- let e = l.value?.uid;
125
+ async function S() {
126
+ let e = s.value?.uid;
127
127
  if (!e) {
128
128
  b(void 0);
129
129
  return;
130
130
  }
131
- let t = await m(y(e));
131
+ let t = await g(y(e));
132
132
  b(t.exists() ? t.data() : void 0);
133
133
  }
134
- n.watchStarted || (n.watchStarted = !0, r(l, async (e) => {
134
+ n.watchStarted || (n.watchStarted = !0, i(s, async (e) => {
135
135
  if (d.value = !0, !e) {
136
136
  u.value = "user", b(void 0), d.value = !1;
137
137
  return;
138
138
  }
139
139
  try {
140
- u.value = await C(e);
140
+ u.value = await x(e);
141
141
  } catch {
142
142
  u.value = "user";
143
143
  } finally {
144
144
  d.value = !1;
145
145
  }
146
146
  }, { immediate: !0 }));
147
- let E = e(() => !!l.value), k = e(() => u.value === "admin"), A = e(() => u.value === "staff"), j = e(() => k.value || A.value), ae = e(() => B(p.value, h.value, Date.now(), v())), M = e(() => {
148
- let e = _();
149
- return e <= 0 ? !1 : H(p.value, h.value, Date.now(), e, v());
150
- }), N = e(() => {
151
- let e = _();
147
+ let C = e(() => !!s.value), E = e(() => u.value === "admin"), O = e(() => u.value === "staff"), k = e(() => E.value || O.value), F = e(() => G(f.value, m.value, Date.now(), _())), I = e(() => {
148
+ let e = h();
149
+ return e <= 0 ? !1 : q(f.value, m.value, Date.now(), e, _());
150
+ }), L = e(() => {
151
+ let e = h();
152
152
  if (e <= 0) return null;
153
- let t = V(p.value, h.value, Date.now(), e, v());
154
- return t == null ? null : U(t);
155
- }), oe = e(() => {
156
- if (!M.value) return null;
157
- let e = N.value;
153
+ let t = K(f.value, m.value, Date.now(), e, _());
154
+ return t == null ? null : J(t);
155
+ }), ne = e(() => {
156
+ if (!I.value) return null;
157
+ let e = L.value;
158
158
  return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
159
159
  });
160
- async function se() {
161
- let e = l.value?.uid, t = _();
160
+ async function R() {
161
+ let e = s.value?.uid, t = h();
162
162
  if (!e || t <= 0) return {
163
163
  effectiveCount: 0,
164
164
  restricted: !1,
165
165
  messageSuffix: ""
166
166
  };
167
- let n = y(e), r = await m(n);
168
- r.exists() ? await w(n, {
169
- moderationBlockCount: g(1),
170
- moderationBlockAt: x(),
171
- updatedAt: x()
172
- }) : await S(n, {
167
+ let n = y(e), r = await g(n);
168
+ r.exists() ? await D(n, {
169
+ moderationBlockCount: v(1),
170
+ moderationBlockAt: w(),
171
+ updatedAt: w()
172
+ }) : await T(n, {
173
173
  role: "user",
174
- displayName: l.value?.displayName ?? null,
174
+ displayName: s.value?.displayName ?? null,
175
175
  moderationBlockCount: 1,
176
- moderationBlockAt: x(),
177
- updatedAt: x()
176
+ moderationBlockAt: w(),
177
+ updatedAt: w()
178
178
  });
179
179
  let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
180
- p.value = i, h.value = a;
181
- let o = B(i, a, a, v()), s = H(i, a, a, t, v()), c = ` (경고 ${o}/${t})`;
182
- if (s) {
183
- let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
184
- c = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
180
+ f.value = i, m.value = a;
181
+ let o = G(i, a, a, _()), c = q(i, a, a, t, _()), l = ` (경고 ${o}/${t})`;
182
+ if (c) {
183
+ let e = K(i, a, a, t, _()), n = e == null ? null : J(e, a);
184
+ l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
185
185
  }
186
186
  return {
187
187
  effectiveCount: o,
188
- restricted: s,
189
- messageSuffix: c
188
+ restricted: c,
189
+ messageSuffix: l
190
190
  };
191
191
  }
192
- function P() {
193
- if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
194
- return o;
192
+ function z() {
193
+ if (!a) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
194
+ return a;
195
195
  }
196
- async function ce() {
197
- return ne(P(), new D());
196
+ async function re() {
197
+ return P(z(), new A());
198
198
  }
199
- async function le() {
200
- let e = new ee("apple.com");
201
- return e.addScope("email"), e.addScope("name"), ne(P(), e);
199
+ async function ie() {
200
+ let e = new j("apple.com");
201
+ return e.addScope("email"), e.addScope("name"), P(z(), e);
202
202
  }
203
- async function F(e, t) {
204
- return te(P(), e, t);
203
+ async function B(e, t) {
204
+ return N(z(), e, t);
205
205
  }
206
- async function I(e, t, n) {
207
- let r = await O(P(), e, t);
208
- return n && await ie(r.user, { displayName: n }), r;
206
+ async function ae(e, t, n) {
207
+ let r = await M(z(), e, t);
208
+ return n && await te(r.user, { displayName: n }), r;
209
209
  }
210
- async function ue() {
211
- await re(P());
210
+ async function oe() {
211
+ await ee(z());
212
212
  }
213
- function R(e) {
214
- return E.value && (l.value?.uid === e.authorUid || j.value);
213
+ function V(e) {
214
+ return C.value && (s.value?.uid === e.authorUid || k.value);
215
215
  }
216
- function W(e) {
217
- return E.value && (l.value?.uid === e.authorUid || j.value);
216
+ function H(e) {
217
+ return C.value && (s.value?.uid === e.authorUid || k.value);
218
218
  }
219
219
  return {
220
- user: l,
221
- isSignedIn: E,
222
- isAdmin: k,
223
- isStaff: A,
224
- canManageContent: j,
220
+ user: s,
221
+ isSignedIn: C,
222
+ isAdmin: E,
223
+ isStaff: O,
224
+ canManageContent: k,
225
225
  rolePending: d,
226
- moderationBlockCount: ae,
227
- isWriteRestricted: M,
228
- restrictedUntilLabel: N,
229
- restrictedMessage: oe,
230
- signInWithGoogle: ce,
231
- signInWithApple: le,
232
- signInWithEmail: F,
233
- signUpWithEmail: I,
234
- signOut: ue,
235
- canEdit: R,
236
- canDelete: R,
237
- canDeleteComment: W,
238
- recordContentModerationBlock: se,
239
- refreshBoardUser: T
226
+ moderationBlockCount: F,
227
+ isWriteRestricted: I,
228
+ restrictedUntilLabel: L,
229
+ restrictedMessage: ne,
230
+ signInWithGoogle: re,
231
+ signInWithApple: ie,
232
+ signInWithEmail: B,
233
+ signUpWithEmail: ae,
234
+ signOut: oe,
235
+ canEdit: V,
236
+ canDelete: V,
237
+ canDeleteComment: H,
238
+ recordContentModerationBlock: R,
239
+ refreshBoardUser: S
240
240
  };
241
241
  }
242
242
  //#endregion
243
243
  //#region src/utils/slugify.ts
244
- function K(e) {
244
+ function de(e) {
245
245
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
246
246
  }
247
- function pe(e, t = 60) {
247
+ function fe(e, t = 60) {
248
248
  return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
249
249
  }
250
250
  //#endregion
251
251
  //#region src/utils/extractEditorImageUrls.ts
252
- function me(e) {
252
+ function pe(e) {
253
253
  if (!e) return [];
254
254
  let t = /* @__PURE__ */ new Set();
255
255
  for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
@@ -258,7 +258,7 @@ function me(e) {
258
258
  }
259
259
  //#endregion
260
260
  //#region src/utils/storagePath.ts
261
- function he(e) {
261
+ function me(e) {
262
262
  try {
263
263
  let t = e.match(/\/o\/([^?]+)/);
264
264
  return t?.[1] ? decodeURIComponent(t[1]) : null;
@@ -266,25 +266,25 @@ function he(e) {
266
266
  return null;
267
267
  }
268
268
  }
269
- function q(e) {
269
+ function he(e) {
270
270
  return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
271
271
  }
272
272
  //#endregion
273
273
  //#region src/utils/postPreview.ts
274
- function J(e) {
274
+ function ge(e) {
275
275
  return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
276
276
  }
277
- function ge(e) {
277
+ function _e(e) {
278
278
  try {
279
- let t = new URL(J(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
279
+ let t = new URL(ge(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
280
280
  if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
281
281
  if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
282
282
  } catch {
283
283
  return;
284
284
  }
285
285
  }
286
- function _e(e, t = []) {
287
- let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? J(r[1]) : a, s = o ? ge(o) : void 0, c = n ? J(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
286
+ function ve(e, t = []) {
287
+ let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? ge(r[1]) : a, s = o ? _e(o) : void 0, c = n ? ge(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
288
288
  return {
289
289
  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),
290
290
  ...c ? { previewImage: c } : {},
@@ -293,26 +293,26 @@ function _e(e, t = []) {
293
293
  }
294
294
  //#endregion
295
295
  //#region src/composables/useMemiBoardPosts.ts
296
- async function Y(e) {
297
- let t = await ae(e).catch(() => ({
296
+ async function ye(e) {
297
+ let t = await ne(e).catch(() => ({
298
298
  items: [],
299
299
  prefixes: []
300
300
  }));
301
- await Promise.all([...t.items.map((e) => k(e)), ...t.prefixes.map((e) => Y(e))]);
301
+ await Promise.all([...t.items.map((e) => F(e)), ...t.prefixes.map((e) => ye(e))]);
302
302
  }
303
- async function ve(e, t) {
303
+ async function be(e, t) {
304
304
  let n = [...new Set([...t].filter(Boolean))];
305
- await Promise.all(n.map((t) => k(M(e, t)).catch(() => {})));
305
+ await Promise.all(n.map((t) => F(R(e, t)).catch(() => {})));
306
306
  }
307
- function ye() {
308
- let e = L(), t = c(), n = o(), r = () => e.collectionPrefix, i = () => l(t, `${r()}Posts`), a = (e) => f(t, `${r()}Posts`, e), s = (e) => f(t, `${r()}Posts`, e, "body", "main"), g = (e) => l(t, `${r()}Posts`, e, "comments");
309
- function D() {
310
- return f(i()).id;
307
+ function xe() {
308
+ let e = U(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
309
+ function m() {
310
+ return p(i()).id;
311
311
  }
312
- async function ee(e = {}) {
312
+ async function v(e = {}) {
313
313
  let t = e.pageSize ?? 20, n = [];
314
- e.category && n.push(T("category", "==", e.category)), n.push(y("createdAt", "desc")), e.cursor && n.push(C(e.cursor)), n.push(_(t + 1));
315
- let r = await h(b(i(), ...n)), a = r.docs.slice(0, t);
314
+ e.category && n.push(O("category", "==", e.category)), n.push(S("createdAt", "desc")), e.cursor && n.push(E(e.cursor)), n.push(y(t + 1));
315
+ let r = await _(C(i(), ...n)), a = r.docs.slice(0, t);
316
316
  return {
317
317
  posts: a.map((e) => ({
318
318
  id: e.id,
@@ -322,24 +322,24 @@ function ye() {
322
322
  hasMore: r.docs.length > t
323
323
  };
324
324
  }
325
- async function O(e) {
326
- let [t, n] = await Promise.all([m(a(e)), m(s(e))]);
325
+ async function x(e) {
326
+ let [t, n] = await Promise.all([g(a(e)), g(o(e))]);
327
327
  return t.exists() ? {
328
328
  id: t.id,
329
329
  ...t.data(),
330
330
  content: n.exists() ? n.data().content ?? "" : ""
331
331
  } : null;
332
332
  }
333
- async function te(e, t) {
334
- let n = (await h(b(i(), T("category", "==", e), T("slug", "==", t), _(1)))).docs[0];
335
- return n ? O(n.id) : null;
333
+ async function A(e, t) {
334
+ let n = (await _(C(i(), O("category", "==", e), O("slug", "==", t), y(1)))).docs[0];
335
+ return n ? x(n.id) : null;
336
336
  }
337
- async function ne(e) {
337
+ async function j(e) {
338
338
  if (!e.category || !e.createdAt) return {
339
339
  previous: null,
340
340
  next: null
341
341
  };
342
- let t = [T("category", "==", e.category), y("createdAt", "desc")], [n, r] = await Promise.all([h(b(i(), ...t, C(e.createdAt), _(1))), h(b(i(), ...t, p(e.createdAt), v(1)))]), a = (e) => {
342
+ let t = [O("category", "==", e.category), S("createdAt", "desc")], [n, r] = await Promise.all([_(C(i(), ...t, E(e.createdAt), y(1))), _(C(i(), ...t, h(e.createdAt), b(1)))]), a = (e) => {
343
343
  let t = e.docs[0];
344
344
  return t ? {
345
345
  id: t.id,
@@ -351,11 +351,11 @@ function ye() {
351
351
  next: a(r)
352
352
  };
353
353
  }
354
- async function re(e) {
355
- let t = K(e.title) || "post", n = t, r = 1;
356
- for (; !(await h(b(i(), T("category", "==", e.category || ""), T("slug", "==", n), _(1)))).empty;) r++, n = `${t}-${r}`;
357
- let o = e.postId || D(), c = _e(e.content, e.attachments);
358
- return await S(a(o), {
354
+ async function M(e) {
355
+ let t = de(e.title) || "post", n = t, r = 1;
356
+ for (; !(await _(C(i(), O("category", "==", e.category || ""), O("slug", "==", n), y(1)))).empty;) r++, n = `${t}-${r}`;
357
+ let s = e.postId || m(), c = ve(e.content, e.attachments);
358
+ return await T(a(s), {
359
359
  slug: n,
360
360
  title: e.title,
361
361
  ...c,
@@ -368,92 +368,249 @@ function ye() {
368
368
  authorPhoto: e.authorPhoto,
369
369
  moderationStatus: "approved",
370
370
  moderationModel: e.moderationModel ?? null,
371
- createdAt: x(),
372
- updatedAt: x()
373
- }), await S(s(o), { content: e.content }), n;
371
+ createdAt: w(),
372
+ updatedAt: w()
373
+ }), await T(o(s), { content: e.content }), n;
374
374
  }
375
- async function ie(e, t) {
376
- let n = _e(t.content, t.attachments);
377
- await Promise.all([w(a(e), {
375
+ async function N(e, t) {
376
+ let n = ve(t.content, t.attachments);
377
+ await Promise.all([D(a(e), {
378
378
  title: t.title,
379
379
  ...n,
380
- previewImage: n.previewImage ? n.previewImage : d(),
381
- videoUrl: n.videoUrl ? n.videoUrl : d(),
380
+ previewImage: n.previewImage ? n.previewImage : f(),
381
+ videoUrl: n.videoUrl ? n.videoUrl : f(),
382
382
  tags: t.tags ?? [],
383
- category: t.category ? t.category : d(),
383
+ category: t.category ? t.category : f(),
384
384
  attachments: t.attachments ?? [],
385
- updatedAt: x()
386
- }), S(s(e), { content: t.content }, { merge: !0 })]);
385
+ updatedAt: w()
386
+ }), T(o(e), { content: t.content }, { merge: !0 })]);
387
387
  }
388
- async function k(e) {
389
- let [i, o] = await Promise.all([m(a(e)), m(s(e))]);
388
+ async function P(e) {
389
+ let [i, s] = await Promise.all([g(a(e)), g(o(e))]);
390
390
  if (!i.exists()) return;
391
- let c = i.data(), l = o.exists() ? String(o.data().content ?? "") : "", d = await h(g(e));
392
- for (let e = 0; e < d.docs.length; e += 450) {
393
- let n = E(t);
394
- d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
391
+ let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await _(c(e));
392
+ for (let e = 0; e < f.docs.length; e += 450) {
393
+ let n = k(t);
394
+ f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
395
395
  }
396
- let f = j(n), p = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
397
- for (let t of c.attachments ?? []) if (t?.path) {
398
- p.add(t.path);
399
- let n = q(t.path);
400
- n && n !== e && _.add(n);
396
+ let p = L(n), m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
397
+ for (let t of l.attachments ?? []) if (t?.path) {
398
+ m.add(t.path);
399
+ let n = he(t.path);
400
+ n && n !== e && h.add(n);
401
401
  }
402
- for (let t of me(l)) {
403
- let n = he(t);
402
+ for (let t of pe(u)) {
403
+ let n = me(t);
404
404
  if (!n) continue;
405
- p.add(n);
405
+ m.add(n);
406
406
  let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
407
- r !== n && r.includes("/images/thumbnails/") && p.add(r);
407
+ r !== n && r.includes("/images/thumbnails/") && m.add(r);
408
408
  let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
409
- i && !n.includes("/thumbnails/") && p.add(`${i[1]}thumbnails/${i[2]}.jpg`);
410
- let a = q(n);
411
- a && a !== e && _.add(a);
409
+ i && !n.includes("/thumbnails/") && m.add(`${i[1]}thumbnails/${i[2]}.jpg`);
410
+ let a = he(n);
411
+ a && a !== e && h.add(a);
412
412
  }
413
- await u(s(e)), await Y(M(f, `${r()}/posts/${e}`));
414
- for (let e of _) await Y(M(f, `${r()}/posts/${e}`));
415
- await ve(f, p), await u(a(e));
413
+ await d(o(e)), await ye(R(p, `${r()}/posts/${e}`));
414
+ for (let e of h) await ye(R(p, `${r()}/posts/${e}`));
415
+ await be(p, m), await d(a(e));
416
416
  }
417
417
  return {
418
- createPostId: D,
419
- getPosts: ee,
420
- getPost: O,
421
- getPostBySlug: te,
422
- getAdjacentPosts: ne,
423
- createPost: re,
424
- updatePost: ie,
425
- deletePost: k
418
+ createPostId: m,
419
+ getPosts: v,
420
+ getPost: x,
421
+ getPostBySlug: A,
422
+ getAdjacentPosts: j,
423
+ createPost: M,
424
+ updatePost: N,
425
+ deletePost: P
426
426
  };
427
427
  }
428
428
  //#endregion
429
429
  //#region src/composables/useMemiBoardComments.ts
430
- function be(t) {
431
- let n = L(), r = c(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => b(l(r, `${a()}Posts`, o.value, "comments"), y("createdAt", "asc"))), u = i(s);
432
- async function d(e) {
433
- let t = E(r), n = a(), i = f(l(r, `${n}Posts`, o.value, "comments"));
430
+ var Se = 10, Ce = 5;
431
+ function we(n, a = {}) {
432
+ let o = U(), s = l(), c = () => o.collectionPrefix, d = e(() => typeof n == "string" ? n : n.value), f = r([]), h = r([]), g = r(a.subscribe !== !1), b = r([]), T = r(!1), D = r(a.subscribe !== !1), A = r(a.subscribe !== !1), j = null, M, N;
433
+ function P(e) {
434
+ return e.createdAt?.toMillis?.() ?? 0;
435
+ }
436
+ let ee = e(() => {
437
+ let e = /* @__PURE__ */ new Map();
438
+ for (let t of [
439
+ ...f.value,
440
+ ...h.value,
441
+ ...b.value
442
+ ]) t.id && e.set(t.id, t);
443
+ return [...e.values()].sort((e, t) => P(e) - P(t) || String(e.id).localeCompare(String(t.id)));
444
+ });
445
+ i(d, async (e) => {
446
+ if (b.value = [], f.value = [], h.value = [], j = null, clearTimeout(M), M = void 0, N?.(), N = void 0, D.value = a.subscribe !== !1, a.subscribe !== !1) {
447
+ g.value = !0, A.value = !0;
448
+ try {
449
+ let t = _(C(u(s, `${c()}Posts`, e, "comments"), S("createdAt", "desc"), S(m(), "desc")));
450
+ if (await Promise.all([t.then((t) => {
451
+ if (d.value !== e) return;
452
+ let n = t.docs.map((e) => ({
453
+ id: e.id,
454
+ ...e.data()
455
+ }));
456
+ h.value = n.filter((e) => e.parentId === void 0);
457
+ }), F()]), d.value !== e) return;
458
+ } finally {
459
+ d.value === e && (g.value = !1, A.value = !1);
460
+ }
461
+ }
462
+ }, { immediate: !0 });
463
+ function te() {
464
+ N?.(), N = x(C(u(s, `${c()}Posts`, d.value, "comments"), O("parentId", "==", null), S("createdAt", "desc"), S(m(), "desc"), y(1)), (e) => {
465
+ let t = new Map(f.value.map((e) => [e.id, e]));
466
+ for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
467
+ id: n.doc.id,
468
+ ...n.doc.data()
469
+ });
470
+ f.value = [...t.values()];
471
+ }, (e) => {
472
+ console.error("[memi-board:comments] onSnapshot failed", e);
473
+ });
474
+ }
475
+ async function F() {
476
+ if (!(T.value || !D.value)) {
477
+ T.value = !0;
478
+ try {
479
+ let e = [
480
+ O("parentId", "==", null),
481
+ S("createdAt", "asc"),
482
+ S(m(), "asc"),
483
+ ...j ? [E(j)] : [],
484
+ y(11)
485
+ ], t = await _(C(u(s, `${c()}Posts`, d.value, "comments"), ...e)), n = t.docs.slice(0, Se), r = n.map((e) => ({
486
+ id: e.id,
487
+ ...e.data()
488
+ })), i = new Set(b.value.map((e) => e.id));
489
+ b.value.push(...r.filter((e) => !i.has(e.id))), j = n.at(-1) ?? j, t.size <= Se && (D.value = !1, M = setTimeout(() => {
490
+ M = void 0, te();
491
+ }, 1e3));
492
+ } finally {
493
+ T.value = !1;
494
+ }
495
+ }
496
+ }
497
+ t(() => {
498
+ clearTimeout(M), N?.();
499
+ });
500
+ async function I(e) {
501
+ let t = k(s), n = c(), r = p(u(s, `${n}Posts`, d.value, "comments"));
502
+ t.set(r, {
503
+ postId: d.value,
504
+ body: e.body,
505
+ authorUid: e.authorUid,
506
+ authorName: e.authorName,
507
+ authorPhoto: e.authorPhoto,
508
+ moderationStatus: "approved",
509
+ parentId: null,
510
+ rootId: r.id,
511
+ depth: 0,
512
+ replyToUid: null,
513
+ replyToName: null,
514
+ replyCount: 0,
515
+ isReply: !1,
516
+ createdAt: w()
517
+ }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(1) }), await t.commit();
518
+ }
519
+ async function L(e) {
520
+ if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
521
+ let t = k(s), n = c(), r = u(s, `${n}Posts`, d.value, "comments"), i = p(r), a = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
522
+ if (!a) throw Error("댓글 스레드를 찾을 수 없습니다.");
523
+ let o = Math.min((e.parent.depth ?? 0) + 1, 2);
434
524
  t.set(i, {
435
- postId: o.value,
525
+ postId: d.value,
436
526
  body: e.body,
437
527
  authorUid: e.authorUid,
438
528
  authorName: e.authorName,
439
529
  authorPhoto: e.authorPhoto,
440
530
  moderationStatus: "approved",
441
- createdAt: x()
442
- }), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
531
+ parentId: e.parent.id,
532
+ rootId: a,
533
+ depth: o,
534
+ replyToUid: e.parent.authorUid,
535
+ replyToName: e.parent.authorName,
536
+ replyCount: 0,
537
+ isReply: !0,
538
+ createdAt: w()
539
+ }), t.update(p(r, a), { replyCount: v(1) }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(1) }), await t.commit();
443
540
  }
444
- async function p(e) {
445
- let t = E(r), n = a();
446
- t.delete(f(r, `${n}Posts`, o.value, "comments", e)), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
541
+ async function ne(e) {
542
+ if (!e.id) return;
543
+ if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error("답글이 있는 댓글은 현재 삭제할 수 없습니다.");
544
+ let t = k(s), n = c();
545
+ t.delete(p(s, `${n}Posts`, d.value, "comments", e.id)), e.parentId && e.rootId && t.update(p(s, `${n}Posts`, d.value, "comments", e.rootId), { replyCount: v(-1) }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(-1) }), await t.commit(), b.value = b.value.filter((t) => t.id !== e.id), h.value = h.value.filter((t) => t.id !== e.id), f.value = f.value.filter((t) => t.id !== e.id);
447
546
  }
448
547
  return {
449
- comments: u,
450
- addComment: d,
451
- deleteComment: p
548
+ comments: ee,
549
+ commentsPending: e(() => A.value || g.value),
550
+ hasMore: D,
551
+ loadingMore: T,
552
+ loadMore: F,
553
+ addComment: I,
554
+ addReply: L,
555
+ deleteComment: ne
556
+ };
557
+ }
558
+ function Te(e, n) {
559
+ let i = U(), a = l(), o = r([]), s = r(!1), c = r(!1), d = r(!0), f = null, p;
560
+ function h(e) {
561
+ return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
562
+ }
563
+ function g() {
564
+ p ||= x(C(u(a, `${i.collectionPrefix}Posts`, e, "comments"), O("rootId", "==", n), O("isReply", "==", !0), S("createdAt", "desc"), S(m(), "desc"), y(1)), (e) => {
565
+ let t = new Map(o.value.map((e) => [e.id, e]));
566
+ for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
567
+ id: n.doc.id,
568
+ ...n.doc.data()
569
+ });
570
+ o.value = h([...t.values()]);
571
+ }, (e) => {
572
+ console.error("[memi-board:replies] onSnapshot failed", e);
573
+ });
574
+ }
575
+ async function v() {
576
+ if (!(s.value || !d.value)) {
577
+ s.value = !0;
578
+ try {
579
+ let t = [
580
+ O("rootId", "==", n),
581
+ O("isReply", "==", !0),
582
+ S("createdAt", "asc"),
583
+ S(m(), "asc"),
584
+ ...f ? [E(f)] : [],
585
+ y(6)
586
+ ], r = await _(C(u(a, `${i.collectionPrefix}Posts`, e, "comments"), ...t)), s = new Set(o.value.map((e) => e.id)), l = r.docs.slice(0, Ce), p = l.map((e) => ({
587
+ id: e.id,
588
+ ...e.data()
589
+ })).filter((e) => !s.has(e.id));
590
+ o.value = h([...o.value, ...p]), f = l.at(-1) ?? f, c.value = !0, r.size <= Ce && (d.value = !1, g());
591
+ } finally {
592
+ s.value = !1;
593
+ }
594
+ }
595
+ }
596
+ function b() {
597
+ p?.(), p = void 0, o.value = [], f = null, c.value = !1, d.value = !0;
598
+ }
599
+ async function w() {
600
+ b(), await v();
601
+ }
602
+ return t(() => p?.()), {
603
+ replies: o,
604
+ loading: s,
605
+ loaded: c,
606
+ hasMore: d,
607
+ loadMore: v,
608
+ refresh: w
452
609
  };
453
610
  }
454
611
  //#endregion
455
612
  //#region src/utils/moderation-prompt.ts
456
- var xe = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", Se = {
613
+ var Ee = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", De = {
457
614
  abuse: "욕설·비속어·모욕",
458
615
  spam: "스팸·광고",
459
616
  adult: "선정적·성인 내용",
@@ -461,17 +618,17 @@ var xe = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
461
618
  other: "부적절한 내용",
462
619
  none: ""
463
620
  };
464
- function X(e) {
621
+ function Z(e) {
465
622
  if (e.via === "local" && e.localHit) {
466
623
  let t = e.localHit.trim();
467
624
  return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
468
625
  }
469
626
  let t = (e.reason || "").trim();
470
627
  if (t) return t;
471
- let n = e.category && e.category !== "none" ? Se[e.category] || e.category : "";
628
+ let n = e.category && e.category !== "none" ? De[e.category] || e.category : "";
472
629
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
473
630
  }
474
- var Ce = [
631
+ var Oe = [
475
632
  "씨발",
476
633
  "시발",
477
634
  "시이발",
@@ -497,10 +654,10 @@ var Ce = [
497
654
  "bitch",
498
655
  "asshole"
499
656
  ];
500
- function we(e) {
657
+ function ke(e) {
501
658
  return new RegExp(e.join("|"), "i");
502
659
  }
503
- var Te = /* @__PURE__ */ new Set([
660
+ var Ae = /* @__PURE__ */ new Set([
504
661
  "none",
505
662
  "abuse",
506
663
  "spam",
@@ -508,7 +665,7 @@ var Te = /* @__PURE__ */ new Set([
508
665
  "violence",
509
666
  "other"
510
667
  ]);
511
- function Ee(e) {
668
+ function je(e) {
512
669
  let t = e.trim();
513
670
  if (!t) return null;
514
671
  let n;
@@ -526,7 +683,7 @@ function Ee(e) {
526
683
  if (typeof n != "object" || !n) return null;
527
684
  let r = n;
528
685
  if (typeof r.flagged != "boolean") return null;
529
- let i = typeof r.category == "string" && Te.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
686
+ let i = typeof r.category == "string" && Ae.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
530
687
  return {
531
688
  flagged: r.flagged,
532
689
  category: i,
@@ -535,20 +692,20 @@ function Ee(e) {
535
692
  }
536
693
  //#endregion
537
694
  //#region src/composables/useMemiBoardModeration.ts
538
- var Z = {
695
+ var Me = {
539
696
  flagged: !1,
540
697
  category: "none",
541
698
  reason: "",
542
699
  via: "empty"
543
700
  };
544
- function De() {
545
- let e = L().moderation ?? {}, t = o(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = we([...Ce, ...e.localBlocklist ?? []]);
546
- function s(e) {
701
+ function Ne() {
702
+ let e = U().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = X(), a = ke([...Oe, ...e.localBlocklist ?? []]);
703
+ function o(e) {
547
704
  let t = e.match(a);
548
705
  return t ? {
549
706
  flagged: !0,
550
707
  category: "abuse",
551
- reason: X({
708
+ reason: Z({
552
709
  via: "local",
553
710
  localHit: t[0] || "",
554
711
  category: "abuse"
@@ -557,21 +714,21 @@ function De() {
557
714
  } : null;
558
715
  }
559
716
  function c() {
560
- let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ce(t, {
561
- backend: new se(),
717
+ let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ae(t, {
718
+ backend: new ie(),
562
719
  useLimitedUseAppCheckTokens: r
563
720
  });
564
- return le(i, {
721
+ return oe(i, {
565
722
  model: n,
566
- systemInstruction: xe,
723
+ systemInstruction: Ee,
567
724
  generationConfig: {
568
725
  temperature: 0,
569
726
  maxOutputTokens: 256,
570
727
  responseMimeType: "application/json",
571
- responseSchema: P.object({ properties: {
572
- flagged: P.boolean(),
573
- category: P.string(),
574
- reason: P.string()
728
+ responseSchema: B.object({ properties: {
729
+ flagged: B.boolean(),
730
+ category: B.string(),
731
+ reason: B.string()
575
732
  } })
576
733
  }
577
734
  });
@@ -592,7 +749,7 @@ function De() {
592
749
  };
593
750
  }
594
751
  function u(e, t = "") {
595
- let n = X({
752
+ let n = Z({
596
753
  reason: e.reason,
597
754
  category: e.category,
598
755
  via: e.via
@@ -620,22 +777,22 @@ function De() {
620
777
  };
621
778
  let i = t.trim();
622
779
  if (!i) return {
623
- ...Z,
780
+ ...Me,
624
781
  via: "empty"
625
782
  };
626
- let a = s(i);
783
+ let a = o(i);
627
784
  if (a) return d(a);
628
785
  if (e.enabled === !1) return {
629
- ...Z,
786
+ ...Me,
630
787
  via: "disabled"
631
788
  };
632
789
  try {
633
- let e = Ee((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
790
+ let e = je((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
634
791
  if (!e) return l();
635
792
  let t = {
636
793
  flagged: e.flagged,
637
794
  category: e.category,
638
- reason: X({
795
+ reason: Z({
639
796
  reason: e.reason,
640
797
  category: e.category,
641
798
  via: "ai"
@@ -655,14 +812,14 @@ function De() {
655
812
  via: "restricted"
656
813
  };
657
814
  if (e.enabled === !1 || !e.moderateImages) return {
658
- ...Z,
815
+ ...Me,
659
816
  via: "disabled"
660
817
  };
661
818
  try {
662
819
  let e = await new Promise((e, n) => {
663
820
  let r = new FileReader();
664
821
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
665
- }), n = Ee((await c().generateContent([{ inlineData: {
822
+ }), n = je((await c().generateContent([{ inlineData: {
666
823
  mimeType: t.type || "image/jpeg",
667
824
  data: e
668
825
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -670,7 +827,7 @@ function De() {
670
827
  let r = {
671
828
  flagged: n.flagged,
672
829
  category: n.category,
673
- reason: X({
830
+ reason: Z({
674
831
  reason: n.reason,
675
832
  category: n.category,
676
833
  via: "ai"
@@ -689,7 +846,7 @@ function De() {
689
846
  }
690
847
  //#endregion
691
848
  //#region src/utils/compressImage.ts
692
- async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
849
+ async function Pe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
693
850
  return new Promise((r, i) => {
694
851
  let a = new Image(), o = URL.createObjectURL(e);
695
852
  a.onload = () => {
@@ -709,11 +866,11 @@ async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
709
866
  }
710
867
  //#endregion
711
868
  //#region src/composables/useMemiBoardStorage.ts
712
- var ke = 5242880;
713
- function Ae() {
714
- let e = L(), t = o(), n = () => e.collectionPrefix;
869
+ var Fe = 5242880;
870
+ function Ie() {
871
+ let e = U(), t = s(), n = () => e.collectionPrefix;
715
872
  function r(e, r, i) {
716
- let a = j(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${pe(e.name)}`, s = M(a, o), c = oe(s, e, {
873
+ let a = L(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = R(a, o), c = re(s, e, {
717
874
  contentType: e.type || "application/octet-stream",
718
875
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
719
876
  });
@@ -722,7 +879,7 @@ function Ae() {
722
879
  c.on("state_changed", (e) => {
723
880
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
724
881
  }, n, async () => {
725
- let n = await A(s);
882
+ let n = await I(s);
726
883
  t({
727
884
  name: e.name,
728
885
  url: n,
@@ -736,39 +893,39 @@ function Ae() {
736
893
  };
737
894
  }
738
895
  async function i(e) {
739
- let n = j(t);
740
- await k(M(n, e.path)).catch(() => {});
896
+ let n = L(t);
897
+ await F(R(n, e.path)).catch(() => {});
741
898
  }
742
899
  async function a(e, r) {
743
900
  if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
744
- if (e.size > 5242880) throw Error(`이미지는 ${ke / 1024 / 1024}MB 이하여야 합니다.`);
745
- let i = j(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = pe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = M(i, l);
746
- await N(u, e, { contentType: e.type || "image/png" });
747
- let d = await A(u), f = await Oe(e, {
901
+ if (e.size > 5242880) throw Error(`이미지는 ${Fe / 1024 / 1024}MB 이하여야 합니다.`);
902
+ let i = L(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = R(i, l);
903
+ await z(u, e, { contentType: e.type || "image/png" });
904
+ let d = await I(u), f = await Pe(e, {
748
905
  maxWidth: 400,
749
906
  quality: .8
750
- }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = M(i, p);
751
- return await N(m, f, { contentType: "image/jpeg" }), {
907
+ }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = R(i, p);
908
+ return await z(m, f, { contentType: "image/jpeg" }), {
752
909
  originalUrl: d,
753
910
  originalPath: l,
754
- thumbnailUrl: await A(m),
911
+ thumbnailUrl: await I(m),
755
912
  thumbnailPath: p
756
913
  };
757
914
  }
758
- async function s(e) {
759
- let n = j(t);
760
- await Promise.all([k(M(n, e.originalPath)).catch(() => {}), k(M(n, e.thumbnailPath)).catch(() => {})]);
915
+ async function o(e) {
916
+ let n = L(t);
917
+ await Promise.all([F(R(n, e.originalPath)).catch(() => {}), F(R(n, e.thumbnailPath)).catch(() => {})]);
761
918
  }
762
919
  return {
763
920
  uploadAttachment: r,
764
921
  deleteAttachment: i,
765
922
  uploadEditorImage: a,
766
- deleteEditorImage: s
923
+ deleteEditorImage: o
767
924
  };
768
925
  }
769
926
  //#endregion
770
927
  //#region src/composables/useMemiBoardSettings.ts
771
- var je = [
928
+ var Le = [
772
929
  {
773
930
  id: "free",
774
931
  label: "자유",
@@ -791,8 +948,8 @@ var je = [
791
948
  order: 2
792
949
  }
793
950
  ];
794
- function Me() {
795
- let t = L(), n = c(), { isSignedIn: r } = G(), a = e(() => f(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => l(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => b(o.value, y("order", "asc"))), { data: d, pending: p } = i(s, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), m = e(() => d.value.map((e, t) => ({
951
+ function Re() {
952
+ let t = U(), n = l(), { isSignedIn: r } = X(), i = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => u(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => C(o.value, S("order", "asc"))), { data: c, pending: f } = a(s, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), m = e(() => c.value.map((e, t) => ({
796
953
  ...e,
797
954
  id: e.id,
798
955
  order: typeof e.order == "number" ? e.order : t,
@@ -804,45 +961,45 @@ function Me() {
804
961
  }
805
962
  async function g(e, t = e.order ?? 0) {
806
963
  if (!r.value) throw Error("로그인이 필요합니다.");
807
- let n = e.id.trim(), i = e.label.trim();
808
- if (!n || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
809
- await S(a.value, { updatedAt: x() }, { merge: !0 }), await S(f(o.value, n), {
810
- label: i,
964
+ let n = e.id.trim(), a = e.label.trim();
965
+ if (!n || !a) throw Error("카테고리 ID와 이름을 입력해 주세요.");
966
+ await T(i.value, { updatedAt: w() }, { merge: !0 }), await T(p(o.value, n), {
967
+ label: a,
811
968
  listView: e.listView ?? "default",
812
969
  writeRole: e.writeRole ?? "user",
813
970
  order: t,
814
- updatedAt: x()
971
+ updatedAt: w()
815
972
  }, { merge: !0 });
816
973
  }
817
974
  async function _(e) {
818
975
  if (!r.value) throw Error("로그인이 필요합니다.");
819
- let t = E(n);
820
- t.set(a.value, { updatedAt: x() }, { merge: !0 }), e.forEach((e, n) => {
976
+ let t = k(n);
977
+ t.set(i.value, { updatedAt: w() }, { merge: !0 }), e.forEach((e, n) => {
821
978
  let r = e.id.trim(), i = e.label.trim();
822
979
  if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
823
- t.set(f(o.value, r), {
980
+ t.set(p(o.value, r), {
824
981
  label: i,
825
982
  listView: e.listView ?? "default",
826
983
  writeRole: e.writeRole ?? "user",
827
984
  order: n,
828
- updatedAt: x()
985
+ updatedAt: w()
829
986
  }, { merge: !0 });
830
987
  }), await t.commit();
831
988
  }
832
989
  async function v(e) {
833
990
  if (!r.value) throw Error("로그인이 필요합니다.");
834
- await u(f(o.value, e));
991
+ await d(p(o.value, e));
835
992
  }
836
- async function C() {
993
+ async function y() {
837
994
  return Promise.resolve();
838
995
  }
839
- async function w(e) {
996
+ async function b(e) {
840
997
  let t = e.trim();
841
998
  if (!t) throw Error("카테고리 이름을 입력해 주세요.");
842
999
  if (!r.value) throw Error("로그인이 필요합니다.");
843
1000
  let n = m.value.find((e) => e.label === t);
844
1001
  if (n) return n.id;
845
- let i = K(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
1002
+ let i = de(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
846
1003
  for (; s.has(a);) a = `${i}-${o++}`;
847
1004
  return await g({
848
1005
  id: a,
@@ -855,18 +1012,18 @@ function Me() {
855
1012
  return {
856
1013
  categories: m,
857
1014
  categoryLabel: h,
858
- settingsPending: p,
859
- ensureSettings: C,
860
- addCategory: w,
1015
+ settingsPending: f,
1016
+ ensureSettings: y,
1017
+ addCategory: b,
861
1018
  saveCategory: g,
862
1019
  saveCategories: _,
863
1020
  deleteCategory: v,
864
- DEFAULT_CATEGORIES: je
1021
+ DEFAULT_CATEGORIES: Le
865
1022
  };
866
1023
  }
867
1024
  //#endregion
868
1025
  //#region src/composables/useMemiBoardUsers.ts
869
- var Ne = [
1026
+ var ze = [
870
1027
  {
871
1028
  value: "admin",
872
1029
  label: "관리자",
@@ -883,24 +1040,42 @@ var Ne = [
883
1040
  description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
884
1041
  }
885
1042
  ];
886
- function Pe() {
887
- let e = L(), t = c(), n = `${e.collectionPrefix}Users`, r = i(l(t, n), { ssrKey: `${n}-management` });
888
- async function a(e, r) {
889
- if (!Ne.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
890
- await w(f(t, n, e), {
1043
+ function Be() {
1044
+ let e = U(), t = l(), n = `${e.collectionPrefix}Users`, r = a(u(t, n), { ssrKey: `${n}-management` });
1045
+ async function i(e, r) {
1046
+ if (!ze.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
1047
+ await D(p(t, n, e), {
891
1048
  role: r,
892
- updatedAt: x()
1049
+ updatedAt: w()
893
1050
  });
894
1051
  }
895
1052
  return {
896
1053
  users: r,
897
1054
  usersPending: r.pending,
898
- updateUserRole: a
1055
+ updateUserRole: i
899
1056
  };
900
1057
  }
901
1058
  //#endregion
902
1059
  //#region src/data/versionHistory.ts
903
- var Fe = [
1060
+ var Ve = [
1061
+ {
1062
+ version: "0.9.0",
1063
+ date: "2026-08-04",
1064
+ highlights: [
1065
+ "댓글에 답글을 달고 대화 흐름을 최대 2단계로 볼 수 있어요",
1066
+ "댓글 10개·답글 5개 단위로 이어서 보고 마지막부터 새 댓글을 실시간으로 받아요",
1067
+ "더보기 버튼이 화면에 들어오면 로딩 표시와 함께 다음 댓글을 자동으로 불러와요"
1068
+ ]
1069
+ },
1070
+ {
1071
+ version: "0.8.0",
1072
+ date: "2026-08-04",
1073
+ highlights: [
1074
+ "최근 댓글 5개는 실시간으로 보고 이전 댓글은 5개씩 불러올 수 있어요",
1075
+ "댓글 시간이 상대시간으로 표시되고 전체 시간은 툴팁으로 확인할 수 있어요",
1076
+ "댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"
1077
+ ]
1078
+ },
904
1079
  {
905
1080
  version: "0.7.0",
906
1081
  date: "2026-08-04",
@@ -957,7 +1132,7 @@ var Fe = [
957
1132
  ];
958
1133
  //#endregion
959
1134
  //#region src/utils/formatDate.ts
960
- function Ie(e) {
1135
+ function He(e) {
961
1136
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
962
1137
  }
963
1138
  //#endregion
@@ -969,7 +1144,7 @@ function $(e) {
969
1144
  let t = e;
970
1145
  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;
971
1146
  }
972
- function Le(e) {
1147
+ function Ue(e) {
973
1148
  if (!e?.trim()) return "";
974
1149
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
975
1150
  function c() {
@@ -1034,6 +1209,6 @@ function Le(e) {
1034
1209
  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");
1035
1210
  }
1036
1211
  //#endregion
1037
- export { Ne as BOARD_USER_ROLES, z as DEFAULT_BLOCK_BAN_DECAY_MS, R as DEFAULT_BLOCK_BAN_THRESHOLD, je as DEFAULT_CATEGORIES, ke as EDITOR_IMAGE_MAX_BYTES, Oe as compressImage, ue as configureMemiBoard, B as effectiveModerationBlockCount, me as extractEditorImageUrls, Ie as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, Le as renderMarkdownToHtml, K as slugify, he as storagePathFromDownloadUrl, G as useMemiBoardAuth, be as useMemiBoardComments, L as useMemiBoardConfig, De as useMemiBoardModeration, ye as useMemiBoardPosts, Me as useMemiBoardSettings, Ae as useMemiBoardStorage, Pe as useMemiBoardUsers, Fe as versionHistory };
1212
+ export { ze as BOARD_USER_ROLES, W as DEFAULT_BLOCK_BAN_DECAY_MS, ce as DEFAULT_BLOCK_BAN_THRESHOLD, Le as DEFAULT_CATEGORIES, Fe as EDITOR_IMAGE_MAX_BYTES, Pe as compressImage, se as configureMemiBoard, G as effectiveModerationBlockCount, pe as extractEditorImageUrls, He as formatDate, J as formatRestrictedUntilLabel, q as isModerationWriteRestricted, K as moderationWriteRestrictedUntilMs, he as postNamespaceFromStoragePath, Ue as renderMarkdownToHtml, de as slugify, me as storagePathFromDownloadUrl, X as useMemiBoardAuth, we as useMemiBoardComments, U as useMemiBoardConfig, Ne as useMemiBoardModeration, xe as useMemiBoardPosts, Te as useMemiBoardReplies, Re as useMemiBoardSettings, Ie as useMemiBoardStorage, Be as useMemiBoardUsers, Ve as versionHistory };
1038
1213
 
1039
1214
  //# sourceMappingURL=index.js.map