memi-board 0.22.0 → 0.28.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.
Files changed (48) hide show
  1. package/README.md +26 -40
  2. package/dist/composables/useMemiBoardAuthorMemo.d.ts +19 -0
  3. package/dist/composables/useMemiBoardComments.d.ts +6 -1
  4. package/dist/composables/useMemiBoardSettings.d.ts +2 -2
  5. package/dist/composables/useMemiBoardStorage.d.ts +3 -2
  6. package/dist/composables/useMemiBoardUserComments.d.ts +19 -0
  7. package/dist/composables/useMemiBoardUserPosts.d.ts +4 -2
  8. package/dist/composables/useMemiBoardUserProfile.d.ts +10 -0
  9. package/dist/composables/useMemiBoardUsers.d.ts +5 -4
  10. package/dist/composables/useMemiBoardViews.d.ts +1 -1
  11. package/dist/config.d.ts +8 -2
  12. package/dist/{heic2any-BXb_U1zK.js → heic2any-BHVEj2CD.js} +2 -2
  13. package/dist/{heic2any-BXb_U1zK.js.map → heic2any-BHVEj2CD.js.map} +1 -1
  14. package/dist/index.d.ts +5 -5
  15. package/dist/index.js +870 -906
  16. package/dist/index.js.map +1 -1
  17. package/dist/module.mjs +35 -29
  18. package/dist/module.mjs.map +1 -1
  19. package/dist/runtime/components/Attachments.vue +2 -3
  20. package/dist/runtime/components/AuthorMenu.vue +129 -21
  21. package/dist/runtime/components/AvatarCropModal.vue +108 -0
  22. package/dist/runtime/components/CategorySettings.vue +1 -1
  23. package/dist/runtime/components/CommentItem.vue +8 -7
  24. package/dist/runtime/components/Detail.vue +14 -7
  25. package/dist/runtime/components/Editor.vue +8 -4
  26. package/dist/runtime/components/List.vue +6 -0
  27. package/dist/runtime/components/ListDefault.vue +32 -29
  28. package/dist/runtime/components/ListDense.vue +86 -97
  29. package/dist/runtime/components/ListVideo.vue +13 -8
  30. package/dist/runtime/components/Profile.vue +93 -0
  31. package/dist/runtime/components/ProfileEditModal.vue +190 -0
  32. package/dist/runtime/components/Settings.vue +1 -1
  33. package/dist/runtime/components/UserCommentList.vue +109 -0
  34. package/dist/runtime/components/UserPostList.vue +10 -1
  35. package/dist/runtime/components/Users.vue +1 -1
  36. package/dist/slugify-CDMIDDCN.js +122 -0
  37. package/dist/slugify-CDMIDDCN.js.map +1 -0
  38. package/dist/storage.d.ts +9 -0
  39. package/dist/storage.js +148 -0
  40. package/dist/storage.js.map +1 -0
  41. package/dist/types.d.ts +41 -10
  42. package/dist/utils/boardPaths.d.ts +30 -15
  43. package/docs/firebase-setup.md +3 -4
  44. package/docs/firestore.indexes.json.example +136 -0
  45. package/docs/firestore.rules.example +128 -72
  46. package/docs/storage.rules.example +22 -12
  47. package/package.json +7 -2
  48. package/dist/rolldown-runtime-DtPi1Y-2.js +0 -13
package/dist/index.js CHANGED
@@ -1,106 +1,18 @@
1
- import { n as e, t } from "./rolldown-runtime-DtPi1Y-2.js";
2
- import { computed as n, onScopeDispose as r, reactive as i, ref as a, toValue as o, watch as s } from "vue";
3
- import { collection as c, collectionGroup as l, deleteDoc as u, deleteField as d, doc as f, documentId as p, endBefore as m, getDoc as h, getDocs as g, increment as _, limit as v, limitToLast as y, onSnapshot as b, orderBy as x, query as S, runTransaction as C, serverTimestamp as w, setDoc as T, startAfter as E, updateDoc as D, where as O, writeBatch as k } from "firebase/firestore";
4
- import { useCollection as A, useCurrentUser as j, useDocument as M, useFirebaseApp as N, useFirebaseAuth as P, useFirestore as F } from "vuefire";
5
- import { GoogleAuthProvider as I, OAuthProvider as L, createUserWithEmailAndPassword as R, signInWithEmailAndPassword as z, signInWithPopup as B, signOut as ee, updateProfile as te } from "firebase/auth";
6
- import { deleteObject as V, getDownloadURL as H, getStorage as U, listAll as ne, ref as W, uploadBytes as re, uploadBytesResumable as ie } from "firebase/storage";
7
- import { GoogleAIBackend as ae, Schema as oe, getAI as se, getGenerativeModel as ce } from "firebase/ai";
8
- //#region src/utils/boardPaths.ts
9
- var le = "memiBoards", ue = "memiBoardUsers";
10
- function de(e) {
11
- return {
12
- boardsCollection: e.boardsCollection?.trim() || "memiBoards",
13
- usersCollection: e.usersCollection?.trim() || "memiBoardUsers"
14
- };
15
- }
16
- function fe(e, t) {
17
- return c(e, t.boardsCollection);
18
- }
19
- function pe(e, t, n) {
20
- return f(e, t.boardsCollection, n);
21
- }
22
- function G(e, t, n) {
23
- return f(e, t.boardsCollection, n, "settings", "config");
24
- }
25
- function K(e, t, n) {
26
- return c(e, t.boardsCollection, n, "posts");
27
- }
28
- function q(e, t, n, r) {
29
- return f(e, t.boardsCollection, n, "posts", r);
30
- }
31
- function me(e, t, n, r) {
32
- return f(e, t.boardsCollection, n, "posts", r, "body", "main");
33
- }
34
- function J(e, t, n, r) {
35
- return c(e, t.boardsCollection, n, "posts", r, "comments");
36
- }
37
- function Y(e, t, n, r, i) {
38
- return f(e, t.boardsCollection, n, "posts", r, "comments", i);
39
- }
40
- function he(e, t, n, r, i) {
41
- return f(e, t.boardsCollection, n, "posts", r, "likes", i);
42
- }
43
- function ge(e, t) {
44
- return c(e, t.usersCollection);
45
- }
46
- function _e(e, t, n) {
47
- return f(e, t.usersCollection, n);
48
- }
49
- function ve(e, t) {
50
- return `${e.boardsCollection}/${t}`;
51
- }
52
- function X(e, t, n) {
53
- return `${ve(e, t)}/posts/${n}`;
54
- }
55
- function ye(e, t) {
56
- return `${e.boardsCollection}/${e.usersCollection}/${t}`;
57
- }
58
- //#endregion
59
- //#region src/config.ts
60
- var be = "__MEMI_BOARD_CONFIG__";
61
- function xe() {
62
- let e = globalThis;
63
- return e[be] || (e[be] = i({
64
- boardsCollection: le,
65
- usersCollection: ue,
66
- auth: { providers: ["google", "apple"] },
67
- moderation: {
68
- enabled: !0,
69
- model: "gemini-3.5-flash-lite",
70
- onError: "allow",
71
- moderateImages: !1,
72
- localBlocklist: [],
73
- blockBanThreshold: 3,
74
- blockBanDecayMs: 864e5
75
- },
76
- seo: {
77
- enabled: !0,
78
- siteName: "Board",
79
- siteUrl: "",
80
- defaultOgImage: "",
81
- basePath: "/board"
82
- }
83
- })), e[be];
84
- }
85
- function Se(e) {
86
- let t = xe();
87
- e.boardsCollection != null && e.boardsCollection !== "" && (t.boardsCollection = e.boardsCollection.trim()), e.usersCollection != null && e.usersCollection !== "" && (t.usersCollection = e.usersCollection.trim()), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation), e.seo && Object.assign(t.seo, e.seo);
88
- }
89
- function Ce() {
90
- return xe();
91
- }
92
- function Z() {
93
- return de(Ce());
94
- }
95
- //#endregion
1
+ import { C as e, D as t, E as n, O as r, S as i, T as a, _ as o, a as s, b as c, c as l, d as u, f as d, g as f, h as p, i as m, k as h, l as g, m as _, n as v, o as y, p as b, r as x, s as S, u as C, v as w, w as T, x as E, y as D } from "./slugify-CDMIDDCN.js";
2
+ import { computed as O, onScopeDispose as k, reactive as A, ref as j, toValue as M, watch as N } from "vue";
3
+ import { deleteDoc as P, deleteField as F, doc as I, documentId as L, endBefore as ee, getDoc as R, getDocs as z, increment as B, limit as V, limitToLast as te, onSnapshot as H, orderBy as U, query as W, runTransaction as ne, serverTimestamp as G, setDoc as K, startAfter as q, updateDoc as J, where as Y, writeBatch as X } from "firebase/firestore";
4
+ import { useCollection as re, useCurrentUser as ie, useDocument as ae, useFirebaseApp as oe, useFirebaseAuth as se, useFirestore as Z } from "vuefire";
5
+ import { GoogleAuthProvider as ce, OAuthProvider as le, createUserWithEmailAndPassword as ue, signInWithEmailAndPassword as de, signInWithPopup as fe, signOut as pe, updateProfile as me } from "firebase/auth";
6
+ import { deleteObject as he, getStorage as ge, listAll as _e, ref as ve } from "firebase/storage";
7
+ import { GoogleAIBackend as ye, Schema as be, getAI as xe, getGenerativeModel as Se } from "firebase/ai";
96
8
  //#region src/utils/boardSeo.ts
