memi-board 0.4.12 → 0.4.14

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
2
  import { useCollection as i, useCurrentUser as a, useDocument as o, useFirebaseApp as s, useFirebaseAuth as c, useFirestore as l } from "vuefire";
3
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 E, signOut as ie, updateProfile as ae } from "firebase/auth";
5
- import { deleteObject as D, getDownloadURL as O, getStorage as k, listAll as A, ref as j, uploadBytes as M, uploadBytesResumable as oe } from "firebase/storage";
6
- import { GoogleAIBackend as N, Schema as P, getAI as F, getGenerativeModel as I } from "firebase/ai";
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";
7
7
  //#region src/config.ts
8
- var L = "__MEMI_BOARD_CONFIG__";
9
- function R() {
8
+ var F = "__MEMI_BOARD_CONFIG__";
9
+ function I() {
10
10
  let e = globalThis;
11
- return e[L] || (e[L] = t({
11
+ return e[F] || (e[F] = t({
12
12
  collectionPrefix: "memiBoard",
13
13
  auth: { providers: ["google", "apple"] },
14
14
  moderation: {
@@ -20,19 +20,19 @@ function R() {
20
20
  blockBanThreshold: 3,
21
21
  blockBanDecayMs: 864e5
22
22
  }
23
- })), e[L];
23
+ })), e[F];
24
24
  }
25
- function z(e) {
26
- let t = R();
25
+ function L(e) {
26
+ let t = I();
27
27
  e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
28
28
  }
29
- function B() {
30
- return R();
29
+ function R() {
30
+ return I();
31
31
  }
32
32
  //#endregion
33
33
  //#region src/utils/moderation-strike.ts
34
- var se = 3, V = 864e5;
35
- function H(e, t, n = Date.now(), r = V) {
34
+ var le = 3, z = 864e5;
35
+ function B(e, t, n = Date.now(), r = z) {
36
36
  let i = Math.max(0, Math.floor(Number(e) || 0));
37
37
  if (i <= 0) return 0;
38
38
  let a = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -40,7 +40,7 @@ function H(e, t, n = Date.now(), r = V) {
40
40
  let o = Math.floor((n - a) / r);
41
41
  return Math.max(0, i - o);
42
42
  }
43
- function U(e, t, n = Date.now(), r = 3, i = V) {
43
+ function V(e, t, n = Date.now(), r = 3, i = z) {
44
44
  let a = Math.max(0, Math.floor(Number(e) || 0));
45
45
  if (a < r) return null;
46
46
  let o = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -48,17 +48,17 @@ function U(e, t, n = Date.now(), r = 3, i = V) {
48
48
  let s = o + (a - (r - 1)) * i;
49
49
  return s > n ? s : null;
50
50
  }
51
- function W(e, t, n = Date.now(), r = 3, i = V) {
52
- return U(e, t, n, r, i) != null;
51
+ function H(e, t, n = Date.now(), r = 3, i = z) {
52
+ return V(e, t, n, r, i) != null;
53
53
  }
54
- function G(e, t = Date.now()) {
54
+ function U(e, t = Date.now()) {
55
55
  let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
56
56
  if (r <= 0) return "곧 해제";
57
57
  if (r < 24) return `약 ${r}시간 후 해제`;
58
58
  let i = Math.floor(r / 24), a = r % 24;
59
59
  return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
60
60
  }
61
- function ce(e) {
61
+ function ue(e) {
62
62
  if (e == null) return null;
63
63
  if (typeof e == "number" && Number.isFinite(e)) return e;
64
64
  if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
@@ -70,26 +70,26 @@ function ce(e) {
70
70
  }
71
71
  //#endregion
72
72
  //#region src/composables/useMemiBoardAuth.ts
73
- var K = "__MEMI_BOARD_AUTH_STATE__";
74
- function le() {
73
+ var W = "__MEMI_BOARD_AUTH_STATE__";
74
+ function de() {
75
75
  let e = globalThis;
76
- return e[K] || (e[K] = {
76
+ return e[W] || (e[W] = {
77
77
  role: n("user"),
78
78
  rolePending: n(!0),
79
79
  rawBlockCount: n(0),
80
80
  blockAtMs: n(null),
81
81
  watchStarted: !1
82
- }), e[K];
82
+ }), e[W];
83
83
  }
84
- function q() {
85
- let t = B(), n = le(), i = l(), o = c(), s = a(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: h } = n;
84
+ function G() {
85
+ let t = R(), n = de(), i = l(), o = c(), s = a(), { role: u, rolePending: d, rawBlockCount: f, 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 : V;
92
+ return typeof e == "number" && e > 0 ? e : z;
93
93
  }
94
94
  function y(e) {
95
95
  return p(i, `${t.collectionPrefix}Users`, e);
@@ -99,7 +99,7 @@ function q() {
99
99
  f.value = 0, h.value = null;
100
100
  return;
101
101
  }
102
- f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ce(e.moderationBlockAt);
102
+ f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
103
103
  }
104
104
  async function ee(e) {
105
105
  let t = y(e.uid), n = await m(t);
@@ -136,20 +136,20 @@ function q() {
136
136
  d.value = !1;
137
137
  }
138
138
  }, { immediate: !0 }));
139
- let D = e(() => !!s.value), O = e(() => u.value === "admin"), k = e(() => H(f.value, h.value, Date.now(), v())), A = e(() => {
139
+ let E = e(() => !!s.value), D = e(() => u.value === "admin"), O = e(() => B(f.value, h.value, Date.now(), v())), k = e(() => {
140
140
  let e = _();
141
- return e <= 0 ? !1 : W(f.value, h.value, Date.now(), e, v());
142
- }), j = e(() => {
141
+ return e <= 0 ? !1 : H(f.value, h.value, Date.now(), e, v());
142
+ }), A = e(() => {
143
143
  let e = _();
144
144
  if (e <= 0) return null;
145
- let t = U(f.value, h.value, Date.now(), e, v());
146
- return t == null ? null : G(t);
147
- }), M = e(() => {
148
- if (!A.value) return null;
149
- let e = j.value;
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;
150
150
  return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
151
151
  });
152
- async function oe() {
152
+ async function M() {
153
153
  let e = s.value?.uid, t = _();
154
154
  if (!e || t <= 0) return {
155
155
  effectiveCount: 0,
@@ -170,9 +170,9 @@ function q() {
170
170
  });
171
171
  let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
172
172
  f.value = i, h.value = a;
173
- let o = H(i, a, a, v()), c = W(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
173
+ let o = B(i, a, a, v()), c = H(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
174
174
  if (c) {
175
- let e = U(i, a, a, t, v()), n = e == null ? null : G(e, a);
175
+ let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
176
176
  l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
177
177
  }
178
178
  return {
@@ -186,68 +186,94 @@ function q() {
186
186
  return o;
187
187
  }
188
188
  async function P() {
189
- return E(N(), new T());
189
+ return ie(N(), new T());
190
190
  }
191
- async function F() {
191
+ async function se() {
192
192
  let e = new te("apple.com");
193
- return e.addScope("email"), e.addScope("name"), E(N(), e);
193
+ return e.addScope("email"), e.addScope("name"), ie(N(), e);
194
194
  }
195
- async function I(e, t) {
195
+ async function ce(e, t) {
196
196
  return re(N(), e, t);
197
197
  }
198
- async function L(e, t, n) {
198
+ async function F(e, t, n) {
199
199
  let r = await ne(N(), e, t);
200
- return n && await ae(r.user, { displayName: n }), r;
200
+ return n && await oe(r.user, { displayName: n }), r;
201
201
  }
202
- async function R() {
203
- await ie(N());
202
+ async function I() {
203
+ await ae(N());
204
204
  }
205
- function z(e) {
206
- return D.value && (s.value?.uid === e.authorUid || O.value);
205
+ function L(e) {
206
+ return E.value && (s.value?.uid === e.authorUid || D.value);
207
207
  }
208
- function se(e) {
209
- return D.value && (s.value?.uid === e.authorUid || O.value);
208
+ function le(e) {
209
+ return E.value && (s.value?.uid === e.authorUid || D.value);
210
210
  }
211
211
  return {
212
212
  user: s,
213
- isSignedIn: D,
214
- isAdmin: O,
213
+ isSignedIn: E,
214
+ isAdmin: D,
215
215
  rolePending: d,
216
- moderationBlockCount: k,
217
- isWriteRestricted: A,
218
- restrictedUntilLabel: j,
219
- restrictedMessage: M,
216
+ moderationBlockCount: O,
217
+ isWriteRestricted: k,
218
+ restrictedUntilLabel: A,
219
+ restrictedMessage: j,
220
220
  signInWithGoogle: P,
221
- signInWithApple: F,
222
- signInWithEmail: I,
223
- signUpWithEmail: L,
224
- signOut: R,
225
- canEdit: z,
226
- canDelete: z,
227
- canDeleteComment: se,
228
- recordContentModerationBlock: oe,
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
229
  refreshBoardUser: w
230
230
  };
231
231
  }
232
232
  //#endregion
233
233
  //#region src/utils/slugify.ts
234
- function J(e) {
234
+ function K(e) {
235
235
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
236
236
  }
237
- function ue(e, t = 60) {
237
+ function fe(e, t = 60) {
238
238
  return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
239
239
  }
240
240
  //#endregion
241
+ //#region src/utils/extractEditorImageUrls.ts
242
+ function pe(e) {
243
+ if (!e) return [];
244
+ let t = /* @__PURE__ */ new Set();
245
+ for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
246
+ for (let n of e.matchAll(/<img[^>]*\ssrc=["'](https?:\/\/[^"']+)["']/gi)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
247
+ return [...t];
248
+ }
249
+ //#endregion
250
+ //#region src/utils/storagePath.ts
251
+ function me(e) {
252
+ try {
253
+ let t = e.match(/\/o\/([^?]+)/);
254
+ return t?.[1] ? decodeURIComponent(t[1]) : null;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+ function q(e) {
260
+ return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
261
+ }
262
+ //#endregion
241
263
  //#region src/composables/useMemiBoardPosts.ts
242
- async function de(e) {
243
- let t = await A(e).catch(() => ({
264
+ async function J(e) {
265
+ let t = await k(e).catch(() => ({
244
266
  items: [],
245
267
  prefixes: []
246
268
  }));
247
- await Promise.all([...t.items.map((e) => D(e)), ...t.prefixes.map((e) => de(e))]);
269
+ await Promise.all([...t.items.map((e) => E(e)), ...t.prefixes.map((e) => J(e))]);
270
+ }
271
+ async function he(e, t) {
272
+ let n = [...new Set([...t].filter(Boolean))];
273
+ await Promise.all(n.map((t) => E(A(e, t)).catch(() => {})));
248
274
  }
249
- function fe() {
250
- let e = B(), 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");
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");
251
277
  async function g(e = {}) {
252
278
  let t = e.pageSize ?? 20, n = [];
253
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));
@@ -270,7 +296,7 @@ function fe() {
270
296
  } : null;
271
297
  }
272
298
  async function te(e) {
273
- let t = J(e.title) || "post", n = t, r = 1;
299
+ let t = K(e.title) || "post", n = t, r = 1;
274
300
  for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
275
301
  return await Promise.all([x(a(n), {
276
302
  title: e.title,
@@ -297,13 +323,33 @@ function fe() {
297
323
  }), x(o(e), { content: t.content }, { merge: !0 })]);
298
324
  }
299
325
  async function re(e) {
300
- let i = await h(c(e));
301
- for (let e = 0; e < i.docs.length; e += 450) {
326
+ let [i, s] = await Promise.all([m(a(e)), m(o(e))]);
327
+ 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) {
302
330
  let n = w(t);
303
- i.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
331
+ f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
304
332
  }
305
- let s = k(n);
306
- await d(o(e)), await de(j(s, `${r()}/posts/${e}`)), await d(a(e));
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);
337
+ n && n !== e && _.add(n);
338
+ }
339
+ for (let t of pe(u)) {
340
+ let n = me(t);
341
+ if (!n) continue;
342
+ g.add(n);
343
+ let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
344
+ r !== n && r.includes("/images/thumbnails/") && g.add(r);
345
+ let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
346
+ i && !n.includes("/thumbnails/") && g.add(`${i[1]}thumbnails/${i[2]}.jpg`);
347
+ let a = q(n);
348
+ a && a !== e && _.add(a);
349
+ }
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));
307
353
  }
308
354
  return {
309
355
  getPosts: g,
@@ -315,8 +361,8 @@ function fe() {
315
361
  }
316
362
  //#endregion
317
363
  //#region src/composables/useMemiBoardComments.ts
318
- function pe(t) {
319
- let n = B(), 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);
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);
320
366
  async function d(e) {
321
367
  let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
322
368
  t.set(i, {
@@ -341,7 +387,7 @@ function pe(t) {
341
387
  }
342
388
  //#endregion
343
389
  //#region src/utils/moderation-prompt.ts
344
- var me = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", he = {
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 = {
345
391
  abuse: "욕설·비속어·모욕",
346
392
  spam: "스팸·광고",
347
393
  adult: "선정적·성인 내용",
@@ -356,10 +402,10 @@ function Y(e) {
356
402
  }
357
403
  let t = (e.reason || "").trim();
358
404
  if (t) return t;
359
- let n = e.category && e.category !== "none" ? he[e.category] || e.category : "";
405
+ let n = e.category && e.category !== "none" ? ye[e.category] || e.category : "";
360
406
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
361
407
  }
362
- var ge = [
408
+ var be = [
363
409
  "씨발",
364
410
  "시발",
365
411
  "시이발",
@@ -385,10 +431,10 @@ var ge = [
385
431
  "bitch",
386
432
  "asshole"
387
433
  ];
388
- function _e(e) {
434
+ function xe(e) {
389
435
  return new RegExp(e.join("|"), "i");
390
436
  }
391
- var ve = /* @__PURE__ */ new Set([
437
+ var Se = /* @__PURE__ */ new Set([
392
438
  "none",
393
439
  "abuse",
394
440
  "spam",
@@ -396,7 +442,7 @@ var ve = /* @__PURE__ */ new Set([
396
442
  "violence",
397
443
  "other"
398
444
  ]);
399
- function ye(e) {
445
+ function Ce(e) {
400
446
  let t = e.trim();
401
447
  if (!t) return null;
402
448
  let n;
@@ -414,7 +460,7 @@ function ye(e) {
414
460
  if (typeof n != "object" || !n) return null;
415
461
  let r = n;
416
462
  if (typeof r.flagged != "boolean") return null;
417
- let i = typeof r.category == "string" && ve.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
463
+ let i = typeof r.category == "string" && Se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
418
464
  return {
419
465
  flagged: r.flagged,
420
466
  category: i,
@@ -429,8 +475,8 @@ var X = {
429
475
  reason: "",
430
476
  via: "empty"
431
477
  };
432
- function be() {
433
- let e = B().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = q(), a = _e([...ge, ...e.localBlocklist ?? []]);
478
+ function we() {
479
+ let e = R().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = xe([...be, ...e.localBlocklist ?? []]);
434
480
  function o(e) {
435
481
  let t = e.match(a);
436
482
  return t ? {
@@ -445,13 +491,13 @@ function be() {
445
491
  } : null;
446
492
  }
447
493
  function c() {
448
- let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = F(t, {
494
+ let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
449
495
  backend: new N(),
450
496
  useLimitedUseAppCheckTokens: r
451
497
  });
452
- return I(i, {
498
+ return ce(i, {
453
499
  model: n,
454
- systemInstruction: me,
500
+ systemInstruction: ve,
455
501
  generationConfig: {
456
502
  temperature: 0,
457
503
  maxOutputTokens: 256,
@@ -518,7 +564,7 @@ function be() {
518
564
  via: "disabled"
519
565
  };
520
566
  try {
521
- let e = ye((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
567
+ let e = Ce((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
522
568
  if (!e) return l();
523
569
  let t = {
524
570
  flagged: e.flagged,
@@ -550,7 +596,7 @@ function be() {
550
596
  let e = await new Promise((e, n) => {
551
597
  let r = new FileReader();
552
598
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
553
- }), n = ye((await c().generateContent([{ inlineData: {
599
+ }), n = Ce((await c().generateContent([{ inlineData: {
554
600
  mimeType: t.type || "image/jpeg",
555
601
  data: e
556
602
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -577,7 +623,7 @@ function be() {
577
623
  }
578
624
  //#endregion
579
625
  //#region src/utils/compressImage.ts
580
- async function xe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
626
+ async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
581
627
  return new Promise((r, i) => {
582
628
  let a = new Image(), o = URL.createObjectURL(e);
583
629
  a.onload = () => {
@@ -597,11 +643,11 @@ async function xe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
597
643
  }
598
644
  //#endregion
599
645
  //#region src/composables/useMemiBoardStorage.ts
600
- var Se = 5242880;
601
- function Ce() {
602
- let e = B(), t = s(), n = () => e.collectionPrefix;
646
+ var Ee = 5242880;
647
+ function De() {
648
+ let e = R(), t = s(), n = () => e.collectionPrefix;
603
649
  function r(e, r, i) {
604
- let a = k(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${ue(e.name)}`, s = j(a, o), c = oe(s, e, {
650
+ let a = O(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = A(a, o), c = M(s, e, {
605
651
  contentType: e.type || "application/octet-stream",
606
652
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
607
653
  });
@@ -610,7 +656,7 @@ function Ce() {
610
656
  c.on("state_changed", (e) => {
611
657
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
612
658
  }, n, async () => {
613
- let n = await O(s);
659
+ let n = await D(s);
614
660
  t({
615
661
  name: e.name,
616
662
  url: n,
@@ -624,28 +670,28 @@ function Ce() {
624
670
  };
625
671
  }
626
672
  async function i(e) {
627
- let n = k(t);
628
- await D(j(n, e.path)).catch(() => {});
673
+ let n = O(t);
674
+ await E(A(n, e.path)).catch(() => {});
629
675
  }
630
676
  async function a(e, r) {
631
677
  if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
632
- if (e.size > 5242880) throw Error(`이미지는 ${Se / 1024 / 1024}MB 이하여야 합니다.`);
633
- let i = k(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = ue(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = j(i, l);
634
- await M(u, e, { contentType: e.type || "image/png" });
635
- let d = await O(u), f = await xe(e, {
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, {
636
682
  maxWidth: 400,
637
683
  quality: .8
638
- }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = j(i, p);
639
- return await M(m, f, { contentType: "image/jpeg" }), {
684
+ }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = A(i, p);
685
+ return await j(m, f, { contentType: "image/jpeg" }), {
640
686
  originalUrl: d,
641
687
  originalPath: l,
642
- thumbnailUrl: await O(m),
688
+ thumbnailUrl: await D(m),
643
689
  thumbnailPath: p
644
690
  };
645
691
  }
646
692
  async function o(e) {
647
- let n = k(t);
648
- await Promise.all([D(j(n, e.originalPath)).catch(() => {}), D(j(n, e.thumbnailPath)).catch(() => {})]);
693
+ let n = O(t);
694
+ await Promise.all([E(A(n, e.originalPath)).catch(() => {}), E(A(n, e.thumbnailPath)).catch(() => {})]);
649
695
  }
650
696
  return {
651
697
  uploadAttachment: r,
@@ -659,26 +705,44 @@ function Ce() {
659
705
  var Z = [
660
706
  {
661
707
  id: "free",
662
- label: "자유"
708
+ label: "자유",
709
+ listView: "default"
663
710
  },
664
711
  {
665
712
  id: "notice",
666
- label: "공지"
713
+ label: "공지",
714
+ listView: "default"
667
715
  },
668
716
  {
669
717
  id: "question",
670
- label: "질문"
718
+ label: "질문",
719
+ listView: "default"
671
720
  }
672
721
  ];
673
- function we() {
674
- let t = B(), n = l(), { isSignedIn: i, isAdmin: a } = q(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
722
+ function Oe() {
723
+ 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(() => {
675
724
  let e = c.value?.categories;
676
725
  return e && e.length > 0 ? e : Z;
677
726
  });
678
727
  function f(e) {
679
728
  if (e) return d.value.find((t) => t.id === e)?.label ?? e;
680
729
  }
681
- async function h() {
730
+ async function h(e) {
731
+ if (!i.value) throw Error("로그인이 필요합니다.");
732
+ let t = e.map((e) => ({
733
+ id: e.id.trim(),
734
+ label: e.label.trim(),
735
+ listView: e.listView ?? "default"
736
+ }));
737
+ if (!t.length) throw Error("카테고리를 하나 이상 만들어 주세요.");
738
+ if (t.some((e) => !e.id || !e.label)) throw Error("카테고리 이름을 모두 입력해 주세요.");
739
+ if (new Set(t.map((e) => e.id)).size !== t.length) throw Error("카테고리 ID가 중복되었습니다.");
740
+ await x(s.value, {
741
+ categories: t,
742
+ updatedAt: b()
743
+ });
744
+ }
745
+ async function g() {
682
746
  if (!i.value) return;
683
747
  let e = s.value, t = await m(e).catch(() => null);
684
748
  !t || t.exists() || await x(e, {
@@ -686,14 +750,14 @@ function we() {
686
750
  updatedAt: b()
687
751
  });
688
752
  }
689
- async function g(e) {
753
+ async function _(e) {
690
754
  let t = e.trim();
691
755
  if (!t) throw Error("카테고리 이름을 입력해 주세요.");
692
756
  if (!i.value) throw Error("로그인이 필요합니다.");
693
- await h();
694
- let n = d.value.find((e) => e.label === t || e.id === J(t));
757
+ await g();
758
+ let n = d.value.find((e) => e.label === t || e.id === K(t));
695
759
  if (n) return n.id;
696
- let r = J(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
760
+ let r = K(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
697
761
  if (a.has(r)) {
698
762
  let e = 2;
699
763
  for (; a.has(`${r}-${e}`);) e++;
@@ -701,7 +765,8 @@ function we() {
701
765
  }
702
766
  let o = [...d.value, {
703
767
  id: r,
704
- label: t
768
+ label: t,
769
+ listView: "default"
705
770
  }], c = s.value;
706
771
  return (await m(c)).exists() ? await C(c, {
707
772
  categories: o,
@@ -716,19 +781,25 @@ function we() {
716
781
  i,
717
782
  u
718
783
  ], async ([e, t, n]) => {
719
- n || c.value || !e && !t || await h().catch(() => {});
784
+ n || c.value || !e && !t || await g().catch(() => {});
720
785
  }, { immediate: !0 }), {
721
786
  categories: d,
722
787
  categoryLabel: f,
723
788
  settingsPending: u,
724
- ensureSettings: h,
725
- addCategory: g,
789
+ ensureSettings: g,
790
+ addCategory: _,
791
+ saveCategories: h,
726
792
  DEFAULT_CATEGORIES: Z
727
793
  };
728
794
  }
729
795
  //#endregion
730
796
  //#region src/data/versionHistory.ts
731
- var Te = [
797
+ var ke = [
798
+ {
799
+ version: "0.4.14",
800
+ date: "2026-08-03",
801
+ highlights: ["관리자가 카테고리 이름과 순서를 한 화면에서 관리할 수 있어요", "카테고리마다 일반·이미지·영상 목록 방식을 선택할 수 있어요"]
802
+ },
732
803
  {
733
804
  version: "0.2.2",
734
805
  date: "2026-07-31",
@@ -755,19 +826,10 @@ var Te = [
755
826
  ];
756
827
  //#endregion
757
828
  //#region src/utils/formatDate.ts
758
- function Ee(e) {
829
+ function Ae(e) {
759
830
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
760
831
  }
761
832
  //#endregion
762
- //#region src/utils/extractEditorImageUrls.ts
763
- function De(e) {
764
- if (!e) return [];
765
- let t = /* @__PURE__ */ new Set();
766
- for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
767
- for (let n of e.matchAll(/<img[^>]*\ssrc=["'](https?:\/\/[^"']+)["']/gi)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
768
- return [...t];
769
- }
770
- //#endregion
771
833
  //#region src/utils/renderMarkdown.ts
772
834
  function Q(e) {
773
835
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -776,7 +838,7 @@ function $(e) {
776
838
  let t = e;
777
839
  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;
778
840
  }
779
- function Oe(e) {
841
+ function je(e) {
780
842
  if (!e?.trim()) return "";
781
843
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
782
844
  function c() {
@@ -841,6 +903,6 @@ function Oe(e) {
841
903
  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");
842
904
  }
843
905
  //#endregion
844
- export { V as DEFAULT_BLOCK_BAN_DECAY_MS, se as DEFAULT_BLOCK_BAN_THRESHOLD, Z as DEFAULT_CATEGORIES, Se as EDITOR_IMAGE_MAX_BYTES, xe as compressImage, z as configureMemiBoard, H as effectiveModerationBlockCount, De as extractEditorImageUrls, Ee as formatDate, G as formatRestrictedUntilLabel, W as isModerationWriteRestricted, U as moderationWriteRestrictedUntilMs, Oe as renderMarkdownToHtml, q as useMemiBoardAuth, pe as useMemiBoardComments, B as useMemiBoardConfig, be as useMemiBoardModeration, fe as useMemiBoardPosts, we as useMemiBoardSettings, Ce as useMemiBoardStorage, Te as versionHistory };
906
+ 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 };
845
907
 
846
908
  //# sourceMappingURL=index.js.map