memi-board 0.4.0 → 0.4.4

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,43 +1,48 @@
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
- import { collection as u, deleteDoc as d, doc as f, getDoc as p, getDocs as m, increment as h, limit as g, orderBy as _, query as v, serverTimestamp as y, setDoc as b, startAfter as x, updateDoc as S, writeBatch as C } from "firebase/firestore";
4
- import { GoogleAuthProvider as w, OAuthProvider as T, createUserWithEmailAndPassword as E, signInWithEmailAndPassword as D, signInWithPopup as O, signOut as k, updateProfile as ee } from "firebase/auth";
5
- import { deleteObject as A, getDownloadURL as j, getStorage as M, listAll as N, ref as P, uploadBytesResumable as F } from "firebase/storage";
6
- import { GoogleAIBackend as I, Schema as L, getAI as R, getGenerativeModel as z } from "firebase/ai";
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 w, writeBatch as T } from "firebase/firestore";
4
+ import { GoogleAuthProvider as E, OAuthProvider as D, createUserWithEmailAndPassword as O, signInWithEmailAndPassword as k, signInWithPopup as A, signOut as j, updateProfile as ee } from "firebase/auth";
5
+ import { deleteObject as M, getDownloadURL as N, getStorage as P, listAll as te, ref as F, uploadBytesResumable as ne } from "firebase/storage";
6
+ import { GoogleAIBackend as I, Schema as L, getAI as R, getGenerativeModel as re } from "firebase/ai";
7
7
  //#region src/config.ts
