memi-board 0.4.13 → 0.5.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
1
  import { computed as e, reactive as t, ref as n, watch as r } from "vue";
2
- import { useCollection as i, useCurrentUser as a, useDocument 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, getDoc as m, getDocs as h, increment as g, limit as _, orderBy as v, query as y, serverTimestamp as b, setDoc as x, startAfter as S, updateDoc as C, where as ee, writeBatch as w } from "firebase/firestore";
4
- import { GoogleAuthProvider as T, OAuthProvider as te, createUserWithEmailAndPassword as ne, signInWithEmailAndPassword as re, signInWithPopup as ie, signOut as ae, updateProfile as oe } from "firebase/auth";
5
- import { deleteObject as E, getDownloadURL as D, getStorage as O, listAll as k, ref as A, uploadBytes as j, uploadBytesResumable as M } from "firebase/storage";
6
- import { GoogleAIBackend as N, Schema as P, getAI as se, getGenerativeModel as ce } from "firebase/ai";
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 k, signOut as ne, updateProfile as re } from "firebase/auth";
5
+ import { deleteObject as A, getDownloadURL as j, getStorage as M, listAll as N, ref as P, uploadBytes as ie, uploadBytesResumable as F } from "firebase/storage";
6
+ import { GoogleAIBackend as ae, Schema as I, getAI as oe, getGenerativeModel as se } from "firebase/ai";
7
7
  //#region src/config.ts
