memi-board 0.4.14 → 0.7.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,9 +1,9 @@
1
1
  import { computed as e, reactive as t, ref as n, watch as r } from "vue";
2
- import { useCollection as i, useCurrentUser as a, useDocument as o, useFirebaseApp as s, useFirebaseAuth as c, useFirestore as l } from "vuefire";
3
- import { collection as u, deleteDoc as d, deleteField as f, doc as p, getDoc as m, getDocs as h, increment as g, limit as _, orderBy as v, query as y, serverTimestamp as b, setDoc as x, startAfter as S, updateDoc as C, where as ee, writeBatch as w } from "firebase/firestore";
4
- import { GoogleAuthProvider as T, OAuthProvider as te, createUserWithEmailAndPassword as ne, signInWithEmailAndPassword as re, signInWithPopup as ie, signOut as ae, updateProfile as oe } from "firebase/auth";
5
- import { deleteObject as E, getDownloadURL as D, getStorage as O, listAll as k, ref as A, uploadBytes as j, uploadBytesResumable as M } from "firebase/storage";
6
- import { GoogleAIBackend as N, Schema as P, getAI as se, getGenerativeModel as ce } from "firebase/ai";
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 ne, signOut as re, updateProfile as ie } from "firebase/auth";
5
+ import { deleteObject as k, getDownloadURL as A, getStorage as j, listAll as ae, ref as M, uploadBytes as N, uploadBytesResumable as oe } from "firebase/storage";
6
+ import { GoogleAIBackend as se, Schema as P, getAI as ce, getGenerativeModel as le } from "firebase/ai";
7
7
  //#region src/config.ts
8
8
  var F = "__MEMI_BOARD_CONFIG__";
9
9
  function I() {
@@ -22,16 +22,16 @@ function I() {
22
22
  }
23
23
  })), e[F];
24
24
  }