8
- var B = t({
9
- collectionPrefix: "memiBoard",
10
- auth: { providers: ["google", "apple"] },
11
- moderation: {
12
- enabled: !0,
13
- model: "gemini-2.5-flash",
14
- onError: "allow",
15
- moderateImages: !1,
16
- localBlocklist: []
17
- }
18
- });
19
- function V(e) {
20
- e.collectionPrefix && (B.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(B.auth, e.auth), e.moderation && Object.assign(B.moderation, e.moderation);
8
+ var z = "__MEMI_BOARD_CONFIG__";
9
+ function B() {
10
+ let e = globalThis;
11
+ return e[z] || (e[z] = t({
12
+ collectionPrefix: "memiBoard",
13
+ auth: { providers: ["google", "apple"] },
14
+ moderation: {
15
+ enabled: !0,
16
+ model: "gemini-2.5-flash",
17
+ onError: "allow",
18
+ moderateImages: !1,
19
+ localBlocklist: []
20
+ }
21
+ })), e[z];
21
22
  }
22
- function H() {
23
- return B;
23
+ function ie(e) {
24
+ let t = B();
25
+ e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
26
+ }
27
+ function V() {
28
+ return B();
24
29
  }
25
30
  //#endregion
26
31
  //#region src/composables/useMemiBoardAuth.ts
27
- function U() {
28
- let t = H(), i = l(), o = c(), s = a(), u = n("user"), d = n(!0);
29
- function m(e) {
30
- return f(i, `${t.collectionPrefix}Users`, e);
32
+ function H() {
33
+ let t = V(), i = l(), o = c(), s = a(), u = n("user"), d = n(!0);
34
+ function f(e) {
35
+ return p(i, `${t.collectionPrefix}Users`, e);
31
36
  }
32
37
  async function h(e) {
33
- let t = m(e.uid), n = await p(t);
34
- return n.exists() ? (await S(t, {
38
+ let t = f(e.uid), n = await m(t);
39
+ return n.exists() ? (await C(t, {
35
40
  displayName: e.displayName,
36
- updatedAt: y()
37
- }), n.data().role ?? "user") : (await b(t, {
41
+ updatedAt: b()
42
+ }), n.data().role ?? "user") : (await x(t, {
38
43
  role: "user",
39
44
  displayName: e.displayName,
40
- updatedAt: y()
45
+ updatedAt: b()
41
46
  }), "user");
42
47
  }
43
48
  r(s, async (e) => {
@@ -58,22 +63,22 @@ function U() {
58
63
  if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
59
64
  return o;
60
65
  }
61
- async function x() {
62
- return O(v(), new w());
66
+ async function y() {
67
+ return A(v(), new E());
63
68
  }
64
- async function C() {
65
- let e = new T("apple.com");
66
- return e.addScope("email"), e.addScope("name"), O(v(), e);
69
+ async function S() {
70
+ let e = new D("apple.com");
71
+ return e.addScope("email"), e.addScope("name"), A(v(), e);
67
72
  }
68
- async function A(e, t) {
69
- return D(v(), e, t);
73
+ async function w(e, t) {
74
+ return k(v(), e, t);
70
75
  }
71
- async function j(e, t, n) {
72
- let r = await E(v(), e, t);
76
+ async function T(e, t, n) {
77
+ let r = await O(v(), e, t);
73
78
  return n && await ee(r.user, { displayName: n }), r;
74
79
  }
75
80
  async function M() {
76
- await k(v());
81
+ await j(v());
77
82
  }
78
83
  function N(e) {
79
84
  return g.value && (s.value?.uid === e.authorUid || _.value);
@@ -86,10 +91,10 @@ function U() {
86
91
  isSignedIn: g,
87
92
  isAdmin: _,
88
93
  rolePending: d,
89
- signInWithGoogle: x,
90
- signInWithApple: C,
91
- signInWithEmail: A,
92
- signUpWithEmail: j,
94
+ signInWithGoogle: y,
95
+ signInWithApple: S,
96
+ signInWithEmail: w,
97
+ signUpWithEmail: T,
93
98
  signOut: M,
94
99
  canEdit: N,
95
100
  canDelete: N,
@@ -98,29 +103,27 @@ function U() {
98
103
  }
99
104
  //#endregion
100
105
  //#region src/utils/slugify.ts
101
- function W(e) {
106
+ function U(e) {
102
107
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
103
108
  }
104
- function G(e, t = 60) {
109
+ function W(e, t = 60) {
105
110
  return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
106
111
  }
107
112
  //#endregion
108
113
  //#region src/composables/useMemiBoardPosts.ts
109
- async function K(e) {
110
- let t = await N(e).catch(() => ({
114
+ async function G(e) {
115
+ let t = await te(e).catch(() => ({
111
116
  items: [],
112
117
  prefixes: []
113
118
  }));
114
- await Promise.all([...t.items.map((e) => A(e)), ...t.prefixes.map((e) => K(e))]);
119
+ await Promise.all([...t.items.map((e) => M(e)), ...t.prefixes.map((e) => G(e))]);
115
120
  }
116
- function q() {
117
- let e = H(), t = l(), n = s(), r = e.collectionPrefix, i = () => u(t, `${r}Posts`), a = (e) => f(t, `${r}Posts`, e), o = (e) => f(t, `${r}Posts`, e, "body", "main"), c = (e) => u(t, `${r}Posts`, e, "comments");
118
- async function h(e = {}) {
119
- let t = e.pageSize ?? 20, n = e.cursor ? [
120
- _("createdAt", "desc"),
121
- x(e.cursor),
122
- g(t + 1)
123
- ] : [_("createdAt", "desc"), g(t + 1)], r = await m(v(i(), ...n)), a = r.docs.slice(0, t);
121
+ function K() {
122
+ let e = V(), 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");
123
+ async function g(e = {}) {
124
+ let t = e.pageSize ?? 20, n = [];
125
+ e.category && n.push(w("category", "==", e.category)), n.push(v("createdAt", "desc")), e.cursor && n.push(S(e.cursor)), n.push(_(t + 1));
126
+ let r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
124
127
  return {
125
128
  posts: a.map((e) => ({
126
129
  id: e.id,
@@ -130,18 +133,18 @@ function q() {
130
133
  hasMore: r.docs.length > t
131
134
  };
132
135
  }
133
- async function w(e) {
134
- let [t, n] = await Promise.all([p(a(e)), p(o(e))]);
136
+ async function E(e) {
137
+ let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
135
138
  return t.exists() ? {
136
139
  id: t.id,
137
140
  ...t.data(),
138
141
  content: n.exists() ? n.data().content ?? "" : ""
139
142
  } : null;
140
143
  }
141
- async function T(e) {
142
- let t = W(e.title) || "post", n = t, r = 1;
143
- for (; (await p(a(n))).exists();) r++, n = `${t}-${r}`;
144
- return await Promise.all([b(a(n), {
144
+ async function D(e) {
145
+ let t = U(e.title) || "post", n = t, r = 1;
146
+ for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
147
+ return await Promise.all([x(a(n), {
145
148
  title: e.title,
146
149
  tags: e.tags ?? [],
147
150
  ...e.category ? { category: e.category } : {},
@@ -152,65 +155,65 @@ function q() {
152
155
  authorPhoto: e.authorPhoto,
153
156
  moderationStatus: "approved",
154
157
  moderationModel: e.moderationModel ?? null,
155
- createdAt: y(),
156
- updatedAt: y()
157
- }), b(o(n), { content: e.content })]), n;
158
+ createdAt: b(),
159
+ updatedAt: b()
160
+ }), x(o(n), { content: e.content })]), n;
158
161
  }
159
- async function E(e, t) {
160
- await Promise.all([S(a(e), {
162
+ async function O(e, t) {
163
+ await Promise.all([C(a(e), {
161
164
  title: t.title,
162
165
  tags: t.tags ?? [],
163
- ...t.category ? { category: t.category } : {},
166
+ category: t.category ? t.category : f(),
164
167
  attachments: t.attachments ?? [],
165
- updatedAt: y()
166
- }), b(o(e), { content: t.content })]);
168
+ updatedAt: b()
169
+ }), x(o(e), { content: t.content }, { merge: !0 })]);
167
170
  }
168
- async function D(e) {
169
- let i = await m(c(e));
171
+ async function k(e) {
172
+ let i = await h(c(e));
170
173
  for (let e = 0; e < i.docs.length; e += 450) {
171
- let n = C(t);
174
+ let n = T(t);
172
175
  i.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
173
176
  }
174
- let s = M(n);
175
- await d(o(e)), await K(P(s, `${r}/posts/${e}`)), await d(a(e));
177
+ let s = P(n);
178
+ await d(o(e)), await G(F(s, `${r()}/posts/${e}`)), await d(a(e));
176
179
  }
177
180
  return {
178
- getPosts: h,
179
- getPost: w,
180
- createPost: T,
181
- updatePost: E,
182
- deletePost: D
181
+ getPosts: g,
182
+ getPost: E,
183
+ createPost: D,
184
+ updatePost: O,
185
+ deletePost: k
183
186
  };
184
187
  }
185
188
  //#endregion
186
189
  //#region src/composables/useMemiBoardComments.ts
187
- function J(t) {
188
- let n = H(), r = l(), a = n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => v(u(r, `${a}Posts`, o.value, "comments"), _("createdAt", "asc"))), c = i(s);
190
+ function q(t) {
191
+ let n = V(), 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);
189
192
  async function d(e) {
190
- let t = C(r), n = f(u(r, `${a}Posts`, o.value, "comments"));
191
- t.set(n, {
193
+ let t = T(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
194
+ t.set(i, {
192
195
  postId: o.value,
193
196
  body: e.body,
194
197
  authorUid: e.authorUid,
195
198
  authorName: e.authorName,
196
199
  authorPhoto: e.authorPhoto,
197
200
  moderationStatus: "approved",
198
- createdAt: y()
199
- }), t.update(f(r, `${a}Posts`, o.value), { commentCount: h(1) }), await t.commit();
201
+ createdAt: b()
202
+ }), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
200
203
  }
201
- async function p(e) {
202
- let t = C(r);
203
- t.delete(f(r, `${a}Posts`, o.value, "comments", e)), t.update(f(r, `${a}Posts`, o.value), { commentCount: h(-1) }), await t.commit();
204
+ async function f(e) {
205
+ let t = T(r), n = a();
206
+ t.delete(p(r, `${n}Posts`, o.value, "comments", e)), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
204
207
  }
205
208
  return {
206
209
  comments: c,
207
210
  addComment: d,
208
- deleteComment: p
211
+ deleteComment: f
209
212
  };
210
213
  }
211
214
  //#endregion
212
215
  //#region src/utils/moderation-prompt.ts
213
- var Y = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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- flagged=false면 category는 \"none\", reason은 \"\"", X = [
216
+ var J = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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- flagged=false면 category는 \"none\", reason은 \"\"", ae = [
214
217
  "씨발",
215
218
  "시발",
216
219
  "씨팔",
@@ -234,10 +237,10 @@ var Y = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터
234
237
  "bitch",
235
238
  "asshole"
236
239
  ];
237
- function te(e) {
240
+ function oe(e) {
238
241
  return new RegExp(e.join("|"), "i");
239
242
  }
240
- var ne = /* @__PURE__ */ new Set([
243
+ var se = /* @__PURE__ */ new Set([
241
244
  "none",
242
245
  "abuse",
243
246
  "spam",
@@ -245,7 +248,7 @@ var ne = /* @__PURE__ */ new Set([
245
248
  "violence",
246
249
  "other"
247
250
  ]);
248
- function Z(e) {
251
+ function Y(e) {
249
252
  let t = e.trim();
250
253
  if (!t) return null;
251
254
  let n;
@@ -263,7 +266,7 @@ function Z(e) {
263
266
  if (typeof n != "object" || !n) return null;
264
267
  let r = n;
265
268
  if (typeof r.flagged != "boolean") return null;
266
- let i = typeof r.category == "string" && ne.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
269
+ let i = typeof r.category == "string" && se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
267
270
  return {
268
271
  flagged: r.flagged,
269
272
  category: i,
@@ -272,85 +275,159 @@ function Z(e) {
272
275
  }
273
276
  //#endregion
274
277
  //#region src/composables/useMemiBoardModeration.ts
275
- var Q = {
278
+ var X = {
276
279
  flagged: !1,
277
280
  category: "none",
278
- reason: ""
281
+ reason: "",
282
+ via: "empty"
279
283
  };
280
- function re() {
281
- let e = H().moderation ?? {}, t = s(), n = te([...X, ...e.localBlocklist ?? []]);
284
+ function Z(e, t) {
285
+ console.info(`[memi-board:moderation] ${e}`, t);
286
+ }
287
+ function Q() {
288
+ let e = V().moderation ?? {}, t = s(), n = oe([...ae, ...e.localBlocklist ?? []]);
282
289
  function r(e) {
283
- return n.test(e) ? {
290
+ return e.match(n) ? {
284
291
  flagged: !0,
285
292
  category: "abuse",
286
- reason: "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요."
293
+ reason: "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.",
294
+ via: "local"
287
295
  } : null;
288
296
  }
289
297
  function i() {
290
- let n = R(t, {
298
+ let n = e.model ?? "gemini-2.5-flash", r = R(t, {
291
299
  backend: new I(),
292
300
  useLimitedUseAppCheckTokens: !0
293
301
  });
294
- return z(n, {
295
- model: e.model ?? "gemini-2.5-flash",
296
- systemInstruction: Y,
297
- generationConfig: {
298
- temperature: 0,
299
- maxOutputTokens: 256,
300
- responseMimeType: "application/json",
301
- responseSchema: L.object({ properties: {
302
- flagged: L.boolean(),
303
- category: L.string(),
304
- reason: L.string()
305
- } })
306
- }
307
- });
302
+ return {
303
+ modelName: n,
304
+ model: re(r, {
305
+ model: n,
306
+ systemInstruction: J,
307
+ generationConfig: {
308
+ temperature: 0,
309
+ maxOutputTokens: 256,
310
+ responseMimeType: "application/json",
311
+ responseSchema: L.object({ properties: {
312
+ flagged: L.boolean(),
313
+ category: L.string(),
314
+ reason: L.string()
315
+ } })
316
+ }
317
+ })
318
+ };
308
319
  }
309
320
  function a() {
310
321
  return e.onError === "block" ? {
311
322
  flagged: !0,
312
323
  category: "other",
313
324
  reason: "검열 서비스에 일시적으로 연결할 수 없어 제출이 보류되었습니다. 잠시 후 다시 시도해 주세요.",
314
- error: !0
325
+ error: !0,
326
+ via: "ai-error-block"
315
327
  } : {
316
- ...Q,
317
- error: !0
328
+ flagged: !1,
329
+ category: "none",
330
+ reason: "",
331
+ error: !0,
332
+ via: "ai-error-allow"
318
333
  };
319
334
  }
320
335
  async function o(t) {
321
- let n = t.trim();
322
- if (!n) return Q;
323
- let o = r(n);
324
- if (o) return o;
325
- if (e.enabled === !1) return Q;
336
+ let o = t.trim();
337
+ if (Z("start", {
338
+ preview: o.slice(0, 80),
339
+ length: o.length,
340
+ enabled: e.enabled !== !1,
341
+ model: e.model ?? "gemini-2.5-flash",
342
+ onError: e.onError ?? "allow"
343
+ }), !o) {
344
+ let e = {
345
+ ...X,
346
+ via: "empty"
347
+ };
348
+ return Z("result", e), e;
349
+ }
350
+ let s = r(o);
351
+ if (s) {
352
+ let e = o.match(n)?.[0];
353
+ return Z("local-block", {
354
+ hit: e,
355
+ ...s
356
+ }), Z("result", s), s;
357
+ }
358
+ if (Z("local-pass", { message: "로컬 금칙어 미매칭 → AI Logic 호출" }), e.enabled === !1) {
359
+ let e = {
360
+ ...X,
361
+ via: "disabled"
362
+ };
363
+ return Z("result", e), e;
364
+ }
326
365
  try {
327
- let e = Z((await i().generateContent(`심사할 텍스트:\n"""${n.slice(0, 4e3)}"""`)).response.text());
328
- return e ? {
329
- flagged: e.flagged,
330
- category: e.category,
331
- reason: e.reason
332
- } : (console.warn("[memi-board] 검열 응답 파싱 실패 → onError 정책 적용"), a());
366
+ let { model: e, modelName: t } = i();
367
+ Z("ai-call", {
368
+ modelName: t,
369
+ useLimitedUseAppCheckTokens: !0
370
+ });
371
+ let n = performance.now(), r = await e.generateContent(`심사할 텍스트:\n"""${o.slice(0, 4e3)}"""`), s = Math.round(performance.now() - n), c = r.response.text();
372
+ Z("ai-raw", {
373
+ ms: s,
374
+ raw: c.slice(0, 500)
375
+ });
376
+ let l = Y(c);
377
+ if (!l) {
378
+ Z("ai-parse-fail", { raw: c.slice(0, 300) });
379
+ let e = a();
380
+ return Z("result", e), e;
381
+ }
382
+ let u = {
383
+ flagged: l.flagged,
384
+ category: l.category,
385
+ reason: l.reason,
386
+ via: "ai"
387
+ };
388
+ return Z("result", {
389
+ ...u,
390
+ ms: s
391
+ }), u;
333
392
  } catch (e) {
334
- return console.warn("[memi-board] AI 검열 호출 실패 → onError 정책 적용", e), a();
393
+ Z("ai-error", {
394
+ message: e instanceof Error ? e.message : String(e),
395
+ name: e instanceof Error ? e.name : void 0,
396
+ code: e?.code
397
+ });
398
+ let t = a();
399
+ return Z("result", t), t;
335
400
  }
336
401
  }
337
402
  async function c(t) {
338
- if (e.enabled === !1 || !e.moderateImages) return Q;
403
+ if (e.enabled === !1 || !e.moderateImages) return {
404
+ ...X,
405
+ via: "disabled"
406
+ };
339
407
  try {
340
408
  let e = await new Promise((e, n) => {
341
409
  let r = new FileReader();
342
410
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
343
- }), n = Z((await i().generateContent([{ inlineData: {
411
+ }), { model: n, modelName: r } = i();
412
+ Z("ai-image-call", {
413
+ modelName: r,
414
+ type: t.type,
415
+ size: t.size
416
+ });
417
+ let o = (await n.generateContent([{ inlineData: {
344
418
  mimeType: t.type || "image/jpeg",
345
419
  data: e
346
- } }, "이 이미지를 심사해 주세요."])).response.text());
347
- return n ? {
348
- flagged: n.flagged,
349
- category: n.category,
350
- reason: n.reason
351
- } : a();
420
+ } }, "이 이미지를 심사해 주세요."])).response.text(), s = Y(o);
421
+ if (!s) return Z("ai-image-parse-fail", { raw: o.slice(0, 300) }), a();
422
+ let c = {
423
+ flagged: s.flagged,
424
+ category: s.category,
425
+ reason: s.reason,
426
+ via: "ai"
427
+ };
428
+ return Z("ai-image-result", c), c;
352
429
  } catch (e) {
353
- return console.warn("[memi-board] 이미지 검열 호출 실패 → onError 정책 적용", e), a();
430
+ return Z("ai-image-error", { message: e instanceof Error ? e.message : String(e) }), a();
354
431
  }
355
432
  }
356
433
  return {
@@ -360,10 +437,10 @@ function re() {
360
437
  }
361
438
  //#endregion
362
439
  //#region src/composables/useMemiBoardStorage.ts
363
- function ie() {
364
- let e = H(), t = s(), n = e.collectionPrefix;
440
+ function ce() {
441
+ let e = V(), t = s(), n = () => e.collectionPrefix;
365
442
  function r(e, r, i) {
366
- let a = M(t), o = `${n}/posts/${r}/attachments/${Date.now()}-${G(e.name)}`, s = P(a, o), c = F(s, e, {
443
+ let a = P(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${W(e.name)}`, s = F(a, o), c = ne(s, e, {
367
444
  contentType: e.type || "application/octet-stream",
368
445
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
369
446
  });
@@ -372,7 +449,7 @@ function ie() {
372
449
  c.on("state_changed", (e) => {
373
450
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
374
451
  }, n, async () => {
375
- let n = await j(s);
452
+ let n = await N(s);
376
453
  t({
377
454
  name: e.name,
378
455
  url: n,
@@ -386,8 +463,8 @@ function ie() {
386
463
  };
387
464
  }
388
465
  async function i(e) {
389
- let n = M(t);
390
- await A(P(n, e.path)).catch(() => {});
466
+ let n = P(t);
467
+ await M(F(n, e.path)).catch(() => {});
391
468
  }
392
469
  return {
393
470
  uploadAttachment: r,
@@ -410,30 +487,65 @@ var $ = [
410
487
  label: "질문"
411
488
  }
412
489
  ];
413
- function ae() {
414
- let t = H(), n = l(), { isAdmin: i } = U(), a = t.collectionPrefix, s = f(n, `${a}Settings`, "config"), { data: c, pending: u } = o(s), d = e(() => {
490
+ function le() {
491
+ let t = V(), n = l(), { isSignedIn: i, isAdmin: a } = H(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
415
492
  let e = c.value?.categories;
416
493
  return e && e.length > 0 ? e : $;
417
494
  });
418
- function m(e) {
495
+ function f(e) {
419
496
  if (e) return d.value.find((t) => t.id === e)?.label ?? e;
420
497
  }
421
- return r([i, u], async ([e, t]) => {
422
- if (!e || t || c.value) return;
423
- let n = await p(s).catch(() => null);
424
- !n || n.exists() || await b(s, {
498
+ async function h() {
499
+ if (!i.value) return;
500
+ let e = s.value, t = await m(e).catch(() => null);
501
+ !t || t.exists() || await x(e, {
425
502
  categories: $,
426
- updatedAt: y()
427
- }).catch(() => {});
503
+ updatedAt: b()
504
+ });
505
+ }
506
+ async function g(e) {
507
+ let t = e.trim();
508
+ if (!t) throw Error("카테고리 이름을 입력해 주세요.");
509
+ if (!i.value) throw Error("로그인이 필요합니다.");
510
+ await h();
511
+ let n = d.value.find((e) => e.label === t || e.id === U(t));
512
+ if (n) return n.id;
513
+ let r = U(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
514
+ if (a.has(r)) {
515
+ let e = 2;
516
+ for (; a.has(`${r}-${e}`);) e++;
517
+ r = `${r}-${e}`;
518
+ }
519
+ let o = [...d.value, {
520
+ id: r,
521
+ label: t
522
+ }], c = s.value;
523
+ return (await m(c)).exists() ? await C(c, {
524
+ categories: o,
525
+ updatedAt: b()
526
+ }) : await x(c, {
527
+ categories: o,
528
+ updatedAt: b()
529
+ }), r;
530
+ }
531
+ return r([
532
+ a,
533
+ i,
534
+ u
535
+ ], async ([e, t, n]) => {
536
+ n || c.value || !e && !t || await h().catch(() => {});
428
537
  }, { immediate: !0 }), {
429
538
  categories: d,
430
- categoryLabel: m,
431
- settingsPending: u
539
+ categoryLabel: f,
540
+ settingsPending: u,
541
+ ensureSettings: h,
542
+ addCategory: g,
543
+ DEFAULT_CATEGORIES: $
432
544
  };
433
545
  }
434
546
  //#endregion
435
547
  //#region src/data/versionHistory.ts
436
- var oe = [
548
+ var ue = [
437
549
  {
438
550
  version: "0.2.2",
439
551
  date: "2026-07-31",
@@ -460,10 +572,10 @@ var oe = [
460
572
  ];
461
573
  //#endregion
462
574
  //#region src/utils/formatDate.ts
463
- function se(e) {
575
+ function de(e) {
464
576
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
465
577
  }
466
578
  //#endregion
467
- export { V as configureMemiBoard, se as formatDate, U as useMemiBoardAuth, J as useMemiBoardComments, H as useMemiBoardConfig, re as useMemiBoardModeration, q as useMemiBoardPosts, ae as useMemiBoardSettings, ie as useMemiBoardStorage, oe as versionHistory };
579
+ export { $ as DEFAULT_CATEGORIES, ie as configureMemiBoard, de as formatDate, H as useMemiBoardAuth, q as useMemiBoardComments, V as useMemiBoardConfig, Q as useMemiBoardModeration, K as useMemiBoardPosts, le as useMemiBoardSettings, ce as useMemiBoardStorage, ue as versionHistory };
468
580
 
469
581
  //# sourceMappingURL=index.js.map