8
- var F = "__MEMI_BOARD_CONFIG__";
9
- function I() {
8
+ var L = "__MEMI_BOARD_CONFIG__";
9
+ function R() {
10
10
  let e = globalThis;
11
- return e[F] || (e[F] = t({
11
+ return e[L] || (e[L] = t({
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[L];
24
24
  }
25
- function L(e) {
26
- let t = I();
25
+ function ce(e) {
26
+ let t = R();
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 R() {
30
- return I();
29
+ function z() {
30
+ return R();
31
31
  }
32
32
  //#endregion
33
33
  //#region src/utils/moderation-strike.ts
34
- var le = 3, z = 864e5;
35
- function B(e, t, n = Date.now(), r = z) {
34
+ var le = 3, B = 864e5;
35
+ function V(e, t, n = Date.now(), r = B) {
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 H(e, t, n = Date.now(), r = 3, i = B) {
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,10 +48,10 @@ 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 U(e, t, n = Date.now(), r = 3, i = B) {
52
+ return H(e, t, n, r, i) != null;
53
53
  }
54
- function U(e, t = Date.now()) {
54
+ function W(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}시간 후 해제`;
@@ -70,168 +70,168 @@ function ue(e) {
70
70
  }
71
71
  //#endregion
72
72
  //#region src/composables/useMemiBoardAuth.ts
73
- var W = "__MEMI_BOARD_AUTH_STATE__";
73
+ var G = "__MEMI_BOARD_AUTH_STATE__";
74
74
  function de() {
75
75
  let e = globalThis;
76
- return e[W] || (e[W] = {
76
+ return e[G] || (e[G] = {
77
77
  role: n("user"),
78
78
  rolePending: n(!0),
79
79
  rawBlockCount: n(0),
80
80
  blockAtMs: n(null),
81
81
  watchStarted: !1
82
- }), e[W];
82
+ }), e[G];
83
83
  }
84
- function G() {
85
- let t = R(), n = de(), i = l(), o = c(), s = a(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: h } = n;
84
+ function K() {
85
+ let t = z(), n = de(), i = c(), o = s(), l = a(), { role: u, rolePending: d, rawBlockCount: p, blockAtMs: h } = n;
86
86
  function _() {
87
87
  let e = t.moderation?.blockBanThreshold;
88
88
  return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
89
89
  }
90
90
  function v() {
91
91
  let e = t.moderation?.blockBanDecayMs;
92
- return typeof e == "number" && e > 0 ? e : z;
92
+ return typeof e == "number" && e > 0 ? e : B;
93
93
  }
94
94
  function y(e) {
95
- return p(i, `${t.collectionPrefix}Users`, e);
95
+ return f(i, `${t.collectionPrefix}Users`, e);
96
96
  }
97
- function S(e) {
97
+ function b(e) {
98
98
  if (!e) {
99
- f.value = 0, h.value = null;
99
+ p.value = 0, h.value = null;
100
100
  return;
101
101
  }
102
- f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
102
+ p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
103
103
  }
104
- async function ee(e) {
104
+ async function C(e) {
105
105
  let t = y(e.uid), n = await m(t);
106
- if (!n.exists()) return await x(t, {
106
+ if (!n.exists()) return await S(t, {
107
107
  role: "user",
108
108
  displayName: e.displayName,
109
- updatedAt: b()
110
- }), S({ moderationBlockCount: 0 }), "user";
109
+ updatedAt: x()
110
+ }), b({ moderationBlockCount: 0 }), "user";
111
111
  let r = n.data();
112
- return S(r), await C(t, {
112
+ return b(r), await w(t, {
113
113
  displayName: e.displayName,
114
- updatedAt: b()
114
+ updatedAt: x()
115
115
  }), r.role ?? "user";
116
116
  }
117
- async function w() {
118
- let e = s.value?.uid;
117
+ async function T() {
118
+ let e = l.value?.uid;
119
119
  if (!e) {
120
- S(void 0);
120
+ b(void 0);
121
121
  return;
122
122
  }
123
123
  let t = await m(y(e));
124
- S(t.exists() ? t.data() : void 0);
124
+ b(t.exists() ? t.data() : void 0);
125
125
  }
126
- n.watchStarted || (n.watchStarted = !0, r(s, async (e) => {
126
+ n.watchStarted || (n.watchStarted = !0, r(l, async (e) => {
127
127
  if (d.value = !0, !e) {
128
- u.value = "user", S(void 0), d.value = !1;
128
+ u.value = "user", b(void 0), d.value = !1;
129
129
  return;
130
130
  }
131
131
  try {
132
- u.value = await ee(e);
132
+ u.value = await C(e);
133
133
  } catch {
134
134
  u.value = "user";
135
135
  } finally {
136
136
  d.value = !1;
137
137
  }
138
138
  }, { immediate: !0 }));
139
- let E = e(() => !!s.value), D = e(() => u.value === "admin"), O = e(() => B(f.value, h.value, Date.now(), v())), k = e(() => {
139
+ let E = e(() => !!l.value), A = e(() => u.value === "admin"), j = e(() => V(p.value, h.value, Date.now(), v())), M = e(() => {
140
140
  let e = _();
141
- return e <= 0 ? !1 : H(f.value, h.value, Date.now(), e, v());
142
- }), A = e(() => {
141
+ return e <= 0 ? !1 : U(p.value, h.value, Date.now(), e, v());
142
+ }), N = e(() => {
143
143
  let e = _();
144
144
  if (e <= 0) return null;
145
- let t = V(f.value, h.value, Date.now(), e, v());
146
- return t == null ? null : U(t);
147
- }), j = e(() => {
148
- if (!k.value) return null;
149
- let e = A.value;
145
+ let t = H(p.value, h.value, Date.now(), e, v());
146
+ return t == null ? null : W(t);
147
+ }), P = e(() => {
148
+ if (!M.value) return null;
149
+ let e = N.value;
150
150
  return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
151
151
  });
152
- async function M() {
153
- let e = s.value?.uid, t = _();
152
+ async function ie() {
153
+ let e = l.value?.uid, t = _();
154
154
  if (!e || t <= 0) return {
155
155
  effectiveCount: 0,
156
156
  restricted: !1,
157
157
  messageSuffix: ""
158
158
  };
159
159
  let n = y(e), r = await m(n);
160
- r.exists() ? await C(n, {
160
+ r.exists() ? await w(n, {
161
161
  moderationBlockCount: g(1),
162
- moderationBlockAt: b(),
163
- updatedAt: b()
164
- }) : await x(n, {
162
+ moderationBlockAt: x(),
163
+ updatedAt: x()
164
+ }) : await S(n, {
165
165
  role: "user",
166
- displayName: s.value?.displayName ?? null,
166
+ displayName: l.value?.displayName ?? null,
167
167
  moderationBlockCount: 1,
168
- moderationBlockAt: b(),
169
- updatedAt: b()
168
+ moderationBlockAt: x(),
169
+ updatedAt: x()
170
170
  });
171
171
  let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
172
- f.value = i, h.value = a;
173
- let o = B(i, a, a, v()), c = H(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
174
- if (c) {
175
- let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
176
- l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
172
+ p.value = i, h.value = a;
173
+ let o = V(i, a, a, v()), s = U(i, a, a, t, v()), c = ` (경고 ${o}/${t})`;
174
+ if (s) {
175
+ let e = H(i, a, a, t, v()), n = e == null ? null : W(e, a);
176
+ c = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
177
177
  }
178
178
  return {
179
179
  effectiveCount: o,
180
- restricted: c,
181
- messageSuffix: l
180
+ restricted: s,
181
+ messageSuffix: c
182
182
  };
183
183
  }
184
- function N() {
184
+ function F() {
185
185
  if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
186
186
  return o;
187
187
  }
188
- async function P() {
189
- return ie(N(), new T());
188
+ async function ae() {
189
+ return k(F(), new D());
190
190
  }
191
- async function se() {
192
- let e = new te("apple.com");
193
- return e.addScope("email"), e.addScope("name"), ie(N(), e);
191
+ async function I() {
192
+ let e = new ee("apple.com");
193
+ return e.addScope("email"), e.addScope("name"), k(F(), e);
194
194
  }
195
- async function ce(e, t) {
196
- return re(N(), e, t);
195
+ async function oe(e, t) {
196
+ return te(F(), e, t);
197
197
  }
198
- async function F(e, t, n) {
199
- let r = await ne(N(), e, t);
200
- return n && await oe(r.user, { displayName: n }), r;
198
+ async function se(e, t, n) {
199
+ let r = await O(F(), e, t);
200
+ return n && await re(r.user, { displayName: n }), r;
201
201
  }
202
- async function I() {
203
- await ae(N());
202
+ async function L() {
203
+ await ne(F());
204
204
  }
205
- function L(e) {
206
- return E.value && (s.value?.uid === e.authorUid || D.value);
205
+ function R(e) {
206
+ return E.value && (l.value?.uid === e.authorUid || A.value);
207
207
  }
208
- function le(e) {
209
- return E.value && (s.value?.uid === e.authorUid || D.value);
208
+ function ce(e) {
209
+ return E.value && (l.value?.uid === e.authorUid || A.value);
210
210
  }
211
211
  return {
212
- user: s,
212
+ user: l,
213
213
  isSignedIn: E,
214
- isAdmin: D,
214
+ isAdmin: A,
215
215
  rolePending: d,
216
- moderationBlockCount: O,
217
- isWriteRestricted: k,
218
- restrictedUntilLabel: A,
219
- restrictedMessage: j,
220
- signInWithGoogle: P,
221
- signInWithApple: se,
222
- signInWithEmail: ce,
223
- signUpWithEmail: F,
224
- signOut: I,
225
- canEdit: L,
226
- canDelete: L,
227
- canDeleteComment: le,
228
- recordContentModerationBlock: M,
229
- refreshBoardUser: w
216
+ moderationBlockCount: j,
217
+ isWriteRestricted: M,
218
+ restrictedUntilLabel: N,
219
+ restrictedMessage: P,
220
+ signInWithGoogle: ae,
221
+ signInWithApple: I,
222
+ signInWithEmail: oe,
223
+ signUpWithEmail: se,
224
+ signOut: L,
225
+ canEdit: R,
226
+ canDelete: R,
227
+ canDeleteComment: ce,
228
+ recordContentModerationBlock: ie,
229
+ refreshBoardUser: T
230
230
  };
231
231
  }
232
232
  //#endregion
233
233
  //#region src/utils/slugify.ts
234
- function K(e) {
234
+ function q(e) {
235
235
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
236
236
  }
237
237
  function fe(e, t = 60) {
@@ -256,28 +256,53 @@ function me(e) {
256
256
  return null;
257
257
  }
258
258
  }
259
- function q(e) {
259
+ function J(e) {
260
260
  return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
261
261
  }
262
262
  //#endregion
263
+ //#region src/utils/postPreview.ts
264
+ function Y(e) {
265
+ return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
266
+ }
267
+ function he(e) {
268
+ try {
269
+ let t = new URL(Y(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
270
+ if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
271
+ if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
272
+ } catch {
273
+ return;
274
+ }
275
+ }
276
+ function ge(e, t = []) {
277
+ let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? Y(r[1]) : a, s = o ? he(o) : void 0, c = n ? Y(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
278
+ return {
279
+ 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),
280
+ ...c ? { previewImage: c } : {},
281
+ ...o ? { videoUrl: o } : {}
282
+ };
283
+ }
284
+ //#endregion
263
285
  //#region src/composables/useMemiBoardPosts.ts
264
- async function J(e) {
265
- let t = await k(e).catch(() => ({
286
+ async function X(e) {
287
+ let t = await N(e).catch(() => ({
266
288
  items: [],
267
289
  prefixes: []
268
290
  }));
269
- await Promise.all([...t.items.map((e) => E(e)), ...t.prefixes.map((e) => J(e))]);
291
+ await Promise.all([...t.items.map((e) => A(e)), ...t.prefixes.map((e) => X(e))]);
270
292
  }
271
- async function he(e, t) {
293
+ async function _e(e, t) {
272
294
  let n = [...new Set([...t].filter(Boolean))];
273
- await Promise.all(n.map((t) => E(A(e, t)).catch(() => {})));
295
+ await Promise.all(n.map((t) => A(P(e, t)).catch(() => {})));
274
296
  }
275
- function ge() {
276
- let e = R(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
277
- async function g(e = {}) {
297
+ function ve() {
298
+ let e = z(), t = c(), n = o(), r = () => e.collectionPrefix, i = () => l(t, `${r()}Posts`), a = (e) => f(t, `${r()}Posts`, e), s = (e) => f(t, `${r()}Posts`, e, "body", "main"), g = (e) => l(t, `${r()}Posts`, e, "comments");
299
+ function D() {
300
+ return f(i()).id;
301
+ }
302
+ async function ee(e = {}) {
278
303
  let t = e.pageSize ?? 20, n = [];
279
- e.category && n.push(ee("category", "==", e.category)), n.push(v("createdAt", "desc")), e.cursor && n.push(S(e.cursor)), n.push(_(t + 1));
280
- let r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
304
+ e.category && n.push(T("category", "==", e.category)), n.push(y("createdAt", "desc")), e.cursor && n.push(C(e.cursor)), n.push(_(t + 1));
305
+ let r = await h(b(i(), ...n)), a = r.docs.slice(0, t);
281
306
  return {
282
307
  posts: a.map((e) => ({
283
308
  id: e.id,
@@ -287,19 +312,43 @@ function ge() {
287
312
  hasMore: r.docs.length > t
288
313
  };
289
314
  }
290
- async function T(e) {
291
- let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
315
+ async function O(e) {
316
+ let [t, n] = await Promise.all([m(a(e)), m(s(e))]);
292
317
  return t.exists() ? {
293
318
  id: t.id,
294
319
  ...t.data(),
295
320
  content: n.exists() ? n.data().content ?? "" : ""
296
321
  } : null;
297
322
  }
298
- async function te(e) {
299
- let t = K(e.title) || "post", n = t, r = 1;
300
- for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
301
- return await Promise.all([x(a(n), {
323
+ async function te(e, t) {
324
+ let n = (await h(b(i(), T("category", "==", e), T("slug", "==", t), _(1)))).docs[0];
325
+ return n ? O(n.id) : null;
326
+ }
327
+ async function k(e) {
328
+ if (!e.category || !e.createdAt) return {
329
+ previous: null,
330
+ next: null
331
+ };
332
+ 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) => {
333
+ let t = e.docs[0];
334
+ return t ? {
335
+ id: t.id,
336
+ ...t.data()
337
+ } : null;
338
+ };
339
+ return {
340
+ previous: a(n),
341
+ next: a(r)
342
+ };
343
+ }
344
+ async function ne(e) {
345
+ let t = q(e.title) || "post", n = t, r = 1;
346
+ for (; !(await h(b(i(), T("category", "==", e.category || ""), T("slug", "==", n), _(1)))).empty;) r++, n = `${t}-${r}`;
347
+ let o = e.postId || D(), c = ge(e.content, e.attachments);
348
+ return await S(a(o), {
349
+ slug: n,
302
350
  title: e.title,
351
+ ...c,
303
352
  tags: e.tags ?? [],
304
353
  ...e.category ? { category: e.category } : {},
305
354
  attachments: e.attachments ?? [],
@@ -309,62 +358,69 @@ function ge() {
309
358
  authorPhoto: e.authorPhoto,
310
359
  moderationStatus: "approved",
311
360
  moderationModel: e.moderationModel ?? null,
312
- createdAt: b(),
313
- updatedAt: b()
314
- }), x(o(n), { content: e.content })]), n;
361
+ createdAt: x(),
362
+ updatedAt: x()
363
+ }), await S(s(o), { content: e.content }), n;
315
364
  }
316
- async function ne(e, t) {
317
- await Promise.all([C(a(e), {
365
+ async function re(e, t) {
366
+ let n = ge(t.content, t.attachments);
367
+ await Promise.all([w(a(e), {
318
368
  title: t.title,
369
+ ...n,
370
+ previewImage: n.previewImage ? n.previewImage : d(),
371
+ videoUrl: n.videoUrl ? n.videoUrl : d(),
319
372
  tags: t.tags ?? [],
320
- category: t.category ? t.category : f(),
373
+ category: t.category ? t.category : d(),
321
374
  attachments: t.attachments ?? [],
322
- updatedAt: b()
323
- }), x(o(e), { content: t.content }, { merge: !0 })]);
375
+ updatedAt: x()
376
+ }), S(s(e), { content: t.content }, { merge: !0 })]);
324
377
  }
325
- async function re(e) {
326
- let [i, s] = await Promise.all([m(a(e)), m(o(e))]);
378
+ async function A(e) {
379
+ let [i, o] = await Promise.all([m(a(e)), m(s(e))]);
327
380
  if (!i.exists()) return;
328
- let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await h(c(e));
329
- for (let e = 0; e < f.docs.length; e += 450) {
330
- let n = w(t);
331
- f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
381
+ let c = i.data(), l = o.exists() ? String(o.data().content ?? "") : "", d = await h(g(e));
382
+ for (let e = 0; e < d.docs.length; e += 450) {
383
+ let n = E(t);
384
+ d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
332
385
  }
333
- let p = O(n), g = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
334
- for (let t of l.attachments ?? []) if (t?.path) {
335
- g.add(t.path);
336
- let n = q(t.path);
386
+ let f = M(n), p = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
387
+ for (let t of c.attachments ?? []) if (t?.path) {
388
+ p.add(t.path);
389
+ let n = J(t.path);
337
390
  n && n !== e && _.add(n);
338
391
  }
339
- for (let t of pe(u)) {
392
+ for (let t of pe(l)) {
340
393
  let n = me(t);
341
394
  if (!n) continue;
342
- g.add(n);
395
+ p.add(n);
343
396
  let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
344
- r !== n && r.includes("/images/thumbnails/") && g.add(r);
397
+ r !== n && r.includes("/images/thumbnails/") && p.add(r);
345
398
  let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
346
- i && !n.includes("/thumbnails/") && g.add(`${i[1]}thumbnails/${i[2]}.jpg`);
347
- let a = q(n);
399
+ i && !n.includes("/thumbnails/") && p.add(`${i[1]}thumbnails/${i[2]}.jpg`);
400
+ let a = J(n);
348
401
  a && a !== e && _.add(a);
349
402
  }
350
- await d(o(e)), await J(A(p, `${r()}/posts/${e}`));
351
- for (let e of _) await J(A(p, `${r()}/posts/${e}`));
352
- await he(p, g), await d(a(e));
403
+ await u(s(e)), await X(P(f, `${r()}/posts/${e}`));
404
+ for (let e of _) await X(P(f, `${r()}/posts/${e}`));
405
+ await _e(f, p), await u(a(e));
353
406
  }
354
407
  return {
355
- getPosts: g,
356
- getPost: T,
357
- createPost: te,
358
- updatePost: ne,
359
- deletePost: re
408
+ createPostId: D,
409
+ getPosts: ee,
410
+ getPost: O,
411
+ getPostBySlug: te,
412
+ getAdjacentPosts: k,
413
+ createPost: ne,
414
+ updatePost: re,
415
+ deletePost: A
360
416
  };
361
417
  }
362
418
  //#endregion
363
419
  //#region src/composables/useMemiBoardComments.ts
364
- function _e(t) {
365
- let n = R(), r = l(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => y(u(r, `${a()}Posts`, o.value, "comments"), v("createdAt", "asc"))), c = i(s);
420
+ function ye(t) {
421
+ let n = z(), 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);
366
422
  async function d(e) {
367
- let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
423
+ let t = E(r), n = a(), i = f(l(r, `${n}Posts`, o.value, "comments"));
368
424
  t.set(i, {
369
425
  postId: o.value,
370
426
  body: e.body,
@@ -372,22 +428,22 @@ function _e(t) {
372
428
  authorName: e.authorName,
373
429
  authorPhoto: e.authorPhoto,
374
430
  moderationStatus: "approved",
375
- createdAt: b()
376
- }), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
431
+ createdAt: x()
432
+ }), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
377
433
  }
378
- async function f(e) {
379
- let t = w(r), n = a();
380
- t.delete(p(r, `${n}Posts`, o.value, "comments", e)), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
434
+ async function p(e) {
435
+ let t = E(r), n = a();
436
+ t.delete(f(r, `${n}Posts`, o.value, "comments", e)), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
381
437
  }
382
438
  return {
383
- comments: c,
439
+ comments: u,
384
440
  addComment: d,
385
- deleteComment: f
441
+ deleteComment: p
386
442
  };
387
443
  }
388
444
  //#endregion
389
445
  //#region src/utils/moderation-prompt.ts
390
- var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", ye = {
446
+ var be = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", xe = {
391
447
  abuse: "욕설·비속어·모욕",
392
448
  spam: "스팸·광고",
393
449
  adult: "선정적·성인 내용",
@@ -395,17 +451,17 @@ var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
395
451
  other: "부적절한 내용",
396
452
  none: ""
397
453
  };
398
- function Y(e) {
454
+ function Z(e) {
399
455
  if (e.via === "local" && e.localHit) {
400
456
  let t = e.localHit.trim();
401
457
  return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
402
458
  }
403
459
  let t = (e.reason || "").trim();
404
460
  if (t) return t;
405
- let n = e.category && e.category !== "none" ? ye[e.category] || e.category : "";
461
+ let n = e.category && e.category !== "none" ? xe[e.category] || e.category : "";
406
462
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
407
463
  }
408
- var be = [
464
+ var Se = [
409
465
  "씨발",
410
466
  "시발",
411
467
  "시이발",
@@ -431,10 +487,10 @@ var be = [
431
487
  "bitch",
432
488
  "asshole"
433
489
  ];
434
- function xe(e) {
490
+ function Ce(e) {
435
491
  return new RegExp(e.join("|"), "i");
436
492
  }
437
- var Se = /* @__PURE__ */ new Set([
493
+ var we = /* @__PURE__ */ new Set([
438
494
  "none",
439
495
  "abuse",
440
496
  "spam",
@@ -442,7 +498,7 @@ var Se = /* @__PURE__ */ new Set([
442
498
  "violence",
443
499
  "other"
444
500
  ]);
445
- function Ce(e) {
501
+ function Te(e) {
446
502
  let t = e.trim();
447
503
  if (!t) return null;
448
504
  let n;
@@ -460,7 +516,7 @@ function Ce(e) {
460
516
  if (typeof n != "object" || !n) return null;
461
517
  let r = n;
462
518
  if (typeof r.flagged != "boolean") return null;
463
- let i = typeof r.category == "string" && Se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
519
+ let i = typeof r.category == "string" && we.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
464
520
  return {
465
521
  flagged: r.flagged,
466
522
  category: i,
@@ -469,20 +525,20 @@ function Ce(e) {
469
525
  }
470
526
  //#endregion
471
527
  //#region src/composables/useMemiBoardModeration.ts
472
- var X = {
528
+ var Ee = {
473
529
  flagged: !1,
474
530
  category: "none",
475
531
  reason: "",
476
532
  via: "empty"
477
533
  };
478
- function we() {
479
- let e = R().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = xe([...be, ...e.localBlocklist ?? []]);
480
- function o(e) {
534
+ function De() {
535
+ let e = z().moderation ?? {}, t = o(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = K(), a = Ce([...Se, ...e.localBlocklist ?? []]);
536
+ function s(e) {
481
537
  let t = e.match(a);
482
538
  return t ? {
483
539
  flagged: !0,
484
540
  category: "abuse",
485
- reason: Y({
541
+ reason: Z({
486
542
  via: "local",
487
543
  localHit: t[0] || "",
488
544
  category: "abuse"
@@ -491,21 +547,21 @@ function we() {
491
547
  } : null;
492
548
  }
493
549
  function c() {
494
- let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
495
- backend: new N(),
550
+ let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = oe(t, {
551
+ backend: new ae(),
496
552
  useLimitedUseAppCheckTokens: r
497
553
  });
498
- return ce(i, {
554
+ return se(i, {
499
555
  model: n,
500
- systemInstruction: ve,
556
+ systemInstruction: be,
501
557
  generationConfig: {
502
558
  temperature: 0,
503
559
  maxOutputTokens: 256,
504
560
  responseMimeType: "application/json",
505
- responseSchema: P.object({ properties: {
506
- flagged: P.boolean(),
507
- category: P.string(),
508
- reason: P.string()
561
+ responseSchema: I.object({ properties: {
562
+ flagged: I.boolean(),
563
+ category: I.string(),
564
+ reason: I.string()
509
565
  } })
510
566
  }
511
567
  });
@@ -526,7 +582,7 @@ function we() {
526
582
  };
527
583
  }
528
584
  function u(e, t = "") {
529
- let n = Y({
585
+ let n = Z({
530
586
  reason: e.reason,
531
587
  category: e.category,
532
588
  via: e.via
@@ -554,22 +610,22 @@ function we() {
554
610
  };
555
611
  let i = t.trim();
556
612
  if (!i) return {
557
- ...X,
613
+ ...Ee,
558
614
  via: "empty"
559
615
  };
560
- let a = o(i);
616
+ let a = s(i);
561
617
  if (a) return d(a);
562
618
  if (e.enabled === !1) return {
563
- ...X,
619
+ ...Ee,
564
620
  via: "disabled"
565
621
  };
566
622
  try {
567
- let e = Ce((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
623
+ let e = Te((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
568
624
  if (!e) return l();
569
625
  let t = {
570
626
  flagged: e.flagged,
571
627
  category: e.category,
572
- reason: Y({
628
+ reason: Z({
573
629
  reason: e.reason,
574
630
  category: e.category,
575
631
  via: "ai"
@@ -589,14 +645,14 @@ function we() {
589
645
  via: "restricted"
590
646
  };
591
647
  if (e.enabled === !1 || !e.moderateImages) return {
592
- ...X,
648
+ ...Ee,
593
649
  via: "disabled"
594
650
  };
595
651
  try {
596
652
  let e = await new Promise((e, n) => {
597
653
  let r = new FileReader();
598
654
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
599
- }), n = Ce((await c().generateContent([{ inlineData: {
655
+ }), n = Te((await c().generateContent([{ inlineData: {
600
656
  mimeType: t.type || "image/jpeg",
601
657
  data: e
602
658
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -604,7 +660,7 @@ function we() {
604
660
  let r = {
605
661
  flagged: n.flagged,
606
662
  category: n.category,
607
- reason: Y({
663
+ reason: Z({
608
664
  reason: n.reason,
609
665
  category: n.category,
610
666
  via: "ai"
@@ -623,7 +679,7 @@ function we() {
623
679
  }
624
680
  //#endregion
625
681
  //#region src/utils/compressImage.ts
626
- async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
682
+ async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
627
683
  return new Promise((r, i) => {
628
684
  let a = new Image(), o = URL.createObjectURL(e);
629
685
  a.onload = () => {
@@ -643,11 +699,11 @@ async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
643
699
  }
644
700
  //#endregion
645
701
  //#region src/composables/useMemiBoardStorage.ts
646
- var Ee = 5242880;
647
- function De() {
648
- let e = R(), t = s(), n = () => e.collectionPrefix;
702
+ var ke = 5242880;
703
+ function Ae() {
704
+ let e = z(), t = o(), n = () => e.collectionPrefix;
649
705
  function r(e, r, i) {
650
- let a = O(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = A(a, o), c = M(s, e, {
706
+ let a = M(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = P(a, o), c = F(s, e, {
651
707
  contentType: e.type || "application/octet-stream",
652
708
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
653
709
  });
@@ -656,7 +712,7 @@ function De() {
656
712
  c.on("state_changed", (e) => {
657
713
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
658
714
  }, n, async () => {
659
- let n = await D(s);
715
+ let n = await j(s);
660
716
  t({
661
717
  name: e.name,
662
718
  url: n,
@@ -670,111 +726,150 @@ function De() {
670
726
  };
671
727
  }
672
728
  async function i(e) {
673
- let n = O(t);
674
- await E(A(n, e.path)).catch(() => {});
729
+ let n = M(t);
730
+ await A(P(n, e.path)).catch(() => {});
675
731
  }
676
732
  async function a(e, r) {
677
733
  if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
678
- if (e.size > 5242880) throw Error(`이미지는 ${Ee / 1024 / 1024}MB 이하여야 합니다.`);
679
- let i = O(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = A(i, l);
680
- await j(u, e, { contentType: e.type || "image/png" });
681
- let d = await D(u), f = await Te(e, {
734
+ if (e.size > 5242880) throw Error(`이미지는 ${ke / 1024 / 1024}MB 이하여야 합니다.`);
735
+ let i = M(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 = P(i, l);
736
+ await ie(u, e, { contentType: e.type || "image/png" });
737
+ let d = await j(u), f = await Oe(e, {
682
738
  maxWidth: 400,
683
739
  quality: .8
684
- }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = A(i, p);
685
- return await j(m, f, { contentType: "image/jpeg" }), {
740
+ }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = P(i, p);
741
+ return await ie(m, f, { contentType: "image/jpeg" }), {
686
742
  originalUrl: d,
687
743
  originalPath: l,
688
- thumbnailUrl: await D(m),
744
+ thumbnailUrl: await j(m),
689
745
  thumbnailPath: p
690
746
  };
691
747
  }
692
- async function o(e) {
693
- let n = O(t);
694
- await Promise.all([E(A(n, e.originalPath)).catch(() => {}), E(A(n, e.thumbnailPath)).catch(() => {})]);
748
+ async function s(e) {
749
+ let n = M(t);
750
+ await Promise.all([A(P(n, e.originalPath)).catch(() => {}), A(P(n, e.thumbnailPath)).catch(() => {})]);
695
751
  }
696
752
  return {
697
753
  uploadAttachment: r,
698
754
  deleteAttachment: i,
699
755
  uploadEditorImage: a,
700
- deleteEditorImage: o
756
+ deleteEditorImage: s
701
757
  };
702
758
  }
703
759
  //#endregion
704
760
  //#region src/composables/useMemiBoardSettings.ts
705
- var Z = [
761
+ var je = [
706
762
  {
707
763
  id: "free",
708
- label: "자유"
764
+ label: "자유",
765
+ listView: "default",
766
+ order: 0
709
767
  },
710
768
  {
711
769
  id: "notice",
712
- label: "공지"
770
+ label: "공지",
771
+ listView: "default",
772
+ order: 1
713
773
  },
714
774
  {
715
775
  id: "question",
716
- label: "질문"
776
+ label: "질문",
777
+ listView: "default",
778
+ order: 2
717
779
  }
718
780
  ];
719
- function Oe() {
720
- let t = R(), n = l(), { isSignedIn: i, isAdmin: a } = G(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
721
- let e = c.value?.categories;
722
- return e && e.length > 0 ? e : Z;
723
- });
724
- function f(e) {
725
- if (e) return d.value.find((t) => t.id === e)?.label ?? e;
726
- }
727
- async function h() {
728
- if (!i.value) return;
729
- let e = s.value, t = await m(e).catch(() => null);
730
- !t || t.exists() || await x(e, {
731
- categories: Z,
732
- updatedAt: b()
733
- });
734
- }
735
- async function g(e) {
781
+ function Me() {
782
+ let t = z(), n = c(), { isSignedIn: r } = K(), 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), m = e(() => d.value.map((e, t) => ({
783
+ ...e,
784
+ id: e.id,
785
+ order: typeof e.order == "number" ? e.order : t,
786
+ listView: e.listView ?? "default"
787
+ })));
788
+ function h(e) {
789
+ if (e) return m.value.find((t) => t.id === e)?.label ?? e;
790
+ }
791
+ async function g(e, t = e.order ?? 0) {
792
+ if (!r.value) throw Error("로그인이 필요합니다.");
793
+ let n = e.id.trim(), i = e.label.trim();
794
+ if (!n || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
795
+ await S(a.value, { updatedAt: x() }, { merge: !0 }), await S(f(o.value, n), {
796
+ label: i,
797
+ listView: e.listView ?? "default",
798
+ order: t,
799
+ updatedAt: x()
800
+ }, { merge: !0 });
801
+ }
802
+ async function _(e) {
803
+ if (!r.value) throw Error("로그인이 필요합니다.");
804
+ let t = E(n);
805
+ t.set(a.value, { updatedAt: x() }, { merge: !0 }), e.forEach((e, n) => {
806
+ let r = e.id.trim(), i = e.label.trim();
807
+ if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
808
+ t.set(f(o.value, r), {
809
+ label: i,
810
+ listView: e.listView ?? "default",
811
+ order: n,
812
+ updatedAt: x()
813
+ }, { merge: !0 });
814
+ }), await t.commit();
815
+ }
816
+ async function v(e) {
817
+ if (!r.value) throw Error("로그인이 필요합니다.");
818
+ await u(f(o.value, e));
819
+ }
820
+ async function C() {
821
+ return Promise.resolve();
822
+ }
823
+ async function w(e) {
736
824
  let t = e.trim();
737
825
  if (!t) throw Error("카테고리 이름을 입력해 주세요.");
738
- if (!i.value) throw Error("로그인이 필요합니다.");
739
- await h();
740
- let n = d.value.find((e) => e.label === t || e.id === K(t));
826
+ if (!r.value) throw Error("로그인이 필요합니다.");
827
+ let n = m.value.find((e) => e.label === t);
741
828
  if (n) return n.id;
742
- let r = K(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
743
- if (a.has(r)) {
744
- let e = 2;
745
- for (; a.has(`${r}-${e}`);) e++;
746
- r = `${r}-${e}`;
747
- }
748
- let o = [...d.value, {
749
- id: r,
750
- label: t
751
- }], c = s.value;
752
- return (await m(c)).exists() ? await C(c, {
753
- categories: o,
754
- updatedAt: b()
755
- }) : await x(c, {
756
- categories: o,
757
- updatedAt: b()
758
- }), r;
759
- }
760
- return r([
761
- a,
762
- i,
763
- u
764
- ], async ([e, t, n]) => {
765
- n || c.value || !e && !t || await h().catch(() => {});
766
- }, { immediate: !0 }), {
767
- categories: d,
768
- categoryLabel: f,
769
- settingsPending: u,
770
- ensureSettings: h,
771
- addCategory: g,
772
- DEFAULT_CATEGORIES: Z
829
+ let i = q(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
830
+ for (; s.has(a);) a = `${i}-${o++}`;
831
+ return await g({
832
+ id: a,
833
+ label: t,
834
+ listView: "default",
835
+ order: m.value.length
836
+ }), a;
837
+ }
838
+ return {
839
+ categories: m,
840
+ categoryLabel: h,
841
+ settingsPending: p,
842
+ ensureSettings: C,
843
+ addCategory: w,
844
+ saveCategory: g,
845
+ saveCategories: _,
846
+ deleteCategory: v,
847
+ DEFAULT_CATEGORIES: je
773
848
  };
774
849
  }
775
850
  //#endregion
776
851
  //#region src/data/versionHistory.ts
777
- var ke = [
852
+ var Ne = [
853
+ {
854
+ version: "0.5.0",
855
+ date: "2026-08-04",
856
+ highlights: [
857
+ "게시판마다 일반·이미지·영상 목록 화면을 선택할 수 있어요",
858
+ "YouTube 링크를 붙여넣으면 본문에 영상으로 표시돼요",
859
+ "게시글 아래에서 이전 글·목록·다음 글로 이동할 수 있어요",
860
+ "관리자가 카테고리를 각각 저장하고 관리할 수 있어요"
861
+ ]
862
+ },
863
+ {
864
+ version: "0.4.15",
865
+ date: "2026-08-03",
866
+ highlights: ["설정 화면이 설치된 프로젝트에서 정상적으로 열리도록 고쳤어요"]
867
+ },
868
+ {
869
+ version: "0.4.14",
870
+ date: "2026-08-03",
871
+ highlights: ["관리자가 카테고리 이름과 순서를 한 화면에서 관리할 수 있어요", "카테고리마다 일반·이미지·영상 목록 방식을 선택할 수 있어요"]
872
+ },
778
873
  {
779
874
  version: "0.2.2",
780
875
  date: "2026-07-31",
@@ -801,7 +896,7 @@ var ke = [
801
896
  ];
802
897
  //#endregion
803
898
  //#region src/utils/formatDate.ts
804
- function Ae(e) {
899
+ function Pe(e) {
805
900
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
806
901
  }
807
902
  //#endregion
@@ -813,7 +908,7 @@ function $(e) {
813
908
  let t = e;
814
909
  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;
815
910
  }
816
- function je(e) {
911
+ function Fe(e) {
817
912
  if (!e?.trim()) return "";
818
913
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
819
914
  function c() {
@@ -878,6 +973,6 @@ function je(e) {
878
973
  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");
879
974
  }
880
975
  //#endregion
881
- export { z as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, Z as DEFAULT_CATEGORIES, Ee as EDITOR_IMAGE_MAX_BYTES, Te as compressImage, L as configureMemiBoard, B as effectiveModerationBlockCount, pe as extractEditorImageUrls, Ae as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, je as renderMarkdownToHtml, me as storagePathFromDownloadUrl, G as useMemiBoardAuth, _e as useMemiBoardComments, R as useMemiBoardConfig, we as useMemiBoardModeration, ge as useMemiBoardPosts, Oe as useMemiBoardSettings, De as useMemiBoardStorage, ke as versionHistory };
976
+ export { B as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, je as DEFAULT_CATEGORIES, ke as EDITOR_IMAGE_MAX_BYTES, Oe as compressImage, ce as configureMemiBoard, V as effectiveModerationBlockCount, pe as extractEditorImageUrls, Pe as formatDate, W as formatRestrictedUntilLabel, U as isModerationWriteRestricted, H as moderationWriteRestrictedUntilMs, J as postNamespaceFromStoragePath, Fe as renderMarkdownToHtml, q as slugify, me as storagePathFromDownloadUrl, K as useMemiBoardAuth, ye as useMemiBoardComments, z as useMemiBoardConfig, De as useMemiBoardModeration, ve as useMemiBoardPosts, Me as useMemiBoardSettings, Ae as useMemiBoardStorage, Ne as versionHistory };
882
977
 
883
978
  //# sourceMappingURL=index.js.map