25
- function L(e) {
25
+ function ue(e) {
26
26
  let t = I();
27
27
  e.collectionPrefix && (t.collectionPrefix = e.collectionPrefix), e.auth && Object.assign(t.auth, e.auth), e.moderation && Object.assign(t.moderation, e.moderation);
28
28
  }
29
- function R() {
29
+ function L() {
30
30
  return I();
31
31
  }
32
32
  //#endregion
33
33
  //#region src/utils/moderation-strike.ts
34
- var le = 3, z = 864e5;
34
+ var R = 3, z = 864e5;
35
35
  function B(e, t, n = Date.now(), r = z) {
36
36
  let i = Math.max(0, Math.floor(Number(e) || 0));
37
37
  if (i <= 0) return 0;
@@ -58,7 +58,7 @@ function U(e, t = Date.now()) {
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 de(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();
@@ -71,7 +71,7 @@ function ue(e) {
71
71
  //#endregion
72
72
  //#region src/composables/useMemiBoardAuth.ts
73
73
  var W = "__MEMI_BOARD_AUTH_STATE__";
74
- function de() {
74
+ function fe() {
75
75
  let e = globalThis;
76
76
  return e[W] || (e[W] = {
77
77
  role: n("user"),
@@ -82,7 +82,7 @@ function de() {
82
82
  }), e[W];
83
83
  }
84
84
  function G() {
85
- let t = R(), n = de(), i = l(), o = c(), s = a(), { role: u, rolePending: d, rawBlockCount: f, blockAtMs: h } = n;
85
+ let t = L(), n = fe(), i = c(), o = s(), l = a(), { role: u, rolePending: d, rawBlockCount: p, blockAtMs: h } = n;
86
86
  function _() {
87
87
  let e = t.moderation?.blockBanThreshold;
88
88
  return e === 0 ? 0 : typeof e == "number" && e > 0 ? Math.floor(e) : 3;
@@ -92,141 +92,151 @@ function G() {
92
92
  return typeof e == "number" && e > 0 ? e : z;
93
93
  }
94
94
  function y(e) {
95
- return p(i, `${t.collectionPrefix}Users`, e);
95
+ return f(i, `${t.collectionPrefix}Users`, e);
96
96
  }
97
- function S(e) {
97
+ function b(e) {
98
98
  if (!e) {
99
- f.value = 0, h.value = null;
99
+ p.value = 0, h.value = null;
100
100
  return;
101
101
  }
102
- f.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = ue(e.moderationBlockAt);
102
+ p.value = Math.max(0, Math.floor(Number(e.moderationBlockCount) || 0)), h.value = de(e.moderationBlockAt);
103
103
  }
104
- async function ee(e) {
104
+ async function C(e) {
105
105
  let t = y(e.uid), n = await m(t);
106
- if (!n.exists()) return await x(t, {
106
+ if (!n.exists()) return await S(t, {
107
107
  role: "user",
108
108
  displayName: e.displayName,
109
- updatedAt: b()
110
- }), S({ moderationBlockCount: 0 }), "user";
109
+ email: e.email,
110
+ photoURL: e.photoURL,
111
+ updatedAt: x()
112
+ }), b({ moderationBlockCount: 0 }), "user";
111
113
  let r = n.data();
112
- return S(r), await C(t, {
114
+ return b(r), await w(t, {
113
115
  displayName: e.displayName,
114
- updatedAt: b()
115
- }), r.role ?? "user";
116
+ email: e.email,
117
+ photoURL: e.photoURL,
118
+ updatedAt: x()
119
+ }), [
120
+ "admin",
121
+ "staff",
122
+ "user"
123
+ ].includes(String(r.role)) ? r.role : "user";
116
124
  }
117
- async function w() {
118
- let e = s.value?.uid;
125
+ async function T() {
126
+ let e = l.value?.uid;
119
127
  if (!e) {
120
- S(void 0);
128
+ b(void 0);
121
129
  return;
122
130
  }
123
131
  let t = await m(y(e));
124
- S(t.exists() ? t.data() : void 0);
132
+ b(t.exists() ? t.data() : void 0);
125
133
  }
126
- n.watchStarted || (n.watchStarted = !0, r(s, async (e) => {
134
+ n.watchStarted || (n.watchStarted = !0, r(l, async (e) => {
127
135
  if (d.value = !0, !e) {
128
- u.value = "user", S(void 0), d.value = !1;
136
+ u.value = "user", b(void 0), d.value = !1;
129
137
  return;
130
138
  }
131
139
  try {
132
- u.value = await ee(e);
140
+ u.value = await C(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(() => !!s.value), D = e(() => u.value === "admin"), O = e(() => B(f.value, h.value, Date.now(), v())), k = e(() => {
147
+ let E = e(() => !!l.value), k = e(() => u.value === "admin"), A = e(() => u.value === "staff"), j = e(() => k.value || A.value), ae = e(() => B(p.value, h.value, Date.now(), v())), M = e(() => {
140
148
  let e = _();
141
- return e <= 0 ? !1 : H(f.value, h.value, Date.now(), e, v());
142
- }), A = e(() => {
149
+ return e <= 0 ? !1 : H(p.value, h.value, Date.now(), e, v());
150
+ }), N = e(() => {
143
151
  let e = _();
144
152
  if (e <= 0) return null;
145
- let t = V(f.value, h.value, Date.now(), e, v());
153
+ let t = V(p.value, h.value, Date.now(), e, v());
146
154
  return t == null ? null : U(t);
147
- }), j = e(() => {
148
- if (!k.value) return null;
149
- let e = A.value;
155
+ }), oe = e(() => {
156
+ if (!M.value) return null;
157
+ let e = N.value;
150
158
  return e ? `콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요. (${e})` : "콘텐츠 경고가 누적되어 글·댓글 작성이 잠시 제한됐어요.";
151
159
  });
152
- async function M() {
153
- let e = s.value?.uid, t = _();
160
+ async function se() {
161
+ let e = l.value?.uid, t = _();
154
162
  if (!e || t <= 0) return {
155
163
  effectiveCount: 0,
156
164
  restricted: !1,
157
165
  messageSuffix: ""
158
166
  };
159
167
  let n = y(e), r = await m(n);
160
- r.exists() ? await C(n, {
168
+ r.exists() ? await w(n, {
161
169
  moderationBlockCount: g(1),
162
- moderationBlockAt: b(),
163
- updatedAt: b()
164
- }) : await x(n, {
170
+ moderationBlockAt: x(),
171
+ updatedAt: x()
172
+ }) : await S(n, {
165
173
  role: "user",
166
- displayName: s.value?.displayName ?? null,
174
+ displayName: l.value?.displayName ?? null,
167
175
  moderationBlockCount: 1,
168
- moderationBlockAt: b(),
169
- updatedAt: b()
176
+ moderationBlockAt: x(),
177
+ updatedAt: x()
170
178
  });
171
179
  let i = (r.exists() ? Math.max(0, Math.floor(Number(r.data().moderationBlockCount) || 0)) : 0) + 1, a = Date.now();
172
- f.value = i, h.value = a;
173
- let o = B(i, a, a, v()), c = H(i, a, a, t, v()), l = ` (경고 ${o}/${t})`;
174
- if (c) {
180
+ p.value = i, h.value = a;
181
+ let o = B(i, a, a, v()), s = H(i, a, a, t, v()), c = ` (경고 ${o}/${t})`;
182
+ if (s) {
175
183
  let e = V(i, a, a, t, v()), n = e == null ? null : U(e, a);
176
- l = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
184
+ c = n ? ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다 (${n}).` : ` 경고 ${o}회가 되어 글·댓글 작성이 제한됩니다.`;
177
185
  }
178
186
  return {
179
187
  effectiveCount: o,
180
- restricted: c,
181
- messageSuffix: l
188
+ restricted: s,
189
+ messageSuffix: c
182
190
  };
183
191
  }
184
- function N() {
192
+ function P() {
185
193
  if (!o) throw Error("[memi-board] Firebase Auth가 초기화되지 않았습니다.");
186
194
  return o;
187
195
  }
188
- async function P() {
189
- return ie(N(), new T());
196
+ async function ce() {
197
+ return ne(P(), new D());
190
198
  }
191
- async function se() {
192
- let e = new te("apple.com");
193
- return e.addScope("email"), e.addScope("name"), ie(N(), e);
199
+ async function le() {
200
+ let e = new ee("apple.com");
201
+ return e.addScope("email"), e.addScope("name"), ne(P(), e);
194
202
  }
195
- async function ce(e, t) {
196
- return re(N(), e, t);
203
+ async function F(e, t) {
204
+ return te(P(), e, t);
197
205
  }
198
- async function F(e, t, n) {
199
- let r = await ne(N(), e, t);
200
- return n && await oe(r.user, { displayName: n }), r;
206
+ async function I(e, t, n) {
207
+ let r = await O(P(), e, t);
208
+ return n && await ie(r.user, { displayName: n }), r;
201
209
  }
202
- async function I() {
203
- await ae(N());
210
+ async function ue() {
211
+ await re(P());
204
212
  }
205
- function L(e) {
206
- return E.value && (s.value?.uid === e.authorUid || D.value);
213
+ function R(e) {
214
+ return E.value && (l.value?.uid === e.authorUid || j.value);
207
215
  }
208
- function le(e) {
209
- return E.value && (s.value?.uid === e.authorUid || D.value);
216
+ function W(e) {
217
+ return E.value && (l.value?.uid === e.authorUid || j.value);
210
218
  }
211
219
  return {
212
- user: s,
220
+ user: l,
213
221
  isSignedIn: E,
214
- isAdmin: D,
222
+ isAdmin: k,
223
+ isStaff: A,
224
+ canManageContent: j,
215
225
  rolePending: d,
216
- moderationBlockCount: O,
217
- isWriteRestricted: k,
218
- restrictedUntilLabel: A,
219
- restrictedMessage: j,
220
- signInWithGoogle: P,
221
- signInWithApple: se,
222
- signInWithEmail: ce,
223
- signUpWithEmail: F,
224
- signOut: I,
225
- canEdit: L,
226
- canDelete: L,
227
- canDeleteComment: le,
228
- recordContentModerationBlock: M,
229
- refreshBoardUser: w
226
+ moderationBlockCount: ae,
227
+ isWriteRestricted: M,
228
+ restrictedUntilLabel: N,
229
+ restrictedMessage: oe,
230
+ signInWithGoogle: ce,
231
+ signInWithApple: le,
232
+ signInWithEmail: F,
233
+ signUpWithEmail: I,
234
+ signOut: ue,
235
+ canEdit: R,
236
+ canDelete: R,
237
+ canDeleteComment: W,
238
+ recordContentModerationBlock: se,
239
+ refreshBoardUser: T
230
240
  };
231
241
  }
232
242
  //#endregion
@@ -234,12 +244,12 @@ function G() {
234
244
  function K(e) {
235
245
  return e.trim().replace(/\s+/g, "-").replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
236
246
  }
237
- function fe(e, t = 60) {
247
+ function pe(e, t = 60) {
238
248
  return e.replace(/[^a-zA-Z0-9가-힣ㄱ-ㅎ._-]/g, "-").slice(0, t);
239
249
  }
240
250
  //#endregion
241
251
  //#region src/utils/extractEditorImageUrls.ts
242
- function pe(e) {
252
+ function me(e) {
243
253
  if (!e) return [];
244
254
  let t = /* @__PURE__ */ new Set();
245
255
  for (let n of e.matchAll(/!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/g)) n[1] && t.add(n[1].replaceAll("&amp;", "&"));
@@ -248,7 +258,7 @@ function pe(e) {
248
258
  }
249
259
  //#endregion
250
260
  //#region src/utils/storagePath.ts
251
- function me(e) {
261
+ function he(e) {
252
262
  try {
253
263
  let t = e.match(/\/o\/([^?]+)/);
254
264
  return t?.[1] ? decodeURIComponent(t[1]) : null;
@@ -260,24 +270,49 @@ function q(e) {
260
270
  return e.match(/(?:^|\/)posts\/([^/]+)\//)?.[1] ?? null;
261
271
  }
262
272
  //#endregion
273
+ //#region src/utils/postPreview.ts
274
+ function J(e) {
275
+ return e.replace(/&amp;/g, "&").replace(/&quot;/g, "\"").replace(/&#39;/g, "'");
276
+ }
277
+ function ge(e) {
278
+ try {
279
+ let t = new URL(J(e)), n = t.hostname.toLowerCase().replace(/^www\./, "");
280
+ if (n === "youtu.be") return t.pathname.split("/").filter(Boolean)[0];
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;
282
+ } catch {
283
+ return;
284
+ }
285
+ }
286
+ function _e(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 ? J(r[1]) : a, s = o ? ge(o) : void 0, c = n ? J(n[1]) : i || (s ? `https://i.ytimg.com/vi/${s}/hqdefault.jpg` : void 0);
288
+ return {
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),
290
+ ...c ? { previewImage: c } : {},
291
+ ...o ? { videoUrl: o } : {}
292
+ };
293
+ }
294
+ //#endregion
263
295
  //#region src/composables/useMemiBoardPosts.ts
264
- async function J(e) {
265
- let t = await k(e).catch(() => ({
296
+ async function Y(e) {
297
+ let t = await ae(e).catch(() => ({
266
298
  items: [],
267
299
  prefixes: []
268
300
  }));
269
- await Promise.all([...t.items.map((e) => E(e)), ...t.prefixes.map((e) => J(e))]);
301
+ await Promise.all([...t.items.map((e) => k(e)), ...t.prefixes.map((e) => Y(e))]);
270
302
  }
271
- async function he(e, t) {
303
+ async function ve(e, t) {
272
304
  let n = [...new Set([...t].filter(Boolean))];
273
- await Promise.all(n.map((t) => E(A(e, t)).catch(() => {})));
305
+ await Promise.all(n.map((t) => k(M(e, t)).catch(() => {})));
274
306
  }
275
- function ge() {
276
- let e = R(), t = l(), n = s(), r = () => e.collectionPrefix, i = () => u(t, `${r()}Posts`), a = (e) => p(t, `${r()}Posts`, e), o = (e) => p(t, `${r()}Posts`, e, "body", "main"), c = (e) => u(t, `${r()}Posts`, e, "comments");
277
- async function g(e = {}) {
307
+ function ye() {
308
+ let e = L(), 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");
309
+ function D() {
310
+ return f(i()).id;
311
+ }
312
+ async function ee(e = {}) {
278
313
  let t = e.pageSize ?? 20, n = [];
279
- e.category && n.push(ee("category", "==", e.category)), n.push(v("createdAt", "desc")), e.cursor && n.push(S(e.cursor)), n.push(_(t + 1));
280
- let r = await h(y(i(), ...n)), a = r.docs.slice(0, t);
314
+ e.category && n.push(T("category", "==", e.category)), n.push(y("createdAt", "desc")), e.cursor && n.push(C(e.cursor)), n.push(_(t + 1));
315
+ let r = await h(b(i(), ...n)), a = r.docs.slice(0, t);
281
316
  return {
282
317
  posts: a.map((e) => ({
283
318
  id: e.id,
@@ -287,19 +322,43 @@ function ge() {
287
322
  hasMore: r.docs.length > t
288
323
  };
289
324
  }
290
- async function T(e) {
291
- let [t, n] = await Promise.all([m(a(e)), m(o(e))]);
325
+ async function O(e) {
326
+ let [t, n] = await Promise.all([m(a(e)), m(s(e))]);
292
327
  return t.exists() ? {
293
328
  id: t.id,
294
329
  ...t.data(),
295
330
  content: n.exists() ? n.data().content ?? "" : ""
296
331
  } : null;
297
332
  }
298
- async function te(e) {
333
+ async function te(e, t) {
334
+ let n = (await h(b(i(), T("category", "==", e), T("slug", "==", t), _(1)))).docs[0];
335
+ return n ? O(n.id) : null;
336
+ }
337
+ async function ne(e) {
338
+ if (!e.category || !e.createdAt) return {
339
+ previous: null,
340
+ next: null
341
+ };
342
+ 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) => {
343
+ let t = e.docs[0];
344
+ return t ? {
345
+ id: t.id,
346
+ ...t.data()
347
+ } : null;
348
+ };
349
+ return {
350
+ previous: a(n),
351
+ next: a(r)
352
+ };
353
+ }
354
+ async function re(e) {
299
355
  let t = K(e.title) || "post", n = t, r = 1;
300
- for (; (await m(a(n))).exists();) r++, n = `${t}-${r}`;
301
- return await Promise.all([x(a(n), {
356
+ for (; !(await h(b(i(), T("category", "==", e.category || ""), T("slug", "==", n), _(1)))).empty;) r++, n = `${t}-${r}`;
357
+ let o = e.postId || D(), c = _e(e.content, e.attachments);
358
+ return await S(a(o), {
359
+ slug: n,
302
360
  title: e.title,
361
+ ...c,
303
362
  tags: e.tags ?? [],
304
363
  ...e.category ? { category: e.category } : {},
305
364
  attachments: e.attachments ?? [],
@@ -309,62 +368,69 @@ function ge() {
309
368
  authorPhoto: e.authorPhoto,
310
369
  moderationStatus: "approved",
311
370
  moderationModel: e.moderationModel ?? null,
312
- createdAt: b(),
313
- updatedAt: b()
314
- }), x(o(n), { content: e.content })]), n;
371
+ createdAt: x(),
372
+ updatedAt: x()
373
+ }), await S(s(o), { content: e.content }), n;
315
374
  }
316
- async function ne(e, t) {
317
- await Promise.all([C(a(e), {
375
+ async function ie(e, t) {
376
+ let n = _e(t.content, t.attachments);
377
+ await Promise.all([w(a(e), {
318
378
  title: t.title,
379
+ ...n,
380
+ previewImage: n.previewImage ? n.previewImage : d(),
381
+ videoUrl: n.videoUrl ? n.videoUrl : d(),
319
382
  tags: t.tags ?? [],
320
- category: t.category ? t.category : f(),
383
+ category: t.category ? t.category : d(),
321
384
  attachments: t.attachments ?? [],
322
- updatedAt: b()
323
- }), x(o(e), { content: t.content }, { merge: !0 })]);
385
+ updatedAt: x()
386
+ }), S(s(e), { content: t.content }, { merge: !0 })]);
324
387
  }
325
- async function re(e) {
326
- let [i, s] = await Promise.all([m(a(e)), m(o(e))]);
388
+ async function k(e) {
389
+ let [i, o] = await Promise.all([m(a(e)), m(s(e))]);
327
390
  if (!i.exists()) return;
328
- let l = i.data(), u = s.exists() ? String(s.data().content ?? "") : "", f = await h(c(e));
329
- for (let e = 0; e < f.docs.length; e += 450) {
330
- let n = w(t);
331
- f.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
391
+ let c = i.data(), l = o.exists() ? String(o.data().content ?? "") : "", d = await h(g(e));
392
+ for (let e = 0; e < d.docs.length; e += 450) {
393
+ let n = E(t);
394
+ d.docs.slice(e, e + 450).forEach((e) => n.delete(e.ref)), await n.commit();
332
395
  }
333
- let p = O(n), g = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
334
- for (let t of l.attachments ?? []) if (t?.path) {
335
- g.add(t.path);
396
+ let f = j(n), p = /* @__PURE__ */ new Set(), _ = /* @__PURE__ */ new Set();
397
+ for (let t of c.attachments ?? []) if (t?.path) {
398
+ p.add(t.path);
336
399
  let n = q(t.path);
337
400
  n && n !== e && _.add(n);
338
401
  }
339
- for (let t of pe(u)) {
340
- let n = me(t);
402
+ for (let t of me(l)) {
403
+ let n = he(t);
341
404
  if (!n) continue;
342
- g.add(n);
405
+ p.add(n);
343
406
  let r = n.replace(/\/images\/([^/]+)$/, "/images/thumbnails/$1").replace(/\.[^.]+$/, ".jpg");
344
- r !== n && r.includes("/images/thumbnails/") && g.add(r);
407
+ r !== n && r.includes("/images/thumbnails/") && p.add(r);
345
408
  let i = n.match(/^(.*\/images\/)([^/]+)\.[^.]+$/);
346
- i && !n.includes("/thumbnails/") && g.add(`${i[1]}thumbnails/${i[2]}.jpg`);
409
+ i && !n.includes("/thumbnails/") && p.add(`${i[1]}thumbnails/${i[2]}.jpg`);
347
410
  let a = q(n);
348
411
  a && a !== e && _.add(a);
349
412
  }
350
- await d(o(e)), await J(A(p, `${r()}/posts/${e}`));
351
- for (let e of _) await J(A(p, `${r()}/posts/${e}`));
352
- await he(p, g), await d(a(e));
413
+ await u(s(e)), await Y(M(f, `${r()}/posts/${e}`));
414
+ for (let e of _) await Y(M(f, `${r()}/posts/${e}`));
415
+ await ve(f, p), await u(a(e));
353
416
  }
354
417
  return {
355
- getPosts: g,
356
- getPost: T,
357
- createPost: te,
358
- updatePost: ne,
359
- deletePost: re
418
+ createPostId: D,
419
+ getPosts: ee,
420
+ getPost: O,
421
+ getPostBySlug: te,
422
+ getAdjacentPosts: ne,
423
+ createPost: re,
424
+ updatePost: ie,
425
+ deletePost: k
360
426
  };
361
427
  }
362
428
  //#endregion
363
429
  //#region src/composables/useMemiBoardComments.ts
364
- function _e(t) {
365
- let n = R(), r = l(), a = () => n.collectionPrefix, o = e(() => typeof t == "string" ? t : t.value), s = e(() => y(u(r, `${a()}Posts`, o.value, "comments"), v("createdAt", "asc"))), c = i(s);
430
+ function be(t) {
431
+ let n = L(), 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);
366
432
  async function d(e) {
367
- let t = w(r), n = a(), i = p(u(r, `${n}Posts`, o.value, "comments"));
433
+ let t = E(r), n = a(), i = f(l(r, `${n}Posts`, o.value, "comments"));
368
434
  t.set(i, {
369
435
  postId: o.value,
370
436
  body: e.body,
@@ -372,22 +438,22 @@ function _e(t) {
372
438
  authorName: e.authorName,
373
439
  authorPhoto: e.authorPhoto,
374
440
  moderationStatus: "approved",
375
- createdAt: b()
376
- }), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
441
+ createdAt: x()
442
+ }), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(1) }), await t.commit();
377
443
  }
378
- async function f(e) {
379
- let t = w(r), n = a();
380
- t.delete(p(r, `${n}Posts`, o.value, "comments", e)), t.update(p(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
444
+ async function p(e) {
445
+ let t = E(r), n = a();
446
+ t.delete(f(r, `${n}Posts`, o.value, "comments", e)), t.update(f(r, `${n}Posts`, o.value), { commentCount: g(-1) }), await t.commit();
381
447
  }
382
448
  return {
383
- comments: c,
449
+ comments: u,
384
450
  addComment: d,
385
- deleteComment: f
451
+ deleteComment: p
386
452
  };
387
453
  }
388
454
  //#endregion
389
455
  //#region src/utils/moderation-prompt.ts
390
- var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\n아래 텍스트에 욕설·비속어·성희롱·혐오·위협·스팸·도배성 광고가 있으면 반드시 거부(flagged=true)합니다.\n\nflagged=true 예:\n- 욕설/비속어: 씨발, 시발, 병신, 지랄, 좆, 개새끼, 존나, fuck, shit, bitch 등\n- 성적 모욕, 혐오 발언, 협박\n- 게시판 주제와 무관한 도배성 광고/스팸\n\nflagged=false:\n- 게시판 주제에 맞는 정상적인 글/댓글 (강한 의견이나 정당한 비판은 허용)\n\nJSON만 출력:\n{\"flagged\":boolean,\"category\":\"none\"|\"abuse\"|\"spam\"|\"adult\"|\"violence\"|\"other\",\"reason\":string}\n- flagged=true면 category를 고르고, reason은 한국어로 **무엇이 문제인지** 구체적으로 1문장\n (예: \"욕설(비속어)이 포함되어 게시할 수 없습니다.\", \"광고·스팸성 문구가 있어 게시할 수 없습니다.\")\n 욕설 단어 전체를 그대로 길게 인용하지 말고, 유형(욕설/혐오/스팸 등)을 분명히 밝힌다.\n- flagged=false면 category는 \"none\", reason은 \"\"", ye = {
456
+ var xe = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이터입니다.\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은 \"\"", Se = {
391
457
  abuse: "욕설·비속어·모욕",
392
458
  spam: "스팸·광고",
393
459
  adult: "선정적·성인 내용",
@@ -395,17 +461,17 @@ var ve = "당신은 게시판 콘텐츠를 심사하는 엄격한 모더레이
395
461
  other: "부적절한 내용",
396
462
  none: ""
397
463
  };
398
- function Y(e) {
464
+ function X(e) {
399
465
  if (e.via === "local" && e.localHit) {
400
466
  let t = e.localHit.trim();
401
467
  return t.length > 0 && t.length <= 12 ? `「${t}」 같은 표현은 사용할 수 없습니다. 다른 말로 바꿔 주세요.` : "욕설·비속어가 포함되어 게시할 수 없습니다. 표현을 바꿔 주세요.";
402
468
  }
403
469
  let t = (e.reason || "").trim();
404
470
  if (t) return t;
405
- let n = e.category && e.category !== "none" ? ye[e.category] || e.category : "";
471
+ let n = e.category && e.category !== "none" ? Se[e.category] || e.category : "";
406
472
  return n ? `${n}이(가) 포함되어 게시할 수 없습니다. 내용을 수정해 주세요.` : "게시할 수 없는 내용이 포함되어 있습니다. 내용을 수정해 주세요.";
407
473
  }
408
- var be = [
474
+ var Ce = [
409
475
  "씨발",
410
476
  "시발",
411
477
  "시이발",
@@ -431,10 +497,10 @@ var be = [
431
497
  "bitch",
432
498
  "asshole"
433
499
  ];
434
- function xe(e) {
500
+ function we(e) {
435
501
  return new RegExp(e.join("|"), "i");
436
502
  }
437
- var Se = /* @__PURE__ */ new Set([
503
+ var Te = /* @__PURE__ */ new Set([
438
504
  "none",
439
505
  "abuse",
440
506
  "spam",
@@ -442,7 +508,7 @@ var Se = /* @__PURE__ */ new Set([
442
508
  "violence",
443
509
  "other"
444
510
  ]);
445
- function Ce(e) {
511
+ function Ee(e) {
446
512
  let t = e.trim();
447
513
  if (!t) return null;
448
514
  let n;
@@ -460,7 +526,7 @@ function Ce(e) {
460
526
  if (typeof n != "object" || !n) return null;
461
527
  let r = n;
462
528
  if (typeof r.flagged != "boolean") return null;
463
- let i = typeof r.category == "string" && Se.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
529
+ let i = typeof r.category == "string" && Te.has(r.category) ? r.category : r.flagged ? "other" : "none", a = typeof r.reason == "string" ? r.reason : "";
464
530
  return {
465
531
  flagged: r.flagged,
466
532
  category: i,
@@ -469,20 +535,20 @@ function Ce(e) {
469
535
  }
470
536
  //#endregion
471
537
  //#region src/composables/useMemiBoardModeration.ts
472
- var X = {
538
+ var Z = {
473
539
  flagged: !1,
474
540
  category: "none",
475
541
  reason: "",
476
542
  via: "empty"
477
543
  };
478
- function we() {
479
- let e = R().moderation ?? {}, t = s(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = xe([...be, ...e.localBlocklist ?? []]);
480
- function o(e) {
544
+ function De() {
545
+ let e = L().moderation ?? {}, t = o(), { isWriteRestricted: n, restrictedMessage: r, recordContentModerationBlock: i } = G(), a = we([...Ce, ...e.localBlocklist ?? []]);
546
+ function s(e) {
481
547
  let t = e.match(a);
482
548
  return t ? {
483
549
  flagged: !0,
484
550
  category: "abuse",
485
- reason: Y({
551
+ reason: X({
486
552
  via: "local",
487
553
  localHit: t[0] || "",
488
554
  category: "abuse"
@@ -491,13 +557,13 @@ function we() {
491
557
  } : null;
492
558
  }
493
559
  function c() {
494
- let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = se(t, {
495
- backend: new N(),
560
+ let n = e.model ?? "gemini-3.5-flash-lite", r = e.useLimitedUseAppCheckTokens !== !1, i = ce(t, {
561
+ backend: new se(),
496
562
  useLimitedUseAppCheckTokens: r
497
563
  });
498
- return ce(i, {
564
+ return le(i, {
499
565
  model: n,
500
- systemInstruction: ve,
566
+ systemInstruction: xe,
501
567
  generationConfig: {
502
568
  temperature: 0,
503
569
  maxOutputTokens: 256,
@@ -526,7 +592,7 @@ function we() {
526
592
  };
527
593
  }
528
594
  function u(e, t = "") {
529
- let n = Y({
595
+ let n = X({
530
596
  reason: e.reason,
531
597
  category: e.category,
532
598
  via: e.via
@@ -554,22 +620,22 @@ function we() {
554
620
  };
555
621
  let i = t.trim();
556
622
  if (!i) return {
557
- ...X,
623
+ ...Z,
558
624
  via: "empty"
559
625
  };
560
- let a = o(i);
626
+ let a = s(i);
561
627
  if (a) return d(a);
562
628
  if (e.enabled === !1) return {
563
- ...X,
629
+ ...Z,
564
630
  via: "disabled"
565
631
  };
566
632
  try {
567
- let e = Ce((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
633
+ let e = Ee((await c().generateContent(`심사할 텍스트:\n"""${i.slice(0, 4e3)}"""`)).response.text());
568
634
  if (!e) return l();
569
635
  let t = {
570
636
  flagged: e.flagged,
571
637
  category: e.category,
572
- reason: Y({
638
+ reason: X({
573
639
  reason: e.reason,
574
640
  category: e.category,
575
641
  via: "ai"
@@ -589,14 +655,14 @@ function we() {
589
655
  via: "restricted"
590
656
  };
591
657
  if (e.enabled === !1 || !e.moderateImages) return {
592
- ...X,
658
+ ...Z,
593
659
  via: "disabled"
594
660
  };
595
661
  try {
596
662
  let e = await new Promise((e, n) => {
597
663
  let r = new FileReader();
598
664
  r.onload = () => e(r.result.split(",")[1] ?? ""), r.onerror = n, r.readAsDataURL(t);
599
- }), n = Ce((await c().generateContent([{ inlineData: {
665
+ }), n = Ee((await c().generateContent([{ inlineData: {
600
666
  mimeType: t.type || "image/jpeg",
601
667
  data: e
602
668
  } }, "이 이미지를 심사해 주세요."])).response.text());
@@ -604,7 +670,7 @@ function we() {
604
670
  let r = {
605
671
  flagged: n.flagged,
606
672
  category: n.category,
607
- reason: Y({
673
+ reason: X({
608
674
  reason: n.reason,
609
675
  category: n.category,
610
676
  via: "ai"
@@ -623,7 +689,7 @@ function we() {
623
689
  }
624
690
  //#endregion
625
691
  //#region src/utils/compressImage.ts
626
- async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
692
+ async function Oe(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
627
693
  return new Promise((r, i) => {
628
694
  let a = new Image(), o = URL.createObjectURL(e);
629
695
  a.onload = () => {
@@ -643,11 +709,11 @@ async function Te(e, { maxWidth: t = 400, quality: n = .8 } = {}) {
643
709
  }
644
710
  //#endregion
645
711
  //#region src/composables/useMemiBoardStorage.ts
646
- var Ee = 5242880;
647
- function De() {
648
- let e = R(), t = s(), n = () => e.collectionPrefix;
712
+ var ke = 5242880;
713
+ function Ae() {
714
+ let e = L(), t = o(), n = () => e.collectionPrefix;
649
715
  function r(e, r, i) {
650
- let a = O(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${fe(e.name)}`, s = A(a, o), c = M(s, e, {
716
+ let a = j(t), o = `${n()}/posts/${r}/attachments/${Date.now()}-${pe(e.name)}`, s = M(a, o), c = oe(s, e, {
651
717
  contentType: e.type || "application/octet-stream",
652
718
  contentDisposition: `attachment; filename*=UTF-8''${encodeURIComponent(e.name)}`
653
719
  });
@@ -656,7 +722,7 @@ function De() {
656
722
  c.on("state_changed", (e) => {
657
723
  i && e.totalBytes > 0 && i(e.bytesTransferred / e.totalBytes);
658
724
  }, n, async () => {
659
- let n = await D(s);
725
+ let n = await A(s);
660
726
  t({
661
727
  name: e.name,
662
728
  url: n,
@@ -670,131 +736,196 @@ function De() {
670
736
  };
671
737
  }
672
738
  async function i(e) {
673
- let n = O(t);
674
- await E(A(n, e.path)).catch(() => {});
739
+ let n = j(t);
740
+ await k(M(n, e.path)).catch(() => {});
675
741
  }
676
742
  async function a(e, r) {
677
743
  if (!e.type.startsWith("image/")) throw Error("이미지 파일만 업로드할 수 있습니다.");
678
- if (e.size > 5242880) throw Error(`이미지는 ${Ee / 1024 / 1024}MB 이하여야 합니다.`);
679
- let i = O(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = fe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = A(i, l);
680
- await j(u, e, { contentType: e.type || "image/png" });
681
- let d = await D(u), f = await Te(e, {
744
+ if (e.size > 5242880) throw Error(`이미지는 ${ke / 1024 / 1024}MB 이하여야 합니다.`);
745
+ let i = j(t), a = (e.name.split(".").pop() || e.type.split("/")[1] || "png").replace(/[^a-zA-Z0-9]/g, "").slice(0, 8) || "png", o = pe(e.name.replace(/\.[^.]+$/, "") || "image", 40), s = `${Date.now()}-${o || "image"}`, c = r || `new-${Date.now()}`, l = `${n()}/posts/${c}/images/${s}.${a}`, u = M(i, l);
746
+ await N(u, e, { contentType: e.type || "image/png" });
747
+ let d = await A(u), f = await Oe(e, {
682
748
  maxWidth: 400,
683
749
  quality: .8
684
- }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = A(i, p);
685
- return await j(m, f, { contentType: "image/jpeg" }), {
750
+ }), p = `${n()}/posts/${c}/images/thumbnails/${s}.jpg`, m = M(i, p);
751
+ return await N(m, f, { contentType: "image/jpeg" }), {
686
752
  originalUrl: d,
687
753
  originalPath: l,
688
- thumbnailUrl: await D(m),
754
+ thumbnailUrl: await A(m),
689
755
  thumbnailPath: p
690
756
  };
691
757
  }
692
- async function o(e) {
693
- let n = O(t);
694
- await Promise.all([E(A(n, e.originalPath)).catch(() => {}), E(A(n, e.thumbnailPath)).catch(() => {})]);
758
+ async function s(e) {
759
+ let n = j(t);
760
+ await Promise.all([k(M(n, e.originalPath)).catch(() => {}), k(M(n, e.thumbnailPath)).catch(() => {})]);
695
761
  }
696
762
  return {
697
763
  uploadAttachment: r,
698
764
  deleteAttachment: i,
699
765
  uploadEditorImage: a,
700
- deleteEditorImage: o
766
+ deleteEditorImage: s
701
767
  };
702
768
  }
703
769
  //#endregion
704
770
  //#region src/composables/useMemiBoardSettings.ts
705
- var Z = [
771
+ var je = [
706
772
  {
707
773
  id: "free",
708
774
  label: "자유",
709
- listView: "default"
775
+ listView: "default",
776
+ writeRole: "user",
777
+ order: 0
710
778
  },
711
779
  {
712
780
  id: "notice",
713
781
  label: "공지",
714
- listView: "default"
782
+ listView: "default",
783
+ writeRole: "admin",
784
+ order: 1
715
785
  },
716
786
  {
717
787
  id: "question",
718
788
  label: "질문",
719
- listView: "default"
789
+ listView: "default",
790
+ writeRole: "user",
791
+ order: 2
720
792
  }
721
793
  ];
722
- function Oe() {
723
- let t = R(), n = l(), { isSignedIn: i, isAdmin: a } = G(), s = e(() => p(n, `${t.collectionPrefix}Settings`, "config")), { data: c, pending: u } = o(s), d = e(() => {
724
- let e = c.value?.categories;
725
- return e && e.length > 0 ? e : Z;
726
- });
727
- function f(e) {
728
- if (e) return d.value.find((t) => t.id === e)?.label ?? e;
729
- }
730
- async function h(e) {
731
- if (!i.value) throw Error("로그인이 필요합니다.");
732
- let t = e.map((e) => ({
733
- id: e.id.trim(),
734
- label: e.label.trim(),
735
- listView: e.listView ?? "default"
736
- }));
737
- if (!t.length) throw Error("카테고리를 하나 이상 만들어 주세요.");
738
- if (t.some((e) => !e.id || !e.label)) throw Error("카테고리 이름을 모두 입력해 주세요.");
739
- if (new Set(t.map((e) => e.id)).size !== t.length) throw Error("카테고리 ID가 중복되었습니다.");
740
- await x(s.value, {
741
- categories: t,
742
- updatedAt: b()
743
- });
794
+ function Me() {
795
+ let t = L(), n = c(), { isSignedIn: r } = G(), 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, { ssrKey: `${t.collectionPrefix}Settings/config/categories` }), m = e(() => d.value.map((e, t) => ({
796
+ ...e,
797
+ id: e.id,
798
+ order: typeof e.order == "number" ? e.order : t,
799
+ listView: e.listView ?? "default",
800
+ writeRole: e.writeRole ?? "user"
801
+ })));
802
+ function h(e) {
803
+ if (e) return m.value.find((t) => t.id === e)?.label ?? e;
744
804
  }
745
- async function g() {
746
- if (!i.value) return;
747
- let e = s.value, t = await m(e).catch(() => null);
748
- !t || t.exists() || await x(e, {
749
- categories: Z,
750
- updatedAt: b()
751
- });
805
+ async function g(e, t = e.order ?? 0) {
806
+ if (!r.value) throw Error("로그인이 필요합니다.");
807
+ let n = e.id.trim(), i = e.label.trim();
808
+ if (!n || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
809
+ await S(a.value, { updatedAt: x() }, { merge: !0 }), await S(f(o.value, n), {
810
+ label: i,
811
+ listView: e.listView ?? "default",
812
+ writeRole: e.writeRole ?? "user",
813
+ order: t,
814
+ updatedAt: x()
815
+ }, { merge: !0 });
752
816
  }
753
817
  async function _(e) {
818
+ if (!r.value) throw Error("로그인이 필요합니다.");
819
+ let t = E(n);
820
+ t.set(a.value, { updatedAt: x() }, { merge: !0 }), e.forEach((e, n) => {
821
+ let r = e.id.trim(), i = e.label.trim();
822
+ if (!r || !i) throw Error("카테고리 ID와 이름을 입력해 주세요.");
823
+ t.set(f(o.value, r), {
824
+ label: i,
825
+ listView: e.listView ?? "default",
826
+ writeRole: e.writeRole ?? "user",
827
+ order: n,
828
+ updatedAt: x()
829
+ }, { merge: !0 });
830
+ }), await t.commit();
831
+ }
832
+ async function v(e) {
833
+ if (!r.value) throw Error("로그인이 필요합니다.");
834
+ await u(f(o.value, e));
835
+ }
836
+ async function C() {
837
+ return Promise.resolve();
838
+ }
839
+ async function w(e) {
754
840
  let t = e.trim();
755
841
  if (!t) throw Error("카테고리 이름을 입력해 주세요.");
756
- if (!i.value) throw Error("로그인이 필요합니다.");
757
- await g();
758
- let n = d.value.find((e) => e.label === t || e.id === K(t));
842
+ if (!r.value) throw Error("로그인이 필요합니다.");
843
+ let n = m.value.find((e) => e.label === t);
759
844
  if (n) return n.id;
760
- let r = K(t) || `cat-${Date.now()}`, a = new Set(d.value.map((e) => e.id));
761
- if (a.has(r)) {
762
- let e = 2;
763
- for (; a.has(`${r}-${e}`);) e++;
764
- r = `${r}-${e}`;
765
- }
766
- let o = [...d.value, {
767
- id: r,
845
+ let i = K(t) || `cat-${Date.now()}`, a = i, o = 2, s = new Set(m.value.map((e) => e.id));
846
+ for (; s.has(a);) a = `${i}-${o++}`;
847
+ return await g({
848
+ id: a,
768
849
  label: t,
769
- listView: "default"
770
- }], c = s.value;
771
- return (await m(c)).exists() ? await C(c, {
772
- categories: o,
773
- updatedAt: b()
774
- }) : await x(c, {
775
- categories: o,
776
- updatedAt: b()
777
- }), r;
778
- }
779
- return r([
780
- a,
781
- i,
782
- u
783
- ], async ([e, t, n]) => {
784
- n || c.value || !e && !t || await g().catch(() => {});
785
- }, { immediate: !0 }), {
786
- categories: d,
787
- categoryLabel: f,
788
- settingsPending: u,
789
- ensureSettings: g,
790
- addCategory: _,
791
- saveCategories: h,
792
- DEFAULT_CATEGORIES: Z
850
+ listView: "default",
851
+ writeRole: "user",
852
+ order: m.value.length
853
+ }), a;
854
+ }
855
+ return {
856
+ categories: m,
857
+ categoryLabel: h,
858
+ settingsPending: p,
859
+ ensureSettings: C,
860
+ addCategory: w,
861
+ saveCategory: g,
862
+ saveCategories: _,
863
+ deleteCategory: v,
864
+ DEFAULT_CATEGORIES: je
865
+ };
866
+ }
867
+ //#endregion
868
+ //#region src/composables/useMemiBoardUsers.ts
869
+ var Ne = [
870
+ {
871
+ value: "admin",
872
+ label: "관리자",
873
+ description: "사용자 역할, 게시판 설정과 모든 콘텐츠를 관리합니다."
874
+ },
875
+ {
876
+ value: "staff",
877
+ label: "스태프",
878
+ description: "모든 게시글과 댓글을 관리하지만 사용자 역할과 게시판 설정은 바꿀 수 없습니다."
879
+ },
880
+ {
881
+ value: "user",
882
+ label: "사용자",
883
+ description: "글과 댓글을 작성하고 본인이 작성한 콘텐츠만 수정·삭제할 수 있습니다."
884
+ }
885
+ ];
886
+ function Pe() {
887
+ let e = L(), t = c(), n = `${e.collectionPrefix}Users`, r = i(l(t, n), { ssrKey: `${n}-management` });
888
+ async function a(e, r) {
889
+ if (!Ne.some((e) => e.value === r)) throw Error("지원하지 않는 역할입니다.");
890
+ await w(f(t, n, e), {
891
+ role: r,
892
+ updatedAt: x()
893
+ });
894
+ }
895
+ return {
896
+ users: r,
897
+ usersPending: r.pending,
898
+ updateUserRole: a
793
899
  };
794
900
  }
795
901
  //#endregion
796
902
  //#region src/data/versionHistory.ts
797
- var ke = [
903
+ var Fe = [
904
+ {
905
+ version: "0.7.0",
906
+ date: "2026-08-04",
907
+ highlights: ["카테고리마다 글쓰기 권한을 일반 이상·스태프 이상·관리자만으로 설정할 수 있어요", "게시판 설정 화면의 SSR hydration 경고를 해결했어요"]
908
+ },
909
+ {
910
+ version: "0.6.0",
911
+ date: "2026-08-04",
912
+ highlights: ["관리자가 게시판 사용자를 확인하고 관리자·스태프·사용자 역할을 설정할 수 있어요", "스태프는 게시판 설정이나 사용자 역할은 바꾸지 않고 게시글과 댓글을 관리할 수 있어요"]
913
+ },
914
+ {
915
+ version: "0.5.0",
916
+ date: "2026-08-04",
917
+ highlights: [
918
+ "게시판마다 일반·이미지·영상 목록 화면을 선택할 수 있어요",
919
+ "YouTube 링크를 붙여넣으면 본문에 영상으로 표시돼요",
920
+ "게시글 아래에서 이전 글·목록·다음 글로 이동할 수 있어요",
921
+ "관리자가 카테고리를 각각 저장하고 관리할 수 있어요"
922
+ ]
923
+ },
924
+ {
925
+ version: "0.4.15",
926
+ date: "2026-08-03",
927
+ highlights: ["설정 화면이 설치된 프로젝트에서 정상적으로 열리도록 고쳤어요"]
928
+ },
798
929
  {
799
930
  version: "0.4.14",
800
931
  date: "2026-08-03",
@@ -826,7 +957,7 @@ var ke = [
826
957
  ];
827
958
  //#endregion
828
959
  //#region src/utils/formatDate.ts
829
- function Ae(e) {
960
+ function Ie(e) {
830
961
  return e?.toDate ? e.toDate().toLocaleDateString("ko-KR") : "";
831
962
  }
832
963
  //#endregion
@@ -838,7 +969,7 @@ function $(e) {
838
969
  let t = e;
839
970
  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;
840
971
  }
841
- function je(e) {
972
+ function Le(e) {
842
973
  if (!e?.trim()) return "";
843
974
  let t = e.replace(/\r\n/g, "\n").split("\n"), n = [], r = 0, i = !1, a = [], o = null, s = [];
844
975
  function c() {
@@ -903,6 +1034,6 @@ function je(e) {
903
1034
  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");
904
1035
  }
905
1036
  //#endregion
906
- export { z as DEFAULT_BLOCK_BAN_DECAY_MS, le as DEFAULT_BLOCK_BAN_THRESHOLD, Z as DEFAULT_CATEGORIES, Ee as EDITOR_IMAGE_MAX_BYTES, Te as compressImage, L as configureMemiBoard, B as effectiveModerationBlockCount, pe as extractEditorImageUrls, Ae as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, je as renderMarkdownToHtml, me as storagePathFromDownloadUrl, G as useMemiBoardAuth, _e as useMemiBoardComments, R as useMemiBoardConfig, we as useMemiBoardModeration, ge as useMemiBoardPosts, Oe as useMemiBoardSettings, De as useMemiBoardStorage, ke as versionHistory };
1037
+ export { Ne as BOARD_USER_ROLES, z as DEFAULT_BLOCK_BAN_DECAY_MS, R as DEFAULT_BLOCK_BAN_THRESHOLD, je as DEFAULT_CATEGORIES, ke as EDITOR_IMAGE_MAX_BYTES, Oe as compressImage, ue as configureMemiBoard, B as effectiveModerationBlockCount, me as extractEditorImageUrls, Ie as formatDate, U as formatRestrictedUntilLabel, H as isModerationWriteRestricted, V as moderationWriteRestrictedUntilMs, q as postNamespaceFromStoragePath, Le as renderMarkdownToHtml, K as slugify, he as storagePathFromDownloadUrl, G as useMemiBoardAuth, be as useMemiBoardComments, L as useMemiBoardConfig, De as useMemiBoardModeration, ye as useMemiBoardPosts, Me as useMemiBoardSettings, Ae as useMemiBoardStorage, Pe as useMemiBoardUsers, Fe as versionHistory };
907
1038
 
908
1039
  //# sourceMappingURL=index.js.map