97
- function we(e) {
9
+ function Ce(e) {
98
10
  if (typeof e != "string") return null;
99
11
  let t = e.trim();
100
12
  return /^https?:\/\//i.test(t) ? t : null;
101
13
  }
102
- function Te(e) {
103
- let t = we(e);
14
+ function we(e) {
15
+ let t = Ce(e);
104
16
  if (!t) return null;
105
17
  if (!/firebasestorage\.googleapis\.com/i.test(t)) return t;
106
18
  try {
@@ -117,62 +29,62 @@ function Te(e) {
117
29
  return t;
118
30
  }
119
31
  }
120
- function Ee(e, t, n = "") {
32
+ function Te(e, t, n = "") {
121
33
  let r = t.replace(/\/$/, ""), i = e.trim();
122
- return i ? /^https?:\/\//i.test(i) ? i : `${r}${i.startsWith("/") ? i : `/${i}`}` : n ? Ee(n, t) : r;
34
+ return i ? /^https?:\/\//i.test(i) ? i : `${r}${i.startsWith("/") ? i : `/${i}`}` : n ? Te(n, t) : r;
123
35
  }
124
- function De(e, t, n, r) {
36
+ function Ee(e, t, n, r) {
125
37
  let i = r?.trim().slice(0, 40), a = e.trim() || i || "사진", o = t?.trim(), s = n.trim() || "Board";
126
38
  return o ? `${a} · ${o} | ${s}` : `${a} | ${s}`;
127
39
  }
128
- function Oe(e) {
40
+ function De(e) {
129
41
  let t = [];
130
42
  return e.summary?.trim() ? t.push(e.summary.trim().slice(0, 140)) : t.push("게시판 글"), e.categoryLabel?.trim() && t.push(e.categoryLabel.trim()), e.authorName?.trim() && t.push(e.authorName.trim()), t.join(" · ").slice(0, 200);
131
43
  }
132
- function ke(e, t) {
44
+ function Oe(e, t) {
133
45
  let n = t.trim() || "Board", r = e?.trim();
134
46
  return r && r !== "전체" ? `${r} 게시판 | ${n}` : `게시판 | ${n}`;
135
47
  }
136
- function Ae(e) {
48
+ function ke(e) {
137
49
  if (e.description?.trim()) return e.description.trim().slice(0, 200);
138
50
  let t = e.categoryLabel?.trim(), n = t && t !== "전체" ? `${t} 게시판 — 소식을 확인하세요.` : "게시판 — 공지와 이야기를 확인하세요.", r = (e.recentTitles || []).filter(Boolean).slice(0, 3);
139
51
  return r.length ? `${n} ${r.map((e) => `「${e}」`).join(" ")}`.slice(0, 200) : n.slice(0, 200);
140
52
  }
141
- function je(e) {
53
+ function Ae(e) {
142
54
  let t = (e || "/board").trim() || "/board";
143
55
  return (t.startsWith("/") ? t : `/${t}`).replace(/\/$/, "") || "/board";
144
56
  }
145
57
  //#endregion
146
58
  //#region src/composables/fetchPublicSeo.ts
147
- function Me() {
59
+ function je() {
148
60
  return {
149
- db: F(),
150
- paths: Z()
61
+ db: Z(),
62
+ paths: m()
151
63
  };
152
64
  }
153
- async function Ne(e, t) {
65
+ async function Me(e, n) {
154
66
  try {
155
- let n = await h(G(e.db, e.paths, t));
156
- if (!n.exists() && !(await h(G(e.db, e.paths, t))).exists()) return {
157
- label: t,
67
+ let r = await R(t(e.db, e.paths, n));
68
+ if (!r.exists()) return {
69
+ label: n,
158
70
  description: "",
159
71
  hidden: !1
160
72
  };
161
- let r = n.data() ?? {};
73
+ let i = r.data() ?? {};
162
74
  return {
163
- label: typeof r.label == "string" && r.label.trim() ? r.label.trim() : t,
164
- description: typeof r.description == "string" ? r.description.trim() : "",
165
- hidden: r.visibility === "hidden"
75
+ label: typeof i.label == "string" && i.label.trim() ? i.label.trim() : n,
76
+ description: typeof i.description == "string" ? i.description.trim() : "",
77
+ hidden: i.visibility === "hidden"
166
78
  };
167
79
  } catch {
168
80
  return {
169
- label: t,
81
+ label: n,
170
82
  description: "",
171
83
  hidden: !1
172
84
  };
173
85
  }
174
86
  }
175
- function Pe(e) {
87
+ function Ne(e) {
176
88
  if (e && typeof e == "object" && "toDate" in e) try {
177
89
  let t = e.toDate();
178
90
  return t instanceof Date ? t.toISOString() : null;
@@ -181,36 +93,36 @@ function Pe(e) {
181
93
  }
182
94
  return null;
183
95
  }
184
- async function Fe(e, t, n) {
96
+ async function Pe(e, t, n) {
185
97
  let r = e.trim(), i = t.trim();
186
98
  if (!r || !i) return null;
187
- let a = n ?? Me();
188
- a.paths = de(a.paths);
189
- let o = await Ne(a, r);
190
- if (o.hidden) return null;
99
+ let o = n ?? je();
100
+ o.paths = a(o.paths);
101
+ let s = await Me(o, r);
102
+ if (s.hidden) return null;
191
103
  try {
192
- let e = (await g(S(K(a.db, a.paths, r), O("slug", "==", i), O("listed", "==", !0), O("isPublished", "==", !0), v(1)))).docs[0];
104
+ let e = (await z(W(T(o.db, o.paths), Y("boardId", "==", r), Y("slug", "==", i), Y("listed", "==", !0), Y("isPublished", "==", !0), V(1)))).docs[0];
193
105
  if (!e) return null;
194
106
  let t = e.data() ?? {}, n = typeof t.title == "string" ? t.title.trim() : "";
195
107
  return n ? {
196
108
  id: e.id,
197
109
  slug: typeof t.slug == "string" ? t.slug : i,
198
110
  category: r,
199
- categoryLabel: o.label,
111
+ categoryLabel: s.label,
200
112
  title: n,
201
113
  summary: typeof t.summary == "string" ? t.summary.trim().slice(0, 200) : "",
202
- previewImage: Te(t.previewImage),
114
+ previewImage: we(t.previewImage),
203
115
  authorName: typeof t.authorName == "string" && t.authorName.trim() ? t.authorName.trim() : null,
204
- createdAt: Pe(t.createdAt)
116
+ createdAt: Ne(t.createdAt)
205
117
  } : null;
206
118
  } catch (e) {
207
119
  return import.meta.dev && console.warn("[memi-board] fetchPublicPostForSeo failed", e), null;
208
120
  }
209
121
  }
210
- async function Ie(e, t) {
211
- let n = e?.trim() || null, r = t ?? Me();
212
- if (r.paths = de(r.paths), n) {
213
- let e = await Ne(r, n);
122
+ async function Fe(e, t) {
123
+ let n = e?.trim() || null, r = t ?? je();
124
+ if (r.paths = a(r.paths), n) {
125
+ let e = await Me(r, n);
214
126
  if (e.hidden) return {
215
127
  kind: "category",
216
128
  category: n,
@@ -219,7 +131,7 @@ async function Ie(e, t) {
219
131
  ogImage: null,
220
132
  recentTitles: []
221
133
  };
222
- let { recentTitles: t, ogImage: i } = await Le(r, n);
134
+ let { recentTitles: t, ogImage: i } = await Ie(r, n);
223
135
  return {
224
136
  kind: "category",
225
137
  category: n,
@@ -238,13 +150,13 @@ async function Ie(e, t) {
238
150
  recentTitles: []
239
151
  };
240
152
  }
241
- async function Le(e, t) {
153
+ async function Ie(e, t) {
242
154
  let n = [], r = null;
243
155
  try {
244
- let i = await g(S(K(e.db, e.paths, t), O("listed", "==", !0), O("isPublished", "==", !0), x("createdAt", "desc"), v(12)));
156
+ let i = await z(W(T(e.db, e.paths), Y("boardId", "==", t), Y("listed", "==", !0), Y("isPublished", "==", !0), U("createdAt", "desc"), V(12)));
245
157
  for (let e of i.docs) {
246
158
  let t = e.data() ?? {}, i = typeof t.title == "string" ? t.title.trim() : "";
247
- i && n.length < 5 && n.push(i), r ||= Te(t.previewImage);
159
+ i && n.length < 5 && n.push(i), r ||= we(t.previewImage);
248
160
  }
249
161
  } catch (e) {
250
162
  import.meta.dev && console.warn("[memi-board] loadRecentListed failed", e);
@@ -256,8 +168,8 @@ async function Le(e, t) {
256
168
  }
257
169
  //#endregion
258
170
  //#region src/utils/moderation-strike.ts
259
- var Re = 3, Q = 864e5;
260
- function ze(e, t, n = Date.now(), r = Q) {
171
+ var Le = 3, Re = 864e5;
172
+ function ze(e, t, n = Date.now(), r = Re) {
261
173
  let i = Math.max(0, Math.floor(Number(e) || 0));
262
174
  if (i <= 0) return 0;
263
175
  let a = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -265,7 +177,7 @@ function ze(e, t, n = Date.now(), r = Q) {
265
177
  let o = Math.floor((n - a) / r);
266
178
  return Math.max(0, i - o);
267
179
  }
268
- function Be(e, t, n = Date.now(), r = 3, i = Q) {
180
+ function Be(e, t, n = Date.now(), r = 3, i = Re) {
269
181
  let a = Math.max(0, Math.floor(Number(e) || 0));
270
182
  if (a < r) return null;
271
183
  let o = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -273,7 +185,7 @@ function Be(e, t, n = Date.now(), r = 3, i = Q) {
273
185
  let s = o + (a - (r - 1)) * i;
274
186
  return s > n ? s : null;
275
187
  }
276
- function Ve(e, t, n = Date.now(), r = 3, i = Q) {
188
+ function Ve(e, t, n = Date.now(), r = 3, i = Re) {
277
189
  return Be(e, t, n, r, i) != null;
278
190
  }
279
191
  function He(e, t = Date.now()) {
@@ -299,186 +211,179 @@ var We = "__MEMI_BOARD_AUTH_STATE__";
299
211
  function Ge() {
300
212
  let e = globalThis;
301
213
  return e[We] || (e[We] = {
302
- role: a("user"),
303
- rolePending: a(!0),
304
- rawBlockCount: a(0),
305
- blockAtMs: a(null),
214
+ role: j("user"),
215
+ rolePending: j(!0),
216
+ rawBlockCount: j(0),
217
+ blockAtMs: j(null),
306
218
  watchStarted: !1
307
219
  }), e[We];
308
220
  }
309
221
  function Ke() {
310
- let e = Ce(), t = Ge(), r = F(), i = P(), a = j(), { role: o, rolePending: c, rawBlockCount: l, blockAtMs: u } = t;
311
- function d() {
222
+ let e = s(), t = Ge(), n = Z(), r = se(), i = ie(), { role: a, rolePending: o, rawBlockCount: c, blockAtMs: l } = t;
223
+ function u() {
312
224
  let t = e.moderation?.blockBanThreshold;
313
225
  return t === 0 ? 0 : typeof t == "number" && t > 0 ? Math.floor(t) : 3;
314
226
  }
315
- function f() {
227
+ function d() {
316
228
  let t = e.moderation?.blockBanDecayMs;
317
- return typeof t == "number" && t > 0 ? t : Q;
229
+ return typeof t == "number" && t > 0 ? t : Re;
318
230
  }
319
- function p(e) {
320
- return _e(r, Z(), e);
231
+ function f(e) {
232
+ return b(n, m(), e);
321
233
  }
322
- function m(e) {
234
+ function p(e) {
323
235
  if (!e) {
324
- l.value = 0, u.value = null;
236
+ c.value = 0, l.value = null;
325
237
  return;
326
238
  }
327
- l.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), u.value = Ue(e.moderationBlockAt);
239
+ c.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), l.value = Ue(e.moderationBlockAt);
328
240
  }
329
- async function g(e) {
330
- let t = p(e.uid), n = await h(t);
331
- if (!n.exists()) return await T(t, {
241
+ async function h(e) {
242
+ let t = f(e.uid), r = _(n, m(), e.uid), i = await R(t);
243
+ if (!i.exists()) return await Promise.all([K(t, {
332
244
  role: "user",
333
245
  displayName: e.displayName,
334
- email: e.email,
335
246
  photoURL: e.photoURL,
336
- updatedAt: w()
337
- }), m({ moderationBlockCount: 0 }), "user";
338
- let r = n.data();
339
- return m(r), await D(t, {
247
+ joinedAt: G(),
248
+ lastVisitAt: G(),
249
+ updatedAt: G()
250
+ }), K(r, { email: e.email })]), p({ moderationBlockCount: 0 }), "user";
251
+ let a = i.data();
252
+ return p(a), await Promise.all([J(t, {
340
253
  displayName: e.displayName,
341
- email: e.email,
342
254
  photoURL: e.photoURL,
343
- updatedAt: w()
344
- }), [
255
+ lastVisitAt: G(),
256
+ updatedAt: G()
257
+ }), K(r, { email: e.email }, { merge: !0 })]), [
345
258
  "admin",
346
259
  "staff",
347
260
  "user"
348
- ].includes(String(r.role)) ? r.role : "user";
261
+ ].includes(String(a.role)) ? a.role : "user";
349
262
  }
350
- async function v() {
351
- let e = a.value?.uid;
263
+ async function g() {
264
+ let e = i.value?.uid;
352
265
  if (!e) {
353
- m(void 0);
266
+ p(void 0);
354
267
  return;
355
268
  }
356
- let t = await h(p(e));
357
- m(t.exists() ? t.data() : void 0);
269
+ let t = await R(f(e));
270
+ p(t.exists() ? t.data() : void 0);
358
271
  }
359
- t.watchStarted || (t.watchStarted = !0, s(a, async (e) => {
360
- if (c.value = !0, !e) {
361
- o.value = "user", m(void 0), c.value = !1;
272
+ t.watchStarted || (t.watchStarted = !0, N(i, async (e) => {
273
+ if (o.value = !0, !e) {
274
+ a.value = "user", p(void 0), o.value = !1;
362
275
  return;
363
276
  }
364
277
  try {
365
- o.value = await g(e);
278
+ a.value = await h(e);
366
279
  } catch (e) {
367
- console.error("[memi-board] ensureUserDoc failed", e), o.value = "user", m(void 0);
280
+ console.error("[memi-board] ensureUserDoc failed", e), a.value = "user", p(void 0);
368
281
  } finally {
369
- c.value = !1;
282
+ o.value = !1;
370
283
  }
371
284
  }, { immediate: !0 }));
372
- let y = n(() => !!a.value), b = n(() => o.value === "admin"), x = n(() => o.value === "staff"), S = n(() => b.value || x.value), C = n(() => ze(l.value, u.value, Date.now(), f())), E = n(() => {
373
- let e = d();
374
- return e <= 0 ? !1 : Ve(l.value, u.value, Date.now(), e, f());
375
- }), O = n(() => {
376
- let e = d();
285
+ let v = O(() => !!i.value), y = O(() => a.value === "admin"), x = O(() => a.value === "staff"), S = O(() => y.value || x.value), C = O(() => ze(c.value, l.value, Date.now(), d())), w = O(() => {
286
+ let e = u();
287
+ return e <= 0 ? !1 : Ve(c.value, l.value, Date.now(), e, d());
288
+ }), T = O(() => {
289
+ let e = u();
377
290
  if (e <= 0) return null;
378
- let t = Be(l.value, u.value, Date.now(), e, f());
291
+ let t = Be(c.value, l.value, Date.now(), e, d());
379
292
  return t == null ? null : He(t);
380
- }), k = n(() => {
381
- if (!E.value) return null;
382
- let e = O.value;
293
+ }), E = O(() => {
294
+ if (!w.value) return null;
295
+ let e = T.value;
383
296
  return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
384
297
  });
385
- async function A() {
386
- let e = a.value?.uid, t = d();
298
+ async function D() {
299
+ let e = i.value?.uid, t = u();
387
300
  if (!e || t <= 0) return {
388
301
  effectiveCount: 0,
389
302
  restricted: !1,
390
303
  messageSuffix: ""
391
304
  };
392
- let n = p(e), r = await h(n);
393
- r.exists() ? await D(n, {
394
- moderationBlockCount: _(1),
395
- moderationBlockAt: w(),
396
- updatedAt: w()
397
- }) : await T(n, {
305
+ let n = f(e), r = await R(n);
306
+ r.exists() ? await J(n, {
307
+ moderationBlockCount: B(1),
308
+ moderationBlockAt: G(),
309
+ updatedAt: G()
310
+ }) : await K(n, {
398
311
  role: "user",
399
- displayName: a.value?.displayName ?? null,
312
+ displayName: i.value?.displayName ?? null,
400
313
  moderationBlockCount: 1,
401
- moderationBlockAt: w(),
402
- updatedAt: w()
314
+ moderationBlockAt: G(),
315
+ updatedAt: G()
403
316
  });
404
- let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, o = Date.now();
405
- l.value = i, u.value = o;
406
- let s = ze(i, o, o, f()), c = Ve(i, o, o, t, f()), m = ` (경고 ${s}/${t})`;
407
- if (c) {
408
- let e = Be(i, o, o, t, f()), n = e == null ? null : He(e, o);
317
+ let a = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, o = Date.now();
318
+ c.value = a, l.value = o;
319
+ let s = ze(a, o, o, d()), p = Ve(a, o, o, t, d()), m = ` (경고 ${s}/${t})`;
320
+ if (p) {
321
+ let e = Be(a, o, o, t, d()), n = e == null ? null : He(e, o);
409
322
  m = n ? ` 경고 ${s}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${s}회가 되어 글·댓글 작성이 제한됩니다.`;
410
323
  }
411
324
  return {
412
325
  effectiveCount: s,
413
- restricted: c,
326
+ restricted: p,
414
327
  messageSuffix: m
415
328
  };
416
329
  }
417
- function M() {
418
- if (!i) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
419
- return i;
330
+ function k() {
331
+ if (!r) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
332
+ return r;
420
333
  }
421
- async function N() {
422
- return B(M(), new I());
334
+ async function A() {
335
+ return fe(k(), new ce());
423
336
  }
424
- async function V() {
425
- let e = new L("apple.com");
426
- return e.addScope("email"), e.addScope("name"), B(M(), e);
337
+ async function j() {
338
+ let e = new le("apple.com");
339
+ return e.addScope("email"), e.addScope("name"), fe(k(), e);
427
340
  }
428
- async function H(e, t) {
429
- return z(M(), e, t);
341
+ async function M(e, t) {
342
+ return de(k(), e, t);
430
343
  }
431
- async function U(e, t, n) {
432
- let r = await R(M(), e, t);
433
- return n && await te(r.user, { displayName: n }), r;
344
+ async function P(e, t, n) {
345
+ let r = await ue(k(), e, t);
346
+ return n && await me(r.user, { displayName: n }), r;
434
347
  }
435
- async function ne() {
436
- await ee(M());
348
+ async function F() {
349
+ await pe(k());
437
350
  }
438
- function W(e) {
439
- return y.value && (a.value?.uid === e.authorUid || S.value);
351
+ function I(e) {
352
+ return v.value && (i.value?.uid === e.authorUid || S.value);
440
353
  }
441
- function re(e) {
442
- return y.value && a.value?.uid === e.authorUid;
354
+ function L(e) {
355
+ return v.value && i.value?.uid === e.authorUid;
443
356
  }
444
- function ie(e) {
445
- return y.value && (a.value?.uid === e.authorUid || S.value);
357
+ function ee(e) {
358
+ return v.value && (i.value?.uid === e.authorUid || S.value);
446
359
  }
447
360
  return {
448
- user: a,
449
- isSignedIn: y,
450
- isAdmin: b,
361
+ user: i,
362
+ isSignedIn: v,
363
+ isAdmin: y,
451
364
  isStaff: x,
452
365
  canManageContent: S,
453
- rolePending: c,
366
+ rolePending: o,
454
367
  moderationBlockCount: C,
455
- isWriteRestricted: E,
456
- restrictedUntilLabel: O,
457
- restrictedMessage: k,
458
- signInWithGoogle: N,
459
- signInWithApple: V,
460
- signInWithEmail: H,
461
- signUpWithEmail: U,
462
- signOut: ne,
463
- canEdit: W,
464
- canDelete: W,
465
- canEditComment: re,
466
- canDeleteComment: ie,
467
- recordContentModerationBlock: A,
468
- refreshBoardUser: v
368
+ isWriteRestricted: w,
369
+ restrictedUntilLabel: T,
370
+ restrictedMessage: E,
371
+ signInWithGoogle: A,
372
+ signInWithApple: j,
373
+ signInWithEmail: M,
374
+ signUpWithEmail: P,
375
+ signOut: F,
376
+ canEdit: I,
377
+ canDelete: I,
378
+ canEditComment: L,
379
+ canDeleteComment: ee,
380
+ recordContentModerationBlock: D,
381
+ refreshBoardUser: g
469
382
  };
470
383
  }
471
384
  //#endregion
472
- //#region src/utils/slugify.ts
473
- function qe(e) {
474
- return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
475
- }
476
- function Je(e, t = 60) {
477
- return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
478
- }
479
- //#endregion
480
385
  //#region src/utils/extractEditorImageUrls.ts
481
- function Ye(e) {
386
+ function qe(e) {
482
387
  if (!e) return [];
483
388
  let t = /* @__PURE__ */ new Set();
484
389
  for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
@@ -487,7 +392,7 @@ function Ye(e) {
487
392
  }
488
393
  //#endregion
489
394
  //#region src/utils/storagePath.ts
490
- function Xe(e) {
395
+ function Je(e) {
491
396
  try {
492
397
  let t = e.match(/\/o\/([^?]+)/);
493
398
  return t?.[1] ? decodeURIComponent(t[1]) : null;
@@ -495,59 +400,59 @@ function Xe(e) {
495
400
  return null;
496
401
  }
497
402
  }
498
- function Ze(e) {
403
+ function Ye(e) {
499
404
  return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
500
405
  }
501
406
  //#endregion
502
407
  //#region src/utils/postBody.ts
503
- function Qe(e) {
408
+ function Xe(e) {
504
409
  return String(e || "").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<img\b[^>]*>/gi, " ").replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, " ").replace(/<video\b[\s\S]*?<\/video>/gi, " ").replace(/<audio\b[\s\S]*?<\/audio>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&#160;/g, " ").replace(/&[a-z]+;/gi, " ").replace(/&#\d+;/g, " ").replace(/[\u200B-\u200D\uFEFF\u00AD]/g, "").replace(/\s+/g, " ").trim();
505
410
  }
506
- function $e(e, t = 40) {
507
- let n = Qe(e);
411
+ function Ze(e, t = 40) {
412
+ let n = Xe(e);
508
413
  if (!n) return "";
509
414
  if (n.length <= t) return n;
510
415
  let r = n.slice(0, t), i = r.lastIndexOf(" ");
511
416
  return `${(i >= Math.floor(t * .6) ? r.slice(0, i) : r).trimEnd()}…`;
512
417
  }
513
- function et(e) {
514
- return Qe(e).length > 0;
418
+ function Qe(e) {
419
+ return Xe(e).length > 0;
515
420
  }
516
- function tt(e, t) {
421
+ function $e(e, t) {
517
422
  return /<img\b/i.test(String(e || "")) ? !0 : (t || []).some((e) => !!e?.type?.startsWith("image/"));
518
423
  }
519
- function nt(e) {
520
- return !et(e);
424
+ function et(e) {
425
+ return !Qe(e);
521
426
  }
522
427
  //#endregion
523
428
  //#region src/utils/postPreview.ts
524
- function rt(e) {
429
+ function tt(e) {
525
430
  return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
526
431
  }
527
- function it(e) {
432
+ function nt(e) {
528
433
  if (e) try {
529
- let t = new URL(rt(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
434
+ let t = new URL(tt(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
530
435
  if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
531
436
  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;
532
437
  } catch {
533
438
  return;
534
439
  }
535
440
  }
536
- function at(e) {
537
- let t = it(e.videoUrl);
441
+ function rt(e) {
442
+ let t = nt(e.videoUrl);
538
443
  return t ? `https://i.ytimg.com/vi/${t}/hqdefault.jpg` : e.previewImage || e.attachments?.find((e) => e.type.startsWith("image/"))?.url;
539
444
  }
540
- function ot(e, t = []) {
541
- 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 ? rt(r[1]) : a, s = o ? it(o) : void 0, c = n ? rt(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
445
+ function it(e, t = []) {
446
+ 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 ? tt(r[1]) : a, s = o ? nt(o) : void 0, c = n ? tt(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
542
447
  return {
543
- 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),
448
+ summary: e.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h[1-6]|blockquote)>/gi, "\n").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/[^\S\n]+/g, " ").replace(/ *\n */g, "\n").replace(/\n{2,}/g, "\n").trim().slice(0, 160),
544
449
  ...c ? { previewImage: c } : {},
545
450
  ...o ? { videoUrl: o } : {}
546
451
  };
547
452
  }
548
453
  //#endregion
549
454
  //#region src/composables/useMemiBoardSettings.ts
550
- var st = [
455
+ var at = [
551
456
  {
552
457
  id: "free",
553
458
  label: "자유",
@@ -575,200 +480,199 @@ var st = [
575
480
  visibility: "public",
576
481
  order: 2
577
482
  }
578
- ], ct = st;
579
- function lt(e) {
483
+ ], ot = at;
484
+ function st(e) {
580
485
  return e === "hidden" ? "hidden" : "public";
581
486
  }
582
- function ut(e, t, n) {
487
+ function ct(e, t, n) {
583
488
  return {
584
489
  id: e,
585
490
  label: typeof t.label == "string" && t.label.trim() ? t.label.trim() : e,
586
491
  order: typeof t.order == "number" ? t.order : n,
587
492
  description: (typeof t.description == "string" ? t.description : "") || "",
588
- visibility: lt(t.visibility),
493
+ visibility: st(t.visibility),
589
494
  listView: t.listView ?? "default",
590
495
  writeRole: t.writeRole ?? "user",
591
496
  commentWriteRole: t.commentWriteRole ?? "user",
592
497
  allowedStaffUids: Array.isArray(t.allowedStaffUids) ? t.allowedStaffUids : []
593
498
  };
594
499
  }
595
- function dt() {
596
- let e = () => Z(), t = F(), { isSignedIn: r } = Ke(), i = n(() => S(fe(t, e()), x("order", "asc"))), { data: a, pending: o } = A(i, { ssrKey: ye(e(), "boards") }), s = n(() => a.value.map((e, t) => ut(e.id, e, t))), c = s, l = n(() => s.value.filter((e) => e.visibility !== "hidden")), d = l;
597
- function f(e) {
598
- if (e) return s.value.find((t) => t.id === e)?.label ?? e;
500
+ function lt() {
501
+ let e = () => m(), r = Z(), { isSignedIn: i } = Ke(), a = O(() => W(n(r, e()), U("order", "asc"))), { data: o, pending: s } = re(a, { ssrKey: d(e(), "boards") }), c = O(() => o.value.map((e, t) => ct(e.id, e, t))), l = c, u = O(() => c.value.filter((e) => e.visibility !== "hidden")), f = u;
502
+ function p(e) {
503
+ if (e) return c.value.find((t) => t.id === e)?.label ?? e;
599
504
  }
600
- let p = f;
601
- function m(e) {
602
- if (e) return s.value.find((t) => t.id === e)?.description?.trim() || void 0;
505
+ let h = p;
506
+ function g(e) {
507
+ if (e) return c.value.find((t) => t.id === e)?.description?.trim() || void 0;
603
508
  }
604
- let h = m;
605
- function _(e) {
606
- return e ? s.value.find((t) => t.id === e)?.visibility ?? "public" : "public";
509
+ let _ = g;
510
+ function y(e) {
511
+ return e ? c.value.find((t) => t.id === e)?.visibility ?? "public" : "public";
607
512
  }
608
- let y = _;
609
- function b(e) {
610
- return _(e) === "hidden";
513
+ let b = y;
514
+ function x(e) {
515
+ return y(e) === "hidden";
611
516
  }
612
- let C = b;
613
- function D(e) {
614
- if (e) return s.value.find((t) => t.id === e);
517
+ let S = x;
518
+ function C(e) {
519
+ if (e) return c.value.find((t) => t.id === e);
615
520
  }
616
- async function O(n, r) {
617
- let i = K(t, e(), n), a;
521
+ async function w(t, n) {
522
+ let i;
618
523
  for (;;) {
619
- let e = await g(S(i, x("__name__"), ...a ? [E(a)] : [], v(400)));
620
- if (e.empty) break;
621
- let n = k(t);
622
- for (let t of e.docs) n.update(t.ref, { listed: r });
623
- if (await n.commit(), a = e.docs[e.docs.length - 1], e.docs.length < 400) break;
524
+ let a = await z(W(T(r, e()), Y("boardId", "==", t), U("__name__"), ...i ? [q(i)] : [], V(400)));
525
+ if (a.empty) break;
526
+ let o = X(r);
527
+ for (let e of a.docs) o.update(e.ref, { listed: n });
528
+ if (await o.commit(), i = a.docs[a.docs.length - 1], a.docs.length < 400) break;
624
529
  }
625
530
  }
626
- function j(e, t) {
531
+ function E(e, t) {
627
532
  return {
628
533
  label: e.label.trim(),
629
534
  description: (e.description ?? "").trim(),
630
- visibility: lt(e.visibility),
535
+ visibility: st(e.visibility),
631
536
  listView: e.listView ?? "default",
632
537
  writeRole: e.writeRole ?? "user",
633
538
  commentWriteRole: e.commentWriteRole ?? "user",
634
539
  allowedStaffUids: e.allowedStaffUids ?? [],
635
540
  order: t,
636
- updatedAt: w()
541
+ updatedAt: G()
637
542
  };
638
543
  }
639
- async function M(n, i = n.order ?? 0) {
640
- if (!r.value) throw Error("로그인이 필요합니다.");
641
- let a = n.id.trim(), o = n.label.trim();
642
- if (!a || !o) throw Error("보드 ID와 이름을 입력해 주세요.");
643
- let c = lt(n.visibility), l = s.value.find((e) => e.id === a)?.visibility ?? "public", u = j({
544
+ async function D(n, a = n.order ?? 0) {
545
+ if (!i.value) throw Error("로그인이 필요합니다.");
546
+ let o = n.id.trim(), s = n.label.trim();
547
+ if (!o || !s) throw Error("보드 ID와 이름을 입력해 주세요.");
548
+ let l = st(n.visibility), u = c.value.find((e) => e.id === o)?.visibility ?? "public", d = E({
644
549
  ...n,
645
- label: o,
646
- visibility: c
647
- }, i), d = e();
648
- await T(pe(t, d, a), u, { merge: !0 }), await T(G(t, d, a), u, { merge: !0 }), (l !== c || c === "public") && await O(a, c !== "hidden");
649
- }
650
- let N = M;
651
- async function P(n) {
652
- if (!r.value) throw Error("로그인이 필요합니다.");
653
- let i = new Map(s.value.map((e) => [e.id, e.visibility ?? "public"])), a = k(t), o = e(), c = [];
654
- n.forEach((e, n) => {
655
- let r = e.id.trim(), s = e.label.trim();
656
- if (!r || !s) throw Error("보드 ID와 이름을 입력해 주세요.");
657
- let l = lt(e.visibility), u = j({
658
- ...e,
659
- label: s,
660
- visibility: l
661
- }, n);
662
- a.set(pe(t, o, r), u, { merge: !0 }), a.set(G(t, o, r), u, { merge: !0 }), (i.get(r) ?? "public") !== l && c.push({
663
- id: r,
664
- listed: l !== "hidden"
550
+ label: s,
551
+ visibility: l
552
+ }, a);
553
+ await K(t(r, e(), o), d, { merge: !0 }), (u !== l || l === "public") && await w(o, l !== "hidden");
554
+ }
555
+ let k = D;
556
+ async function A(n) {
557
+ if (!i.value) throw Error("로그인이 필요합니다.");
558
+ let a = new Map(c.value.map((e) => [e.id, e.visibility ?? "public"])), o = X(r), s = [];
559
+ n.forEach((n, i) => {
560
+ let c = n.id.trim(), l = n.label.trim();
561
+ if (!c || !l) throw Error("보드 ID와 이름을 입력해 주세요.");
562
+ let u = st(n.visibility), d = E({
563
+ ...n,
564
+ label: l,
565
+ visibility: u
566
+ }, i);
567
+ o.set(t(r, e(), c), d, { merge: !0 }), (a.get(c) ?? "public") !== u && s.push({
568
+ id: c,
569
+ listed: u !== "hidden"
665
570
  });
666
- }), await a.commit();
667
- for (let e of c) await O(e.id, e.listed);
571
+ }), await o.commit();
572
+ for (let e of s) await w(e.id, e.listed);
668
573
  }
669
- let I = P;
670
- async function L(n) {
671
- if (!r.value) throw Error("로그인이 필요합니다.");
672
- let i = e();
673
- await u(G(t, i, n)).catch(() => {}), await u(pe(t, i, n));
574
+ let j = A;
575
+ async function M(n) {
576
+ if (!i.value) throw Error("로그인이 필요합니다.");
577
+ await P(t(r, e(), n));
674
578
  }
675
- let R = L;
676
- async function z() {
579
+ let N = M;
580
+ async function F() {
677
581
  return Promise.resolve();
678
582
  }
679
- async function B(e) {
583
+ async function I(e) {
680
584
  let t = e.trim();
681
585
  if (!t) throw Error("보드 이름을 입력해 주세요.");
682
- if (!r.value) throw Error("로그인이 필요합니다.");
683
- let n = s.value.find((e) => e.label === t);
586
+ if (!i.value) throw Error("로그인이 필요합니다.");
587
+ let n = c.value.find((e) => e.label === t);
684
588
  if (n) return n.id;
685
- let i = qe(t) || `board-${Date.now()}`, a = i, o = 2, c = new Set(s.value.map((e) => e.id));
686
- for (; c.has(a);) a = `${i}-${o++}`;
687
- return await M({
589
+ let r = v(t) || `board-${Date.now()}`, a = r, o = 2, s = new Set(c.value.map((e) => e.id));
590
+ for (; s.has(a);) a = `${r}-${o++}`;
591
+ return await D({
688
592
  id: a,
689
593
  label: t,
690
594
  visibility: "public",
691
595
  listView: "default",
692
596
  writeRole: "user",
693
597
  commentWriteRole: "user",
694
- order: s.value.length
598
+ order: c.value.length
695
599
  }), a;
696
600
  }
697
601
  return {
698
- boards: s,
699
- publicBoards: l,
700
- boardLabel: f,
701
- boardDescription: m,
702
- boardVisibility: _,
703
- isBoardHidden: b,
704
- getBoard: D,
705
- categories: c,
706
- publicCategories: d,
707
- categoryLabel: p,
708
- categoryDescription: h,
709
- categoryVisibility: y,
710
- isCategoryHidden: C,
711
- settingsPending: o,
712
- ensureSettings: z,
713
- addBoard: B,
714
- addCategory: B,
715
- saveBoard: M,
716
- saveCategory: N,
717
- saveBoards: P,
718
- saveCategories: I,
719
- deleteBoard: L,
720
- deleteCategory: R,
721
- DEFAULT_BOARDS: st,
722
- DEFAULT_CATEGORIES: ct
602
+ boards: c,
603
+ publicBoards: u,
604
+ boardLabel: p,
605
+ boardDescription: g,
606
+ boardVisibility: y,
607
+ isBoardHidden: x,
608
+ getBoard: C,
609
+ categories: l,
610
+ publicCategories: f,
611
+ categoryLabel: h,
612
+ categoryDescription: _,
613
+ categoryVisibility: b,
614
+ isCategoryHidden: S,
615
+ settingsPending: s,
616
+ ensureSettings: F,
617
+ addBoard: I,
618
+ addCategory: I,
619
+ saveBoard: D,
620
+ saveCategory: k,
621
+ saveBoards: A,
622
+ saveCategories: j,
623
+ deleteBoard: M,
624
+ deleteCategory: N,
625
+ DEFAULT_BOARDS: at,
626
+ DEFAULT_CATEGORIES: ot
723
627
  };
724
628
  }
725
629
  //#endregion
726
630
  //#region src/composables/useMemiBoardPosts.ts
727
- async function ft(e) {
728
- let t = await ne(e).catch(() => ({
631
+ async function ut(e) {
632
+ let t = await _e(e).catch(() => ({
729
633
  items: [],
730
634
  prefixes: []
731
635
  }));
732
- await Promise.all([...t.items.map((e) => V(e)), ...t.prefixes.map((e) => ft(e))]);
636
+ await Promise.all([...t.items.map((e) => he(e)), ...t.prefixes.map((e) => ut(e))]);
733
637
  }
734
- async function pt(e, t) {
638
+ async function dt(e, t) {
735
639
  let n = [...new Set([...t].filter(Boolean))];
736
- await Promise.all(n.map((t) => V(W(e, t)).catch(() => {})));
640
+ await Promise.all(n.map((t) => he(ve(e, t)).catch(() => {})));
737
641
  }
738
- function mt(e) {
642
+ function ft(e) {
739
643
  if (!e || typeof e != "object") return !1;
740
644
  let t = "code" in e ? String(e.code) : "";
741
645
  return t === "permission-denied" || t === "firestore/permission-denied";
742
646
  }
743
- function ht(e) {
744
- let t = o(e)?.trim();
647
+ function pt(e) {
648
+ let t = M(e)?.trim();
745
649
  if (!t) throw Error("[memi-board] boardId 가 필요합니다.");
746
650
  return t;
747
651
  }
748
- function gt(e) {
749
- let t = F(), n = N(), { isBoardHidden: r, getBoard: i } = dt(), a = () => Z(), o = () => ht(e), s = () => K(t, a(), o()), c = (e) => q(t, a(), o(), e), l = (e) => me(t, a(), o(), e), p = (e) => J(t, a(), o(), e);
750
- function _() {
751
- return !r(o());
652
+ function mt(t) {
653
+ let n = Z(), r = oe(), { isBoardHidden: a, getBoard: s } = lt(), c = () => m(), l = () => pt(t), u = () => T(n, c()), d = (e) => i(n, c(), e), f = (e) => E(n, c(), e), p = () => o(n, c());
654
+ function h() {
655
+ return !a(l());
752
656
  }
753
- function b() {
754
- return i(o())?.listView === "image";
657
+ function g() {
658
+ return s(l())?.listView === "image";
755
659
  }
756
- function C(e) {
757
- let t = b();
758
- if (!et(e.content)) throw Error(t ? "내용을 입력해 주세요." : "본문에 글자를 입력해 주세요. 이미지나 첨부만으로는 게시할 수 없습니다.");
660
+ function _(e) {
661
+ let t = g();
662
+ if (!Qe(e.content)) throw Error(t ? "내용을 입력해 주세요." : "본문에 글자를 입력해 주세요. 이미지나 첨부만으로는 게시할 수 없습니다.");
759
663
  if (t) {
760
- if (!tt(e.content, e.attachments)) throw Error("사진을 올려 주세요.");
664
+ if (!$e(e.content, e.attachments)) throw Error("사진을 올려 주세요.");
761
665
  return;
762
666
  }
763
667
  if (!e.title?.trim()) throw Error("제목을 입력해 주세요.");
764
668
  }
765
- function A() {
766
- return f(s()).id;
669
+ function y() {
670
+ return I(u()).id;
767
671
  }
768
- async function j(e = {}) {
769
- let t = e.pageSize ?? 20, n = [O("isPublished", "==", !0)];
770
- e.publicOnly !== !1 && n.push(O("listed", "==", !0)), n.push(x("createdAt", "desc")), e.cursor && n.push(E(e.cursor)), n.push(v(t + 1));
771
- let r = await g(S(s(), ...n)), i = r.docs.slice(0, t);
672
+ async function b(e = {}) {
673
+ let t = e.pageSize ?? 20, n = [Y("boardId", "==", l()), Y("isPublished", "==", !0)];
674
+ e.publicOnly !== !1 && n.push(Y("listed", "==", !0)), n.push(U("createdAt", "desc")), e.cursor && n.push(q(e.cursor)), n.push(V(t + 1));
675
+ let r = await z(W(u(), ...n)), i = r.docs.slice(0, t);
772
676
  return {
773
677
  posts: i.map((e) => ({
774
678
  id: e.id,
@@ -778,48 +682,49 @@ function gt(e) {
778
682
  hasMore: r.docs.length > t
779
683
  };
780
684
  }
781
- async function M(e) {
782
- let [t, n] = await Promise.all([h(c(e)), h(l(e))]);
685
+ async function x(e) {
686
+ let [t, n] = await Promise.all([R(d(e)), R(f(e))]);
783
687
  return t.exists() ? {
784
688
  id: t.id,
785
689
  ...t.data(),
786
690
  content: n.exists() ? n.data().content ?? "" : ""
787
691
  } : null;
788
692
  }
789
- async function P(e) {
693
+ async function S(e) {
790
694
  try {
791
- let t = (await g(S(s(), O("slug", "==", e.trim()), v(1)))).docs[0];
695
+ let t = (await z(W(u(), Y("boardId", "==", l()), Y("slug", "==", e.trim()), V(1)))).docs[0];
792
696
  if (!t) return { status: "not-found" };
793
697
  try {
794
- let e = await M(t.id);
698
+ let e = await x(t.id);
795
699
  return e ? {
796
700
  status: "ok",
797
701
  post: e
798
702
  } : { status: "not-found" };
799
703
  } catch (e) {
800
- return mt(e) ? { status: "permission-denied" } : {
704
+ return ft(e) ? { status: "permission-denied" } : {
801
705
  status: "error",
802
706
  message: e instanceof Error ? e.message : String(e)
803
707
  };
804
708
  }
805
709
  } catch (e) {
806
- return mt(e) ? { status: "permission-denied" } : {
710
+ return ft(e) ? { status: "permission-denied" } : {
807
711
  status: "error",
808
712
  message: e instanceof Error ? e.message : String(e)
809
713
  };
810
714
  }
811
715
  }
812
- async function I(e) {
716
+ async function C(e) {
813
717
  if (!e.createdAt) return {
814
718
  previous: null,
815
719
  next: null
816
720
  };
817
721
  try {
818
722
  let t = [
819
- O("isPublished", "==", !0),
820
- ...e.listed === !1 ? [] : [O("listed", "==", !0)],
821
- x("createdAt", "desc")
822
- ], [n, r] = await Promise.all([g(S(s(), ...t, E(e.createdAt), v(1))), g(S(s(), ...t, m(e.createdAt), y(1)))]), i = (e) => {
723
+ Y("boardId", "==", l()),
724
+ Y("isPublished", "==", !0),
725
+ ...e.listed === !1 ? [] : [Y("listed", "==", !0)],
726
+ U("createdAt", "desc")
727
+ ], [n, r] = await Promise.all([z(W(u(), ...t, q(e.createdAt), V(1))), z(W(u(), ...t, ee(e.createdAt), te(1)))]), i = (e) => {
823
728
  let t = e.docs[0];
824
729
  return t ? {
825
730
  id: t.id,
@@ -837,19 +742,20 @@ function gt(e) {
837
742
  };
838
743
  }
839
744
  }
840
- async function L(e) {
841
- C(e);
842
- let t = e.postId || A(), n = b(), r, i;
843
- if (n) r = t, i = $e(e.content);
745
+ async function w(e) {
746
+ _(e);
747
+ let t = e.postId || y(), n = g(), r, i;
748
+ if (n) r = t, i = Ze(e.content);
844
749
  else {
845
750
  i = e.title.trim();
846
- let t = qe(i) || "post";
751
+ let t = v(i) || "post";
847
752
  r = t;
848
753
  let n = 1;
849
- for (; !(await g(S(s(), O("slug", "==", r), v(1)))).empty;) n++, r = `${t}-${n}`;
754
+ for (; !(await z(W(u(), Y("boardId", "==", l()), Y("slug", "==", r), V(1)))).empty;) n++, r = `${t}-${n}`;
850
755
  }
851
- let a = ot(e.content, e.attachments), o = _();
852
- return await T(c(t), {
756
+ let a = it(e.content, e.attachments), o = h();
757
+ return await K(d(t), {
758
+ boardId: l(),
853
759
  slug: r,
854
760
  title: i,
855
761
  ...a,
@@ -865,112 +771,110 @@ function gt(e) {
865
771
  authorPhoto: e.authorPhoto,
866
772
  moderationStatus: "approved",
867
773
  moderationModel: e.moderationModel ?? null,
868
- createdAt: w(),
869
- updatedAt: w()
870
- }), await T(l(t), { content: e.content }), r;
871
- }
872
- async function R(e, t) {
873
- C(t);
874
- let n = b() ? $e(t.content) : t.title.trim(), r = ot(t.content, t.attachments), i = _();
875
- await Promise.all([D(c(e), {
774
+ createdAt: G(),
775
+ updatedAt: G()
776
+ }), await K(f(t), { content: e.content }), r;
777
+ }
778
+ async function D(e, t) {
779
+ _(t);
780
+ let n = g() ? Ze(t.content) : t.title.trim(), r = it(t.content, t.attachments), i = h();
781
+ await Promise.all([J(d(e), {
876
782
  title: n,
877
783
  ...r,
878
- previewImage: r.previewImage ? r.previewImage : d(),
879
- videoUrl: r.videoUrl ? r.videoUrl : d(),
784
+ previewImage: r.previewImage ? r.previewImage : F(),
785
+ videoUrl: r.videoUrl ? r.videoUrl : F(),
880
786
  tags: t.tags ?? [],
881
787
  listed: i,
882
788
  attachments: t.attachments ?? [],
883
- updatedAt: w()
884
- }), T(l(e), { content: t.content }, { merge: !0 })]);
789
+ updatedAt: G()
790
+ }), K(f(e), { content: t.content }, { merge: !0 })]);
885
791
  }
886
- async function z(e) {
887
- await D(c(e), {
792
+ async function O(e) {
793
+ await J(d(e), {
888
794
  isPublished: !0,
889
- publishedAt: w(),
890
- updatedAt: w()
795
+ publishedAt: G(),
796
+ updatedAt: G()
891
797
  });
892
798
  }
893
- async function B(e) {
894
- let [r, i] = await Promise.all([h(c(e)), h(l(e))]);
895
- if (!r.exists()) return;
896
- let s = r.data(), d = i.exists() ? String(i.data().content ?? "") : "", f = await g(p(e));
897
- for (let e = 0; e < f.docs.length; e += 450) {
898
- let n = k(t);
899
- f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
799
+ async function k(t) {
800
+ let [i, a] = await Promise.all([R(d(t)), R(f(t))]);
801
+ if (!i.exists()) return;
802
+ let o = i.data(), s = a.exists() ? String(a.data().content ?? "") : "", l = await z(W(p(), Y("postId", "==", t)));
803
+ for (let e = 0; e < l.docs.length; e += 450) {
804
+ let t = X(n);
805
+ l.docs.slice(e, e + 450).forEach((e) => t.delete(e.ref)), await t.commit();
900
806
  }
901
- let m = U(n), _ = /* @__PURE__ */ new Set(), v = /* @__PURE__ */ new Set();
902
- for (let t of s.attachments ?? []) if (t?.path) {
903
- _.add(t.path);
904
- let n = Ze(t.path);
905
- n && n !== e && v.add(n);
807
+ let u = ge(r), m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
808
+ for (let e of o.attachments ?? []) if (e?.path) {
809
+ m.add(e.path);
810
+ let n = Ye(e.path);
811
+ n && n !== t && h.add(n);
906
812
  }
907
- for (let t of Ye(d)) {
908
- let n = Xe(t);
813
+ for (let e of qe(s)) {
814
+ let n = Je(e);
909
815
  if (!n) continue;
910
- _.add(n);
816
+ m.add(n);
911
817
  let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
912
- r !== n && r.includes("/images/thumbnails/") && _.add(r);
818
+ r !== n && r.includes("/images/thumbnails/") && m.add(r);
913
819
  let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
914
- i && !n.includes("/thumbnails/") && _.add(`${i[1]}thumbnails/${i[2]}.jpg`);
915
- let a = Ze(n);
916
- a && a !== e && v.add(a);
820
+ i && !n.includes("/thumbnails/") && m.add(`${i[1]}thumbnails/${i[2]}.jpg`);
821
+ let a = Ye(n);
822
+ a && a !== t && h.add(a);
917
823
  }
918
- await u(l(e));
919
- let y = o();
920
- await ft(W(m, X(a(), y, e)));
921
- for (let e of v) await ft(W(m, X(a(), y, e)));
922
- await pt(m, _), await u(c(e));
824
+ await P(f(t)), await ut(ve(u, e(c(), t)));
825
+ for (let t of h) await ut(ve(u, e(c(), t)));
826
+ await dt(u, m), await P(d(t));
923
827
  }
924
828
  return {
925
- createPostId: A,
926
- getPosts: j,
927
- getPost: M,
928
- getPostBySlug: P,
929
- getAdjacentPosts: I,
930
- createPost: L,
931
- updatePost: R,
932
- publishPost: z,
933
- deletePost: B
829
+ createPostId: y,
830
+ getPosts: b,
831
+ getPost: x,
832
+ getPostBySlug: S,
833
+ getAdjacentPosts: C,
834
+ createPost: w,
835
+ updatePost: D,
836
+ publishPost: O,
837
+ deletePost: k
934
838
  };
935
839
  }
936
- var _t = 10;
937
- function vt(e, t = {}) {
938
- let i = F(), { isAdmin: o, isStaff: c, user: l } = Ke(), u = () => Z(), d = t.pageSize ?? _t, f = n(() => l.value?.uid ?? ""), m = n(() => ht(e)), h = () => K(i, u(), m.value), _ = n(() => t.publicOnly === void 0 ? !(o.value || c.value) : typeof t.publicOnly == "boolean" ? t.publicOnly : t.publicOnly.value), y = a([]), C = a([]), w = a([]), T = a(!0), D = a(!0), k = a(!1), A = a(""), j = null, M = null, N, P;
939
- function I(e) {
840
+ var ht = 10;
841
+ function gt(e, t = {}) {
842
+ let n = Z(), { isAdmin: r, isStaff: i, user: a } = Ke(), o = () => m(), s = t.pageSize ?? ht, c = O(() => a.value?.uid ?? ""), l = O(() => pt(e)), u = () => T(n, o()), d = O(() => t.publicOnly === void 0 ? !(r.value || i.value) : typeof t.publicOnly == "boolean" ? t.publicOnly : t.publicOnly.value), f = j([]), p = j([]), h = j([]), g = j(!0), _ = j(!0), v = j(!1), y = j(""), b = null, x = null, S, C;
843
+ function w(e) {
940
844
  return e.createdAt?.toMillis?.() ?? 0;
941
845
  }
942
- let L = n(() => {
846
+ let E = O(() => {
943
847
  let e = /* @__PURE__ */ new Map();
944
848
  for (let t of [
945
- ...C.value,
946
- ...y.value,
947
- ...w.value
849
+ ...p.value,
850
+ ...f.value,
851
+ ...h.value
948
852
  ]) t.id && e.set(t.id, t);
949
- return [...e.values()].sort((e, t) => I(t) - I(e) || String(t.id).localeCompare(String(e.id)));
853
+ return [...e.values()].sort((e, t) => w(t) - w(e) || String(t.id).localeCompare(String(e.id)));
950
854
  });
951
- function R() {
952
- let e = [O("isPublished", "==", !0)];
953
- return _.value && e.push(O("listed", "==", !0)), e;
855
+ function D() {
856
+ let e = [Y("boardId", "==", l.value), Y("isPublished", "==", !0)];
857
+ return d.value && e.push(Y("listed", "==", !0)), e;
954
858
  }
955
- function z() {
956
- N?.(), N = b(S(h(), ...R(), x("createdAt", "desc"), x(p(), "desc"), v(d)), (e) => {
957
- y.value = e.docs.map((e) => ({
859
+ function A() {
860
+ S?.(), S = H(W(u(), ...D(), U("createdAt", "desc"), U(L(), "desc"), V(s)), (e) => {
861
+ f.value = e.docs.map((e) => ({
958
862
  id: e.id,
959
863
  ...e.data()
960
- })), j = e.docs.at(-1) ?? null, M || (D.value = e.docs.length >= d), T.value = !1;
864
+ })), b = e.docs.at(-1) ?? null, x || (_.value = e.docs.length >= s), g.value = !1;
961
865
  }, (e) => {
962
- console.error("[memi-board:postList] onSnapshot failed", e), A.value = e.message || String(e), T.value = !1;
866
+ console.error("[memi-board:postList] onSnapshot failed", e), y.value = e.message || String(e), g.value = !1;
963
867
  });
964
868
  }
965
- function B() {
966
- P?.();
967
- let e = f.value;
869
+ function M() {
870
+ C?.();
871
+ let e = c.value;
968
872
  if (!e) {
969
- w.value = [];
873
+ h.value = [];
970
874
  return;
971
875
  }
972
- P = b(S(h(), O("authorUid", "==", e), O("isPublished", "==", !1), x("createdAt", "desc"), v(20)), (e) => {
973
- w.value = e.docs.map((e) => ({
876
+ C = H(W(u(), Y("boardId", "==", l.value), Y("authorUid", "==", e), Y("isPublished", "==", !1), U("createdAt", "desc"), V(20)), (e) => {
877
+ h.value = e.docs.map((e) => ({
974
878
  id: e.id,
975
879
  ...e.data()
976
880
  }));
@@ -978,380 +882,529 @@ function vt(e, t = {}) {
978
882
  console.error("[memi-board:postList] own draft onSnapshot failed", e);
979
883
  });
980
884
  }
981
- async function ee() {
982
- if (!(k.value || !D.value || T.value)) {
983
- k.value = !0, A.value = "";
885
+ async function P() {
886
+ if (!(v.value || !_.value || g.value)) {
887
+ v.value = !0, y.value = "";
984
888
  try {
985
- let e = M ?? j, t = [
986
- ...R(),
987
- x("createdAt", "desc"),
988
- x(p(), "desc"),
989
- ...e ? [E(e)] : [],
990
- v(d + 1)
991
- ], n = await g(S(h(), ...t)), r = n.docs.slice(0, d), i = r.map((e) => ({
889
+ let e = x ?? b, t = [
890
+ ...D(),
891
+ U("createdAt", "desc"),
892
+ U(L(), "desc"),
893
+ ...e ? [q(e)] : [],
894
+ V(s + 1)
895
+ ], n = await z(W(u(), ...t)), r = n.docs.slice(0, s), i = r.map((e) => ({
992
896
  id: e.id,
993
897
  ...e.data()
994
- })), a = new Set([...C.value, ...y.value].map((e) => e.id));
995
- C.value.push(...i.filter((e) => !a.has(e.id))), M = r.at(-1) ?? M, D.value = n.docs.length > d;
898
+ })), a = new Set([...p.value, ...f.value].map((e) => e.id));
899
+ p.value.push(...i.filter((e) => !a.has(e.id))), x = r.at(-1) ?? x, _.value = n.docs.length > s;
996
900
  } catch (e) {
997
- A.value = e.message || String(e), console.error("[memi-board:postList] loadMore failed", e);
901
+ y.value = e.message || String(e), console.error("[memi-board:postList] loadMore failed", e);
998
902
  } finally {
999
- k.value = !1;
903
+ v.value = !1;
1000
904
  }
1001
905
  }
1002
906
  }
1003
- function te() {
1004
- N?.(), y.value = [], C.value = [], j = null, M = null, D.value = !0, A.value = "", T.value = !0, z(), B();
907
+ function F() {
908
+ S?.(), f.value = [], p.value = [], b = null, x = null, _.value = !0, y.value = "", g.value = !0, A(), M();
1005
909
  }
1006
- return s([
1007
- m,
1008
- _,
1009
- f
1010
- ], te, { immediate: !0 }), r(() => {
1011
- N?.(), P?.();
910
+ return N([
911
+ l,
912
+ d,
913
+ c
914
+ ], F, { immediate: !0 }), k(() => {
915
+ S?.(), C?.();
1012
916
  }), {
1013
- posts: L,
1014
- postsPending: T,
1015
- hasMore: D,
1016
- loadingMore: k,
1017
- loadError: A,
1018
- loadMore: ee
917
+ posts: E,
918
+ postsPending: g,
919
+ hasMore: _,
920
+ loadingMore: v,
921
+ loadError: y,
922
+ loadMore: P
1019
923
  };
1020
924
  }
1021
- function yt(e, t) {
1022
- let r = F(), i = () => Z(), a = n(() => ht(e)), o = n(() => typeof t == "string" ? t : t.value), s = n(() => q(r, i(), a.value, o.value)), c = n(() => me(r, i(), a.value, o.value)), { data: l, pending: u } = M(s), { data: d, pending: f } = M(c);
925
+ function _t(e, t) {
926
+ let n = Z(), r = () => m(), a = O(() => pt(e)), o = O(() => typeof t == "string" ? t : t.value), s = O(() => i(n, r(), o.value)), c = O(() => E(n, r(), o.value)), { data: l, pending: u } = ae(s), { data: d, pending: f } = ae(c);
1023
927
  return {
1024
- post: n(() => l.value ? {
928
+ post: O(() => !l.value || l.value.boardId !== a.value ? null : {
1025
929
  ...l.value,
1026
930
  id: o.value,
1027
931
  content: d.value?.content ?? ""
1028
- } : null),
1029
- pending: n(() => u.value || f.value)
932
+ }),
933
+ pending: O(() => u.value || f.value)
1030
934
  };
1031
935
  }
1032
936
  //#endregion
1033
937
  //#region src/composables/useMemiBoardUserPosts.ts
1034
- var bt = 10;
1035
- function xt(e, t = {}) {
1036
- let i = F(), c = t.pageSize ?? bt, u = n(() => o(e)?.trim() || ""), d = a([]), f = a([]), m = a(!0), h = a(!0), _ = a(!1), y = a(""), C = null, w = null, T;
1037
- function D(e) {
1038
- let t = e.ref.parent.parent?.id ?? "";
938
+ var vt = 10;
939
+ function yt(e, t = {}) {
940
+ let n = Z(), r = () => m(), i = () => T(n, r()), a = t.pageSize ?? vt, o = O(() => M(e)?.trim() || ""), s = j([]), c = j([]), l = j(!0), u = j(!0), d = j(!1), f = j(""), p = null, h = null, g;
941
+ function _(e) {
1039
942
  return {
1040
943
  id: e.id,
1041
- boardId: t,
1042
944
  ...e.data()
1043
945
  };
1044
946
  }
1045
- function k(e) {
947
+ function v(e) {
1046
948
  return e.createdAt?.toMillis?.() ?? 0;
1047
949
  }
1048
- let A = n(() => {
950
+ let y = O(() => {
1049
951
  let e = /* @__PURE__ */ new Map();
1050
- for (let t of [...f.value, ...d.value]) t.id && e.set(t.id, t);
1051
- return [...e.values()].sort((e, t) => k(t) - k(e) || String(t.id).localeCompare(String(e.id)));
952
+ for (let t of [...c.value, ...s.value]) t.id && e.set(t.id, t);
953
+ return [...e.values()].sort((e, t) => v(t) - v(e) || String(t.id).localeCompare(String(e.id)));
1052
954
  });
1053
- function j() {
955
+ function b() {
1054
956
  return [
1055
- O("authorUid", "==", u.value),
1056
- O("isPublished", "==", !0),
1057
- O("listed", "==", !0)
957
+ Y("authorUid", "==", o.value),
958
+ Y("isPublished", "==", !0),
959
+ Y("listed", "==", !0)
1058
960
  ];
1059
961
  }
1060
- function M() {
1061
- if (T?.(), !u.value) {
1062
- m.value = !1;
962
+ function x() {
963
+ if (g?.(), !o.value) {
964
+ l.value = !1;
1063
965
  return;
1064
966
  }
1065
- T = b(S(l(i, "posts"), ...j(), x("createdAt", "desc"), x(p(), "desc"), v(c)), (e) => {
1066
- d.value = e.docs.map(D), C = e.docs.at(-1) ?? null, w || (h.value = e.docs.length >= c), m.value = !1;
967
+ g = H(W(i(), ...b(), U("createdAt", "desc"), U(L(), "desc"), V(a)), (e) => {
968
+ s.value = e.docs.map(_), p = e.docs.at(-1) ?? null, h || (u.value = e.docs.length >= a), l.value = !1;
1067
969
  }, (e) => {
1068
- console.error("[memi-board:userPosts] onSnapshot failed", e), y.value = e.message || String(e), m.value = !1;
970
+ console.error("[memi-board:userPosts] onSnapshot failed", e), f.value = e.message || String(e), l.value = !1;
1069
971
  });
1070
972
  }
1071
- async function N() {
1072
- if (!(_.value || !h.value || m.value || !u.value)) {
1073
- _.value = !0, y.value = "";
973
+ async function S() {
974
+ if (!(d.value || !u.value || l.value || !o.value)) {
975
+ d.value = !0, f.value = "";
1074
976
  try {
1075
- let e = w ?? C, t = [
1076
- ...j(),
1077
- x("createdAt", "desc"),
1078
- x(p(), "desc"),
1079
- ...e ? [E(e)] : [],
1080
- v(c + 1)
1081
- ], n = await g(S(l(i, "posts"), ...t)), r = n.docs.slice(0, c), a = r.map(D), o = new Set([...f.value, ...d.value].map((e) => e.id));
1082
- f.value.push(...a.filter((e) => !o.has(e.id))), w = r.at(-1) ?? w, h.value = n.docs.length > c;
977
+ let e = h ?? p, t = [
978
+ ...b(),
979
+ U("createdAt", "desc"),
980
+ U(L(), "desc"),
981
+ ...e ? [q(e)] : [],
982
+ V(a + 1)
983
+ ], n = await z(W(i(), ...t)), r = n.docs.slice(0, a), o = r.map(_), l = new Set([...c.value, ...s.value].map((e) => e.id));
984
+ c.value.push(...o.filter((e) => !l.has(e.id))), h = r.at(-1) ?? h, u.value = n.docs.length > a;
1083
985
  } catch (e) {
1084
- y.value = e.message || String(e), console.error("[memi-board:userPosts] loadMore failed", e);
986
+ f.value = e.message || String(e), console.error("[memi-board:userPosts] loadMore failed", e);
1085
987
  } finally {
1086
- _.value = !1;
988
+ d.value = !1;
1087
989
  }
1088
990
  }
1089
991
  }
1090
- function P() {
1091
- T?.(), d.value = [], f.value = [], C = null, w = null, h.value = !0, y.value = "", m.value = !0, M();
992
+ function C() {
993
+ g?.(), s.value = [], c.value = [], p = null, h = null, u.value = !0, f.value = "", l.value = !0, x();
1092
994
  }
1093
- return s(u, P, { immediate: !0 }), r(() => T?.()), {
1094
- posts: A,
1095
- postsPending: m,
1096
- hasMore: h,
1097
- loadingMore: _,
1098
- loadError: y,
1099
- loadMore: N
995
+ return N(o, C, { immediate: !0 }), k(() => g?.()), {
996
+ posts: y,
997
+ postsPending: l,
998
+ hasMore: u,
999
+ loadingMore: d,
1000
+ loadError: f,
1001
+ loadMore: S
1002
+ };
1003
+ }
1004
+ //#endregion
1005
+ //#region src/composables/useMemiBoardUserComments.ts
1006
+ var bt = 10;
1007
+ function xt(e, t = {}) {
1008
+ let n = Z(), r = () => m(), i = () => o(n, r()), a = t.pageSize ?? bt, s = O(() => M(e)?.trim() || ""), c = j([]), l = j([]), u = j(!0), d = j(!0), f = j(!1), p = j(""), h = null, g = null, _;
1009
+ function v(e) {
1010
+ return {
1011
+ id: e.id,
1012
+ ...e.data()
1013
+ };
1014
+ }
1015
+ function y(e) {
1016
+ return e.createdAt?.toMillis?.() ?? 0;
1017
+ }
1018
+ let b = O(() => {
1019
+ let e = /* @__PURE__ */ new Map();
1020
+ for (let t of [...l.value, ...c.value]) t.id && e.set(t.id, t);
1021
+ return [...e.values()].sort((e, t) => y(t) - y(e) || String(t.id).localeCompare(String(e.id)));
1022
+ });
1023
+ function x() {
1024
+ return [Y("authorUid", "==", s.value)];
1025
+ }
1026
+ function S() {
1027
+ if (_?.(), !s.value) {
1028
+ u.value = !1;
1029
+ return;
1030
+ }
1031
+ _ = H(W(i(), ...x(), U("createdAt", "desc"), U(L(), "desc"), V(a)), (e) => {
1032
+ c.value = e.docs.map(v), h = e.docs.at(-1) ?? null, g || (d.value = e.docs.length >= a), u.value = !1;
1033
+ }, (e) => {
1034
+ console.error("[memi-board:userComments] onSnapshot failed", e), p.value = e.message || String(e), u.value = !1;
1035
+ });
1036
+ }
1037
+ async function C() {
1038
+ if (!(f.value || !d.value || u.value || !s.value)) {
1039
+ f.value = !0, p.value = "";
1040
+ try {
1041
+ let e = g ?? h, t = [
1042
+ ...x(),
1043
+ U("createdAt", "desc"),
1044
+ U(L(), "desc"),
1045
+ ...e ? [q(e)] : [],
1046
+ V(a + 1)
1047
+ ], n = await z(W(i(), ...t)), r = n.docs.slice(0, a), o = r.map(v), s = new Set([...l.value, ...c.value].map((e) => e.id));
1048
+ l.value.push(...o.filter((e) => !s.has(e.id))), g = r.at(-1) ?? g, d.value = n.docs.length > a;
1049
+ } catch (e) {
1050
+ p.value = e.message || String(e), console.error("[memi-board:userComments] loadMore failed", e);
1051
+ } finally {
1052
+ f.value = !1;
1053
+ }
1054
+ }
1055
+ }
1056
+ function w() {
1057
+ _?.(), c.value = [], l.value = [], h = null, g = null, d.value = !0, p.value = "", u.value = !0, S();
1058
+ }
1059
+ return N(s, w, { immediate: !0 }), k(() => _?.()), {
1060
+ comments: b,
1061
+ commentsPending: u,
1062
+ hasMore: d,
1063
+ loadingMore: f,
1064
+ loadError: p,
1065
+ loadMore: C
1066
+ };
1067
+ }
1068
+ //#endregion
1069
+ //#region src/composables/useMemiBoardAuthorMemo.ts
1070
+ var St = 100, Ct = /* @__PURE__ */ new Map();
1071
+ function Q(e) {
1072
+ let t = Ct.get(e);
1073
+ return t || (t = A({
1074
+ text: "",
1075
+ sentiment: null,
1076
+ loaded: !1,
1077
+ pending: !1,
1078
+ saving: !1,
1079
+ error: ""
1080
+ }), Ct.set(e, t)), t;
1081
+ }
1082
+ function wt(e) {
1083
+ let t = Z(), n = () => m(), { user: r } = Ke(), i = O(() => M(e)), a = O(() => `${r.value?.uid ?? ""}:${i.value}`), o = O(() => {
1084
+ let e = r.value?.uid, a = i.value;
1085
+ return !e || !a ? null : u(t, n(), e, a);
1086
+ }), s = O({
1087
+ get: () => Q(a.value).text,
1088
+ set: (e) => {
1089
+ Q(a.value).text = e;
1090
+ }
1091
+ }), c = O(() => Q(a.value).sentiment), l = O(() => Q(a.value).loaded), d = O(() => Q(a.value).pending), f = O(() => Q(a.value).saving), p = O(() => Q(a.value).error);
1092
+ async function h() {
1093
+ let e = o.value;
1094
+ if (!e) return;
1095
+ let t = Q(a.value);
1096
+ t.pending = !0, t.error = "";
1097
+ try {
1098
+ let n = (await R(e)).data();
1099
+ t.text = n ? n.text ?? "" : "", t.sentiment = n ? n.sentiment ?? null : null, t.loaded = !0;
1100
+ } catch (e) {
1101
+ t.error = e.message || String(e), console.error("[memi-board:authorMemo] load failed", e);
1102
+ } finally {
1103
+ t.pending = !1;
1104
+ }
1105
+ }
1106
+ async function g(e, t) {
1107
+ let n = o.value;
1108
+ if (!n) return;
1109
+ let r = e.trim();
1110
+ if (r.length > 100) throw Error(`메모는 ${100 .toLocaleString()}자까지 작성할 수 있습니다.`);
1111
+ let i = Q(a.value);
1112
+ i.saving = !0, i.error = "";
1113
+ try {
1114
+ await K(n, {
1115
+ text: r,
1116
+ sentiment: t,
1117
+ updatedAt: G()
1118
+ }), i.text = r, i.sentiment = t, i.loaded = !0;
1119
+ } catch (e) {
1120
+ throw i.error = e.message || String(e), console.error("[memi-board:authorMemo] save failed", e), e;
1121
+ } finally {
1122
+ i.saving = !1;
1123
+ }
1124
+ }
1125
+ async function _() {
1126
+ let e = o.value;
1127
+ if (!e) return;
1128
+ let t = Q(a.value);
1129
+ t.saving = !0, t.error = "";
1130
+ try {
1131
+ await P(e), t.text = "", t.sentiment = null, t.loaded = !0;
1132
+ } catch (e) {
1133
+ throw t.error = e.message || String(e), console.error("[memi-board:authorMemo] remove failed", e), e;
1134
+ } finally {
1135
+ t.saving = !1;
1136
+ }
1137
+ }
1138
+ return {
1139
+ text: s,
1140
+ sentiment: c,
1141
+ loaded: l,
1142
+ pending: d,
1143
+ saving: f,
1144
+ error: p,
1145
+ load: h,
1146
+ save: g,
1147
+ remove: _
1100
1148
  };
1101
1149
  }
1102
1150
  //#endregion
1103
1151
  //#region src/composables/useMemiBoardComments.ts
1104
- var St = 10, Ct = 5, wt = 1e3;
1105
- function Tt(e) {
1152
+ var Tt = 10, Et = 5, Dt = 1e3;
1153
+ function Ot(e) {
1106
1154
  let t = e.trim();
1107
1155
  if (!t) throw Error("댓글 내용을 입력해 주세요.");
1108
- if (t.length > 1e3) throw Error(`댓글은 ${wt.toLocaleString()}자까지 작성할 수 있습니다.`);
1156
+ if (t.length > 1e3) throw Error(`댓글은 ${Dt.toLocaleString()}자까지 작성할 수 있습니다.`);
1109
1157
  return t;
1110
1158
  }
1111
- function Et(e, t, i = {}) {
1112
- let c = F(), l = () => Z(), u = n(() => o(e)), d = n(() => o(t)), m = a([]), h = a([]), y = a(i.subscribe !== !1), C = a([]), T = a(!1), A = a(i.subscribe !== !1), j = a(i.subscribe !== !1), M = null, N, P;
1113
- function I(e) {
1159
+ function kt(e, t, n = {}) {
1160
+ let r = Z(), a = () => m(), s = () => o(r, a()), c = O(() => M(e)), l = O(() => M(t)), u = j([]), d = j([]), p = j(n.subscribe !== !1), h = j([]), g = j(!1), _ = j(n.subscribe !== !1), v = j(n.subscribe !== !1), y = null, b, x;
1161
+ function S(e) {
1114
1162
  return e.createdAt?.toMillis?.() ?? 0;
1115
1163
  }
1116
- let L = n(() => {
1164
+ let C = O(() => {
1117
1165
  let e = /* @__PURE__ */ new Map();
1118
1166
  for (let t of [
1119
- ...m.value,
1120
- ...h.value,
1121
- ...C.value
1167
+ ...u.value,
1168
+ ...d.value,
1169
+ ...h.value
1122
1170
  ]) t.id && e.set(t.id, t);
1123
- return [...e.values()].sort((e, t) => I(e) - I(t) || String(e.id).localeCompare(String(t.id)));
1171
+ return [...e.values()].sort((e, t) => S(e) - S(t) || String(e.id).localeCompare(String(t.id)));
1124
1172
  });
1125
- s([d, u], async ([e]) => {
1126
- if (C.value = [], m.value = [], h.value = [], M = null, clearTimeout(N), N = void 0, P?.(), P = void 0, A.value = i.subscribe !== !1, i.subscribe !== !1) {
1127
- y.value = !0, j.value = !0;
1173
+ N([l, c], async ([e]) => {
1174
+ if (h.value = [], u.value = [], d.value = [], y = null, clearTimeout(b), b = void 0, x?.(), x = void 0, _.value = n.subscribe !== !1, n.subscribe !== !1) {
1175
+ p.value = !0, v.value = !0;
1128
1176
  try {
1129
- let t = g(S(J(c, l(), u.value, e), x("createdAt", "desc"), x(p(), "desc")));
1177
+ let t = z(W(s(), Y("postId", "==", e), U("createdAt", "desc"), U(L(), "desc")));
1130
1178
  if (await Promise.all([t.then((t) => {
1131
- if (d.value !== e) return;
1179
+ if (l.value !== e) return;
1132
1180
  let n = t.docs.map((e) => ({
1133
1181
  id: e.id,
1134
1182
  ...e.data()
1135
1183
  }));
1136
- h.value = n.filter((e) => e.parentId === void 0);
1137
- }), z()]), d.value !== e) return;
1184
+ d.value = n.filter((e) => e.parentId === void 0);
1185
+ }), T()]), l.value !== e) return;
1138
1186
  } finally {
1139
- d.value === e && (y.value = !1, j.value = !1);
1187
+ l.value === e && (p.value = !1, v.value = !1);
1140
1188
  }
1141
1189
  }
1142
1190
  }, { immediate: !0 });
1143
- function R() {
1144
- P?.(), P = b(S(J(c, l(), u.value, d.value), O("parentId", "==", null), x("createdAt", "desc"), x(p(), "desc"), v(1)), (e) => {
1145
- let t = new Map(m.value.map((e) => [e.id, e]));
1191
+ function w() {
1192
+ x?.(), x = H(W(s(), Y("postId", "==", l.value), Y("parentId", "==", null), U("createdAt", "desc"), U(L(), "desc"), V(1)), (e) => {
1193
+ let t = new Map(u.value.map((e) => [e.id, e]));
1146
1194
  for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
1147
1195
  id: n.doc.id,
1148
1196
  ...n.doc.data()
1149
1197
  });
1150
- m.value = [...t.values()];
1198
+ u.value = [...t.values()];
1151
1199
  }, (e) => {
1152
1200
  console.error("[memi-board:comments] onSnapshot failed", e);
1153
1201
  });
1154
1202
  }
1155
- async function z() {
1156
- if (!(T.value || !A.value)) {
1157
- T.value = !0;
1203
+ async function T() {
1204
+ if (!(g.value || !_.value)) {
1205
+ g.value = !0;
1158
1206
  try {
1159
1207
  let e = [
1160
- O("parentId", "==", null),
1161
- x("createdAt", "asc"),
1162
- x(p(), "asc"),
1163
- ...M ? [E(M)] : [],
1164
- v(11)
1165
- ], t = await g(S(J(c, l(), u.value, d.value), ...e)), n = t.docs.slice(0, St), r = n.map((e) => ({
1208
+ Y("postId", "==", l.value),
1209
+ Y("parentId", "==", null),
1210
+ U("createdAt", "asc"),
1211
+ U(L(), "asc"),
1212
+ ...y ? [q(y)] : [],
1213
+ V(11)
1214
+ ], t = await z(W(s(), ...e)), n = t.docs.slice(0, Tt), r = n.map((e) => ({
1166
1215
  id: e.id,
1167
1216
  ...e.data()
1168
- })), i = new Set(C.value.map((e) => e.id));
1169
- C.value.push(...r.filter((e) => !i.has(e.id))), M = n.at(-1) ?? M, t.size <= St && (A.value = !1, N = setTimeout(() => {
1170
- N = void 0, R();
1217
+ })), i = new Set(h.value.map((e) => e.id));
1218
+ h.value.push(...r.filter((e) => !i.has(e.id))), y = n.at(-1) ?? y, t.size <= Tt && (_.value = !1, b = setTimeout(() => {
1219
+ b = void 0, w();
1171
1220
  }, 1e3));
1172
1221
  } finally {
1173
- T.value = !1;
1222
+ g.value = !1;
1174
1223
  }
1175
1224
  }
1176
1225
  }
1177
- r(() => {
1178
- clearTimeout(N), P?.();
1226
+ k(() => {
1227
+ clearTimeout(b), x?.();
1179
1228
  });
1180
- async function B(e) {
1181
- let t = Tt(e.body), n = k(c), r = f(J(c, l(), u.value, d.value));
1182
- n.set(r, {
1183
- postId: d.value,
1229
+ async function E(e) {
1230
+ let t = Ot(e.body), n = X(r), o = I(s());
1231
+ n.set(o, {
1232
+ postId: l.value,
1233
+ boardId: c.value,
1184
1234
  body: t,
1185
1235
  authorUid: e.authorUid,
1186
1236
  authorName: e.authorName,
1187
1237
  authorPhoto: e.authorPhoto,
1188
1238
  moderationStatus: "approved",
1189
1239
  parentId: null,
1190
- rootId: r.id,
1240
+ rootId: o.id,
1191
1241
  depth: 0,
1192
1242
  replyToUid: null,
1193
1243
  replyToName: null,
1194
1244
  replyCount: 0,
1195
1245
  isReply: !1,
1196
- createdAt: w(),
1197
- updatedAt: w()
1198
- }), n.update(q(c, l(), u.value, d.value), { commentCount: _(1) }), await n.commit();
1246
+ createdAt: G(),
1247
+ updatedAt: G()
1248
+ }), n.update(i(r, a(), l.value), { commentCount: B(1) }), await n.commit();
1199
1249
  }
1200
- async function ee(e) {
1201
- let t = Tt(e.body);
1250
+ async function D(e) {
1251
+ let t = Ot(e.body);
1202
1252
  if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
1203
- let n = k(c), r = J(c, l(), u.value, d.value), i = f(r), a = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
1204
- if (!a) throw Error("댓글 스레드를 찾을 수 없습니다.");
1205
- let o = Math.min((e.parent.depth ?? 0) + 1, 2);
1206
- n.set(i, {
1207
- postId: d.value,
1253
+ let n = X(r), o = I(s()), u = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
1254
+ if (!u) throw Error("댓글 스레드를 찾을 수 없습니다.");
1255
+ let d = Math.min((e.parent.depth ?? 0) + 1, 2);
1256
+ n.set(o, {
1257
+ postId: l.value,
1258
+ boardId: c.value,
1208
1259
  body: t,
1209
1260
  authorUid: e.authorUid,
1210
1261
  authorName: e.authorName,
1211
1262
  authorPhoto: e.authorPhoto,
1212
1263
  moderationStatus: "approved",
1213
1264
  parentId: e.parent.id,
1214
- rootId: a,
1215
- depth: o,
1265
+ rootId: u,
1266
+ depth: d,
1216
1267
  replyToUid: e.parent.authorUid,
1217
1268
  replyToName: e.parent.authorName,
1218
1269
  replyCount: 0,
1219
1270
  isReply: !0,
1220
- createdAt: w(),
1221
- updatedAt: w()
1222
- }), n.update(f(r, a), { replyCount: _(1) }), n.update(q(c, l(), u.value, d.value), { commentCount: _(1) }), await n.commit();
1271
+ createdAt: G(),
1272
+ updatedAt: G()
1273
+ }), n.update(f(r, a(), u), { replyCount: B(1) }), n.update(i(r, a(), l.value), { commentCount: B(1) }), await n.commit();
1223
1274
  }
1224
- async function te(e) {
1275
+ async function A(e) {
1225
1276
  if (!e.id) return;
1226
1277
  if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error("답글이 있는 댓글은 현재 삭제할 수 없습니다.");
1227
- let t = k(c);
1228
- t.delete(Y(c, l(), u.value, d.value, e.id)), e.parentId && e.rootId && t.update(Y(c, l(), u.value, d.value, e.rootId), { replyCount: _(-1) }), t.update(q(c, l(), u.value, d.value), { commentCount: _(-1) }), await t.commit(), C.value = C.value.filter((t) => t.id !== e.id), h.value = h.value.filter((t) => t.id !== e.id), m.value = m.value.filter((t) => t.id !== e.id);
1278
+ let t = X(r);
1279
+ t.delete(f(r, a(), e.id)), e.parentId && e.rootId && t.update(f(r, a(), e.rootId), { replyCount: B(-1) }), t.update(i(r, a(), l.value), { commentCount: B(-1) }), await t.commit(), h.value = h.value.filter((t) => t.id !== e.id), d.value = d.value.filter((t) => t.id !== e.id), u.value = u.value.filter((t) => t.id !== e.id);
1229
1280
  }
1230
- async function V(e, t) {
1231
- let n = Tt(t);
1232
- await D(Y(c, l(), u.value, d.value, e), {
1281
+ async function P(e, t) {
1282
+ let n = Ot(t);
1283
+ await J(f(r, a(), e), {
1233
1284
  body: n,
1234
- updatedAt: w()
1285
+ updatedAt: G()
1235
1286
  });
1236
1287
  }
1237
- async function H(e, t, n) {
1238
- await D(Y(c, l(), u.value, d.value, e), {
1288
+ async function F(e, t, n) {
1289
+ await J(f(r, a(), e), {
1239
1290
  isBlinded: t,
1240
- moderatedAt: w(),
1291
+ moderatedAt: G(),
1241
1292
  moderatedBy: n
1242
1293
  });
1243
1294
  }
1244
1295
  return {
1245
- comments: L,
1246
- commentsPending: n(() => j.value || y.value),
1247
- hasMore: A,
1248
- loadingMore: T,
1249
- loadMore: z,
1250
- addComment: B,
1251
- addReply: ee,
1252
- updateComment: V,
1253
- setCommentBlinded: H,
1254
- deleteComment: te
1296
+ comments: C,
1297
+ commentsPending: O(() => v.value || p.value),
1298
+ hasMore: _,
1299
+ loadingMore: g,
1300
+ loadMore: T,
1301
+ addComment: E,
1302
+ addReply: D,
1303
+ updateComment: P,
1304
+ setCommentBlinded: F,
1305
+ deleteComment: A
1255
1306
  };
1256
1307
  }
1257
- function Dt(e, t, n) {
1258
- let i = F(), o = () => Z(), s = a([]), c = a(!1), l = a(!1), u = a(!0), d = null, f;
1259
- function m(e) {
1308
+ function At(e, t, n) {
1309
+ let r = Z(), i = () => m(), a = () => o(r, i()), s = j([]), c = j(!1), l = j(!1), u = j(!0), d = null, f;
1310
+ function p(e) {
1260
1311
  return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
1261
1312
  }
1262
1313
  function h() {
1263
- f ||= b(S(J(i, o(), e, t), O("rootId", "==", n), O("isReply", "==", !0), x("createdAt", "desc"), x(p(), "desc"), v(1)), (e) => {
1314
+ f ||= H(W(a(), Y("rootId", "==", n), Y("isReply", "==", !0), U("createdAt", "desc"), U(L(), "desc"), V(1)), (e) => {
1264
1315
  let t = new Map(s.value.map((e) => [e.id, e]));
1265
1316
  for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
1266
1317
  id: n.doc.id,
1267
1318
  ...n.doc.data()
1268
1319
  });
1269
- s.value = m([...t.values()]);
1320
+ s.value = p([...t.values()]);
1270
1321
  }, (e) => {
1271
1322
  console.error("[memi-board:replies] onSnapshot failed", e);
1272
1323
  });
1273
1324
  }
1274
- async function _() {
1325
+ async function g() {
1275
1326
  if (!(c.value || !u.value)) {
1276
1327
  c.value = !0;
1277
1328
  try {
1278
- let r = [
1279
- O("rootId", "==", n),
1280
- O("isReply", "==", !0),
1281
- x("createdAt", "asc"),
1282
- x(p(), "asc"),
1283
- ...d ? [E(d)] : [],
1284
- v(6)
1285
- ], a = await g(S(J(i, o(), e, t), ...r)), c = new Set(s.value.map((e) => e.id)), f = a.docs.slice(0, Ct), _ = f.map((e) => ({
1329
+ let e = [
1330
+ Y("rootId", "==", n),
1331
+ Y("isReply", "==", !0),
1332
+ U("createdAt", "asc"),
1333
+ U(L(), "asc"),
1334
+ ...d ? [q(d)] : [],
1335
+ V(6)
1336
+ ], t = await z(W(a(), ...e)), r = new Set(s.value.map((e) => e.id)), i = t.docs.slice(0, Et), o = i.map((e) => ({
1286
1337
  id: e.id,
1287
1338
  ...e.data()
1288
- })).filter((e) => !c.has(e.id));
1289
- s.value = m([...s.value, ..._]), d = f.at(-1) ?? d, l.value = !0, a.size <= Ct && (u.value = !1, h());
1339
+ })).filter((e) => !r.has(e.id));
1340
+ s.value = p([...s.value, ...o]), d = i.at(-1) ?? d, l.value = !0, t.size <= Et && (u.value = !1, h());
1290
1341
  } finally {
1291
1342
  c.value = !1;
1292
1343
  }
1293
1344
  }
1294
1345
  }
1295
- function y() {
1346
+ function _() {
1296
1347
  f?.(), f = void 0, s.value = [], d = null, l.value = !1, u.value = !0;
1297
1348
  }
1298
- async function C() {
1299
- y(), await _();
1349
+ async function v() {
1350
+ _(), await g();
1300
1351
  }
1301
- return r(() => f?.()), {
1352
+ return k(() => f?.()), {
1302
1353
  replies: s,
1303
1354
  loading: c,
1304
1355
  loaded: l,
1305
1356
  hasMore: u,
1306
- loadMore: _,
1307
- refresh: C
1357
+ loadMore: g,
1358
+ refresh: v
1308
1359
  };
1309
1360
  }
1310
1361
  //#endregion
1311
1362
  //#region src/composables/useMemiBoardLikes.ts
1312
- function Ot(e, t) {
1313
- let n = () => Z(), i = F(), o = j(), c = a(!1), l = a(!1), u;
1314
- function d(r) {
1315
- return he(i, n(), e, t, r);
1316
- }
1317
- s(o, (e) => {
1318
- if (u?.(), u = void 0, !e) {
1319
- c.value = !1;
1363
+ function jt(e, t) {
1364
+ let n = () => m(), r = Z(), a = ie(), o = j(!1), s = j(!1), c;
1365
+ function l(e) {
1366
+ return w(r, n(), t, e);
1367
+ }
1368
+ N(a, (e) => {
1369
+ if (c?.(), c = void 0, !e) {
1370
+ o.value = !1;
1320
1371
  return;
1321
1372
  }
1322
- u = b(d(e.uid), (e) => {
1323
- c.value = e.exists();
1373
+ c = H(l(e.uid), (e) => {
1374
+ o.value = e.exists();
1324
1375
  }, (e) => {
1325
1376
  console.error("[memi-board:likes] onSnapshot failed", e);
1326
1377
  });
1327
- }, { immediate: !0 }), r(() => u?.());
1328
- async function f() {
1329
- let r = o.value?.uid;
1330
- if (!r) throw Error("로그인이 필요합니다.");
1331
- if (l.value) return c.value;
1332
- l.value = !0;
1378
+ }, { immediate: !0 }), k(() => c?.());
1379
+ async function u() {
1380
+ let c = a.value?.uid;
1381
+ if (!c) throw Error("로그인이 필요합니다.");
1382
+ if (s.value) return o.value;
1383
+ s.value = !0;
1333
1384
  try {
1334
- let a = d(r), o = q(i, n(), e, t);
1335
- return await C(i, async (e) => (await e.get(a)).exists() ? (e.delete(a), e.update(o, { likeCount: _(-1) }), !1) : (e.set(a, {
1336
- uid: r,
1337
- createdAt: w()
1338
- }), e.update(o, { likeCount: _(1) }), !0));
1385
+ let a = l(c), o = i(r, n(), t);
1386
+ return await ne(r, async (n) => (await n.get(a)).exists() ? (n.delete(a), n.update(o, { likeCount: B(-1) }), !1) : (n.set(a, {
1387
+ uid: c,
1388
+ postId: t,
1389
+ boardId: e,
1390
+ createdAt: G()
1391
+ }), n.update(o, { likeCount: B(1) }), !0));
1339
1392
  } finally {
1340
- l.value = !1;
1393
+ s.value = !1;
1341
1394
  }
1342
1395
  }
1343
1396
  return {
1344
- isLiked: c,
1345
- likePending: l,
1346
- toggleLike: f
1397
+ isLiked: o,
1398
+ likePending: s,
1399
+ toggleLike: u
1347
1400
  };
1348
1401
  }
1349
1402
  //#endregion
1350
1403
  //#region src/composables/useMemiBoardViews.ts
1351
- function kt() {
1352
- let e = () => Z(), t = F();
1404
+ function Mt() {
1405
+ let e = () => m(), t = Z();
1353
1406
  function n(t, n) {
1354
- let { boardsCollection: r } = e();
1407
+ let { postsCollection: r } = e();
1355
1408
  return `memi-board:viewed:${r}:${t}:${n}`;
1356
1409
  }
1357
1410
  function r(e, t) {
@@ -1362,33 +1415,33 @@ function kt() {
1362
1415
  return !1;
1363
1416
  }
1364
1417
  }
1365
- function i(e, t) {
1418
+ function a(e, t) {
1366
1419
  if (!(typeof sessionStorage > "u")) try {
1367
1420
  sessionStorage.setItem(n(e, t), "1");
1368
1421
  } catch {}
1369
1422
  }
1370
- async function a(a, o) {
1371
- let s = a?.trim(), c = o?.trim();
1372
- if (!(!s || !c) && !r(s, c)) {
1373
- i(s, c);
1423
+ async function o(o, s) {
1424
+ let c = o?.trim(), l = s?.trim();
1425
+ if (!(!c || !l) && !r(c, l)) {
1426
+ a(c, l);
1374
1427
  try {
1375
- await D(q(t, e(), s, c), { viewCount: _(1) });
1428
+ await J(i(t, e(), l), { viewCount: B(1) });
1376
1429
  } catch (e) {
1377
1430
  try {
1378
- sessionStorage.removeItem(n(s, c));
1431
+ sessionStorage.removeItem(n(c, l));
1379
1432
  } catch {}
1380
1433
  import.meta.dev && console.warn("[memi-board] recordView failed", e);
1381
1434
  }
1382
1435
  }
1383
1436
  }
1384
1437
  return {
1385
- recordView: a,
1438
+ recordView: o,
1386
1439
  hasRecordedView: r
1387
1440
  };
1388
1441
  }
1389
1442
  //#endregion
1390
1443
  //#region src/utils/moderation-prompt.ts
1391
- var At = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", jt = {
1444
+ var Nt = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", Pt = {
1392
1445
  abuse: "욕설·비속어·모욕",
1393
1446
  spam: "스팸·광고",
1394
1447
  adult: "선정적·성인 내용",
@@ -1396,17 +1449,17 @@ var At = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
1396
1449
  other: "부적절한 내용",
1397
1450
  none: ""
1398
1451
  };
1399
- function Mt(e) {
1452
+ function Ft(e) {
1400
1453
  if (e.via === "local" && e.localHit) {
1401
1454
  let t = e.localHit.trim();
1402
1455
  return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
1403
1456
  }
1404
1457
  let t = (e.reason || "").trim();
1405
1458
  if (t) return t;
1406
- let n = e.category && e.category !== "none" ? jt[e.category] || e.category : "";
1459
+ let n = e.category && e.category !== "none" ? Pt[e.category] || e.category : "";
1407
1460
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
1408
1461
  }
1409
- var Nt = [
1462
+ var It = [
1410
1463
  "씨발",
1411
1464
  "시발",
1412
1465
  "시이발",
@@ -1432,10 +1485,10 @@ var Nt = [
1432
1485
  "bitch",
1433
1486
  "asshole"
1434
1487
  ];
1435
- function Pt(e) {
1488
+ function Lt(e) {
1436
1489
  return new RegExp(e.join("|"), "i");
1437
1490
  }
1438
- var Ft = /* @__PURE__ */ new Set([
1491
+ var Rt = /* @__PURE__ */ new Set([
1439
1492
  "none",
1440
1493
  "abuse",
1441
1494
  "spam",
@@ -1443,7 +1496,7 @@ var Ft = /* @__PURE__ */ new Set([
1443
1496
  "violence",
1444
1497
  "other"
1445
1498
  ]);
1446
- function It(e) {
1499
+ function zt(e) {
1447
1500
  let t = e.trim();
1448
1501
  if (!t) return null;
1449
1502
  let n;
@@ -1461,7 +1514,7 @@ function It(e) {
1461
1514
  if (typeof n != "object" || !n) return null;
1462
1515
  let r = n;
1463
1516
  if (typeof r.flagged != "boolean") return null;
1464
- let i = typeof r.category == "string" && Ft.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
1517
+ let i = typeof r.category == "string" && Rt.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
1465
1518
  return {
1466
1519
  flagged: r.flagged,
1467
1520
  category: i,
@@ -1470,20 +1523,20 @@ function It(e) {
1470
1523
  }
1471
1524
  //#endregion
1472
1525
  //#region src/composables/useMemiBoardModeration.ts
1473
- var Lt = {
1526
+ var Bt = {
1474
1527
  flagged: !1,
1475
1528
  category: "none",
1476
1529
  reason: "",
1477
1530
  via: "empty"
1478
1531
  };
1479
- function Rt() {
1480
- let e = Ce().moderation ?? {}, t = N(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = Ke(), a = Pt([...Nt, ...e.localBlocklist ?? []]);
1532
+ function Vt() {
1533
+ let e = s().moderation ?? {}, t = oe(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = Ke(), a = Lt([...It, ...e.localBlocklist ?? []]);
1481
1534
  function o(e) {
1482
1535
  let t = e.match(a);
1483
1536
  return t ? {
1484
1537
  flagged: !0,
1485
1538
  category: "abuse",
1486
- reason: Mt({
1539
+ reason: Ft({
1487
1540
  via: "local",
1488
1541
  localHit: t[0] || "",
1489
1542
  category: "abuse"
@@ -1491,27 +1544,27 @@ function Rt() {
1491
1544
  via: "local"
1492
1545
  } : null;
1493
1546
  }
1494
- function s() {
1495
- let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
1496
- backend: new ae(),
1547
+ function c() {
1548
+ let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = xe(t, {
1549
+ backend: new ye(),
1497
1550
  useLimitedUseAppCheckTokens: r
1498
1551
  });
1499
- return ce(i, {
1552
+ return Se(i, {
1500
1553
  model: n,
1501
- systemInstruction: At,
1554
+ systemInstruction: Nt,
1502
1555
  generationConfig: {
1503
1556
  temperature: 0,
1504
1557
  maxOutputTokens: 256,
1505
1558
  responseMimeType: "application/json",
1506
- responseSchema: oe.object({ properties: {
1507
- flagged: oe.boolean(),
1508
- category: oe.string(),
1509
- reason: oe.string()
1559
+ responseSchema: be.object({ properties: {
1560
+ flagged: be.boolean(),
1561
+ category: be.string(),
1562
+ reason: be.string()
1510
1563
  } })
1511
1564
  }
1512
1565
  });
1513
1566
  }
1514
- function c() {
1567
+ function l() {
1515
1568
  return e.onError === "block" ? {
1516
1569
  flagged: !0,
1517
1570
  category: "other",
@@ -1526,8 +1579,8 @@ function Rt() {
1526
1579
  via: "ai-error-allow"
1527
1580
  };
1528
1581
  }
1529
- function l(e, t = "") {
1530
- let n = Mt({
1582
+ function u(e, t = "") {
1583
+ let n = Ft({
1531
1584
  reason: e.reason,
1532
1585
  category: e.category,
1533
1586
  via: e.via
@@ -1537,16 +1590,16 @@ function Rt() {
1537
1590
  reason: r
1538
1591
  };
1539
1592
  }
1540
- async function u(e) {
1593
+ async function d(e) {
1541
1594
  if (!e.flagged || e.error || e.via !== "local" && e.via !== "ai") return e;
1542
1595
  try {
1543
1596
  let { messageSuffix: t } = await i();
1544
- return l(e, t);
1597
+ return u(e, t);
1545
1598
  } catch (t) {
1546
- return console.warn("[memi-board] record moderation strike failed", t instanceof Error ? t.message : t), l(e);
1599
+ return console.warn("[memi-board] record moderation strike failed", t instanceof Error ? t.message : t), u(e);
1547
1600
  }
1548
1601
  }
1549
- async function d(t) {
1602
+ async function f(t) {
1550
1603
  if (n.value) return {
1551
1604
  flagged: !0,
1552
1605
  category: "other",
@@ -1555,34 +1608,34 @@ function Rt() {
1555
1608
  };
1556
1609
  let i = t.trim();
1557
1610
  if (!i) return {
1558
- ...Lt,
1611
+ ...Bt,
1559
1612
  via: "empty"
1560
1613
  };
1561
1614
  let a = o(i);
1562
- if (a) return u(a);
1615
+ if (a) return d(a);
1563
1616
  if (e.enabled === !1) return {
1564
- ...Lt,
1617
+ ...Bt,
1565
1618
  via: "disabled"
1566
1619
  };
1567
1620
  try {
1568
- let e = It((await s().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
1569
- if (!e) return c();
1621
+ let e = zt((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
1622
+ if (!e) return l();
1570
1623
  let t = {
1571
1624
  flagged: e.flagged,
1572
1625
  category: e.category,
1573
- reason: Mt({
1626
+ reason: Ft({
1574
1627
  reason: e.reason,
1575
1628
  category: e.category,
1576
1629
  via: "ai"
1577
1630
  }),
1578
1631
  via: "ai"
1579
1632
  };
1580
- return t.flagged ? u(t) : t;
1633
+ return t.flagged ? d(t) : t;
1581
1634
  } catch (e) {
1582
- return console.warn("[memi-board] moderation AI failed", e instanceof Error ? e.message : e), c();
1635
+ return console.warn("[memi-board] moderation AI failed", e instanceof Error ? e.message : e), l();
1583
1636
  }
1584
1637
  }
1585
- async function f(t) {
1638
+ async function p(t) {
1586
1639
  if (n.value) return {
1587
1640
  flagged: !0,
1588
1641
  category: "other",
@@ -1590,178 +1643,41 @@ function Rt() {
1590
1643
  via: "restricted"
1591
1644
  };
1592
1645
  if (e.enabled === !1 || !e.moderateImages) return {
1593
- ...Lt,
1646
+ ...Bt,
1594
1647
  via: "disabled"
1595
1648
  };
1596
1649
  try {
1597
1650
  let e = await new Promise((e, n) => {
1598
1651
  let r = new FileReader();
1599
1652
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
1600
- }), n = It((await s().generateContent([{ inlineData: {
1653
+ }), n = zt((await c().generateContent([{ inlineData: {
1601
1654
  mimeType: t.type || "image/jpeg",
1602
1655
  data: e
1603
1656
  } }, "이 이미지를 심사해 주세요."])).response.text());
1604
- if (!n) return c();
1657
+ if (!n) return l();
1605
1658
  let r = {
1606
1659
  flagged: n.flagged,
1607
1660
  category: n.category,
1608
- reason: Mt({
1661
+ reason: Ft({
1609
1662
  reason: n.reason,
1610
1663
  category: n.category,
1611
1664
  via: "ai"
1612
1665
  }),
1613
1666
  via: "ai"
1614
1667
  };
1615
- return r.flagged ? u(r) : r;
1668
+ return r.flagged ? d(r) : r;
1616
1669
  } catch (e) {
1617
- return console.warn("[memi-board] moderation AI image failed", e instanceof Error ? e.message : e), c();
1618
- }
1619
- }
1620
- return {
1621
- checkText: d,
1622
- checkImage: f
1623
- };
1624
- }
1625
- //#endregion
1626
- //#region src/utils/compressImage.ts
1627
- async function zt(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
1628
- return new Promise((r, i) => {
1629
- let a = new Image(), o = URL.createObjectURL(e);
1630
- a.onload = () => {
1631
- URL.revokeObjectURL(o);
1632
- let e = Math.min(1, t / a.naturalWidth), s = document.createElement("canvas");
1633
- s.width = Math.round(a.naturalWidth * e), s.height = Math.round(a.naturalHeight * e);
1634
- let c = s.getContext("2d");
1635
- if (!c) {
1636
- i(/* @__PURE__ */ Error("Canvas를 사용할 수 없습니다"));
1637
- return;
1638
- }
1639
- c.drawImage(a, 0, 0, s.width, s.height), s.toBlob((e) => e ? r(e) : i(/* @__PURE__ */ Error("압축 실패")), "image/jpeg", n);
1640
- }, a.onerror = () => {
1641
- URL.revokeObjectURL(o), i(/* @__PURE__ */ Error("이미지 로드 실패"));
1642
- }, a.src = o;
1643
- });
1644
- }
1645
- //#endregion
1646
- //#region src/composables/useMemiBoardStorage.ts
1647
- var Bt = 5242880, Vt = 26214400;
1648
- function Ht(e) {
1649
- let t = (e.type || "").toLowerCase(), n = e.name.toLowerCase();
1650
- return t === "image/heic" || t === "image/heif" || n.endsWith(".heic") || n.endsWith(".heif");
1651
- }
1652
- async function Ut(t, n = .85) {
1653
- let { default: r } = await import("./heic2any-BXb_U1zK.js").then((t) => /* @__PURE__ */ e(t.default, 1)), i = await r({
1654
- blob: t,
1655
- toType: "image/jpeg",
1656
- quality: n
1657
- }), a = Array.isArray(i) ? i[0] : i;
1658
- if (!(a instanceof Blob)) throw Error("HEIC 변환 결과가 비어 있습니다.");
1659
- return a;
1660
- }
1661
- async function Wt(e) {
1662
- if (e.size <= 5242880) return e;
1663
- let t = await zt(e, {
1664
- maxWidth: 2560,
1665
- quality: .85
1666
- });
1667
- if (t.size > 5242880 && (t = await zt(e, {
1668
- maxWidth: 2048,
1669
- quality: .75
1670
- })), t.size > 5242880) throw Error("압축 후에도 이미지가 5MB를 초과합니다. 더 작은 이미지를 선택해 주세요.");
1671
- return t;
1672
- }
1673
- async function Gt(e) {
1674
- if (e.size > 26214400) throw Error(`원본 이미지는 ${Vt / 1024 / 1024}MB 이하여야 합니다.`);
1675
- if (Ht(e)) try {
1676
- return {
1677
- blob: await Wt(await Ut(e, .85)),
1678
- compressed: !0
1679
- };
1680
- } catch (t) {
1681
- if (t instanceof Error && t.message.includes("압축 후에도")) throw t;
1682
- try {
1683
- return {
1684
- blob: await Wt(await zt(e, {
1685
- maxWidth: 2560,
1686
- quality: .85
1687
- })),
1688
- compressed: !0
1689
- };
1690
- } catch {
1691
- throw console.error("[memi-board] HEIC convert failed", t), Error("HEIC 사진을 변환하지 못했습니다. 잠시 후 다시 시도하거나 JPG로 저장해 올려 주세요.");
1670
+ return console.warn("[memi-board] moderation AI image failed", e instanceof Error ? e.message : e), l();
1692
1671
  }
1693
1672
  }
1694
- if (e.size <= 5242880) return {
1695
- blob: e,
1696
- compressed: !1
1697
- };
1698
- try {
1699
- return {
1700
- blob: await Wt(e),
1701
- compressed: !0
1702
- };
1703
- } catch (e) {
1704
- throw e instanceof Error && e.message.includes("압축 후에도") ? e : Error("이 이미지 형식은 브라우저에서 최적화할 수 없습니다. JPG, PNG 또는 WebP로 변환해 주세요.");
1705
- }
1706
- }
1707
- function Kt() {
1708
- let e = () => Z(), t = N();
1709
- function n(n, r, i, a) {
1710
- let o = U(t), s = `${X(e(), r, i)}/attachments/${Date.now()}-${Je(n.name)}`, c = W(o, s), l = ie(c, n, {
1711
- contentType: n.type || "application/octet-stream",
1712
- contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(n.name)}`
1713
- });
1714
- return {
1715
- promise: new Promise((e, t) => {
1716
- l.on("state_changed", (e) => {
1717
- a && e.totalBytes > 0 && a(e.bytesTransferred / e.totalBytes);
1718
- }, t, async () => {
1719
- let t = await H(c);
1720
- e({
1721
- name: n.name,
1722
- url: t,
1723
- path: s,
1724
- size: n.size,
1725
- type: n.type
1726
- });
1727
- });
1728
- }),
1729
- cancel: () => l.cancel()
1730
- };
1731
- }
1732
- async function r(e) {
1733
- let n = U(t);
1734
- await V(W(n, e.path)).catch(() => {});
1735
- }
1736
- async function i(n, r, i) {
1737
- if (!n.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
1738
- let a = await Gt(n), o = U(t), s = (a.compressed ? "jpg" : n.name.split(".").pop() || n.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", c = Je(n.name.replace(/\.[^.]+$/, "") || "image", 40), l = `${Date.now()}-${c || "image"}`, u = i || `new-${Date.now()}`, d = X(e(), r, u), f = `${d}/images/${l}.${s}`, p = W(o, f);
1739
- await re(p, a.blob, { contentType: a.blob.type || "image/jpeg" });
1740
- let m = await H(p), h = await zt(a.blob, {
1741
- maxWidth: 400,
1742
- quality: .8
1743
- }), g = `${d}/images/thumbnails/${l}.jpg`, _ = W(o, g);
1744
- return await re(_, h, { contentType: "image/jpeg" }), {
1745
- originalUrl: m,
1746
- originalPath: f,
1747
- thumbnailUrl: await H(_),
1748
- thumbnailPath: g
1749
- };
1750
- }
1751
- async function a(e) {
1752
- let n = U(t);
1753
- await Promise.all([V(W(n, e.originalPath)).catch(() => {}), V(W(n, e.thumbnailPath)).catch(() => {})]);
1754
- }
1755
1673
  return {
1756
- uploadAttachment: n,
1757
- deleteAttachment: r,
1758
- uploadEditorImage: i,
1759
- deleteEditorImage: a
1674
+ checkText: f,
1675
+ checkImage: p
1760
1676
  };
1761
1677
  }
1762
1678
  //#endregion
1763
1679
  //#region src/composables/useMemiBoardUsers.ts
1764
- var qt = [
1680
+ var Ht = [
1765
1681
  {
1766
1682
  value: "admin",
1767
1683
  label: "관리자",
@@ -1778,24 +1694,72 @@ var qt = [
1778
1694
  description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
1779
1695
  }
1780
1696
  ];
1781
- function Jt(e = {}) {
1782
- let t = () => Z(), r = F(), i = n(() => o(e.enabled) ?? !0 ? ge(r, t()) : null), a = A(i, { ssrKey: ye(t(), "users-management") });
1783
- async function s(e, n) {
1784
- if (!qt.some((e) => e.value === n)) throw Error("지원하지 않는 역할입니다.");
1785
- await D(_e(r, t(), e), {
1786
- role: n,
1787
- updatedAt: w()
1697
+ function Ut(e = {}) {
1698
+ let t = () => m(), n = Z(), r = O(() => M(e.enabled) ?? !0 ? p(n, t()) : null), i = re(r, { ssrKey: d(t(), "users-management") });
1699
+ async function a(e, r) {
1700
+ if (!Ht.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
1701
+ await J(b(n, t(), e), {
1702
+ role: r,
1703
+ updatedAt: G()
1788
1704
  });
1789
1705
  }
1790
1706
  return {
1791
- users: a,
1792
- usersPending: a.pending,
1793
- updateUserRole: s
1707
+ users: i,
1708
+ usersPending: i.pending,
1709
+ updateUserRole: a
1710
+ };
1711
+ }
1712
+ //#endregion
1713
+ //#region src/composables/useMemiBoardUserProfile.ts
1714
+ function Wt(e) {
1715
+ let t = Z(), n = () => m(), r = O(() => M(e)), i = O(() => {
1716
+ let e = r.value;
1717
+ return e ? b(t, n(), e) : null;
1718
+ }), { data: a, pending: o } = ae(i);
1719
+ return {
1720
+ profile: a,
1721
+ profilePending: o
1794
1722
  };
1795
1723
  }
1796
1724
  //#endregion
1797
1725
  //#region src/data/versionHistory.ts
1798
- var Yt = [
1726
+ var Gt = [
1727
+ {
1728
+ version: "0.27.0",
1729
+ date: "2026-08-12",
1730
+ highlights: [
1731
+ "일반 목록 디자인을 다듬었어요 — 사진이 있는 글은 제목과 작성자 사이에 동그란 미리보기로 보여요",
1732
+ "조밀 보기를 커뮤니티 게시판처럼 한 줄로 다시 짰어요 — 시간·작성자·조회·추천·게시판을 한 줄에, 사진 있으면 오른쪽에 작은 미리보기가 붙어요",
1733
+ "두 줄 이상으로 쓴 글은 목록 요약에서도 줄바꿈이 그대로 보여요"
1734
+ ]
1735
+ },
1736
+ {
1737
+ version: "0.26.0",
1738
+ date: "2026-08-12",
1739
+ highlights: ["다른 사람 글을 보다가 그 사람에 대해 나만 보는 메모를 남길 수 있어요 — 좋음/나쁨 표시도 같이 남겨요", "메모는 작성자 이름 옆 아이콘을 눌러서 쓰고, 다른 사람은(관리자 포함) 절대 못 봐요"]
1740
+ },
1741
+ {
1742
+ version: "0.25.0",
1743
+ date: "2026-08-12",
1744
+ highlights: ["\"작성글 보기\"처럼 \"작성한 댓글 보기\"도 생겼어요 — 여러 게시판에 흩어진 내 댓글을 한 곳에서 모아 봐요"],
1745
+ note: "이 기능을 쓰려면 호스트가 Firestore 보안 규칙을 최신 예시로 갱신해야 해요."
1746
+ },
1747
+ {
1748
+ version: "0.24.0",
1749
+ date: "2026-08-11",
1750
+ highlights: ["사진 올리는 코드를 분리해서, 게시판이 더 안정적으로 빌드되고 열려요"]
1751
+ },
1752
+ {
1753
+ version: "0.23.0",
1754
+ date: "2026-08-11",
1755
+ highlights: [
1756
+ "글·댓글·좋아요를 게시판 아래 중첩시키지 않고 memiBoardPosts/memiBoardComments/memiBoardLikes로 따로 모아요 — 보드 ID는 각 글의 필드로 들어가요",
1757
+ "작성자별 글 모음이 더 가벼워졌어요 — 여러 게시판을 뒤지는 대신 한 번에 찾아요",
1758
+ "\"내가 좋아요한 글\" 같은 기능을 나중에 붙이기 쉬워졌어요",
1759
+ "날짜에 마우스를 올려야 보이던 작성 시각을 이제 눌러서 볼 수 있어요 — 폰에서도 똑같이 동작해요"
1760
+ ],
1761
+ note: "기존 게시판 데이터와 호환되지 않아요 — 새로 설치하는 것처럼 다시 시작해야 해요."
1762
+ },
1799
1763
  {
1800
1764
  version: "0.21.1",
1801
1765
  date: "2026-08-07",
@@ -1928,7 +1892,7 @@ var Yt = [
1928
1892
  highlights: ["글쓰기·댓글·첨부·로그인 권한으로 게시판을 바로 시작할 수 있어요", "부적절한 글·댓글은 AI 검열로 막을 수 있어요"],
1929
1893
  note: "실제 보안은 호스트 Firebase 규칙 등록이 필요합니다."
1930
1894
  }
1931
- ], Xt = /* @__PURE__ */ t(((e, t) => {
1895
+ ], Kt = /* @__PURE__ */ r(((e, t) => {
1932
1896
  (function(n, r) {
1933
1897
  typeof e == "object" && t !== void 0 ? t.exports = r() : typeof define == "function" && define.amd ? define(r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs = r();
1934
1898
  })(e, (function() {
@@ -2222,7 +2186,7 @@ var Yt = [
2222
2186
  return w(1e3 * e);
2223
2187
  }, w.en = b[y], w.Ls = b, w.p = {}, w;
2224
2188
  }));
2225
- })), Zt = /* @__PURE__ */ t(((e, t) => {
2189
+ })), qt = /* @__PURE__ */ r(((e, t) => {
2226
2190
  (function(n, r) {
2227
2191
  typeof e == "object" && t !== void 0 ? t.exports = r() : typeof define == "function" && define.amd ? define(r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs_plugin_relativeTime = r();
2228
2192
  })(e, (function() {
@@ -2326,9 +2290,9 @@ var Yt = [
2326
2290
  };
2327
2291
  };
2328
2292
  }));
2329
- })), Qt = /* @__PURE__ */ t(((e, t) => {
2293
+ })), Jt = /* @__PURE__ */ r(((e, t) => {
2330
2294
  (function(n, r) {
2331
- typeof e == "object" && t !== void 0 ? t.exports = r(Xt()) : typeof define == "function" && define.amd ? define(["dayjs"], r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs_locale_ko = r(n.dayjs);
2295
+ typeof e == "object" && t !== void 0 ? t.exports = r(Kt()) : typeof define == "function" && define.amd ? define(["dayjs"], r) : (n = typeof globalThis < "u" ? globalThis : n || self).dayjs_locale_ko = r(n.dayjs);
2332
2296
  })(e, (function(e) {
2333
2297
  function t(e) {
2334
2298
  return e && typeof e == "object" && "default" in e ? e : { default: e };
@@ -2376,31 +2340,31 @@ var Yt = [
2376
2340
  };
2377
2341
  return n.default.locale(r, null, !0), r;
2378
2342
  }));
2379
- })), $t = /* @__PURE__ */ e(Xt(), 1), en = /* @__PURE__ */ e(Zt(), 1);
2380
- Qt(), $t.default.extend(en.default), $t.default.locale("ko");
2381
- function tn(e) {
2343
+ })), Yt = /* @__PURE__ */ h(Kt(), 1), Xt = /* @__PURE__ */ h(qt(), 1);
2344
+ Jt(), Yt.default.extend(Xt.default), Yt.default.locale("ko");
2345
+ function Zt(e) {
2382
2346
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
2383
2347
  }
2384
- function nn(e, t = Date.now()) {
2385
- return e?.toDate ? (0, $t.default)(e.toDate()).from((0, $t.default)(t)) : "방금 전";
2348
+ function Qt(e, t = Date.now()) {
2349
+ return e?.toDate ? (0, Yt.default)(e.toDate()).from((0, Yt.default)(t)) : "방금 전";
2386
2350
  }
2387
- function rn(e) {
2388
- return e?.toDate ? (0, $t.default)(e.toDate()).format("YYYY년 M월 D일 HH:mm:ss") : "시간 확인 중";
2351
+ function $t(e) {
2352
+ return e?.toDate ? (0, Yt.default)(e.toDate()).format("YYYY년 M월 D일 HH:mm:ss") : "시간 확인 중";
2389
2353
  }
2390
- function an(e, t) {
2391
- let n = rn(e), r = e?.toMillis?.(), i = t?.toMillis?.();
2392
- return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${rn(t)}`];
2354
+ function en(e, t) {
2355
+ let n = $t(e), r = e?.toMillis?.(), i = t?.toMillis?.();
2356
+ return i == null || r == null || r === i ? [n] : [`생성시간: ${n}`, `수정시간: ${$t(t)}`];
2393
2357
  }
2394
2358
  //#endregion
2395
2359
  //#region src/utils/renderMarkdown.ts
2396
2360
  function $(e) {
2397
2361
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2398
2362
  }
2399
- function on(e) {
2363
+ function tn(e) {
2400
2364
  let t = e;
2401
2365
  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;
2402
2366
  }
2403
- function sn(e) {
2367
+ function nn(e) {
2404
2368
  if (!e?.trim()) return "";
2405
2369
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
2406
2370
  function c() {
@@ -2409,7 +2373,7 @@ function sn(e) {
2409
2373
  return;
2410
2374
  }
2411
2375
  let e = o;
2412
- n.push(`<${e}>${s.map((e) => `<li>${on(e)}</li>`).join("")}</${e}>`), o = null, s = [];
2376
+ n.push(`<${e}>${s.map((e) => `<li>${tn(e)}</li>`).join("")}</${e}>`), o = null, s = [];
2413
2377
  }
2414
2378
  for (; r < t.length;) {
2415
2379
  let e = t[r] ?? "", l = e.match(/^```(\w*)\s*$/);
@@ -2429,14 +2393,14 @@ function sn(e) {
2429
2393
  if (u) {
2430
2394
  c();
2431
2395
  let e = u[1].length;
2432
- n.push(`<h${e} class="font-semibold mt-4 mb-2">${on($(u[2]))}</h${e}>`), r++;
2396
+ n.push(`<h${e} class="font-semibold mt-4 mb-2">${tn($(u[2]))}</h${e}>`), r++;
2433
2397
  continue;
2434
2398
  }
2435
2399
  if (e.startsWith("> ")) {
2436
2400
  c();
2437
2401
  let e = [];
2438
2402
  for (; r < t.length && (t[r] ?? "").startsWith("> ");) e.push($((t[r] ?? "").slice(2))), r++;
2439
- n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${on(e.join("<br />"))}</blockquote>`);
2403
+ n.push(`<blockquote class="border-l-4 border-default pl-3 text-muted my-3">${tn(e.join("<br />"))}</blockquote>`);
2440
2404
  continue;
2441
2405
  }
2442
2406
  let d = e.match(/^[-*+]\s+(.+)$/);
@@ -2460,11 +2424,11 @@ function sn(e) {
2460
2424
  if (!e.trim() || /^(#{1,3}\s|```|[-*+]\s|\d+\.\s|> |(-{3,}|\*{3,})\s*$)/.test(e)) break;
2461
2425
  p.push($(e)), r++;
2462
2426
  }
2463
- n.push(`<p class="my-2 leading-relaxed">${on(p.join("<br />"))}</p>`);
2427
+ n.push(`<p class="my-2 leading-relaxed">${tn(p.join("<br />"))}</p>`);
2464
2428
  }
2465
2429
  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");
2466
2430
  }
2467
2431
  //#endregion
2468
- export { qt as BOARD_USER_ROLES, wt as COMMENT_BODY_MAX_LENGTH, Q as DEFAULT_BLOCK_BAN_DECAY_MS, Re as DEFAULT_BLOCK_BAN_THRESHOLD, st as DEFAULT_BOARDS, le as DEFAULT_BOARDS_COLLECTION, ct as DEFAULT_CATEGORIES, ue as DEFAULT_USERS_COLLECTION, Bt as EDITOR_IMAGE_MAX_BYTES, Vt as EDITOR_IMAGE_SOURCE_MAX_BYTES, we as asHttpUrl, pe as boardDoc, Ae as boardListOgDescription, ke as boardListOgTitle, me as boardPostBodyDoc, Y as boardPostCommentDoc, J as boardPostCommentsCol, q as boardPostDoc, he as boardPostLikeDoc, Oe as boardPostOgDescription, De as boardPostOgTitle, X as boardPostStorageFolder, K as boardPostsCol, G as boardSettingsDoc, ye as boardSsrKey, ve as boardStorageRoot, _e as boardUserDoc, ge as boardUsersCol, fe as boardsCol, ot as buildPostPreview, zt as compressImage, Se as configureMemiBoard, ze as effectiveModerationBlockCount, Ye as extractEditorImageUrls, Ie as fetchPublicListForSeo, Fe as fetchPublicPostForSeo, tn as formatDate, rn as formatFullDate, nn as formatRelativeDate, He as formatRestrictedUntilLabel, an as formatTimestampDetails, tt as hasBodyImage, et as hasBodyText, nt as isContentEmpty, mt as isFirestorePermissionDenied, Ve as isModerationWriteRestricted, Be as moderationWriteRestrictedUntilMs, je as normalizeBasePath, Qe as plainTextFromHtml, Ze as postNamespaceFromStoragePath, sn as renderMarkdownToHtml, de as resolveBoardPathConfig, Me as resolvePublicSeoDb, qe as slugify, Xe as storagePathFromDownloadUrl, $e as titleFromBody, Ee as toAbsoluteUrl, Te as toBoardOgImageUrl, Z as useBoardPathConfig, Ke as useMemiBoardAuth, Et as useMemiBoardComments, Ce as useMemiBoardConfig, Ot as useMemiBoardLikes, Rt as useMemiBoardModeration, yt as useMemiBoardPost, vt as useMemiBoardPostList, gt as useMemiBoardPosts, Dt as useMemiBoardReplies, dt as useMemiBoardSettings, Kt as useMemiBoardStorage, xt as useMemiBoardUserPosts, Jt as useMemiBoardUsers, kt as useMemiBoardViews, Yt as versionHistory, at as videoListCoverUrl, it as youtubeId };
2432
+ export { St as AUTHOR_MEMO_MAX_LENGTH, Ht as BOARD_USER_ROLES, Dt as COMMENT_BODY_MAX_LENGTH, Re as DEFAULT_BLOCK_BAN_DECAY_MS, Le as DEFAULT_BLOCK_BAN_THRESHOLD, at as DEFAULT_BOARDS, ot as DEFAULT_CATEGORIES, y as DEFAULT_COMMENTS_COLLECTION, S as DEFAULT_LIKES_COLLECTION, l as DEFAULT_POSTS_COLLECTION, g as DEFAULT_SETTINGS_COLLECTION, C as DEFAULT_USERS_COLLECTION, Ce as asHttpUrl, ke as boardListOgDescription, Oe as boardListOgTitle, De as boardPostOgDescription, Ee as boardPostOgTitle, d as boardSsrKey, b as boardUserDoc, p as boardUsersCol, it as buildPostPreview, f as commentDoc, o as commentsCol, x as configureMemiBoard, ze as effectiveModerationBlockCount, qe as extractEditorImageUrls, Fe as fetchPublicListForSeo, Pe as fetchPublicPostForSeo, Zt as formatDate, $t as formatFullDate, Qt as formatRelativeDate, He as formatRestrictedUntilLabel, en as formatTimestampDetails, $e as hasBodyImage, Qe as hasBodyText, et as isContentEmpty, ft as isFirestorePermissionDenied, Ve as isModerationWriteRestricted, w as likeDoc, D as likeDocId, c as likesCol, Be as moderationWriteRestrictedUntilMs, Ae as normalizeBasePath, Xe as plainTextFromHtml, E as postBodyDoc, i as postDoc, Ye as postNamespaceFromStoragePath, e as postStorageFolder, T as postsCol, nn as renderMarkdownToHtml, a as resolveBoardPathConfig, je as resolvePublicSeoDb, n as settingsCol, t as settingsDoc, v as slugify, Je as storagePathFromDownloadUrl, Ze as titleFromBody, Te as toAbsoluteUrl, we as toBoardOgImageUrl, m as useBoardPathConfig, Ke as useMemiBoardAuth, wt as useMemiBoardAuthorMemo, kt as useMemiBoardComments, s as useMemiBoardConfig, jt as useMemiBoardLikes, Vt as useMemiBoardModeration, _t as useMemiBoardPost, gt as useMemiBoardPostList, mt as useMemiBoardPosts, At as useMemiBoardReplies, lt as useMemiBoardSettings, xt as useMemiBoardUserComments, yt as useMemiBoardUserPosts, Wt as useMemiBoardUserProfile, Ut as useMemiBoardUsers, Mt as useMemiBoardViews, Gt as versionHistory, rt as videoListCoverUrl, nt as youtubeId };
2469
2433
 
2470
2434
  //# sourceMappingURL=index.js.map