memi-board 0.5.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { computed as e, reactive as t, ref as n, watch as r } from "vue";
2
- import { useCollection as i, useCurrentUser as a, useFirebaseApp as o, useFirebaseAuth as s, useFirestore as c } from "vuefire";
3
- import { collection as l, deleteDoc as u, deleteField as d, doc as f, endBefore as p, getDoc as m, getDocs as h, increment as g, limit as _, limitToLast as v, orderBy as y, query as b, serverTimestamp as x, setDoc as S, startAfter as C, updateDoc as w, where as T, writeBatch as E } from "firebase/firestore";
4
- import { GoogleAuthProvider as D, OAuthProvider as ee, createUserWithEmailAndPassword as O, signInWithEmailAndPassword as te, signInWithPopup as k, signOut as ne, updateProfile as re } from "firebase/auth";
5
- import { deleteObject as A, getDownloadURL as j, getStorage as M, listAll as N, ref as P, uploadBytes as ie, uploadBytesResumable as F } from "firebase/storage";
6
- import { GoogleAIBackend as ae, Schema as I, getAI as oe, getGenerativeModel as se } from "firebase/ai";
1
+ import { computed as e, onScopeDispose as t, reactive as n, ref as r, watch as i } from "vue";
2
+ import { useCollection as a, useCurrentUser as o, useFirebaseApp as s, useFirebaseAuth as c, useFirestore as l } from "vuefire";
3
+ import { collection as u, deleteDoc as d, deleteField as f, doc as p, documentId as m, endBefore as h, getDoc as g, getDocs as _, increment as v, limit as y, limitToLast as b, onSnapshot as x, orderBy as S, query 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 { GoogleAuthProvider as A, OAuthProvider as j, createUserWithEmailAndPassword as M, signInWithEmailAndPassword as N, signInWithPopup as P, signOut as ee, updateProfile as te } from "firebase/auth";
5
+ import { deleteObject as F, getDownloadURL as I, getStorage as L, listAll as ne, ref as R, uploadBytes as z, uploadBytesResumable as re } from "firebase/storage";
6
+ import { GoogleAIBackend as ie, Schema as B, getAI as ae, getGenerativeModel as oe } from "firebase/ai";
7
7
  //#region src/config.ts
8
- var L = "__MEMI_BOARD_CONFIG__";
9
- function R() {
8
+ var V = "__MEMI_BOARD_CONFIG__";
9
+ function H() {
10
10
  let e = globalThis;
11
- return e[L] || (e[L] = t({
11
+ return e[V] || (e[V] = n({
12
12
  collectionPrefix: "memiBoard",
13
13
  auth: { providers: ["google", "apple"] },
14
14
  moderation: {
@@ -20,19 +20,19 @@ function R() {
20
20
  blockBanThreshold: 3,
21
21
  blockBanDecayMs: 864e5
22
22
  }
23
- })), e[L];
23
+ })), e[V];
24
24
  }
25
- function ce(e) {
26
- let t = R();
25
+ function se(e) {
26
+ let t = H();
27
27
  e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
28
28
  }
29
- function z() {
30
- return R();
29
+ function U() {
30
+ return H();
31
31
  }
32
32
  //#endregion
33
33
  //#region src/utils/moderation-strike.ts
34
- var le = 3, B = 864e5;
35
- function V(e, t, n = Date.now(), r = B) {
34
+ var ce = 3, W = 864e5;
35
+ function G(e, t, n = Date.now(), r = W) {
36
36
  let i = Math.max(0, Math.floor(Number(e) || 0));
37
37
  if (i <= 0) return 0;
38
38
  let a = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -40,7 +40,7 @@ function V(e, t, n = Date.now(), r = B) {
40
40
  let o = Math.floor((n - a) / r);
41
41
  return Math.max(0, i - o);
42
42
  }
43
- function H(e, t, n = Date.now(), r = 3, i = B) {
43
+ function K(e, t, n = Date.now(), r = 3, i = W) {
44
44
  let a = Math.max(0, Math.floor(Number(e) || 0));
45
45
  if (a < r) return null;
46
46
  let o = typeof t == "number" && Number.isFinite(t) ? t : null;
@@ -48,17 +48,17 @@ function H(e, t, n = Date.now(), r = 3, i = B) {
48
48
  let s = o + (a - (r - 1)) * i;
49
49
  return s > n ? s : null;
50
50
  }
51
- function U(e, t, n = Date.now(), r = 3, i = B) {
52
- return H(e, t, n, r, i) != null;
51
+ function q(e, t, n = Date.now(), r = 3, i = W) {
52
+ return K(e, t, n, r, i) != null;
53
53
  }
54
- function W(e, t = Date.now()) {
54
+ function J(e, t = Date.now()) {
55
55
  let n = Math.max(0, e - t), r = Math.ceil(n / 36e5);
56
56
  if (r <= 0) return "곧 해제";
57
57
  if (r < 24) return `약 ${r}시간 후 해제`;
58
58
  let i = Math.floor(r / 24), a = r % 24;
59
59
  return a === 0 ? `약 ${i}일 후 해제` : `약 ${i}일 ${a}시간 후 해제`;
60
60
  }
61
- function ue(e) {
61
+ function le(e) {
62
62
  if (e == null) return null;
63
63
  if (typeof e == "number" && Number.isFinite(e)) return e;
64
64
  if (typeof e == "object" && e && "toMillis" in e && typeof e.toMillis == "function") return e.toMillis();
@@ -70,168 +70,178 @@ function ue(e) {
70
70
  }
71
71
  //#endregion
72
72
  //#region src/composables/useMemiBoardAuth.ts
73
- var G = "__MEMI_BOARD_AUTH_STATE__";
74
- function de() {
73
+ var Y = "__MEMI_BOARD_AUTH_STATE__";
74
+ function ue() {
75
75
  let e = globalThis;
76
- return e[G] || (e[G] = {
77
- role: n("user"),
78
- rolePending: n(!0),
79
- rawBlockCount: n(0),
80
- blockAtMs: n(null),
76
+ return e[Y] || (e[Y] = {
77
+ role: r("user"),
78
+ rolePending: r(!0),
79
+ rawBlockCount: r(0),
80
+ blockAtMs: r(null),
81
81
  watchStarted: !1
82
- }), e[G];
82
+ }), e[Y];
83
83
  }
84
- function K() {
85
- let t = z(), n = de(), i = c(), o = s(), l = a(), { role: u, rolePending: d, rawBlockCount: p, blockAtMs: h } = n;
86
- function _() {
84
+ function X() {
85
+ let t = U(), n = ue(), r = l(), a = c(), s = o(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: m } = n;
86
+ function h() {
87
87
  let e = t.moderation?.blockBanThreshold;
88
88
  return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
89
89
  }
90
- function v() {
90
+ function _() {
91
91
  let e = t.moderation?.blockBanDecayMs;
92
- return typeof e == "number" && e > 0 ? e : B;
92
+ return typeof e == "number" && e > 0 ? e : W;
93
93
  }
94
94
  function y(e) {
95
- return f(i, `${t.collectionPrefix}Users`, e);
95
+ return p(r, `${t.collectionPrefix}Users`, e);
96
96
  }
97
97
  function b(e) {
98
98
  if (!e) {
99
- p.value = 0, h.value = null;
99
+ f.value = 0, m.value = null;
100
100
  return;
101
101
  }
102
- p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
102
+ f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), m.value = le(e.moderationBlockAt);
103
103
  }
104
- async function C(e) {
105
- let t = y(e.uid), n = await m(t);
106
- if (!n.exists()) return await S(t, {
104
+ async function x(e) {
105
+ let t = y(e.uid), n = await g(t);
106
+ if (!n.exists()) return await T(t, {
107
107
  role: "user",
108
108
  displayName: e.displayName,
109
- updatedAt: x()
109
+ email: e.email,
110
+ photoURL: e.photoURL,
111
+ updatedAt: w()
110
112
  }), b({ moderationBlockCount: 0 }), "user";
111
113
  let r = n.data();
112
- return b(r), await w(t, {
114
+ return b(r), await D(t, {
113
115
  displayName: e.displayName,
114
- updatedAt: x()
115
- }), r.role ?? "user";
116
+ email: e.email,
117
+ photoURL: e.photoURL,
118
+ updatedAt: w()
119
+ }), [
120
+ "admin",
121
+ "staff",
122
+ "user"
123
+ ].includes(String(r.role)) ? r.role : "user";
116
124
  }
117
- async function T() {
118
- let e = l.value?.uid;
125
+ async function S() {
126
+ let e = s.value?.uid;
119
127
  if (!e) {
120
128
  b(void 0);
121
129
  return;
122
130
  }
123
- let t = await m(y(e));
131
+ let t = await g(y(e));
124
132
  b(t.exists() ? t.data() : void 0);
125
133
  }
126
- n.watchStarted || (n.watchStarted = !0, r(l, async (e) => {
134
+ n.watchStarted || (n.watchStarted = !0, i(s, async (e) => {
127
135
  if (d.value = !0, !e) {
128
136
  u.value = "user", b(void 0), d.value = !1;
129
137
  return;
130
138
  }
131
139
  try {
132
- u.value = await C(e);
140
+ u.value = await x(e);
133
141
  } catch {
134
142
  u.value = "user";
135
143
  } finally {
136
144
  d.value = !1;
137
145
  }
138
146
  }, { immediate: !0 }));
139
- let E = e(() => !!l.value), A = e(() => u.value === "admin"), j = e(() => V(p.value, h.value, Date.now(), v())), M = e(() => {
140
- let e = _();
141
- return e <= 0 ? !1 : U(p.value, h.value, Date.now(), e, v());
142
- }), N = e(() => {
143
- let e = _();
147
+ let C = e(() => !!s.value), E = e(() => u.value === "admin"), O = e(() => u.value === "staff"), k = e(() => E.value || O.value), F = e(() => G(f.value, m.value, Date.now(), _())), I = e(() => {
148
+ let e = h();
149
+ return e <= 0 ? !1 : q(f.value, m.value, Date.now(), e, _());
150
+ }), L = e(() => {
151
+ let e = h();
144
152
  if (e <= 0) return null;
145
- let t = H(p.value, h.value, Date.now(), e, v());
146
- return t == null ? null : W(t);
147
- }), P = e(() => {
148
- if (!M.value) return null;
149
- let e = N.value;
153
+ let t = K(f.value, m.value, Date.now(), e, _());
154
+ return t == null ? null : J(t);
155
+ }), ne = e(() => {
156
+ if (!I.value) return null;
157
+ let e = L.value;
150
158
  return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
151
159
  });
152
- async function ie() {
153
- let e = l.value?.uid, t = _();
160
+ async function R() {
161
+ let e = s.value?.uid, t = h();
154
162
  if (!e || t <= 0) return {
155
163
  effectiveCount: 0,
156
164
  restricted: !1,
157
165
  messageSuffix: ""
158
166
  };
159
- let n = y(e), r = await m(n);
160
- r.exists() ? await w(n, {
161
- moderationBlockCount: g(1),
162
- moderationBlockAt: x(),
163
- updatedAt: x()
164
- }) : await S(n, {
167
+ let n = y(e), r = await g(n);
168
+ r.exists() ? await D(n, {
169
+ moderationBlockCount: v(1),
170
+ moderationBlockAt: w(),
171
+ updatedAt: w()
172
+ }) : await T(n, {
165
173
  role: "user",
166
- displayName: l.value?.displayName ?? null,
174
+ displayName: s.value?.displayName ?? null,
167
175
  moderationBlockCount: 1,
168
- moderationBlockAt: x(),
169
- updatedAt: x()
176
+ moderationBlockAt: w(),
177
+ updatedAt: w()
170
178
  });
171
179
  let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
172
- p.value = i, h.value = a;
173
- let o = V(i, a, a, v()), s = U(i, a, a, t, v()), c = ` (경고 ${o}/${t})`;
174
- if (s) {
175
- let e = H(i, a, a, t, v()), n = e == null ? null : W(e, a);
176
- c = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
180
+ f.value = i, m.value = a;
181
+ let o = G(i, a, a, _()), c = q(i, a, a, t, _()), l = ` (경고 ${o}/${t})`;
182
+ if (c) {
183
+ let e = K(i, a, a, t, _()), n = e == null ? null : J(e, a);
184
+ l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
177
185
  }
178
186
  return {
179
187
  effectiveCount: o,
180
- restricted: s,
181
- messageSuffix: c
188
+ restricted: c,
189
+ messageSuffix: l
182
190
  };
183
191
  }
184
- function F() {
185
- if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
186
- return o;
192
+ function z() {
193
+ if (!a) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
194
+ return a;
187
195
  }
188
- async function ae() {
189
- return k(F(), new D());
196
+ async function re() {
197
+ return P(z(), new A());
190
198
  }
191
- async function I() {
192
- let e = new ee("apple.com");
193
- return e.addScope("email"), e.addScope("name"), k(F(), e);
199
+ async function ie() {
200
+ let e = new j("apple.com");
201
+ return e.addScope("email"), e.addScope("name"), P(z(), e);
194
202
  }
195
- async function oe(e, t) {
196
- return te(F(), e, t);
203
+ async function B(e, t) {
204
+ return N(z(), e, t);
197
205
  }
198
- async function se(e, t, n) {
199
- let r = await O(F(), e, t);
200
- return n && await re(r.user, { displayName: n }), r;
206
+ async function ae(e, t, n) {
207
+ let r = await M(z(), e, t);
208
+ return n && await te(r.user, { displayName: n }), r;
201
209
  }
202
- async function L() {
203
- await ne(F());
210
+ async function oe() {
211
+ await ee(z());
204
212
  }
205
- function R(e) {
206
- return E.value && (l.value?.uid === e.authorUid || A.value);
213
+ function V(e) {
214
+ return C.value && (s.value?.uid === e.authorUid || k.value);
207
215
  }
208
- function ce(e) {
209
- return E.value && (l.value?.uid === e.authorUid || A.value);
216
+ function H(e) {
217
+ return C.value && (s.value?.uid === e.authorUid || k.value);
210
218
  }
211
219
  return {
212
- user: l,
213
- isSignedIn: E,
214
- isAdmin: A,
220
+ user: s,
221
+ isSignedIn: C,
222
+ isAdmin: E,
223
+ isStaff: O,
224
+ canManageContent: k,
215
225
  rolePending: d,
216
- moderationBlockCount: j,
217
- isWriteRestricted: M,
218
- restrictedUntilLabel: N,
219
- restrictedMessage: P,
220
- signInWithGoogle: ae,
221
- signInWithApple: I,
222
- signInWithEmail: oe,
223
- signUpWithEmail: se,
224
- signOut: L,
225
- canEdit: R,
226
- canDelete: R,
227
- canDeleteComment: ce,
228
- recordContentModerationBlock: ie,
229
- refreshBoardUser: T
226
+ moderationBlockCount: F,
227
+ isWriteRestricted: I,
228
+ restrictedUntilLabel: L,
229
+ restrictedMessage: ne,
230
+ signInWithGoogle: re,
231
+ signInWithApple: ie,
232
+ signInWithEmail: B,
233
+ signUpWithEmail: ae,
234
+ signOut: oe,
235
+ canEdit: V,
236
+ canDelete: V,
237
+ canDeleteComment: H,
238
+ recordContentModerationBlock: R,
239
+ refreshBoardUser: S
230
240
  };
231
241
  }
232
242
  //#endregion
233
243
  //#region src/utils/slugify.ts
234
- function q(e) {
244
+ function de(e) {
235
245
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
236
246
  }
237
247
  function fe(e, t = 60) {
@@ -256,25 +266,25 @@ function me(e) {
256
266
  return null;
257
267
  }
258
268
  }
259
- function J(e) {
269
+ function he(e) {
260
270
  return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
261
271
  }
262
272
  //#endregion
263
273
  //#region src/utils/postPreview.ts
264
- function Y(e) {
274
+ function ge(e) {
265
275
  return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
266
276
  }
267
- function he(e) {
277
+ function _e(e) {
268
278
  try {
269
- let t = new URL(Y(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
279
+ let t = new URL(ge(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
270
280
  if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
271
281
  if (n.endsWith("youtube.com") || n === "youtube-nocookie.com") return t.pathname.startsWith("/embed/") || t.pathname.startsWith("/shorts/") ? t.pathname.split("/")[2] : t.searchParams.get("v") || void 0;
272
282
  } catch {
273
283
  return;
274
284
  }
275
285
  }
276
- function ge(e, t = []) {
277
- let n = e.match(/<img\b[^>]*\bsrc=["']([^"']+)["']/i), r = e.match(/<iframe\b[^>]*\bsrc=["']([^"']+)["']/i), i = t.find((e) => e.type.startsWith("image/"))?.url, a = t.find((e) => e.type.startsWith("video/"))?.url, o = r ? Y(r[1]) : a, s = o ? he(o) : void 0, c = n ? Y(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
286
+ function ve(e, t = []) {
287
+ 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 ? ge(r[1]) : a, s = o ? _e(o) : void 0, c = n ? ge(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
278
288
  return {
279
289
  summary: e.replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/\s+/g, " ").trim().slice(0, 160),
280
290
  ...c ? { previewImage: c } : {},
@@ -283,26 +293,26 @@ function ge(e, t = []) {
283
293
  }
284
294
  //#endregion
285
295
  //#region src/composables/useMemiBoardPosts.ts
286
- async function X(e) {
287
- let t = await N(e).catch(() => ({
296
+ async function ye(e) {
297
+ let t = await ne(e).catch(() => ({
288
298
  items: [],
289
299
  prefixes: []
290
300
  }));
291
- await Promise.all([...t.items.map((e) => A(e)), ...t.prefixes.map((e) => X(e))]);
301
+ await Promise.all([...t.items.map((e) => F(e)), ...t.prefixes.map((e) => ye(e))]);
292
302
  }
293
- async function _e(e, t) {
303
+ async function be(e, t) {
294
304
  let n = [...new Set([...t].filter(Boolean))];
295
- await Promise.all(n.map((t) => A(P(e, t)).catch(() => {})));
305
+ await Promise.all(n.map((t) => F(R(e, t)).catch(() => {})));
296
306
  }
297
- function ve() {
298
- let e = z(), t = c(), n = o(), r = () => e.collectionPrefix, i = () => l(t, `${r()}Posts`), a = (e) => f(t, `${r()}Posts`, e), s = (e) => f(t, `${r()}Posts`, e, "body", "main"), g = (e) => l(t, `${r()}Posts`, e, "comments");
299
- function D() {
300
- return f(i()).id;
307
+ function xe() {
308
+ let e = U(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
309
+ function m() {
310
+ return p(i()).id;
301
311
  }
302
- async function ee(e = {}) {
312
+ async function v(e = {}) {
303
313
  let t = e.pageSize ?? 20, n = [];
304
- e.category && n.push(T("category", "==", e.category)), n.push(y("createdAt", "desc")), e.cursor && n.push(C(e.cursor)), n.push(_(t + 1));
305
- let r = await h(b(i(), ...n)), a = r.docs.slice(0, t);
314
+ e.category && n.push(O("category", "==", e.category)), n.push(S("createdAt", "desc")), e.cursor && n.push(E(e.cursor)), n.push(y(t + 1));
315
+ let r = await _(C(i(), ...n)), a = r.docs.slice(0, t);
306
316
  return {
307
317
  posts: a.map((e) => ({
308
318
  id: e.id,
@@ -312,24 +322,24 @@ function ve() {
312
322
  hasMore: r.docs.length > t
313
323
  };
314
324
  }
315
- async function O(e) {
316
- let [t, n] = await Promise.all([m(a(e)), m(s(e))]);
325
+ async function x(e) {
326
+ let [t, n] = await Promise.all([g(a(e)), g(o(e))]);
317
327
  return t.exists() ? {
318
328
  id: t.id,
319
329
  ...t.data(),
320
330
  content: n.exists() ? n.data().content ?? "" : ""
321
331
  } : null;
322
332
  }
323
- async function te(e, t) {
324
- let n = (await h(b(i(), T("category", "==", e), T("slug", "==", t), _(1)))).docs[0];
325
- return n ? O(n.id) : null;
333
+ async function A(e, t) {
334
+ let n = (await _(C(i(), O("category", "==", e), O("slug", "==", t), y(1)))).docs[0];
335
+ return n ? x(n.id) : null;
326
336
  }
327
- async function k(e) {
337
+ async function j(e) {
328
338
  if (!e.category || !e.createdAt) return {
329
339
  previous: null,
330
340
  next: null
331
341
  };
332
- let t = [T("category", "==", e.category), y("createdAt", "desc")], [n, r] = await Promise.all([h(b(i(), ...t, C(e.createdAt), _(1))), h(b(i(), ...t, p(e.createdAt), v(1)))]), a = (e) => {
342
+ let t = [O("category", "==", e.category), S("createdAt", "desc")], [n, r] = await Promise.all([_(C(i(), ...t, E(e.createdAt), y(1))), _(C(i(), ...t, h(e.createdAt), b(1)))]), a = (e) => {
333
343
  let t = e.docs[0];
334
344
  return t ? {
335
345
  id: t.id,
@@ -341,11 +351,11 @@ function ve() {
341
351
  next: a(r)
342
352
  };
343
353
  }
344
- async function ne(e) {
345
- let t = q(e.title) || "post", n = t, r = 1;
346
- for (; !(await h(b(i(), T("category", "==", e.category || ""), T("slug", "==", n), _(1)))).empty;) r++, n = `${t}-${r}`;
347
- let o = e.postId || D(), c = ge(e.content, e.attachments);
348
- return await S(a(o), {
354
+ async function M(e) {
355
+ let t = de(e.title) || "post", n = t, r = 1;
356
+ for (; !(await _(C(i(), O("category", "==", e.category || ""), O("slug", "==", n), y(1)))).empty;) r++, n = `${t}-${r}`;
357
+ let s = e.postId || m(), c = ve(e.content, e.attachments);
358
+ return await T(a(s), {
349
359
  slug: n,
350
360
  title: e.title,
351
361
  ...c,
@@ -358,92 +368,249 @@ function ve() {
358
368
  authorPhoto: e.authorPhoto,
359
369
  moderationStatus: "approved",
360
370
  moderationModel: e.moderationModel ?? null,
361
- createdAt: x(),
362
- updatedAt: x()
363
- }), await S(s(o), { content: e.content }), n;
371
+ createdAt: w(),
372
+ updatedAt: w()
373
+ }), await T(o(s), { content: e.content }), n;
364
374
  }
365
- async function re(e, t) {
366
- let n = ge(t.content, t.attachments);
367
- await Promise.all([w(a(e), {
375
+ async function N(e, t) {
376
+ let n = ve(t.content, t.attachments);
377
+ await Promise.all([D(a(e), {
368
378
  title: t.title,
369
379
  ...n,
370
- previewImage: n.previewImage ? n.previewImage : d(),
371
- videoUrl: n.videoUrl ? n.videoUrl : d(),
380
+ previewImage: n.previewImage ? n.previewImage : f(),
381
+ videoUrl: n.videoUrl ? n.videoUrl : f(),
372
382
  tags: t.tags ?? [],
373
- category: t.category ? t.category : d(),
383
+ category: t.category ? t.category : f(),
374
384
  attachments: t.attachments ?? [],
375
- updatedAt: x()
376
- }), S(s(e), { content: t.content }, { merge: !0 })]);
385
+ updatedAt: w()
386
+ }), T(o(e), { content: t.content }, { merge: !0 })]);
377
387
  }
378
- async function A(e) {
379
- let [i, o] = await Promise.all([m(a(e)), m(s(e))]);
388
+ async function P(e) {
389
+ let [i, s] = await Promise.all([g(a(e)), g(o(e))]);
380
390
  if (!i.exists()) return;
381
- let c = i.data(), l = o.exists() ? String(o.data().content ?? "") : "", d = await h(g(e));
382
- for (let e = 0; e < d.docs.length; e += 450) {
383
- let n = E(t);
384
- d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
391
+ let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await _(c(e));
392
+ for (let e = 0; e < f.docs.length; e += 450) {
393
+ let n = k(t);
394
+ f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
385
395
  }
386
- let f = M(n), p = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
387
- for (let t of c.attachments ?? []) if (t?.path) {
388
- p.add(t.path);
389
- let n = J(t.path);
390
- n && n !== e && _.add(n);
396
+ let p = L(n), m = /* @__PURE__ */ new Set(), h = /* @__PURE__ */ new Set();
397
+ for (let t of l.attachments ?? []) if (t?.path) {
398
+ m.add(t.path);
399
+ let n = he(t.path);
400
+ n && n !== e && h.add(n);
391
401
  }
392
- for (let t of pe(l)) {
402
+ for (let t of pe(u)) {
393
403
  let n = me(t);
394
404
  if (!n) continue;
395
- p.add(n);
405
+ m.add(n);
396
406
  let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
397
- r !== n && r.includes("/images/thumbnails/") && p.add(r);
407
+ r !== n && r.includes("/images/thumbnails/") && m.add(r);
398
408
  let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
399
- i && !n.includes("/thumbnails/") && p.add(`${i[1]}thumbnails/${i[2]}.jpg`);
400
- let a = J(n);
401
- a && a !== e && _.add(a);
409
+ i && !n.includes("/thumbnails/") && m.add(`${i[1]}thumbnails/${i[2]}.jpg`);
410
+ let a = he(n);
411
+ a && a !== e && h.add(a);
402
412
  }
403
- await u(s(e)), await X(P(f, `${r()}/posts/${e}`));
404
- for (let e of _) await X(P(f, `${r()}/posts/${e}`));
405
- await _e(f, p), await u(a(e));
413
+ await d(o(e)), await ye(R(p, `${r()}/posts/${e}`));
414
+ for (let e of h) await ye(R(p, `${r()}/posts/${e}`));
415
+ await be(p, m), await d(a(e));
406
416
  }
407
417
  return {
408
- createPostId: D,
409
- getPosts: ee,
410
- getPost: O,
411
- getPostBySlug: te,
412
- getAdjacentPosts: k,
413
- createPost: ne,
414
- updatePost: re,
415
- deletePost: A
418
+ createPostId: m,
419
+ getPosts: v,
420
+ getPost: x,
421
+ getPostBySlug: A,
422
+ getAdjacentPosts: j,
423
+ createPost: M,
424
+ updatePost: N,
425
+ deletePost: P
416
426
  };
417
427
  }
418
428
  //#endregion
419
429
  //#region src/composables/useMemiBoardComments.ts
420
- function ye(t) {
421
- let n = z(), r = c(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => b(l(r, `${a()}Posts`, o.value, "comments"), y("createdAt", "asc"))), u = i(s);
422
- async function d(e) {
423
- let t = E(r), n = a(), i = f(l(r, `${n}Posts`, o.value, "comments"));
430
+ var Se = 10, Ce = 5;
431
+ function we(n, a = {}) {
432
+ let o = U(), s = l(), c = () => o.collectionPrefix, d = e(() => typeof n == "string" ? n : n.value), f = r([]), h = r([]), g = r(a.subscribe !== !1), b = r([]), T = r(!1), D = r(a.subscribe !== !1), A = r(a.subscribe !== !1), j = null, M, N;
433
+ function P(e) {
434
+ return e.createdAt?.toMillis?.() ?? 0;
435
+ }
436
+ let ee = e(() => {
437
+ let e = /* @__PURE__ */ new Map();
438
+ for (let t of [
439
+ ...f.value,
440
+ ...h.value,
441
+ ...b.value
442
+ ]) t.id && e.set(t.id, t);
443
+ return [...e.values()].sort((e, t) => P(e) - P(t) || String(e.id).localeCompare(String(t.id)));
444
+ });
445
+ i(d, async (e) => {
446
+ if (b.value = [], f.value = [], h.value = [], j = null, clearTimeout(M), M = void 0, N?.(), N = void 0, D.value = a.subscribe !== !1, a.subscribe !== !1) {
447
+ g.value = !0, A.value = !0;
448
+ try {
449
+ let t = _(C(u(s, `${c()}Posts`, e, "comments"), S("createdAt", "desc"), S(m(), "desc")));
450
+ if (await Promise.all([t.then((t) => {
451
+ if (d.value !== e) return;
452
+ let n = t.docs.map((e) => ({
453
+ id: e.id,
454
+ ...e.data()
455
+ }));
456
+ h.value = n.filter((e) => e.parentId === void 0);
457
+ }), F()]), d.value !== e) return;
458
+ } finally {
459
+ d.value === e && (g.value = !1, A.value = !1);
460
+ }
461
+ }
462
+ }, { immediate: !0 });
463
+ function te() {
464
+ N?.(), N = x(C(u(s, `${c()}Posts`, d.value, "comments"), O("parentId", "==", null), S("createdAt", "desc"), S(m(), "desc"), y(1)), (e) => {
465
+ let t = new Map(f.value.map((e) => [e.id, e]));
466
+ for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
467
+ id: n.doc.id,
468
+ ...n.doc.data()
469
+ });
470
+ f.value = [...t.values()];
471
+ }, (e) => {
472
+ console.error("[memi-board:comments] onSnapshot failed", e);
473
+ });
474
+ }
475
+ async function F() {
476
+ if (!(T.value || !D.value)) {
477
+ T.value = !0;
478
+ try {
479
+ let e = [
480
+ O("parentId", "==", null),
481
+ S("createdAt", "asc"),
482
+ S(m(), "asc"),
483
+ ...j ? [E(j)] : [],
484
+ y(11)
485
+ ], t = await _(C(u(s, `${c()}Posts`, d.value, "comments"), ...e)), n = t.docs.slice(0, Se), r = n.map((e) => ({
486
+ id: e.id,
487
+ ...e.data()
488
+ })), i = new Set(b.value.map((e) => e.id));
489
+ b.value.push(...r.filter((e) => !i.has(e.id))), j = n.at(-1) ?? j, t.size <= Se && (D.value = !1, M = setTimeout(() => {
490
+ M = void 0, te();
491
+ }, 1e3));
492
+ } finally {
493
+ T.value = !1;
494
+ }
495
+ }
496
+ }
497
+ t(() => {
498
+ clearTimeout(M), N?.();
499
+ });
500
+ async function I(e) {
501
+ let t = k(s), n = c(), r = p(u(s, `${n}Posts`, d.value, "comments"));
502
+ t.set(r, {
503
+ postId: d.value,
504
+ body: e.body,
505
+ authorUid: e.authorUid,
506
+ authorName: e.authorName,
507
+ authorPhoto: e.authorPhoto,
508
+ moderationStatus: "approved",
509
+ parentId: null,
510
+ rootId: r.id,
511
+ depth: 0,
512
+ replyToUid: null,
513
+ replyToName: null,
514
+ replyCount: 0,
515
+ isReply: !1,
516
+ createdAt: w()
517
+ }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(1) }), await t.commit();
518
+ }
519
+ async function L(e) {
520
+ if (!e.parent.id) throw Error("답글 대상 댓글을 찾을 수 없습니다.");
521
+ let t = k(s), n = c(), r = u(s, `${n}Posts`, d.value, "comments"), i = p(r), a = e.parent.parentId == null ? e.parent.id : e.parent.rootId;
522
+ if (!a) throw Error("댓글 스레드를 찾을 수 없습니다.");
523
+ let o = Math.min((e.parent.depth ?? 0) + 1, 2);
424
524
  t.set(i, {
425
- postId: o.value,
525
+ postId: d.value,
426
526
  body: e.body,
427
527
  authorUid: e.authorUid,
428
528
  authorName: e.authorName,
429
529
  authorPhoto: e.authorPhoto,
430
530
  moderationStatus: "approved",
431
- createdAt: x()
432
- }), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
531
+ parentId: e.parent.id,
532
+ rootId: a,
533
+ depth: o,
534
+ replyToUid: e.parent.authorUid,
535
+ replyToName: e.parent.authorName,
536
+ replyCount: 0,
537
+ isReply: !0,
538
+ createdAt: w()
539
+ }), t.update(p(r, a), { replyCount: v(1) }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(1) }), await t.commit();
433
540
  }
434
- async function p(e) {
435
- let t = E(r), n = a();
436
- t.delete(f(r, `${n}Posts`, o.value, "comments", e)), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
541
+ async function ne(e) {
542
+ if (!e.id) return;
543
+ if (e.parentId == null && (e.replyCount ?? 0) > 0) throw Error("답글이 있는 댓글은 현재 삭제할 수 없습니다.");
544
+ let t = k(s), n = c();
545
+ t.delete(p(s, `${n}Posts`, d.value, "comments", e.id)), e.parentId && e.rootId && t.update(p(s, `${n}Posts`, d.value, "comments", e.rootId), { replyCount: v(-1) }), t.update(p(s, `${n}Posts`, d.value), { commentCount: v(-1) }), await t.commit(), b.value = b.value.filter((t) => t.id !== e.id), h.value = h.value.filter((t) => t.id !== e.id), f.value = f.value.filter((t) => t.id !== e.id);
437
546
  }
438
547
  return {
439
- comments: u,
440
- addComment: d,
441
- deleteComment: p
548
+ comments: ee,
549
+ commentsPending: e(() => A.value || g.value),
550
+ hasMore: D,
551
+ loadingMore: T,
552
+ loadMore: F,
553
+ addComment: I,
554
+ addReply: L,
555
+ deleteComment: ne
556
+ };
557
+ }
558
+ function Te(e, n) {
559
+ let i = U(), a = l(), o = r([]), s = r(!1), c = r(!1), d = r(!0), f = null, p;
560
+ function h(e) {
561
+ return e.sort((e, t) => (e.createdAt?.toMillis?.() ?? 0) - (t.createdAt?.toMillis?.() ?? 0) || String(e.id).localeCompare(String(t.id)));
562
+ }
563
+ function g() {
564
+ p ||= x(C(u(a, `${i.collectionPrefix}Posts`, e, "comments"), O("rootId", "==", n), O("isReply", "==", !0), S("createdAt", "desc"), S(m(), "desc"), y(1)), (e) => {
565
+ let t = new Map(o.value.map((e) => [e.id, e]));
566
+ for (let n of e.docChanges()) n.type !== "removed" && t.set(n.doc.id, {
567
+ id: n.doc.id,
568
+ ...n.doc.data()
569
+ });
570
+ o.value = h([...t.values()]);
571
+ }, (e) => {
572
+ console.error("[memi-board:replies] onSnapshot failed", e);
573
+ });
574
+ }
575
+ async function v() {
576
+ if (!(s.value || !d.value)) {
577
+ s.value = !0;
578
+ try {
579
+ let t = [
580
+ O("rootId", "==", n),
581
+ O("isReply", "==", !0),
582
+ S("createdAt", "asc"),
583
+ S(m(), "asc"),
584
+ ...f ? [E(f)] : [],
585
+ y(6)
586
+ ], r = await _(C(u(a, `${i.collectionPrefix}Posts`, e, "comments"), ...t)), s = new Set(o.value.map((e) => e.id)), l = r.docs.slice(0, Ce), p = l.map((e) => ({
587
+ id: e.id,
588
+ ...e.data()
589
+ })).filter((e) => !s.has(e.id));
590
+ o.value = h([...o.value, ...p]), f = l.at(-1) ?? f, c.value = !0, r.size <= Ce && (d.value = !1, g());
591
+ } finally {
592
+ s.value = !1;
593
+ }
594
+ }
595
+ }
596
+ function b() {
597
+ p?.(), p = void 0, o.value = [], f = null, c.value = !1, d.value = !0;
598
+ }
599
+ async function w() {
600
+ b(), await v();
601
+ }
602
+ return t(() => p?.()), {
603
+ replies: o,
604
+ loading: s,
605
+ loaded: c,
606
+ hasMore: d,
607
+ loadMore: v,
608
+ refresh: w
442
609
  };
443
610
  }
444
611
  //#endregion
445
612
  //#region src/utils/moderation-prompt.ts
446
- var be = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", xe = {
613
+ var Ee = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", De = {
447
614
  abuse: "욕설·비속어·모욕",
448
615
  spam: "스팸·광고",
449
616
  adult: "선정적·성인 내용",
@@ -458,10 +625,10 @@ function Z(e) {
458
625
  }
459
626
  let t = (e.reason || "").trim();
460
627
  if (t) return t;
461
- let n = e.category && e.category !== "none" ? xe[e.category] || e.category : "";
628
+ let n = e.category && e.category !== "none" ? De[e.category] || e.category : "";
462
629
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
463
630
  }
464
- var Se = [
631
+ var Oe = [
465
632
  "씨발",
466
633
  "시발",
467
634
  "시이발",
@@ -487,10 +654,10 @@ var Se = [
487
654
  "bitch",
488
655
  "asshole"
489
656
  ];
490
- function Ce(e) {
657
+ function ke(e) {
491
658
  return new RegExp(e.join("|"), "i");
492
659
  }
493
- var we = /* @__PURE__ */ new Set([
660
+ var Ae = /* @__PURE__ */ new Set([
494
661
  "none",
495
662
  "abuse",
496
663
  "spam",
@@ -498,7 +665,7 @@ var we = /* @__PURE__ */ new Set([
498
665
  "violence",
499
666
  "other"
500
667
  ]);
501
- function Te(e) {
668
+ function je(e) {
502
669
  let t = e.trim();
503
670
  if (!t) return null;
504
671
  let n;
@@ -516,7 +683,7 @@ function Te(e) {
516
683
  if (typeof n != "object" || !n) return null;
517
684
  let r = n;
518
685
  if (typeof r.flagged != "boolean") return null;
519
- let i = typeof r.category == "string" && we.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
686
+ let i = typeof r.category == "string" && Ae.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
520
687
  return {
521
688
  flagged: r.flagged,
522
689
  category: i,
@@ -525,15 +692,15 @@ function Te(e) {
525
692
  }
526
693
  //#endregion
527
694
  //#region src/composables/useMemiBoardModeration.ts
528
- var Ee = {
695
+ var Me = {
529
696
  flagged: !1,
530
697
  category: "none",
531
698
  reason: "",
532
699
  via: "empty"
533
700
  };
534
- function De() {
535
- let e = z().moderation ?? {}, t = o(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = K(), a = Ce([...Se, ...e.localBlocklist ?? []]);
536
- function s(e) {
701
+ function Ne() {
702
+ let e = U().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = X(), a = ke([...Oe, ...e.localBlocklist ?? []]);
703
+ function o(e) {
537
704
  let t = e.match(a);
538
705
  return t ? {
539
706
  flagged: !0,
@@ -547,21 +714,21 @@ function De() {
547
714
  } : null;
548
715
  }
549
716
  function c() {
550
- let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = oe(t, {
551
- backend: new ae(),
717
+ let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ae(t, {
718
+ backend: new ie(),
552
719
  useLimitedUseAppCheckTokens: r
553
720
  });
554
- return se(i, {
721
+ return oe(i, {
555
722
  model: n,
556
- systemInstruction: be,
723
+ systemInstruction: Ee,
557
724
  generationConfig: {
558
725
  temperature: 0,
559
726
  maxOutputTokens: 256,
560
727
  responseMimeType: "application/json",
561
- responseSchema: I.object({ properties: {
562
- flagged: I.boolean(),
563
- category: I.string(),
564
- reason: I.string()
728
+ responseSchema: B.object({ properties: {
729
+ flagged: B.boolean(),
730
+ category: B.string(),
731
+ reason: B.string()
565
732
  } })
566
733
  }
567
734
  });
@@ -610,17 +777,17 @@ function De() {
610
777
  };
611
778
  let i = t.trim();
612
779
  if (!i) return {
613
- ...Ee,
780
+ ...Me,
614
781
  via: "empty"
615
782
  };
616
- let a = s(i);
783
+ let a = o(i);
617
784
  if (a) return d(a);
618
785
  if (e.enabled === !1) return {
619
- ...Ee,
786
+ ...Me,
620
787
  via: "disabled"
621
788
  };
622
789
  try {
623
- let e = Te((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
790
+ let e = je((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
624
791
  if (!e) return l();
625
792
  let t = {
626
793
  flagged: e.flagged,
@@ -645,14 +812,14 @@ function De() {
645
812
  via: "restricted"
646
813
  };
647
814
  if (e.enabled === !1 || !e.moderateImages) return {
648
- ...Ee,
815
+ ...Me,
649
816
  via: "disabled"
650
817
  };
651
818
  try {
652
819
  let e = await new Promise((e, n) => {
653
820
  let r = new FileReader();
654
821
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
655
- }), n = Te((await c().generateContent([{ inlineData: {
822
+ }), n = je((await c().generateContent([{ inlineData: {
656
823
  mimeType: t.type || "image/jpeg",
657
824
  data: e
658
825
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -679,7 +846,7 @@ function De() {
679
846
  }
680
847
  //#endregion
681
848
  //#region src/utils/compressImage.ts
682
- async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
849
+ async function Pe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
683
850
  return new Promise((r, i) => {
684
851
  let a = new Image(), o = URL.createObjectURL(e);
685
852
  a.onload = () => {
@@ -699,11 +866,11 @@ async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
699
866
  }
700
867
  //#endregion
701
868
  //#region src/composables/useMemiBoardStorage.ts
702
- var ke = 5242880;
703
- function Ae() {
704
- let e = z(), t = o(), n = () => e.collectionPrefix;
869
+ var Fe = 5242880;
870
+ function Ie() {
871
+ let e = U(), t = s(), n = () => e.collectionPrefix;
705
872
  function r(e, r, i) {
706
- let a = M(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = P(a, o), c = F(s, e, {
873
+ let a = L(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = R(a, o), c = re(s, e, {
707
874
  contentType: e.type || "application/octet-stream",
708
875
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
709
876
  });
@@ -712,7 +879,7 @@ function Ae() {
712
879
  c.on("state_changed", (e) => {
713
880
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
714
881
  }, n, async () => {
715
- let n = await j(s);
882
+ let n = await I(s);
716
883
  t({
717
884
  name: e.name,
718
885
  url: n,
@@ -726,130 +893,199 @@ function Ae() {
726
893
  };
727
894
  }
728
895
  async function i(e) {
729
- let n = M(t);
730
- await A(P(n, e.path)).catch(() => {});
896
+ let n = L(t);
897
+ await F(R(n, e.path)).catch(() => {});
731
898
  }
732
899
  async function a(e, r) {
733
900
  if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
734
- if (e.size > 5242880) throw Error(`이미지는 ${ke / 1024 / 1024}MB 이하여야 합니다.`);
735
- let i = M(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = P(i, l);
736
- await ie(u, e, { contentType: e.type || "image/png" });
737
- let d = await j(u), f = await Oe(e, {
901
+ if (e.size > 5242880) throw Error(`이미지는 ${Fe / 1024 / 1024}MB 이하여야 합니다.`);
902
+ let i = L(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = R(i, l);
903
+ await z(u, e, { contentType: e.type || "image/png" });
904
+ let d = await I(u), f = await Pe(e, {
738
905
  maxWidth: 400,
739
906
  quality: .8
740
- }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = P(i, p);
741
- return await ie(m, f, { contentType: "image/jpeg" }), {
907
+ }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = R(i, p);
908
+ return await z(m, f, { contentType: "image/jpeg" }), {
742
909
  originalUrl: d,
743
910
  originalPath: l,
744
- thumbnailUrl: await j(m),
911
+ thumbnailUrl: await I(m),
745
912
  thumbnailPath: p
746
913
  };
747
914
  }
748
- async function s(e) {
749
- let n = M(t);
750
- await Promise.all([A(P(n, e.originalPath)).catch(() => {}), A(P(n, e.thumbnailPath)).catch(() => {})]);
915
+ async function o(e) {
916
+ let n = L(t);
917
+ await Promise.all([F(R(n, e.originalPath)).catch(() => {}), F(R(n, e.thumbnailPath)).catch(() => {})]);
751
918
  }
752
919
  return {
753
920
  uploadAttachment: r,
754
921
  deleteAttachment: i,
755
922
  uploadEditorImage: a,
756
- deleteEditorImage: s
923
+ deleteEditorImage: o
757
924
  };
758
925
  }
759
926
  //#endregion
760
927
  //#region src/composables/useMemiBoardSettings.ts
761
- var je = [
928
+ var Le = [
762
929
  {
763
930
  id: "free",
764
931
  label: "자유",
765
932
  listView: "default",
933
+ writeRole: "user",
766
934
  order: 0
767
935
  },
768
936
  {
769
937
  id: "notice",
770
938
  label: "공지",
771
939
  listView: "default",
940
+ writeRole: "admin",
772
941
  order: 1
773
942
  },
774
943
  {
775
944
  id: "question",
776
945
  label: "질문",
777
946
  listView: "default",
947
+ writeRole: "user",
778
948
  order: 2
779
949
  }
780
950
  ];
781
- function Me() {
782
- let t = z(), n = c(), { isSignedIn: r } = K(), a = e(() => f(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => l(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => b(o.value, y("order", "asc"))), { data: d, pending: p } = i(s), m = e(() => d.value.map((e, t) => ({
951
+ function Re() {
952
+ let t = U(), n = l(), { isSignedIn: r } = X(), i = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), o = e(() => u(n, `${t.collectionPrefix}Settings`, "config", "categories")), s = e(() => C(o.value, S("order", "asc"))), { data: c, pending: f } = a(s, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), m = e(() => c.value.map((e, t) => ({
783
953
  ...e,
784
954
  id: e.id,
785
955
  order: typeof e.order == "number" ? e.order : t,
786
- listView: e.listView ?? "default"
956
+ listView: e.listView ?? "default",
957
+ writeRole: e.writeRole ?? "user"
787
958
  })));
788
959
  function h(e) {
789
960
  if (e) return m.value.find((t) => t.id === e)?.label ?? e;
790
961
  }
791
962
  async function g(e, t = e.order ?? 0) {
792
963
  if (!r.value) throw Error("로그인이 필요합니다.");
793
- let n = e.id.trim(), i = e.label.trim();
794
- if (!n || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
795
- await S(a.value, { updatedAt: x() }, { merge: !0 }), await S(f(o.value, n), {
796
- label: i,
964
+ let n = e.id.trim(), a = e.label.trim();
965
+ if (!n || !a) throw Error("카테고리 ID와 이름을 입력해 주세요.");
966
+ await T(i.value, { updatedAt: w() }, { merge: !0 }), await T(p(o.value, n), {
967
+ label: a,
797
968
  listView: e.listView ?? "default",
969
+ writeRole: e.writeRole ?? "user",
798
970
  order: t,
799
- updatedAt: x()
971
+ updatedAt: w()
800
972
  }, { merge: !0 });
801
973
  }
802
974
  async function _(e) {
803
975
  if (!r.value) throw Error("로그인이 필요합니다.");
804
- let t = E(n);
805
- t.set(a.value, { updatedAt: x() }, { merge: !0 }), e.forEach((e, n) => {
976
+ let t = k(n);
977
+ t.set(i.value, { updatedAt: w() }, { merge: !0 }), e.forEach((e, n) => {
806
978
  let r = e.id.trim(), i = e.label.trim();
807
979
  if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
808
- t.set(f(o.value, r), {
980
+ t.set(p(o.value, r), {
809
981
  label: i,
810
982
  listView: e.listView ?? "default",
983
+ writeRole: e.writeRole ?? "user",
811
984
  order: n,
812
- updatedAt: x()
985
+ updatedAt: w()
813
986
  }, { merge: !0 });
814
987
  }), await t.commit();
815
988
  }
816
989
  async function v(e) {
817
990
  if (!r.value) throw Error("로그인이 필요합니다.");
818
- await u(f(o.value, e));
991
+ await d(p(o.value, e));
819
992
  }
820
- async function C() {
993
+ async function y() {
821
994
  return Promise.resolve();
822
995
  }
823
- async function w(e) {
996
+ async function b(e) {
824
997
  let t = e.trim();
825
998
  if (!t) throw Error("카테고리 이름을 입력해 주세요.");
826
999
  if (!r.value) throw Error("로그인이 필요합니다.");
827
1000
  let n = m.value.find((e) => e.label === t);
828
1001
  if (n) return n.id;
829
- let i = q(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
1002
+ let i = de(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
830
1003
  for (; s.has(a);) a = `${i}-${o++}`;
831
1004
  return await g({
832
1005
  id: a,
833
1006
  label: t,
834
1007
  listView: "default",
1008
+ writeRole: "user",
835
1009
  order: m.value.length
836
1010
  }), a;
837
1011
  }
838
1012
  return {
839
1013
  categories: m,
840
1014
  categoryLabel: h,
841
- settingsPending: p,
842
- ensureSettings: C,
843
- addCategory: w,
1015
+ settingsPending: f,
1016
+ ensureSettings: y,
1017
+ addCategory: b,
844
1018
  saveCategory: g,
845
1019
  saveCategories: _,
846
1020
  deleteCategory: v,
847
- DEFAULT_CATEGORIES: je
1021
+ DEFAULT_CATEGORIES: Le
1022
+ };
1023
+ }
1024
+ //#endregion
1025
+ //#region src/composables/useMemiBoardUsers.ts
1026
+ var ze = [
1027
+ {
1028
+ value: "admin",
1029
+ label: "관리자",
1030
+ description: "사용자 역할, 게시판 설정과 모든 콘텐츠를 관리합니다."
1031
+ },
1032
+ {
1033
+ value: "staff",
1034
+ label: "스태프",
1035
+ description: "모든 게시글과 댓글을 관리하지만 사용자 역할과 게시판 설정은 바꿀 수 없습니다."
1036
+ },
1037
+ {
1038
+ value: "user",
1039
+ label: "사용자",
1040
+ description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
1041
+ }
1042
+ ];
1043
+ function Be() {
1044
+ let e = U(), t = l(), n = `${e.collectionPrefix}Users`, r = a(u(t, n), { ssrKey: `${n}-management` });
1045
+ async function i(e, r) {
1046
+ if (!ze.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
1047
+ await D(p(t, n, e), {
1048
+ role: r,
1049
+ updatedAt: w()
1050
+ });
1051
+ }
1052
+ return {
1053
+ users: r,
1054
+ usersPending: r.pending,
1055
+ updateUserRole: i
848
1056
  };
849
1057
  }
850
1058
  //#endregion
851
1059
  //#region src/data/versionHistory.ts
852
- var Ne = [
1060
+ var Ve = [
1061
+ {
1062
+ version: "0.9.0",
1063
+ date: "2026-08-04",
1064
+ highlights: [
1065
+ "댓글에 답글을 달고 대화 흐름을 최대 2단계로 볼 수 있어요",
1066
+ "댓글 10개·답글 5개 단위로 이어서 보고 마지막부터 새 댓글을 실시간으로 받아요",
1067
+ "더보기 버튼이 화면에 들어오면 로딩 표시와 함께 다음 댓글을 자동으로 불러와요"
1068
+ ]
1069
+ },
1070
+ {
1071
+ version: "0.8.0",
1072
+ date: "2026-08-04",
1073
+ highlights: [
1074
+ "최근 댓글 5개는 실시간으로 보고 이전 댓글은 5개씩 불러올 수 있어요",
1075
+ "댓글 시간이 상대시간으로 표시되고 전체 시간은 툴팁으로 확인할 수 있어요",
1076
+ "댓글 작성 영역을 대화 흐름의 맨 아래로 옮겼어요"
1077
+ ]
1078
+ },
1079
+ {
1080
+ version: "0.7.0",
1081
+ date: "2026-08-04",
1082
+ highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"]
1083
+ },
1084
+ {
1085
+ version: "0.6.0",
1086
+ date: "2026-08-04",
1087
+ highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"]
1088
+ },
853
1089
  {
854
1090
  version: "0.5.0",
855
1091
  date: "2026-08-04",
@@ -896,7 +1132,7 @@ var Ne = [
896
1132
  ];
897
1133
  //#endregion
898
1134
  //#region src/utils/formatDate.ts
899
- function Pe(e) {
1135
+ function He(e) {
900
1136
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
901
1137
  }
902
1138
  //#endregion
@@ -908,7 +1144,7 @@ function $(e) {
908
1144
  let t = e;
909
1145
  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;
910
1146
  }
911
- function Fe(e) {
1147
+ function Ue(e) {
912
1148
  if (!e?.trim()) return "";
913
1149
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
914
1150
  function c() {
@@ -973,6 +1209,6 @@ function Fe(e) {
973
1209
  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");
974
1210
  }
975
1211
  //#endregion
976
- export { B as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, je as DEFAULT_CATEGORIES, ke as EDITOR_IMAGE_MAX_BYTES, Oe as compressImage, ce as configureMemiBoard, V as effectiveModerationBlockCount, pe as extractEditorImageUrls, Pe as formatDate, W as formatRestrictedUntilLabel, U as isModerationWriteRestricted, H as moderationWriteRestrictedUntilMs, J as postNamespaceFromStoragePath, Fe as renderMarkdownToHtml, q as slugify, me as storagePathFromDownloadUrl, K as useMemiBoardAuth, ye as useMemiBoardComments, z as useMemiBoardConfig, De as useMemiBoardModeration, ve as useMemiBoardPosts, Me as useMemiBoardSettings, Ae as useMemiBoardStorage, Ne as versionHistory };
1212
+ export { ze as BOARD_USER_ROLES, W as DEFAULT_BLOCK_BAN_DECAY_MS, ce as DEFAULT_BLOCK_BAN_THRESHOLD, Le as DEFAULT_CATEGORIES, Fe as EDITOR_IMAGE_MAX_BYTES, Pe as compressImage, se as configureMemiBoard, G as effectiveModerationBlockCount, pe as extractEditorImageUrls, He as formatDate, J as formatRestrictedUntilLabel, q as isModerationWriteRestricted, K as moderationWriteRestrictedUntilMs, he as postNamespaceFromStoragePath, Ue as renderMarkdownToHtml, de as slugify, me as storagePathFromDownloadUrl, X as useMemiBoardAuth, we as useMemiBoardComments, U as useMemiBoardConfig, Ne as useMemiBoardModeration, xe as useMemiBoardPosts, Te as useMemiBoardReplies, Re as useMemiBoardSettings, Ie as useMemiBoardStorage, Be as useMemiBoardUsers, Ve as versionHistory };
977
1213
 
978
1214
  //# sourceMappingURL=index.js.map