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